跳至主要内容

Scenario 1: 為個人客戶創建標準訂單

User Story: US-201: 創建訂單

Given: 系統初始狀態

已登入用戶

{
"userId": "staff-001",
"email": "staff@florist.com",
"role": "SALES_STAFF",
"tenantId": "ROLE_FLORIST-abc",
"tenantName": "ABC 花店",
"name": "王小明"
}

現有客戶資料

{
"customerId": "cust-123",
"tenantId": "ROLE_FLORIST-abc",
"name": "李大華",
"phone": "0912-345-678",
"email": "david@example.com",
"type": "INDIVIDUAL",
"addresses": [
{
"address": "台北市信義區信義路五段 7 號",
"isDefault": true
}
],
"createdAt": "2025-10-15T09:00:00Z"
}

可用商品

[
{
"productId": "prod-001",
"tenantId": "ROLE_FLORIST-abc",
"name": "玫瑰花束(12 朵)",
"description": "精選紅玫瑰,適合生日、紀念日",
"basePrice": 1200,
"stock": 10,
"status": "active"
},
{
"productId": "prod-002",
"tenantId": "ROLE_FLORIST-abc",
"name": "百合盆花",
"description": "白色百合,適合居家擺設",
"basePrice": 800,
"stock": 5,
"status": "active"
}
]

租戶設定

{
"tenantId": "ROLE_FLORIST-abc",
"tenantCode": "ABC",
"taxRate": 0.05,
"closedDays": ["2025-11-05", "2025-11-06"]
}

When: 執行操作

API 請求

POST /api/v1/orders
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
"customerId": "cust-123",
"items": [
{
"productId": "prod-001",
"quantity": 2,
"price": 1200,
"notes": "不要包含百合"
}
],
"deliveryAddress": "台北市信義區信義路五段 7 號",
"deliveryPhone": "0912-345-678",
"deliveryDate": "2025-11-01",
"deliveryTimeSlot": "14:00-18:00",
"cardMessage": "生日快樂!",
"recipientName": "張三",
"recipientPhone": "0987-654-321"
}

UI 操作流程(參考)

  1. 使用者點擊「創建訂單」按鈕
  2. 在客戶選擇下拉選單中搜尋「李大華」並選擇
  3. 點擊「添加商品」按鈕
  4. 選擇「玫瑰花束(12 朵)」,數量設為 2
  5. 在商品備註中輸入「不要包含百合」
  6. 填寫配送地址「台北市信義區信義路五段 7 號」
  7. 填寫聯絡電話「0912-345-678」
  8. 選擇配送日期「2025-11-01」與時段「14:00-18:00」
  9. 填寫卡片訊息「生日快樂!」
  10. 填寫收件人姓名「張三」、電話「0987-654-321」
  11. 點擊「提交訂單」按鈕

Then: 預期結果

系統響應

{
"orderId": "order-001",
"orderNumber": "ABC-20251031-0001",
"status": "pending_confirmation",
"tenantId": "ROLE_FLORIST-abc",
"customerId": "cust-123",
"customerName": "李大華",
"items": [
{
"id": "item-001",
"productId": "prod-001",
"productName": "玫瑰花束(12 朵)",
"quantity": 2,
"price": 1200,
"subtotal": 2400,
"notes": "不要包含百合"
}
],
"deliveryInfo": {
"address": "台北市信義區信義路五段 7 號",
"phone": "0912-345-678",
"date": "2025-11-01",
"timeSlot": "14:00-18:00",
"recipientName": "張三",
"recipientPhone": "0987-654-321"
},
"cardMessage": "生日快樂!",
"pricing": {
"subtotal": 2400,
"tax": 120,
"total": 2520
},
"createdAt": "2025-10-31T10:30:00Z",
"createdBy": "staff-001",
"createdByName": "王小明"
}

HTTP 狀態碼: 201 Created

數據庫變更

orders 表新增記錄

{
"id": "order-001",
"orderNumber": "ABC-20251031-0001",
"tenantId": "ROLE_FLORIST-abc",
"customerId": "cust-123",
"status": "pending_confirmation",
"deliveryAddress": "台北市信義區信義路五段 7 號",
"deliveryPhone": "0912-345-678",
"deliveryDate": "2025-11-01",
"deliveryTimeSlot": "14:00-18:00",
"recipientName": "張三",
"recipientPhone": "0987-654-321",
"cardMessage": "生日快樂!",
"subtotal": 2400,
"tax": 120,
"total": 2520,
"createdAt": "2025-10-31T10:30:00Z",
"createdBy": "staff-001",
"updatedAt": "2025-10-31T10:30:00Z",
"updatedBy": "staff-001"
}

order_items 表新增記錄

{
"id": "item-001",
"orderId": "order-001",
"productId": "prod-001",
"productName": "玫瑰花束(12 朵)",
"quantity": 2,
"price": 1200,
"subtotal": 2400,
"notes": "不要包含百合",
"createdAt": "2025-10-31T10:30:00Z"
}

audit_logs 表記錄操作

