Compare commits
2 Commits
29cdf37b71
...
904132e460
| Author | SHA1 | Date | |
|---|---|---|---|
| 904132e460 | |||
| a05acd96dc |
@@ -6,66 +6,79 @@ use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Modules\Integration\Models\SalesOrder;
|
||||
use App\Modules\Integration\Models\SalesOrderItem;
|
||||
use App\Modules\Inventory\Services\InventoryService;
|
||||
use App\Modules\Inventory\Models\Product;
|
||||
use App\Modules\Inventory\Models\Warehouse;
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
use App\Modules\Inventory\Contracts\ProductServiceInterface;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class OrderSyncController extends Controller
|
||||
{
|
||||
protected $inventoryService;
|
||||
protected $productService;
|
||||
|
||||
public function __construct(InventoryService $inventoryService)
|
||||
{
|
||||
public function __construct(
|
||||
InventoryServiceInterface $inventoryService,
|
||||
ProductServiceInterface $productService
|
||||
) {
|
||||
$this->inventoryService = $inventoryService;
|
||||
$this->productService = $productService;
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
// 冪等性處理:若訂單已存在,回傳已建立的訂單資訊
|
||||
$existingOrder = SalesOrder::where('external_order_id', $request->external_order_id)->first();
|
||||
if ($existingOrder) {
|
||||
return response()->json([
|
||||
'message' => 'Order already exists',
|
||||
'order_id' => $existingOrder->id,
|
||||
], 200);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'external_order_id' => 'required|string|unique:sales_orders,external_order_id',
|
||||
'warehouse' => 'nullable|string',
|
||||
'warehouse_id' => 'nullable|exists:warehouses,id',
|
||||
'items' => 'required|array',
|
||||
'warehouse_id' => 'nullable|integer',
|
||||
'payment_method' => 'nullable|string|in:cash,credit_card,line_pay,ecpay,transfer,other',
|
||||
'sold_at' => 'nullable|date',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.pos_product_id' => 'required|string',
|
||||
'items.*.qty' => 'required|numeric|min:0.0001',
|
||||
'items.*.price' => 'required|numeric',
|
||||
'items.*.price' => 'required|numeric|min:0',
|
||||
]);
|
||||
|
||||
try {
|
||||
return DB::transaction(function () use ($request) {
|
||||
// 1. Create Order
|
||||
// 1. 建立訂單
|
||||
$order = SalesOrder::create([
|
||||
'external_order_id' => $request->external_order_id,
|
||||
'status' => 'completed',
|
||||
'payment_method' => $request->payment_method ?? 'cash',
|
||||
'total_amount' => 0, // Will calculate
|
||||
'total_amount' => 0,
|
||||
'sold_at' => $request->sold_at ?? now(),
|
||||
'raw_payload' => $request->all(),
|
||||
]);
|
||||
|
||||
// Find Warehouse (Default to "銷售倉庫")
|
||||
// 2. 查找或建立倉庫
|
||||
$warehouseId = $request->warehouse_id;
|
||||
|
||||
|
||||
if (empty($warehouseId)) {
|
||||
$warehouseName = $request->warehouse ?: '銷售倉庫';
|
||||
$warehouse = Warehouse::firstOrCreate(['name' => $warehouseName], [
|
||||
'code' => 'SALES-' . strtoupper(bin2hex(random_bytes(4))),
|
||||
'type' => 'system_sales',
|
||||
'is_active' => true,
|
||||
]);
|
||||
$warehouse = $this->inventoryService->findOrCreateWarehouseByName($warehouseName);
|
||||
$warehouseId = $warehouse->id;
|
||||
}
|
||||
|
||||
$totalAmount = 0;
|
||||
|
||||
// 3. 處理訂單明細
|
||||
foreach ($request->items as $itemData) {
|
||||
// Find product by external ID (Strict Check)
|
||||
$product = Product::where('external_pos_id', $itemData['pos_product_id'])->first();
|
||||
// 透過介面查找產品
|
||||
$product = $this->productService->findByExternalPosId($itemData['pos_product_id']);
|
||||
|
||||
if (!$product) {
|
||||
throw new \Exception("Product not found for POS ID: " . $itemData['pos_product_id'] . ". Please sync product first.");
|
||||
throw new \Exception(
|
||||
"Product not found for POS ID: " . $itemData['pos_product_id'] . ". Please sync product first."
|
||||
);
|
||||
}
|
||||
|
||||
$qty = $itemData['qty'];
|
||||
@@ -73,23 +86,23 @@ class OrderSyncController extends Controller
|
||||
$lineTotal = $qty * $price;
|
||||
$totalAmount += $lineTotal;
|
||||
|
||||
// 2. Create Order Item
|
||||
// 建立訂單明細
|
||||
SalesOrderItem::create([
|
||||
'sales_order_id' => $order->id,
|
||||
'product_id' => $product->id,
|
||||
'product_name' => $product->name, // Snapshot name
|
||||
'product_name' => $product->name,
|
||||
'quantity' => $qty,
|
||||
'price' => $price,
|
||||
'total' => $lineTotal,
|
||||
]);
|
||||
|
||||
// 3. Deduct Stock (Force negative allowed for POS orders)
|
||||
// 4. 扣除庫存(強制模式,允許負庫存)
|
||||
$this->inventoryService->decreaseStock(
|
||||
$product->id,
|
||||
$warehouseId,
|
||||
$qty,
|
||||
"POS Order: " . $order->external_order_id,
|
||||
true // Force = true
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ namespace App\Modules\Integration\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Modules\Inventory\Services\ProductService;
|
||||
use App\Modules\Inventory\Contracts\ProductServiceInterface;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProductSyncController extends Controller
|
||||
{
|
||||
protected $productService;
|
||||
|
||||
public function __construct(ProductService $productService)
|
||||
public function __construct(ProductServiceInterface $productService)
|
||||
{
|
||||
$this->productService = $productService;
|
||||
}
|
||||
@@ -19,12 +19,17 @@ class ProductSyncController extends Controller
|
||||
public function upsert(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'external_pos_id' => 'required|string',
|
||||
'name' => 'required|string',
|
||||
'price' => 'nullable|numeric',
|
||||
'barcode' => 'nullable|string',
|
||||
'category' => 'nullable|string',
|
||||
'unit' => 'nullable|string',
|
||||
'external_pos_id' => 'required|string|max:255',
|
||||
'name' => 'required|string|max:255',
|
||||
'price' => 'nullable|numeric|min:0|max:99999999.99',
|
||||
'barcode' => 'nullable|string|max:100',
|
||||
'category' => 'nullable|string|max:100',
|
||||
'unit' => 'nullable|string|max:100',
|
||||
'brand' => 'nullable|string|max:100',
|
||||
'specification' => 'nullable|string|max:255',
|
||||
'cost_price' => 'nullable|numeric|min:0|max:99999999.99',
|
||||
'member_price' => 'nullable|numeric|min:0|max:99999999.99',
|
||||
'wholesale_price' => 'nullable|numeric|min:0|max:99999999.99',
|
||||
'updated_at' => 'nullable|date',
|
||||
]);
|
||||
|
||||
@@ -40,7 +45,9 @@ class ProductSyncController extends Controller
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Product Sync Failed', ['error' => $e->getMessage(), 'payload' => $request->all()]);
|
||||
return response()->json(['message' => 'Sync failed'], 500);
|
||||
return response()->json([
|
||||
'message' => 'Sync failed: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ namespace App\Modules\Integration;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Modules\Integration\Middleware\TenantIdentificationMiddleware;
|
||||
|
||||
class IntegrationServiceProvider extends ServiceProvider
|
||||
@@ -13,8 +16,13 @@ class IntegrationServiceProvider extends ServiceProvider
|
||||
$this->loadRoutesFrom(__DIR__ . '/Routes/api.php');
|
||||
$this->loadMigrationsFrom(__DIR__ . '/Database/Migrations');
|
||||
|
||||
// Register Middleware Alias
|
||||
// 註冊 Middleware 別名
|
||||
Route::aliasMiddleware('integration.tenant', TenantIdentificationMiddleware::class);
|
||||
|
||||
// 定義 Integration API 速率限制(每分鐘 60 次,依 Token 使用者識別)
|
||||
RateLimiter::for('integration', function (Request $request) {
|
||||
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
|
||||
});
|
||||
}
|
||||
|
||||
public function register()
|
||||
|
||||
@@ -5,7 +5,7 @@ use App\Modules\Integration\Controllers\ProductSyncController;
|
||||
use App\Modules\Integration\Controllers\OrderSyncController;
|
||||
|
||||
Route::prefix('api/v1/integration')
|
||||
->middleware(['api', 'integration.tenant', 'auth:sanctum']) // integration.tenant middleware to identify tenant
|
||||
->middleware(['api', 'throttle:integration', 'integration.tenant', 'auth:sanctum'])
|
||||
->group(function () {
|
||||
Route::post('products/upsert', [ProductSyncController::class, 'upsert']);
|
||||
Route::post('orders', [OrderSyncController::class, 'store']);
|
||||
|
||||
@@ -131,4 +131,12 @@ interface InventoryServiceInterface
|
||||
* @return array
|
||||
*/
|
||||
public function getDashboardStats(): array;
|
||||
|
||||
/**
|
||||
* 依倉庫名稱查找或建立倉庫(供外部整合用)。
|
||||
*
|
||||
* @param string $warehouseName
|
||||
* @return object
|
||||
*/
|
||||
public function findOrCreateWarehouseByName(string $warehouseName);
|
||||
}
|
||||
25
app/Modules/Inventory/Contracts/ProductServiceInterface.php
Normal file
25
app/Modules/Inventory/Contracts/ProductServiceInterface.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Inventory\Contracts;
|
||||
|
||||
/**
|
||||
* 產品服務介面 — 供跨模組使用(如 Integration 模組)。
|
||||
*/
|
||||
interface ProductServiceInterface
|
||||
{
|
||||
/**
|
||||
* 透過外部 POS ID 進行產品新增或更新(Upsert)。
|
||||
*
|
||||
* @param array $data
|
||||
* @return object
|
||||
*/
|
||||
public function upsertFromPos(array $data);
|
||||
|
||||
/**
|
||||
* 透過外部 POS ID 查找產品。
|
||||
*
|
||||
* @param string $externalPosId
|
||||
* @return object|null
|
||||
*/
|
||||
public function findByExternalPosId(string $externalPosId);
|
||||
}
|
||||
@@ -4,13 +4,16 @@ namespace App\Modules\Inventory;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
use App\Modules\Inventory\Contracts\ProductServiceInterface;
|
||||
use App\Modules\Inventory\Services\InventoryService;
|
||||
use App\Modules\Inventory\Services\ProductService;
|
||||
|
||||
class InventoryServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(InventoryServiceInterface::class, InventoryService::class);
|
||||
$this->app->bind(ProductServiceInterface::class, ProductService::class);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
|
||||
@@ -590,5 +590,22 @@ class InventoryService implements InventoryServiceInterface
|
||||
'abnormalItems' => $abnormalItems,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 依倉庫名稱查找或建立倉庫(供外部整合用)。
|
||||
*
|
||||
* @param string $warehouseName
|
||||
* @return Warehouse
|
||||
*/
|
||||
public function findOrCreateWarehouseByName(string $warehouseName)
|
||||
{
|
||||
return Warehouse::firstOrCreate(
|
||||
['name' => $warehouseName],
|
||||
[
|
||||
'code' => 'SALES-' . strtoupper(bin2hex(random_bytes(4))),
|
||||
'type' => 'system_sales',
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Modules\Inventory\Services;
|
||||
|
||||
use App\Modules\Inventory\Contracts\ProductServiceInterface;
|
||||
use App\Modules\Inventory\Models\Product;
|
||||
use App\Modules\Inventory\Models\Category;
|
||||
use App\Modules\Inventory\Models\Unit;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProductService
|
||||
class ProductService implements ProductServiceInterface
|
||||
{
|
||||
/**
|
||||
* Upsert product from external POS source.
|
||||
@@ -40,13 +41,20 @@ class ProductService
|
||||
$product->barcode = $data['barcode'] ?? $product->barcode;
|
||||
$product->price = $data['price'] ?? 0;
|
||||
|
||||
// Map newly added extended fields
|
||||
if (isset($data['brand'])) $product->brand = $data['brand'];
|
||||
if (isset($data['specification'])) $product->specification = $data['specification'];
|
||||
if (isset($data['cost_price'])) $product->cost_price = $data['cost_price'];
|
||||
if (isset($data['member_price'])) $product->member_price = $data['member_price'];
|
||||
if (isset($data['wholesale_price'])) $product->wholesale_price = $data['wholesale_price'];
|
||||
|
||||
// Generate Code if missing (use code or external_id)
|
||||
if (empty($product->code)) {
|
||||
$product->code = $data['code'] ?? $product->external_pos_id;
|
||||
}
|
||||
|
||||
// Handle Category (Default: 未分類)
|
||||
if (empty($product->category_id)) {
|
||||
// Handle Category — 每次同步都更新(若有傳入)
|
||||
if (!empty($data['category']) || empty($product->category_id)) {
|
||||
$categoryName = $data['category'] ?? '未分類';
|
||||
$category = Category::firstOrCreate(
|
||||
['name' => $categoryName],
|
||||
@@ -55,8 +63,8 @@ class ProductService
|
||||
$product->category_id = $category->id;
|
||||
}
|
||||
|
||||
// Handle Base Unit (Default: 個)
|
||||
if (empty($product->base_unit_id)) {
|
||||
// Handle Base Unit — 每次同步都更新(若有傳入)
|
||||
if (!empty($data['unit']) || empty($product->base_unit_id)) {
|
||||
$unitName = $data['unit'] ?? '個';
|
||||
$unit = Unit::firstOrCreate(['name' => $unitName]);
|
||||
$product->base_unit_id = $unit->id;
|
||||
@@ -69,4 +77,15 @@ class ProductService
|
||||
return $product;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 透過外部 POS ID 查找產品。
|
||||
*
|
||||
* @param string $externalPosId
|
||||
* @return Product|null
|
||||
*/
|
||||
public function findByExternalPosId(string $externalPosId)
|
||||
{
|
||||
return Product::where('external_pos_id', $externalPosId)->first();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,3 +12,16 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:npm]
|
||||
command=/usr/bin/npm run dev
|
||||
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||
environment=LARAVEL_SAIL="1"
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
|
||||
@@ -130,7 +130,8 @@ export default function ManualIndex({ toc, currentSlug, content }: Props) {
|
||||
#manual-article blockquote { margin: 0.75rem 0 !important; padding: 0.25rem 1rem !important; border-left: 4px solid var(--primary-main); background: #f8fafc; border-radius: 0 4px 4px 0; }
|
||||
#manual-article img { margin: 1rem 0 !important; border-radius: 8px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
|
||||
#manual-article code { padding: 0.1rem 0.3rem; background: #e6f7f3; color: #018a6a; border-radius: 4px; font-size: 0.85em; font-family: ui-monospace, monospace; }
|
||||
#manual-article pre { margin: 0.75rem 0 !important; padding: 0.75rem !important; background: #1e293b; color: #f8fafc; border-radius: 8px; overflow-x: auto; }
|
||||
#manual-article pre { margin: 0.75rem 0 !important; padding: 1rem !important; background: #f8fafc; color: #334155; border: 1px solid #e2e8f0; border-radius: 8px; overflow-x: auto; box-shadow: inset 0 1px 2px rgba(0,0,0,0.02); }
|
||||
#manual-article pre code { background: transparent; padding: 0; color: inherit; font-size: 13.5px; }
|
||||
`}} />
|
||||
<div className="max-w-4xl mx-auto p-4 md:p-10 lg:p-12">
|
||||
<article id="manual-article" className="prose prose-slate max-w-none">
|
||||
|
||||
173
resources/markdown/manual/api-integration.md
Normal file
173
resources/markdown/manual/api-integration.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# 第三方系統 API 對接手冊
|
||||
|
||||
Star ERP 系統提供外部整合 API (Integration API) 供電商前台、POS 機或其他第三方系統串接。
|
||||
所有的整合 API 均受到 Laravel Sanctum Token 與多租戶 (Multi-tenant) Middleware 保護。
|
||||
|
||||
## 基礎連線資訊
|
||||
|
||||
- **API Base URL**: `https://[租戶網域]/api/v1/integration` (依實際部署網址為準,單機開發為 `http://localhost/api/v1/integration`)
|
||||
- **Headers 要求**:
|
||||
- `Accept: application/json`
|
||||
- `Content-Type: application/json`
|
||||
- `Authorization: Bearer {YOUR_API_TOKEN}` (由 ERP 系統管理員核發的 Sanctum Token)
|
||||
- **速率限制**:每位使用者每分鐘最多 60 次請求。超過時會回傳 `429 Too Many Requests`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 產品資料同步 (Upsert Product)
|
||||
|
||||
此 API 用於將第三方系統(如 POS)的產品資料單向同步至 ERP。採用 Upsert 邏輯:若 `external_pos_id` 存在則更新資料,不存在則新增產品。
|
||||
|
||||
- **Endpoint**: `/products/upsert`
|
||||
- **Method**: `POST`
|
||||
|
||||
### Request Body (JSON)
|
||||
|
||||
| 欄位名稱 | 類型 | 必填 | 說明 |
|
||||
| :--- | :--- | :---: | :--- |
|
||||
| `external_pos_id` | String | **是** | 在 POS 系統中的唯一商品 ID (Primary Key) |
|
||||
| `name` | String | **是** | 商品名稱 (最大 255 字元) |
|
||||
| `price` | Decimal | 否 | 商品售價 (預設 0) |
|
||||
| `barcode` | String | 否 | 商品條碼 (最大 100 字元) |
|
||||
| `category` | String | 否 | 商品分類名稱。若系統中不存在,會自動建立 (最大 100 字元) |
|
||||
| `unit` | String | 否 | 商品單位 (例如:個、杯、件)。若不存在會自動建立 (最大 100 字元) |
|
||||
| `brand` | String | 否 | 商品品牌名稱 (最大 100 字元) |
|
||||
| `specification` | String | 否 | 商品規格描述 (最大 255 字元) |
|
||||
| `cost_price` | Decimal | 否 | 成本價 (預設 0) |
|
||||
| `member_price` | Decimal | 否 | 會員價 (預設 0) |
|
||||
| `wholesale_price` | Decimal | 否 | 批發價 (預設 0) |
|
||||
| `is_active` | Boolean| 否 | 是否上架/啟用 (預設 true) |
|
||||
| `updated_at` | String | 否 | POS 端的最後更新時間 (格式: YYYY-MM-DD HH:mm:ss) |
|
||||
|
||||
**請求範例:**
|
||||
```json
|
||||
{
|
||||
"external_pos_id": "POS-PROD-9001",
|
||||
"name": "特級冷壓初榨橄欖油 500ml",
|
||||
"price": 380.00,
|
||||
"barcode": "4711234567890",
|
||||
"category": "調味料",
|
||||
"unit": "瓶",
|
||||
"brand": "健康王",
|
||||
"specification": "500ml / 玻璃瓶裝",
|
||||
"cost_price": 250.00,
|
||||
"member_price": 350.00,
|
||||
"wholesale_price": 300.00,
|
||||
"is_active": true,
|
||||
"updated_at": "2024-03-15 14:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
**Success (HTTP 200)**
|
||||
```json
|
||||
{
|
||||
"message": "Product synced successfully",
|
||||
"data": {
|
||||
"id": 15,
|
||||
"external_pos_id": "POS-ITEM-001"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 訂單資料寫入與扣庫 (Create Order)
|
||||
|
||||
此 API 用於讓第三方系統(如 POS 結帳後)將「已成交」的訂單推送到 ERP。
|
||||
**重要提醒**:寫入訂單的同時,ERP 會自動且**強制**扣除對應倉庫的庫存(允許扣至負數,以利後續盤點或補單)。
|
||||
|
||||
- **Endpoint**: `/orders`
|
||||
- **Method**: `POST`
|
||||
|
||||
### Request Body (JSON)
|
||||
|
||||
| 欄位名稱 | 型態 | 必填 | 說明 |
|
||||
| :--- | :--- | :---: | :--- |
|
||||
| `external_order_id` | String | **是** | 第三方系統中的唯一訂單編號,不可重複 (Unique) |
|
||||
| `warehouse_id` | Integer | 否 | 指定出貨倉庫的系統 ID (若已知) |
|
||||
| `warehouse` | String | 否 | 指定出貨倉庫名稱。若 `warehouse_id` 與此欄皆未傳,系統將預設寫入並建立「銷售倉庫」 |
|
||||
| `payment_method` | String | 否 | 付款方式,僅接受:`cash`, `credit_card`, `line_pay`, `ecpay`, `transfer`, `other`。預設為 `cash` |
|
||||
| `sold_at` | String(Date) | 否 | 交易發生時間,預設為當下時間 (格式: YYYY-MM-DD HH:mm:ss) |
|
||||
| `items` | Array | **是** | 訂單明細陣列,至少需包含一筆商品 |
|
||||
|
||||
#### `items` 陣列欄位說明:
|
||||
|
||||
| 欄位名稱 | 型態 | 必填 | 說明 |
|
||||
| :--- | :--- | :---: | :--- |
|
||||
| `pos_product_id` | String | **是** | 對應產品的 `external_pos_id`。**注意:商品必須先透過產品同步 API 建立至 ERP 中。** |
|
||||
| `qty` | Number | **是** | 銷售數量 (必須 > 0) |
|
||||
| `price` | Number | **是** | 銷售單價 |
|
||||
|
||||
**請求範例:**
|
||||
```json
|
||||
{
|
||||
"external_order_id": "ORD-20231026-0001",
|
||||
"warehouse": "台北大安門市",
|
||||
"payment_method": "credit_card",
|
||||
"sold_at": "2023-10-26 14:30:00",
|
||||
"items": [
|
||||
{
|
||||
"pos_product_id": "POS-ITEM-001",
|
||||
"qty": 2,
|
||||
"price": 450
|
||||
},
|
||||
{
|
||||
"pos_product_id": "POS-ITEM-005",
|
||||
"qty": 1,
|
||||
"price": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
**Success (HTTP 201)**
|
||||
```json
|
||||
{
|
||||
"message": "Order synced and stock deducted successfully",
|
||||
"order_id": 42
|
||||
}
|
||||
```
|
||||
|
||||
**Error: Product Not Found (HTTP 400)**
|
||||
若 `items` 內傳入了未曾同步過的 `pos_product_id`,會導致寫入失敗。
|
||||
```json
|
||||
{
|
||||
"message": "Sync failed: Product not found for POS ID: POS-ITEM-999. Please sync product first."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 幂等性說明 (Idempotency)
|
||||
|
||||
訂單 API 支援幂等性處理:若傳入已存在的 `external_order_id`,系統不會報錯,而是回傳該訂單的資訊:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Order already exists",
|
||||
"order_id": 42
|
||||
}
|
||||
```
|
||||
|
||||
這讓第三方系統在網路問題導致重送時,不會產生重複訂單或重複扣庫。
|
||||
|
||||
---
|
||||
|
||||
## 常見問題與除錯 (FAQ)
|
||||
|
||||
1. **收到 `401 Unauthorized` 錯誤?**
|
||||
- 請檢查請求標頭 (Header) 是否有正確攜帶 `Authorization: Bearer {Token}`。
|
||||
- 確認該 Token 尚未過期或被撤銷。
|
||||
|
||||
2. **收到 `422 Unprocessable Entity` 錯誤?**
|
||||
- 代表傳送的 JSON 欄位不符合格式要求,例如必填欄位遺漏、數量為負數、或 `payment_method` 不在允許的值中。Laravel 會在回應的 `errors` 物件中詳細說明哪個欄位驗證失敗。
|
||||
|
||||
3. **收到 `429 Too Many Requests` 錯誤?**
|
||||
- 代表已超過速率限制(每分鐘 60 次),請稍後再試。
|
||||
|
||||
4. **庫存扣除邏輯**
|
||||
- POS 訂單寫入視為「已發生之事實」,系統會無條件扣除庫存。若該產品在指定倉庫原先庫存為 0,訂單寫入後庫存將變為負數,提醒門市人員需進行調撥補貨。
|
||||
@@ -27,6 +27,10 @@
|
||||
{
|
||||
"title": "常見問題 (FAQ)",
|
||||
"slug": "faq"
|
||||
},
|
||||
{
|
||||
"title": "外部系統 API 對接",
|
||||
"slug": "api-integration"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user