Files
star-erp/app/Modules/Integration/Actions/SyncOrderAction.php

176 lines
6.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Modules\Integration\Actions;
use App\Modules\Integration\Models\SalesOrder;
use App\Modules\Integration\Models\SalesOrderItem;
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
use App\Modules\Inventory\Contracts\ProductServiceInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\ValidationException;
class SyncOrderAction
{
protected $inventoryService;
protected $productService;
public function __construct(
InventoryServiceInterface $inventoryService,
ProductServiceInterface $productService
) {
$this->inventoryService = $inventoryService;
$this->productService = $productService;
}
/**
* 執行訂單同步
*
* @param array $data
* @return array 包含 orders 建立結果的資訊
* @throws ValidationException
* @throws \Exception
*/
public function execute(array $data)
{
$externalOrderId = $data['external_order_id'];
// 使用 Cache::lock 防護高併發,鎖定該訂單號 10 秒
// 此處需要 cache store 支援鎖 (如 memcached, dynamodb, redis, database, file, array)
// Laravel 預設的 file/redis 都支援。若無法取得鎖,表示有另一個相同的請求正在處理
$lock = Cache::lock("sync_order_{$externalOrderId}", 10);
if (!$lock->get()) {
throw ValidationException::withMessages([
'external_order_id' => ["The order {$externalOrderId} is currently being processed by another transaction. Please try again later."]
]);
}
try {
// 冪等性處理:若訂單已存在,回傳已建立的訂單資訊
$existingOrder = SalesOrder::where('external_order_id', $externalOrderId)->first();
if ($existingOrder) {
return [
'status' => 'exists',
'message' => 'Order already exists',
'order_id' => $existingOrder->id,
];
}
// --- 預檢 (Pre-flight check) 僅使用 product_id ---
$items = $data['items'];
$targetErpIds = array_column($items, 'product_id');
// 一次性查出所有相關的 Product
$productsById = $this->productService->findByIds($targetErpIds)->keyBy('id');
$resolvedProducts = [];
$missingIds = [];
foreach ($items as $index => $item) {
$productId = $item['product_id'];
$product = $productsById->get($productId);
if ($product) {
$resolvedProducts[$index] = $product;
} else {
$missingIds[] = $productId;
}
}
if (!empty($missingIds)) {
throw ValidationException::withMessages([
'items' => ["The following product IDs are not found: " . implode(', ', array_unique($missingIds)) . ". Please ensure these products exist in the system."]
]);
}
// --- 執行寫入交易 ---
$result = DB::transaction(function () use ($data, $items, $resolvedProducts) {
// 1. 查找倉庫(提前至建立訂單前,以便判定來源)
$warehouseCode = $data['warehouse_code'];
$warehouses = $this->inventoryService->getWarehousesByCodes([$warehouseCode]);
if ($warehouses->isEmpty()) {
throw ValidationException::withMessages([
'warehouse_code' => ["Warehouse with code {$warehouseCode} not found."]
]);
}
$warehouse = $warehouses->first();
$warehouseId = $warehouse->id;
// 2. 自動判定來源:若是販賣機倉庫則標記為 vending其餘為 pos
$source = ($warehouse->type === \App\Enums\WarehouseType::VENDING) ? 'vending' : 'pos';
// 3. 建立訂單
$order = SalesOrder::create([
'external_order_id' => $data['external_order_id'],
'name' => $data['name'],
'status' => 'completed',
'payment_method' => $data['payment_method'] ?? 'cash',
'total_amount' => $data['total_amount'],
'total_qty' => $data['total_qty'],
'sold_at' => $data['sold_at'] ?? now(),
'raw_payload' => $data,
'source' => $source,
'source_label' => $data['source_label'] ?? null,
]);
$totalAmount = 0;
// 3. 處理訂單明細
$orderItemsData = [];
foreach ($items as $index => $itemData) {
$product = $resolvedProducts[$index];
$qty = $itemData['qty'];
$price = $itemData['price'];
$batchNumber = $itemData['batch_number'] ?? null;
$lineTotal = $qty * $price;
$totalAmount += $lineTotal;
$orderItemsData[] = [
'sales_order_id' => $order->id,
'product_id' => $product->id,
'product_name' => $product->name,
'quantity' => $qty,
'price' => $price,
'total' => $lineTotal,
'created_at' => now(),
'updated_at' => now(),
];
// 4. 扣除庫存(強制模式,允許負庫存)
$this->inventoryService->decreaseStock(
$product->id,
$warehouseId,
$qty,
"POS Order: " . $order->external_order_id,
true,
null, // Slot (location)
\App\Modules\Integration\Models\SalesOrder::class,
$order->id,
$batchNumber
);
}
// Batch insert order items
SalesOrderItem::insert($orderItemsData);
$order->update(['total_amount' => $totalAmount]);
return [
'status' => 'created',
'message' => 'Order synced and stock deducted successfully',
'order_id' => $order->id,
];
});
return $result;
} finally {
// 無論成功失敗,最後釋放鎖定
$lock->release();
}
}
}