{
"id": "log-001",
"tenantId": "ROLE_FLORIST-abc",
"userId": "staff-001",
"action": "ORDER_CREATED",
"resourceType": "Order",
"resourceId": "order-001",
"details": {
"orderNumber": "ABC-20251031-0001",
"customerId": "cust-123",
"total": 2520
},
"timestamp": "2025-10-31T10:30:00Z"
}

副作用

發送通知給客戶

  • 通知類型: Email + SMS
  • 收件人: 李大華 (david@example.com, 0912-345-678)
  • 內容:
    親愛的李大華,您好!

    您的訂單 ABC-20251031-0001 已成功創建。

    訂單詳情:
    - 商品:玫瑰花束(12 朵) x 2
    - 配送日期:2025-11-01 (14:00-18:00)
    - 配送地址:台北市信義區信義路五段 7 號
    - 總金額:NT$ 2,520

    我們會在訂單確認後盡快為您製作。

    ABC 花店 敬上

更新商品庫存(可選)

  • prod-001 的「保留庫存」增加 2(訂單確認後才扣除實際庫存)

邊界條件與錯誤場景

錯誤場景 1: 缺少必填欄位(customerId)

請求

POST /api/v1/orders
Authorization: Bearer {token}
Content-Type: application/json

{
"items": [
{
"productId": "prod-001",
"quantity": 2,
"price": 1200
}
],
"deliveryAddress": "台北市信義區信義路五段 7 號",
"deliveryDate": "2025-11-01"
}

預期響應

{
"error": "VALIDATION_ERROR",
"message": "缺少必填欄位",
"fields": [
{
"field": "customerId",
"message": "請選擇客戶"
},
{
"field": "deliveryPhone",
"message": "請填寫聯絡電話"
},
{
"field": "deliveryTimeSlot",
"message": "請選擇配送時段"
}
]
}

HTTP 狀態碼: 400 Bad Request


錯誤場景 2: 配送日期無效(早於今天)

請求

POST /api/v1/orders
Authorization: Bearer {token}
Content-Type: application/json

{
"customerId": "cust-123",
"items": [{...}],
"deliveryAddress": "台北市信義區信義路五段 7 號",
"deliveryPhone": "0912-345-678",
"deliveryDate": "2025-10-20",
"deliveryTimeSlot": "14:00-18:00"
}

假設今天是: 2025-10-31

預期響應

{
"error": "INVALID_DELIVERY_DATE",
"message": "配送日期不能早於今天",
"field": "deliveryDate",
"meta": {
"today": "2025-10-31",
"requestedDate": "2025-10-20"
}
}

HTTP 狀態碼: 400 Bad Request


錯誤場景 3: 配送日期為店休日

請求

POST /api/v1/orders
Authorization: Bearer {token}
Content-Type: application/json

{
"customerId": "cust-123",
"items": [{...}],
"deliveryAddress": "台北市信義區信義路五段 7 號",
"deliveryPhone": "0912-345-678",
"deliveryDate": "2025-11-05",
"deliveryTimeSlot": "14:00-18:00"
}

租戶店休日: ["2025-11-05", "2025-11-06"]

預期響應

{
"error": "CLOSED_DAY",
"message": "所選日期為店休日,無法配送",
"field": "deliveryDate",
"meta": {
"requestedDate": "2025-11-05",
"closedDays": ["2025-11-05", "2025-11-06"]
}
}

HTTP 狀態碼: 400 Bad Request


錯誤場景 4: 商品數量無效

請求

POST /api/v1/orders
Authorization: Bearer {token}
Content-Type: application/json

{
"customerId": "cust-123",
"items": [
{
"productId": "prod-001",
"quantity": 0,
"price": 1200
}
],
"deliveryAddress": "台北市信義區信義路五段 7 號",
"deliveryPhone": "0912-345-678",
"deliveryDate": "2025-11-01",
"deliveryTimeSlot": "14:00-18:00"
}

預期響應

{
"error": "VALIDATION_ERROR",
"message": "商品數量必須大於 0",
"field": "items[0].quantity",
"meta": {
"productId": "prod-001",
"quantity": 0
}
}

HTTP 狀態碼: 400 Bad Request


錯誤場景 5: 權限不足(非店員角色)

請求

POST /api/v1/orders
Authorization: Bearer {token_for_delivery_user}
Content-Type: application/json

{
"customerId": "cust-123",
"items": [{...}],
"deliveryAddress": "...",
"deliveryPhone": "...",
"deliveryDate": "2025-11-01",
"deliveryTimeSlot": "14:00-18:00"
}

用戶角色: DELIVERY_PERSONNEL(非 SALES_STAFF 或更高)

預期響應

{
"error": "FORBIDDEN",
"message": "您沒有權限創建訂單",
"requiredRole": "SALES_STAFF"
}

HTTP 狀態碼: 403 Forbidden


自動化測試範例

使用 Jest + MSW

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { OrderForm } from '@/applets/order-applet/components/OrderForm';

describe('US-201: Scenario 1 - Create order for individual customer', () => {
it('should create order successfully with valid data', async () => {
// Arrange
const user = userEvent.setup();
render(<OrderForm />);

// Act - 選擇客戶
const customerSelect = screen.getByLabelText('客戶');
await user.click(customerSelect);
await user.click(screen.getByText('李大華'));

// Act - 添加商品
await user.click(screen.getByText('添加商品'));
const productSelect = screen.getByLabelText('商品');
await user.click(productSelect);
await user.click(screen.getByText('玫瑰花束(12 朵)'));

const quantityInput = screen.getByLabelText('數量');
await user.clear(quantityInput);
await user.type(quantityInput, '2');

// Act - 填寫配送資訊
await user.type(screen.getByLabelText('配送地址'), '台北市信義區信義路五段 7 號');
await user.type(screen.getByLabelText('聯絡電話'), '0912-345-678');

// Act - 提交訂單
await user.click(screen.getByRole('button', { name: '創建訂單' }));

// Assert
await waitFor(() => {
expect(screen.getByText(/訂單創建成功/i)).toBeInTheDocument();
});

// 驗證 API 呼叫
// MSW 會攔截並返回 mock 響應
});

it('should show validation error when customerId is missing', async () => {
const user = userEvent.setup();
render(<OrderForm />);

// 直接提交(不選擇客戶)
await user.click(screen.getByRole('button', { name: '創建訂單' }));

await waitFor(() => {
expect(screen.getByText('請選擇客戶')).toBeInTheDocument();
});
});

it('should show error when delivery date is in the past', async () => {
const user = userEvent.setup();
render(<OrderForm />);

// ... 填寫其他欄位 ...

// 選擇過去的日期
const dateInput = screen.getByLabelText('配送日期');
await user.type(dateInput, '2025-10-20');

await user.click(screen.getByRole('button', { name: '創建訂單' }));

await waitFor(() => {
expect(screen.getByText('配送日期不能早於今天')).toBeInTheDocument();
});
});
});

使用 Cypress (E2E)

describe('US-201: Scenario 1 - Create order E2E', () => {
beforeEach(() => {
// 登入
cy.login('staff@florist.com', 'Password123!');
cy.visit('/orders/new');
});

it('should create order successfully for individual customer', () => {
// 選擇客戶
cy.get('[data-testid="customer-select"]').click();
cy.contains('李大華').click();

// 添加商品
cy.get('[data-testid="add-product-button"]').click();
cy.get('[data-testid="product-select"]').click();
cy.contains('玫瑰花束(12 朵)').click();
cy.get('[data-testid="quantity-input"]').clear().type('2');

// 填寫配送資訊
cy.get('[data-testid="delivery-address"]').type('台北市信義區信義路五段 7 號');
cy.get('[data-testid="delivery-phone"]').type('0912-345-678');
cy.get('[data-testid="delivery-date"]').type('2025-11-01');
cy.get('[data-testid="delivery-time-slot"]').select('14:00-18:00');

// 填寫卡片訊息
cy.get('[data-testid="card-message"]').type('生日快樂!');

// 提交訂單
cy.get('[data-testid="submit-order-button"]').click();

// 驗證成功訊息
cy.contains('訂單創建成功').should('be.visible');

// 驗證導航到訂單詳情頁
cy.url().should('include', '/orders/ABC-20251031-0001');

// 驗證訂單資訊
cy.contains('ABC-20251031-0001').should('be.visible');
cy.contains('李大華').should('be.visible');
cy.contains('NT$ 2,520').should('be.visible');
});
});

測試數據維護

Mock API 配置

檔案位置: src/mocks/handlers/orders.ts

import { http, HttpResponse } from 'msw';

export const orderHandlers = [
http.post('/api/v1/orders', async ({ request }) => {
const body = await request.json();

// 驗證必填欄位
if (!body.customerId) {
return HttpResponse.json(
{ error: 'VALIDATION_ERROR', message: '請選擇客戶', field: 'customerId' },
{ status: 400 }
);
}

// 生成訂單編號
const orderNumber = `ABC-20251031-0001`;

// 返回成功響應
return HttpResponse.json(
{
orderId: 'order-001',
orderNumber,
status: 'pending_confirmation',
...body,
pricing: {
subtotal: 2400,
tax: 120,
total: 2520
},
createdAt: new Date().toISOString(),
createdBy: 'staff-001'
},
{ status: 201 }
);
})
];

種子數據

檔案位置: src/mocks/data/seeds.ts

確保以下數據存在:

  • 客戶「李大華」(cust-123)
  • 商品「玫瑰花束(12 朵)」(prod-001)
  • 租戶設定(稅率 5%,店休日)

備註 (Notes)

為何需要詳細的 SBE?

  • 消除歧義: 用具體數據說明「創建訂單」的確切行為
  • 可執行性: 範例可直接轉為自動化測試
  • 溝通工具: 產品、開發、測試對需求有一致理解

與 Mock API 的整合

此場景的所有請求/響應範例都可在本地 Mock API 環境中驗證:

npm run dev
# 在瀏覽器 Console 或 Postman 測試 API

最後更新: 2025-10-31