# Shopee Outbound API WES Mapping Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Implement Shopee Automation outbound interfaces plus protocol sections `4.1`-`5.8`, with `edi-shopee` as the external Shopee-facing entry and WES `ShopeeInboundApiHandler` as the internal business handler.

**Architecture:** Shopee calls `C:\work\gitlab\shopee\edi-shopee`; `edi-shopee` owns Shopee URL paths, JWT validation/generation, protocol response shape, EDI logging, and forwarding to WES. WES business handling stays in `wes-loghub/wms-api/src/main/groovy/com/ittx/wms/api/service/platform/ShopeeInboundApiHandler.groovy`, reached by `edi-shopee` through the existing WES sync API with `api=shopee...`.

**Tech Stack:** Groovy, Spring Boot, CBT `ResponseMessage`, WES `SyncApiController`/`ApiService` platform dispatch, MySQL/Flyway, `edi-shopee` HTTP/JWT integration.

---

## Scope

This plan covers the outbound module, inventory/rack-transfer module, and basic-data module interfaces present in this protocol file, based on:

- `docs/Shopee_Automation_Vendor_接入协议手册_出库模块映射WES字段_V1.2.md`
- `wes-loghub/wms-api/src/main/groovy/com/ittx/wms/api/service/platform/ShopeeInboundApiHandler.groovy`
- `C:\work\gitlab\shopee\edi-shopee`

Do not implement inbound module interfaces in this plan. Include all concrete interfaces from `3.1`-`3.4`, `4.1`-`4.7`, and `5.1`-`5.8`.

## Current State

- `SyncApiController` already receives standard platform calls and delegates through `ApiService`; this remains the internal WES target called by `edi-shopee`.
- `ApiService` already delegates `ess.*` and `shopee.*` APIs to `PlatformInboundApiService`.
- `ShopeeInboundApiHandler` exists but has no outbound cases implemented.
- Confirmed: `4.2`-`4.5` Vendor-to-WES inventory/rack-transfer interfaces all enter WES through `ShopeeInboundApiHandler`; the handler then reuses existing `wms-transfer` and `wms-inventory` services.
- Confirmed: WES-to-Vendor basic-data interfaces in `5.2`-`5.8` are driven by scheduled jobs using a three-step `.confirm` -> `.get` -> `.uploadStatus` flow. Protocol `5.5` remains Vendor-to-WES and is not part of that scheduled outbound flow.
- Confirmed: Shopee extension fields are persisted through the dictionary mechanism only. Do not add WES entity columns for Shopee-only fields; use different dictionary group types for different usage locations.
- Confirmed: All 50 concrete interfaces in this plan must be implemented in this delivery. New Shopee-specific implementation classes must live under a `shopee` subpackage; keep only the existing platform entry handler in `service/platform`.
- `ShipmentHeader` already contains key Shopee fields: `cutOffTime`, `purchaseTime`, `orderTime`, `shopeeWave`, `waveRule`, `kickOutWave`, `ticketType`, `pickType`, `orderStructure`, `groupKey`.
- `ShopeeWaveService` has low-water scheduling and persistence, but the actual upstream wave-number request is still a placeholder.
- `WaveRuleMatchService` has Shopee wave binding placeholders and local binding/rollback logic.
- `wms-transfer` already contains transfer order services; use those existing transfer entities/services for rack-transfer data extraction before adding any new persistence.
- `edi-shopee` already owns outbound HTTP/token patterns and is the Shopee-facing entry for both inbound calls from Shopee and outbound calls to Shopee.

## File Structure

### wes-loghub

- Modify: `wms-api/src/main/groovy/com/ittx/wms/api/config/domain/PlatformApiConstants.groovy`
  - Add Shopee outbound API names.
- Modify: `wms-api/src/main/groovy/com/ittx/wms/api/service/platform/ShopeeInboundApiHandler.groovy`
  - Keep as the existing platform dispatch entry; add switch cases for all 50 concrete APIs and delegate to services in the `shopee` subpackage.
- Create: `wms-api/src/main/groovy/com/ittx/wms/api/domain/platform/shopee/ShopeeOutboundApiCmds.groovy`
  - Store request DTOs for all 50 Shopee protocol APIs.
- Create: `wms-api/src/main/groovy/com/ittx/wms/api/domain/platform/shopee/ShopeeOutboundApiResults.groovy`
  - Store response DTOs when a list result is required.
- Create: `wms-api/src/main/groovy/com/ittx/wms/api/service/platform/shopee/ShopeeOutboundShipmentMappingService.groovy`
  - Convert Shopee request fields into existing WES entities and dictionaries.
- Modify: `wms-wave/src/main/groovy/com/ittx/wms/wave/service/hairo/ShopeeWaveService.groovy`
  - Replace `requestShopeeWaveNumbers` placeholder with a call to `edi-shopee`.
- Modify: `wms-wave/src/main/groovy/com/ittx/wms/wave/service/hairo/WaveRuleMatchService.groovy`
  - Replace Shopee wave bind placeholder with a call to `edi-shopee` and parse failed shipment IDs.
- Do not modify WES transfer-order business hooks for `4.1` in this phase.
  - Only implement the `4.1`, `4.6`, and `4.7` interface surfaces and `edi-shopee` forwarding paths; the real移库业务触发点 remains explicitly out of scope.
- Create only if no existing transfer service method is suitable: `wms-api/src/main/groovy/com/ittx/wms/api/service/platform/shopee/ShopeeRackTransferMappingService.groovy`
  - Define `4.1`, `4.6`, and `4.7` request contracts and response normalization only; do not read `TransferOrderHeader`/detail data until the business trigger is confirmed later.
- Modify: `src/main/resources/application.yml` and profile-specific config only if no existing system-parameter path is available.
  - Add internal `edi-shopee` endpoint only if it cannot be represented by existing API/customer config.

### edi-shopee

- Create or modify: `src/main/groovy/com/ittx/edi/erp/service/ShopeeJwtService.groovy`
  - Generate and verify HS256 JWT per Shopee protocol.
- Create or modify: `src/main/groovy/com/ittx/edi/erp/service/ShopeeAutomationVendorService.groovy`
  - Validate Shopee inbound requests, forward to WES, build Shopee outbound URLs, set headers, call Shopee Automation Vendor APIs, normalize responses.
- Create or modify: `src/main/groovy/com/ittx/edi/erp/controller/ShopeeAutomationController.groovy`
  - External endpoints called by Shopee plus internal endpoints consumed by `wes-loghub` for wave-number request and wave binding.
- Modify: `src/main/resources/application-zzw.yml`.
  - Read Shopee JWT/account/host settings from `customerMap.shopee`; do not introduce a second config path in `application-dev.yml` or `application-k8s.yml` for this phase.

## Interface Inventory

Protocol alignment from `docs/Shopee_Automation_Vendor_接入协议手册_出库模块映射WES字段_V1.2.md`:

- Outbound module title count: 45 entries under `3.1` to `3.4`.
- Outbound module URL count: 35 concrete, unique `/api/v2/automation/.../outbound/...` APIs.
- Reused/deprecated/explanation count: 10 title entries.
- Inventory/rack-transfer URL count: 7 concrete APIs under `4.1` to `4.7`.
- Basic-data URL count: 8 concrete APIs under `5.1` to `5.8`.
- Total implemented protocol API inventory in this plan: 50 concrete APIs.

### Concrete Outbound APIs

| No. | Section | Direction | Shopee/edi-shopee path | WES api constant | Implementation note |
| --- | --- | --- | --- | --- | --- |
| 1 | 3.1.1 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/salesorder/create_sub_picking_task` | `shopee.outbound.salesorder.createSubPickingTask` | WES 下发销售 SubPickingTask |
| 2 | 3.1.2 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/salesorder/cancel_sub_picking_task` | `shopee.outbound.salesorder.cancelSubPickingTask` | WES 取消销售 SubPickingTask |
| 3 | 3.1.3 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/salesorder/update_task_flag` | `shopee.outbound.salesorder.updateTaskFlag` | 更新 urgent flag |
| 4 | 3.1.5 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/start_picking_task` | `shopee.outbound.salesorder.startPickingTask` | Vendor 开始拣货任务 |
| 5 | 3.1.6 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/bind_picking_task` | `shopee.outbound.salesorder.bindPickingTask` | Vendor 实绑拣货任务 |
| 6 | 3.1.7 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/sync_picking_detail` | `shopee.outbound.salesorder.syncPickingDetail` | 增量拣货明细 |
| 7 | 3.1.8 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/change_picking_device` | `shopee.outbound.salesorder.changePickingDevice` | SubPickingTask 换箱，不产生新任务 |
| 8 | 3.1.9 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/complete_picking_task` | `shopee.outbound.salesorder.completePickingTask` | 完成拣货任务 |
| 9 | 3.1.10 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/search_picking_process_guide` | `shopee.outbound.salesorder.searchPickingProcessGuide` | 查询流程指引 |
| 10 | 3.1.11 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/salesorder/query_picking_detail` | `shopee.outbound.salesorder.queryPickingDetail` | 查询 Vendor 拣货任务列表 |
| 11 | 3.2.1 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/salesorder/create_order` | `shopee.outbound.salesorder.createOrder` | WMS 给 Vendor 下销售订单 |
| 12 | 3.2.2 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/salesorder/cancel_order` | `shopee.outbound.salesorder.cancelOrderToVendor` | WMS 向 Vendor 撤回订单，协议标题为“捞单” |
| 13 | 3.2.3 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/cancel_order` | `shopee.outbound.salesorder.vendorCancelOrder` | Vendor 向 WMS 撤单 |
| 14 | 3.2.4 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/salesorder/lock_unlock_order` | `shopee.outbound.salesorder.lockUnlockOrder` | WMS 下发锁单/解锁 |
| 15 | 3.2.5 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/cache_task_number` | `shopee.outbound.salesorder.cacheTaskNumber` | 获取号段缓存 |
| 16 | 3.2.7 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/create_picking_task` | `shopee.outbound.salesorder.createPickingTask` | 创建 picking task |
| 17 | 3.2.8 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/append_order` | `shopee.outbound.salesorder.appendOrder` | 追加订单明细 |
| 18 | 3.2.9 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/change_device` | `shopee.outbound.salesorder.changeDevice` | Flow Pick 换箱，产生新任务 |
| 19 | 3.2.10 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/salesorder/vendor_complete_task` | `shopee.outbound.salesorder.vendorCompleteTask` | Vendor 完成 task |
| 20 | 3.2.11 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/salesorder/search_lock_order_list` | `shopee.outbound.salesorder.searchLockOrderList` | 查询冻结状态订单列表 |
| 21 | 3.3.1 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/rtsmto/create_require` | `shopee.outbound.rtsmto.createRequire` | RTS/MTO 需求下发 |
| 22 | 3.3.2 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/rtsmto/cancel_require` | `shopee.outbound.rtsmto.cancelRequire` | RTS/MTO 需求取消 |
| 23 | 3.3.3 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/rtsmto/sync_occupied_require` | `shopee.outbound.rtsmto.syncOccupiedRequire` | 同步占用信息 |
| 24 | 3.3.6 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/rtsmto/create_picking_task` | `shopee.outbound.rtsmto.createPickingTask` | RTS/MTO 创建 picking task |
| 25 | 3.3.7 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/rtsmto/bind_picking_task` | `shopee.outbound.rtsmto.bindPickingTask` | RTS/MTO 实绑任务 |
| 26 | 3.3.8 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/rtsmto/append_order` | `shopee.outbound.rtsmto.appendOrder` | RTS/MTO 追加 |
| 27 | 3.3.9 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/rtsmto/sync_picking_detail` | `shopee.outbound.rtsmto.syncPickingDetail` | RTS/MTO 增量拣货明细 |
| 28 | 3.3.10 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/rtsmto/complete_task` | `shopee.outbound.rtsmto.completeTask` | RTS/MTO 完成 task |
| 29 | 3.3.11 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/rtsmto/change_picking_device` | `shopee.outbound.rtsmto.changePickingDevice` | RTS/MTO 换箱；协议原文 URL 有多余空格，实施时按无空格路径 |
| 30 | 3.4.1 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/mto/create_picking_task` | `shopee.outbound.mto.createPickingTask` | MTO 拣货任务下发 |
| 31 | 3.4.2 | WMS -> Vendor | `/api/v2/automation/tovendor/outbound/mto/cancel_picking_task` | `shopee.outbound.mto.cancelPickingTask` | MTO 取消拣货任务 |
| 32 | 3.4.5 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/mto/start_picking_task` | `shopee.outbound.mto.startPickingTask` | MTO 开始拣货 |
| 33 | 3.4.6 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/mto/bind_picking_task` | `shopee.outbound.mto.bindPickingTask` | MTO 真实绑定 |
| 34 | 3.4.7 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/mto/sync_picking_detail` | `shopee.outbound.mto.syncPickingDetail` | MTO 增量拣货明细 |
| 35 | 3.4.8 | Vendor -> WMS | `/api/v2/automation/toshopee/outbound/mto/complete_picking_task` | `shopee.outbound.mto.completePickingTask` | MTO 完成拣货任务 |

### Concrete Inventory and Basic APIs

| No. | Section | Direction | Shopee/edi-shopee path | WES api constant | Implementation note |
| --- | --- | --- | --- | --- | --- |
| 36 | 4.1 | WMS -> Vendor | `/api/v2/automation/tovendor/inventory/racktransfer/create_rt_order` | `shopee.inventory.racktransfer.createRtOrder` | WES 给 Vendor 同步移库占用库存 |
| 37 | 4.2 | Vendor -> WMS | `/api/v2/automation/toshopee/inventory/racktransfer/occupy_device` | `shopee.inventory.racktransfer.occupyDevice` | Vendor 占用或释放 PickingDevice |
| 38 | 4.3 | Vendor -> WMS | `/api/v2/automation/toshopee/inventory/racktransfer/submit_picking_sku_list` | `shopee.inventory.racktransfer.submitPickingSkuList` | Vendor 整箱同步移库拣货明细 |
| 39 | 4.4 | Vendor -> WMS | `/api/v2/automation/toshopee/inventory/racktransfer/sync_picking_sku_list` | `shopee.inventory.racktransfer.syncPickingSkuList` | Vendor 定时回传 WMS 创建移库单拣货明细 |
| 40 | 4.5 | Vendor -> WMS | `/api/v2/automation/toshopee/inventory/racktransfer/handle_diff_stock` | `shopee.inventory.racktransfer.handleDiffStock` | Vendor 盘点差异调减同步给 WMS |
| 41 | 4.6 | WMS -> Vendor | `/api/v2/automation/tovendor/inventory/racktransfer/query_picking_list` | `shopee.inventory.racktransfer.queryPickingList` | WES 查询 Vendor 移库拣货明细用于对账 |
| 42 | 4.7 | WMS -> Vendor | `/api/v2/automation/tovendor/inventory/racktransfer/update_rt_order` | `shopee.inventory.racktransfer.updateRtOrder` | WES 更新移库单优先级或取消状态 |
| 43 | 5.1 | Vendor -> WMS | `/api/v2/automation/toshopee/basic/user/query_user_exist` | `shopee.basic.user.queryUserExist` | Vendor 查询 WES 用户是否存在 |
| 44 | 5.2 | WMS -> Vendor | `/api/v2/automation/tovendor/basic/inventory/batch_status_update` | `shopee.basic.inventory.batchStatusUpdate` | WES 同步批次状态给 Vendor |
| 45 | 5.3 | WMS -> Vendor | `/api/v2/automation/tovendor/basic/inventory/query_sku_batch_inventory` | `shopee.basic.inventory.querySkuBatchInventory` | WES 查询 Vendor 批次库存信息 |
| 46 | 5.4 | WMS -> Vendor | `/api/v2/automation/tovendor/basic/inventory/sync_reconciliation_result` | `shopee.basic.inventory.syncReconciliationResult` | WES 同步库存对账结果给 Vendor |
| 47 | 5.5 | Vendor -> WMS | `/api/v2/automation/toshopee/basic/sku/query_sku_detail` | `shopee.basic.sku.querySkuDetail` | Vendor 查询 WES SKU 信息 |
| 48 | 5.6 | WMS -> Vendor | `/api/v2/automation/tovendor/basic/sku/sync_sku_info` | `shopee.basic.sku.syncSkuInfo` | WES 同步 SKU 信息给 Vendor |
| 49 | 5.7 | WMS -> Vendor | `/api/v2/automation/tovendor/basic/robot/query_agv_status` | `shopee.basic.robot.queryAgvStatus` | WES 查询 Vendor AGV 设备信息 |
| 50 | 5.8 | WMS -> Vendor | `/api/v2/automation/tovendor/basic/inventory/query_vendor_unit_list` | `shopee.basic.inventory.queryVendorUnitList` | WES 查询 Vendor Unit 信息 |

### Reused or Deprecated Title Entries

| Section | Title | Handling |
| --- | --- | --- |
| 3.1.4 | Vendor绑定/解绑device(空占)【复用basic接口】废弃 | 不新增出库接口 |
| 3.2.6 | Vendor绑定解绑设备【复用basic接口】废弃 | 不新增出库接口 |
| 3.2.12 | 复用接口列表 | 说明项，不新增接口 |
| 3.3.4 | Vendor绑定/解绑device【废弃】 | 不新增出库接口 |
| 3.3.5 | Vendor向wms获取号段缓存【复用】 | 复用 3.2.5 |
| 3.3.12 | 查询Vendor拣货任务列表【复用】 | 复用 3.1.11 |
| 3.3.13 | 更新需求的urgent_flag【复用】 | 复用 3.1.3 |
| 3.4.3 | WMS向Vendor更新task的urgent flag【复用3.1.3】 | 复用 3.1.3 |
| 3.4.4 | Vendor绑定/解绑device【复用basic接口】废弃 | 不新增出库接口 |
| 3.4.9 | 查询Vendor拣货任务列表【复用】 | 复用 3.1.11 |

## Interface Names

Add constants for all 50 concrete APIs above. All 50 must be implemented in this delivery; the constant inventory must stay aligned to the table and to Task 1 Step 1.

Two `cancel_order` protocol paths exist with different directions, so they use separate WES API identifiers: `cancelOrderToVendor` for `tovendor` and `vendorCancelOrder` for `toshopee`.

Use these names as WES internal API identifiers. Shopee does not call WES directly; Shopee calls `edi-shopee`, and `edi-shopee` forwards to WES `SyncApiController` with `/api/wms/api/sync/in?api=<constant>&format=json`.

---

### Task 1: Add Shopee Constants and DTOs

**Files:**
- Modify: `C:\work\gitlab\shopee\wes-loghub\wms-api\src\main\groovy\com\ittx\wms\api\config\domain\PlatformApiConstants.groovy`
- Create: `C:\work\gitlab\shopee\wes-loghub\wms-api\src\main\groovy\com\ittx\wms\api\domain\platform\shopee\ShopeeOutboundApiCmds.groovy`
- Create: `C:\work\gitlab\shopee\wes-loghub\wms-api\src\main\groovy\com\ittx\wms\api\domain\platform\shopee\ShopeeOutboundApiResults.groovy`

- [ ] **Step 1: Add constants**

Add the full 50-interface constant set under `PlatformApiConstants.ShopeeInterfaceType`. Do not leave any of these 50 constants as `ApiMessages.MSG_INTF_0005` in the final implementation:

```groovy
static final String SALES_CREATE_SUB_PICKING_TASK = 'shopee.outbound.salesorder.createSubPickingTask'
static final String SALES_CANCEL_SUB_PICKING_TASK = 'shopee.outbound.salesorder.cancelSubPickingTask'
static final String SALES_UPDATE_TASK_FLAG = 'shopee.outbound.salesorder.updateTaskFlag'
static final String SALES_ORDER_CREATE = 'shopee.outbound.salesorder.createOrder'
static final String SALES_CANCEL_ORDER_TO_VENDOR = 'shopee.outbound.salesorder.cancelOrderToVendor'
static final String SALES_SEARCH_LOCK_ORDER_LIST = 'shopee.outbound.salesorder.searchLockOrderList'
static final String SALES_VENDOR_CANCEL_ORDER = 'shopee.outbound.salesorder.vendorCancelOrder'
static final String SALES_LOCK_UNLOCK_ORDER = 'shopee.outbound.salesorder.lockUnlockOrder'
static final String SALES_CACHE_TASK_NUMBER = 'shopee.outbound.salesorder.cacheTaskNumber'
static final String SALES_START_PICKING_TASK = 'shopee.outbound.salesorder.startPickingTask'
static final String SALES_BIND_PICKING_TASK = 'shopee.outbound.salesorder.bindPickingTask'
static final String SALES_SYNC_PICKING_DETAIL = 'shopee.outbound.salesorder.syncPickingDetail'
static final String SALES_CHANGE_PICKING_DEVICE = 'shopee.outbound.salesorder.changePickingDevice'
static final String SALES_COMPLETE_PICKING_TASK = 'shopee.outbound.salesorder.completePickingTask'
static final String SALES_SEARCH_PICKING_PROCESS_GUIDE = 'shopee.outbound.salesorder.searchPickingProcessGuide'
static final String SALES_QUERY_PICKING_DETAIL = 'shopee.outbound.salesorder.queryPickingDetail'
static final String SALES_CREATE_PICKING_TASK = 'shopee.outbound.salesorder.createPickingTask'
static final String SALES_APPEND_ORDER = 'shopee.outbound.salesorder.appendOrder'
static final String SALES_CHANGE_DEVICE = 'shopee.outbound.salesorder.changeDevice'
static final String SALES_VENDOR_COMPLETE_TASK = 'shopee.outbound.salesorder.vendorCompleteTask'
static final String RTSMTO_CREATE_REQUIRE = 'shopee.outbound.rtsmto.createRequire'
static final String RTSMTO_CANCEL_REQUIRE = 'shopee.outbound.rtsmto.cancelRequire'
static final String RTSMTO_SYNC_OCCUPIED_REQUIRE = 'shopee.outbound.rtsmto.syncOccupiedRequire'
static final String RTSMTO_CREATE_PICKING_TASK = 'shopee.outbound.rtsmto.createPickingTask'
static final String RTSMTO_BIND_PICKING_TASK = 'shopee.outbound.rtsmto.bindPickingTask'
static final String RTSMTO_APPEND_ORDER = 'shopee.outbound.rtsmto.appendOrder'
static final String RTSMTO_SYNC_PICKING_DETAIL = 'shopee.outbound.rtsmto.syncPickingDetail'
static final String RTSMTO_COMPLETE_TASK = 'shopee.outbound.rtsmto.completeTask'
static final String RTSMTO_CHANGE_PICKING_DEVICE = 'shopee.outbound.rtsmto.changePickingDevice'
static final String MTO_CREATE_PICKING_TASK = 'shopee.outbound.mto.createPickingTask'
static final String MTO_CANCEL_PICKING_TASK = 'shopee.outbound.mto.cancelPickingTask'
static final String MTO_START_PICKING_TASK = 'shopee.outbound.mto.startPickingTask'
static final String MTO_BIND_PICKING_TASK = 'shopee.outbound.mto.bindPickingTask'
static final String MTO_SYNC_PICKING_DETAIL = 'shopee.outbound.mto.syncPickingDetail'
static final String MTO_COMPLETE_PICKING_TASK = 'shopee.outbound.mto.completePickingTask'
static final String RACK_TRANSFER_CREATE_RT_ORDER = 'shopee.inventory.racktransfer.createRtOrder'
static final String RACK_TRANSFER_OCCUPY_DEVICE = 'shopee.inventory.racktransfer.occupyDevice'
static final String RACK_TRANSFER_SUBMIT_PICKING_SKU_LIST = 'shopee.inventory.racktransfer.submitPickingSkuList'
static final String RACK_TRANSFER_SYNC_PICKING_SKU_LIST = 'shopee.inventory.racktransfer.syncPickingSkuList'
static final String RACK_TRANSFER_HANDLE_DIFF_STOCK = 'shopee.inventory.racktransfer.handleDiffStock'
static final String RACK_TRANSFER_QUERY_PICKING_LIST = 'shopee.inventory.racktransfer.queryPickingList'
static final String RACK_TRANSFER_UPDATE_RT_ORDER = 'shopee.inventory.racktransfer.updateRtOrder'
static final String BASIC_USER_QUERY_USER_EXIST = 'shopee.basic.user.queryUserExist'
static final String BASIC_INVENTORY_BATCH_STATUS_UPDATE = 'shopee.basic.inventory.batchStatusUpdate'
static final String BASIC_INVENTORY_QUERY_SKU_BATCH_INVENTORY = 'shopee.basic.inventory.querySkuBatchInventory'
static final String BASIC_INVENTORY_SYNC_RECONCILIATION_RESULT = 'shopee.basic.inventory.syncReconciliationResult'
static final String BASIC_SKU_QUERY_SKU_DETAIL = 'shopee.basic.sku.querySkuDetail'
static final String BASIC_SKU_SYNC_SKU_INFO = 'shopee.basic.sku.syncSkuInfo'
static final String BASIC_ROBOT_QUERY_AGV_STATUS = 'shopee.basic.robot.queryAgvStatus'
static final String BASIC_INVENTORY_QUERY_VENDOR_UNIT_LIST = 'shopee.basic.inventory.queryVendorUnitList'
```

- [ ] **Step 2: Add DTO shells**

Create DTOs with only fields present in the protocol tables and mapped to WES fields. Use Groovy classes, no Lombok, no new validation framework.

Minimum classes:

```groovy
package com.ittx.wms.api.domain.platform.shopee

import java.io.Serializable

class ShopeeCreateSubPickingTaskCmd implements Serializable {
    String whs_id
    String sub_pickup_id
    Integer urgent_flag
    Long cut_off_time
    Long ship_by_date
    Long purchase_time
    Long order_time
    Long ctime
    Integer can_group_picking
    String group_key
    List<ShopeeSkuInfoCmd> sku_info_list = []
    List<ShopeeSingleAttrCmd> single_attr_list = []
    List<ShopeeMultiAttrCmd> multi_attr_list = []
    Integer ticket_type
}

class ShopeeSkuInfoCmd implements Serializable {
    String sku_id
    BigDecimal qty
    Integer block_type
    Integer quality
}

class ShopeeSingleAttrCmd implements Serializable {
    String attr_key
    String attr_value_id
    String attr_value_type
    String attr_value_name
}

class ShopeeMultiAttrCmd implements Serializable {
    String attr_key
    List<ShopeeAttrCmd> attr_value_list = []
}

class ShopeeAttrCmd implements Serializable {
    String attr_value_id
    String attr_value_type
    String attr_value_name
}

class ShopeeCancelSubPickingTaskCmd implements Serializable {
    String whs_id
    String sub_pickup_id
}

class ShopeeCreateRtOrderCmd implements Serializable {
    String whs_id
    String rt_order_id
    List<ShopeeRtAttrCmd> attr_list = []
    Long ctime
    String operator
    Integer ticket_type
    List<ShopeeRtSkuInfoCmd> sku_info_list = []
}

class ShopeeRtAttrCmd implements Serializable {
    String attr_key
    String attr_value_id
    String attr_value_name
}

class ShopeeRtSkuInfoCmd implements Serializable {
    String sku_id
    Integer block_type
    BigDecimal qty
    Integer sku_quality
}

class ShopeeUpdateTaskFlagCmd implements Serializable {
    String whs_id
    Integer biz_type
    Integer ticket_type
    List<String> ticket_number_list = []
    Integer urgent_flag
}
```

- [ ] **Step 3: Compile-check DTO syntax**

Run:

```powershell
cd C:\work\gitlab\shopee\wes-loghub
.\gradlew :wms-api:compileGroovy --no-problems-report
```

Expected: Groovy compile succeeds, or dependency resolution fails due local Nexus credentials. If Nexus fails, record the exact failure.

### Task 2: Wire ShopeeInboundApiHandler

**Files:**
- Modify: `C:\work\gitlab\shopee\wes-loghub\wms-api\src\main\groovy\com\ittx\wms\api\service\platform\ShopeeInboundApiHandler.groovy`
- Create: `C:\work\gitlab\shopee\wes-loghub\wms-api\src\main\groovy\com\ittx\wms\api\service\platform\shopee\ShopeeOutboundShipmentMappingService.groovy`

- [ ] **Step 1: Inject the mapping service**

Add:

```groovy
@Autowired
ShopeeOutboundShipmentMappingService shopeeOutboundShipmentMappingSvc
```

- [ ] **Step 2: Add switch cases**

Use the constants from Task 1. Add switch cases for all 50 concrete APIs and delegate to focused services under `com.ittx.wms.api.service.platform.shopee`.

```groovy
case PlatformApiConstants.ShopeeInterfaceType.SALES_CREATE_SUB_PICKING_TASK:
    return createSubPickingTask(session, data)
case PlatformApiConstants.ShopeeInterfaceType.SALES_CANCEL_SUB_PICKING_TASK:
    return cancelSubPickingTask(session, data)
case PlatformApiConstants.ShopeeInterfaceType.SALES_UPDATE_TASK_FLAG:
    return updateTaskFlag(session, data)
case PlatformApiConstants.ShopeeInterfaceType.RACK_TRANSFER_OCCUPY_DEVICE:
    return shopeeRackTransferSvc.occupyDevice(session, data)
case PlatformApiConstants.ShopeeInterfaceType.BASIC_SKU_QUERY_SKU_DETAIL:
    return shopeeBasicDataSvc.querySkuDetail(session, data)
```

Final implementation must not return `ApiMessages.MSG_INTF_0005` for any of the 50 concrete APIs in the inventory.

- [ ] **Step 3: Add parse/validate methods**

Implement each method with the same pattern as `EssInboundApiHandler`: parse, validate required fields, set `CURRENT_WAREHOUSE`, call service.

Example:

```groovy
private ResponseMessage createSubPickingTask(TtxSession session, String data) {
    ShopeeCreateSubPickingTaskCmd cmd
    try {
        cmd = convertValue(data, ShopeeCreateSubPickingTaskCmd.class)
    } catch (Throwable ignored) {
        return ResponseMessageFactory.error(ApiMessages.MSG_INTF_0003)
    }
    ResponseMessage validateRsp = validateCreateSubPickingTask(cmd)
    if (validateRsp.hasError()) return validateRsp
    session.params[WmsConstants.CURRENT_WAREHOUSE] = cmd.whs_id
    return shopeeOutboundShipmentMappingSvc.createSubPickingTask(session, cmd)
}
```

- [ ] **Step 4: Compile**

Run:

```powershell
cd C:\work\gitlab\shopee\wes-loghub
.\gradlew :wms-api:compileGroovy --no-problems-report
```

Expected: Compile succeeds or reports only external dependency credential issues.

### Task 3: Implement Sales SubPickingTask Mapping

**Files:**
- Modify: `C:\work\gitlab\shopee\wes-loghub\wms-api\src\main\groovy\com\ittx\wms\api\service\platform\shopee\ShopeeOutboundShipmentMappingService.groovy`
- Reuse: `ShipmentHeaderService`, `ShipmentDetailService`, existing config/dictionary service if available

- [ ] **Step 1: Implement `createSubPickingTask`**

Map fields:

| Shopee field | WES field |
| --- | --- |
| `whs_id` | `shipment_header.warehouseCode` |
| `sub_pickup_id` | `shipment_header.erpOrderCode` and `shipment_header.shopeeWave` |
| `urgent_flag` | `shipment_header.priority` |
| `cut_off_time` | `shipment_header.cutOffTime` |
| `ship_by_date` | `shipment_header.scheduledShipDate` |
| `purchase_time` | `shipment_header.purchaseTime` |
| `order_time` | `shipment_header.orderTime` |
| `ctime` | dictionary only if business needs query/display; otherwise request log only |
| `can_group_picking` | dictionary only |
| `group_key` | `shipment_header.groupKey` |
| `ticket_type` | `shipment_header.ticketType`; `shipmentType` defaults to `XSCK` |
| `max_order_size` attr | dictionary only |
| `channel_id` attr | dictionary only |
| `fulfillment_chain_id` attr | dictionary only |

Use WES entity fields only for existing core business facts already present in the domain. All Shopee-only extension attributes go through the dictionary mechanism, not new columns.

- [ ] **Step 2: Implement idempotency**

Use `warehouseCode + erpOrderCode`:

```sql
select id from shipment_header where warehouseCode = ? and erpOrderCode = ? and processType = 'NORMAL'
```

If found, update mutable Shopee fields and replace details only if the task is still in pool. If task has started, return a non-retryable parameter/business error.

- [ ] **Step 3: Create details**

For each `sku_info_list` row:

| Shopee field | WES field |
| --- | --- |
| `sku_id` | `shipment_detail.itemCode` |
| `qty` | `shipment_detail.requestQty` |
| `block_type` | `shipment_detail.shelfLifeSts` |
| `quality` | `shipment_detail.inventorySts` |

Validate SKU existence by reusing existing item/detail service behavior. Do not hand-roll item master queries if a service already validates item code.

- [ ] **Step 4: Map all Shopee extension attributes to dictionary**

For `single_attr_list`, `multi_attr_list`, and Shopee-only scalar fields, write values to the existing field dictionary mechanism. Different usage locations must use different dictionary group types so downstream logic can distinguish meaning.

Minimum group type split:

| Usage location | Dictionary group type |
| --- | --- |
| Outbound sales/sub-picking task header attrs | `SHOPEE_OUTBOUND_TASK_HEADER` |
| Outbound order attrs | `SHOPEE_OUTBOUND_ORDER_HEADER` |
| RTS/MTO demand attrs | `SHOPEE_RTSMTO_DEMAND_HEADER` |
| MTO picking task attrs | `SHOPEE_MTO_TASK_HEADER` |
| Inventory/rack-transfer attrs | `SHOPEE_RACK_TRANSFER_HEADER` |
| Basic-data sync attrs | `SHOPEE_BASIC_DATA` |

If no compatible dictionary table/service exists in the current branch, add a separate dictionary task before implementation; do not add Shopee-only fields to WES domain entities:

```powershell
rg -n "shopee_field_dictionary|FieldDictionary|field_dictionary" C:\work\gitlab\shopee\wes-loghub
```

If absent, add a later task before implementation; do not create it inside this task ad hoc.

- [ ] **Step 5: Compile**

Run:

```powershell
cd C:\work\gitlab\shopee\wes-loghub
.\gradlew :wms-api:compileGroovy :wms-shipping:compileGroovy --no-problems-report
```

### Task 4: Implement Cancel and Urgent Flag Update

**Files:**
- Modify: `C:\work\gitlab\shopee\wes-loghub\wms-api\src\main\groovy\com\ittx\wms\api\service\platform\shopee\ShopeeOutboundShipmentMappingService.groovy`

- [ ] **Step 1: Implement cancel**

Input: `ShopeeCancelSubPickingTaskCmd`.

Behavior:

- Find by `warehouseCode = whs_id`, `erpOrderCode = sub_pickup_id`.
- If not found, return non-retryable business error.
- If already picked/closed, return non-retryable business error.
- Otherwise reuse existing cancellation/status-flow service. If no exact cancel service applies, mark `processType`/status via the same base cancel path used by manual shipment cancel, not a raw SQL-only status mutation.
- Ensure wave matching excludes canceled shipments.

- [ ] **Step 2: Implement urgent flag update**

Input: `ShopeeUpdateTaskFlagCmd`.

Behavior:

- Batch update `shipment_header.priority` by `erpOrderCode in ticket_number_list`.
- Return `success_list`, `fail_list`, and `not_exist_list`.
- Do not fail the entire request because one order is missing.

Return shape:

```groovy
[
    success_list: successCodes,
    fail_list: failCodes,
    not_exist_list: notExistCodes
]
```

- [ ] **Step 3: Compile**

Run:

```powershell
cd C:\work\gitlab\shopee\wes-loghub
.\gradlew :wms-api:compileGroovy :wms-shipping:compileGroovy --no-problems-report
```

### Task 5: Implement edi-shopee JWT Service

**Files:**
- Create: `C:\work\gitlab\shopee\edi-shopee\src\main\groovy\com\ittx\edi\erp\service\ShopeeJwtService.groovy`
- Modify: `C:\work\gitlab\shopee\edi-shopee\src\main\resources\application-zzw.yml`

- [ ] **Step 1: Add JWT generation**

Implement HS256 with Base64 URL encoding:

```groovy
String generateToken(String account, String secret, Long timestampSeconds = System.currentTimeMillis().intdiv(1000L)) {
    Map header = [alg: 'HS256', typ: 'JWT']
    Map payload = [timestamp: timestampSeconds, account: account]
    String signingInput = base64UrlJson(header) + '.' + base64UrlJson(payload)
    String signature = hmacSha256Base64Url(signingInput, secret)
    return signingInput + '.' + signature
}
```

- [ ] **Step 2: Add token validation**

Validation must:

- Split token into 3 segments.
- Verify signature using configured secret.
- Parse payload timestamp.
- Reject if absolute clock drift is greater than 300 seconds.
- Reject if account does not match configured account.

- [ ] **Step 3: Read config from `customerMap.shopee`**

JWT and Shopee host configuration live in `application-zzw.yml` under `customerMap.shopee`:

```yaml
customerMap:
  shopee:
    account: xxx
    secret: xxx
    env: uat
    cid: cn
    connectTimeoutMillis: 3000
    readTimeoutMillis: 10000
```

`ShopeeJwtService` and `ShopeeAutomationVendorService` must read `account`, `secret`, host environment, country id, and timeout values from `customerMap.shopee`. Do not hard-code secrets in code and do not add duplicate Shopee JWT config under another profile file in this phase.

- [ ] **Step 4: Compile edi-shopee**

Run:

```powershell
cd C:\work\gitlab\shopee\edi-shopee
.\gradlew compileGroovy
```

### Task 6: Implement edi-shopee Shopee Automation Gateway

**Files:**
- Create: `C:\work\gitlab\shopee\edi-shopee\src\main\groovy\com\ittx\edi\erp\service\ShopeeAutomationVendorService.groovy`
- Create: `C:\work\gitlab\shopee\edi-shopee\src\main\groovy\com\ittx\edi\erp\controller\ShopeeAutomationController.groovy`

- [ ] **Step 1: Add Shopee inbound controller methods**

Expose Shopee-facing POST endpoints in `edi-shopee`, not in WES:

```groovy
@PostMapping('/api/v2/automation/toshopee/outbound/salesorder/create_sub_picking_task')
Object createSubPickingTask(@RequestHeader('Authorization') String authorization,
                            @RequestBody Map body) {
    return shopeeAutomationVendorSvc.forwardShopeeInbound(
            authorization,
            'shopee.outbound.salesorder.createSubPickingTask',
            body)
}
```

Add the same pattern for implemented Shopee inbound interfaces:

- `/api/v2/automation/toshopee/outbound/salesorder/cancel_sub_picking_task`
- `/api/v2/automation/toshopee/outbound/salesorder/update_task_flag`
- sales order and demand pool paths when their WES handlers are implemented.

- [ ] **Step 2: Validate JWT before forwarding to WES**

For every Shopee inbound request:

- Read `Authorization`.
- Validate JWT using `ShopeeJwtService`.
- Reject invalid account, signature, or timestamp over 5 minutes.
- Return Shopee response shape directly:

```groovy
[retcode: -10000000, message: 'auth failed', data: [:]]
```

- [ ] **Step 3: Forward valid Shopee inbound requests to WES**

Call WES existing sync API from `edi-shopee`:

```text
POST {wesBaseUrl}/api/wms/api/sync/in?api={apiName}&format=json
Content-Type: application/json
Body: original Shopee body
```

Convert WES `ResponseMessage` back to Shopee response shape:

```groovy
[retcode: 0, message: 'success', data: rsp.data ?: [:]]
```

On WES business error, do not implement final Shopee `retcode` mapping yet. Return a generic Shopee failure shape, keep the WES message sanitized, and log the original WES error for later mapping design. Do not expose WES stack traces.

- [ ] **Step 4: Add outbound call method**

Implement:

```groovy
ResponseMessage postToShopee(String module, String subModule, String interfaceName, Map body)
```

Build URL:

```text
https://wms-automation.ssc.{env}.shopee.{cid}/api/v2/automation/{direction}/{module}/{sub_module}/{interface_name}
```

For production, omit `{env}` according to the protocol.

- [ ] **Step 5: Add headers for edi-shopee outbound calls to Shopee**

Headers:

```groovy
[
    'Content-Type': 'application/json',
    'Authorization': shopeeJwtSvc.generateToken(account, secret)
]
```

- [ ] **Step 6: Normalize response from Shopee outbound calls**

Shopee response:

```json
{"retcode":0,"message":"success","data":{}}
```

Convert to CBT:

- `retcode == 0` -> `ResponseMessageFactory.success(data)`
- otherwise -> `ResponseMessageFactory.error(message)` and include retcode in data or parameters

- [ ] **Step 7: Add internal endpoints for WES-to-Shopee calls**

Internal controller methods:

- `POST /internal/shopee/automation/waveNumbers`
- `POST /internal/shopee/automation/bindWave`
- `POST /internal/shopee/automation/cancelOrder`
- `POST /internal/shopee/automation/flowPickChangeBox`
- `POST /internal/shopee/automation/rackTransfer/createRtOrder`

Keep these endpoints internal. They are for `wes-loghub` to call `edi-shopee`; Shopee-facing inbound endpoints are the `/api/v2/automation/toshopee/...` methods above, while WES-to-Vendor calls use `tovendor` paths built by `edi-shopee`.

- [ ] **Step 8: Compile**

Run:

```powershell
cd C:\work\gitlab\shopee\edi-shopee
.\gradlew compileGroovy
```

### Task 7: Replace WES ShopeeWaveService Placeholder

**Files:**
- Modify: `C:\work\gitlab\shopee\wes-loghub\wms-wave\src\main\groovy\com\ittx\wms\wave\service\hairo\ShopeeWaveService.groovy`

- [ ] **Step 1: Add an internal edi-shopee client method**

Prefer a small private method in `ShopeeWaveService` or a focused bean if an existing HTTP utility is available.

Request:

```groovy
[
    warehouseCode: warehouseCode,
    waveType: waveType,
    needQty: needQty
]
```

Response data should be a list of wave codes:

```groovy
[
    [code: 'SW202606040001', waveType: '1'],
    [code: 'SW202606040002', waveType: '1']
]
```

- [ ] **Step 2: Implement `requestShopeeWaveNumbers`**

Replace the placeholder log-only method with:

- Call `edi-shopee` internal endpoint.
- Convert returned codes to `ShopeeWave`.
- Return empty list on call failure and log a process history error.
- Do not generate fake local wave numbers.

- [ ] **Step 3: Verify low-water scheduler**

Run:

```powershell
cd C:\work\gitlab\shopee\wes-loghub
.\gradlew :wms-wave:compileGroovy --no-problems-report
```

### Task 8: Replace WES Wave Binding Placeholder

**Files:**
- Modify: `C:\work\gitlab\shopee\wes-loghub\wms-wave\src\main\groovy\com\ittx\wms\wave\service\hairo\WaveRuleMatchService.groovy`

- [ ] **Step 1: Implement `requestShopeeWaveBind` via edi-shopee**

Request:

```groovy
[
    warehouseCode: warehouseCode,
    shopeeWaveCode: shopeeWaveCode,
    shipmentIds: shipmentIds
]
```

Response:

```groovy
[
    success_list: [1L, 2L],
    fail_list: [3L]
]
```

- [ ] **Step 2: Parse partial failure**

Current code treats bind failure as whole batch failure. Change only if Shopee returns per-order failure. If partial failure exists:

- Mark failed shipments with `kickOutWave`.
- Continue binding successful shipment IDs.
- Return success if at least one shipment bound.

If Shopee returns whole request failure, keep existing whole-batch behavior.

- [ ] **Step 3: Keep lock and rollback semantics**

Do not remove:

- `buildShopeeWaveBindLockKey`
- `isShopeeWaveCodeAvailable`
- `rollbackShipmentShopeeWave`
- `rollbackWaveShipments`

These protect local WES consistency around external calls.

- [ ] **Step 4: Compile**

Run:

```powershell
cd C:\work\gitlab\shopee\wes-loghub
.\gradlew :wms-wave:compileGroovy --no-problems-report
```

### Task 9A: Implement Inventory and Basic Interface Surfaces 4.1-5.8

**Files:**
- Do not modify: `C:\work\gitlab\shopee\wes-loghub\wms-transfer\src\main\groovy\com\ittx\wms\transfer\service\TransferOrderHeaderService.groovy`
- Do not modify: `C:\work\gitlab\shopee\wes-loghub\wms-transfer\src\main\groovy\com\ittx\wms\transfer\service\TransferOrderDetailService.groovy`
- Create only if needed: `C:\work\gitlab\shopee\wes-loghub\wms-api\src\main\groovy\com\ittx\wms\api\service\platform\shopee\ShopeeRackTransferMappingService.groovy`

- [ ] **Step 1: Keep 4.1 business trigger out of scope**

Do not search for or wire the WES移库单创建、定位、锁定、占用完成事件 in this phase.

Implementation target for `4.1` is limited to:

- `edi-shopee` internal endpoint: `POST /internal/shopee/automation/rackTransfer/createRtOrder`
- Shopee outbound call: `POST /api/v2/automation/tovendor/inventory/racktransfer/create_rt_order`
- Request validation, JWT header generation, response normalization, logging, and retry classification.

- [ ] **Step 2: Define the 4.1 request contract without wiring WES transfer data**

Endpoint request shape accepted by `edi-shopee`:

```groovy
[
    whs_id: request.whs_id,
    rt_order_id: request.rt_order_id,
    attr_list: [
        [attr_key: 'urgent_flag', attr_value_id: '1', attr_value_name: '1'],
        [attr_key: 'oos_order_qty', attr_value_id: '0', attr_value_name: '0']
    ].findAll { it.attr_value_id != null },
    ctime: request.ctime,
    operator: request.operator,
    ticket_type: 6,
    sku_info_list: request.sku_info_list.collect {
        [
            sku_id: it.sku_id,
            block_type: it.block_type,
            qty: it.qty,
            sku_quality: it.sku_quality
        ]
    }
]
```

Keep `ticket_type = 6` fixed for Rack Transfer. The later WES business trigger will be responsible for building this body from real transfer entities.

- [ ] **Step 3: Add 4.2-4.5 Vendor-to-WES rack-transfer handlers**

Expose these Shopee-facing `edi-shopee` endpoints and forward them to WES sync API with the WES api name below. WES dispatch must be:

```text
edi-shopee -> /api/wms/api/sync/in?api=<WES api>&format=json -> PlatformInboundApiService -> ShopeeInboundApiHandler
```

| Section | Path | WES api |
| --- | --- | --- |
| 4.2 | `/api/v2/automation/toshopee/inventory/racktransfer/occupy_device` | `shopee.inventory.racktransfer.occupyDevice` |
| 4.3 | `/api/v2/automation/toshopee/inventory/racktransfer/submit_picking_sku_list` | `shopee.inventory.racktransfer.submitPickingSkuList` |
| 4.4 | `/api/v2/automation/toshopee/inventory/racktransfer/sync_picking_sku_list` | `shopee.inventory.racktransfer.syncPickingSkuList` |
| 4.5 | `/api/v2/automation/toshopee/inventory/racktransfer/handle_diff_stock` | `shopee.inventory.racktransfer.handleDiffStock` |

Inside `ShopeeInboundApiHandler`, route to existing `wms-transfer` / `wms-inventory` services first. Do not add a separate WES controller for these four endpoints. Add new service methods only when existing transfer/inventory APIs cannot express the protocol action.

- [ ] **Step 4: Add 4.6-4.7 WMS-to-Vendor rack-transfer interface endpoints only**

Use internal `edi-shopee` endpoints for:

| Section | Internal endpoint | Shopee path |
| --- | --- | --- |
| 4.6 | `/internal/shopee/automation/rackTransfer/queryPickingList` | `/api/v2/automation/tovendor/inventory/racktransfer/query_picking_list` |
| 4.7 | `/internal/shopee/automation/rackTransfer/updateRtOrder` | `/api/v2/automation/tovendor/inventory/racktransfer/update_rt_order` |

Do not bind these endpoints to WES对账任务、移库取消、优先级更新、人工操作 or any other business trigger in this phase.

Keep the `4.1` internal endpoint:

Use the same internal client pattern as wave-number and wave-bind calls:

```text
POST {ediShopeeBaseUrl}/internal/shopee/automation/rackTransfer/createRtOrder
```

`edi-shopee` then calls Shopee:

```text
POST /api/v2/automation/tovendor/inventory/racktransfer/create_rt_order
```

Failure handling:

- Preserve Shopee `retcode` and message in the EDI call log.
- Do not finalize retry/non-retry mapping in this phase.
- Use existing transport failure handling for timeout/network errors only.

- [ ] **Step 5: Add 5.1 and 5.5 Vendor-to-WES basic handlers**

Expose Shopee-facing `edi-shopee` endpoints and forward to WES:

| Section | Path | WES api |
| --- | --- | --- |
| 5.1 | `/api/v2/automation/toshopee/basic/user/query_user_exist` | `shopee.basic.user.queryUserExist` |
| 5.5 | `/api/v2/automation/toshopee/basic/sku/query_sku_detail` | `shopee.basic.sku.querySkuDetail` |

Reuse existing WES user and SKU services. Do not create duplicate user/SKU lookup SQL if a service already exists.

- [ ] **Step 6: Add 5.2-5.4 and 5.6-5.8 scheduled WMS-to-Vendor basic calls**

Confirmed trigger source: scheduled jobs. Each WES-to-Vendor basic-data sync uses this three-step internal flow:

```text
<basic api>.confirm -> <basic api>.get -> <basic api>.uploadStatus
```

Use this pattern for the WES-to-Vendor basic-data interfaces. `5.5` remains a Vendor-to-WES query handled in Step 5.

| Section | Step | Internal endpoint | Shopee path |
| --- | --- | --- | --- |
| 5.2 | `.confirm` | `/internal/shopee/automation/basic/inventory/batchStatusUpdate.confirm` | `/api/v2/automation/tovendor/basic/inventory/batch_status_update` |
| 5.2 | `.get` | `/internal/shopee/automation/basic/inventory/batchStatusUpdate.get` | `/api/v2/automation/tovendor/basic/inventory/batch_status_update` |
| 5.2 | `.uploadStatus` | `/internal/shopee/automation/basic/inventory/batchStatusUpdate.uploadStatus` | `/api/v2/automation/tovendor/basic/inventory/batch_status_update` |
| 5.3 | `.confirm` | `/internal/shopee/automation/basic/inventory/querySkuBatchInventory.confirm` | `/api/v2/automation/tovendor/basic/inventory/query_sku_batch_inventory` |
| 5.3 | `.get` | `/internal/shopee/automation/basic/inventory/querySkuBatchInventory.get` | `/api/v2/automation/tovendor/basic/inventory/query_sku_batch_inventory` |
| 5.3 | `.uploadStatus` | `/internal/shopee/automation/basic/inventory/querySkuBatchInventory.uploadStatus` | `/api/v2/automation/tovendor/basic/inventory/query_sku_batch_inventory` |
| 5.4 | `.confirm` | `/internal/shopee/automation/basic/inventory/syncReconciliationResult.confirm` | `/api/v2/automation/tovendor/basic/inventory/sync_reconciliation_result` |
| 5.4 | `.get` | `/internal/shopee/automation/basic/inventory/syncReconciliationResult.get` | `/api/v2/automation/tovendor/basic/inventory/sync_reconciliation_result` |
| 5.4 | `.uploadStatus` | `/internal/shopee/automation/basic/inventory/syncReconciliationResult.uploadStatus` | `/api/v2/automation/tovendor/basic/inventory/sync_reconciliation_result` |
| 5.6 | `.confirm` | `/internal/shopee/automation/basic/sku/syncSkuInfo.confirm` | `/api/v2/automation/tovendor/basic/sku/sync_sku_info` |
| 5.6 | `.get` | `/internal/shopee/automation/basic/sku/syncSkuInfo.get` | `/api/v2/automation/tovendor/basic/sku/sync_sku_info` |
| 5.6 | `.uploadStatus` | `/internal/shopee/automation/basic/sku/syncSkuInfo.uploadStatus` | `/api/v2/automation/tovendor/basic/sku/sync_sku_info` |
| 5.7 | `.confirm` | `/internal/shopee/automation/basic/robot/queryAgvStatus.confirm` | `/api/v2/automation/tovendor/basic/robot/query_agv_status` |
| 5.7 | `.get` | `/internal/shopee/automation/basic/robot/queryAgvStatus.get` | `/api/v2/automation/tovendor/basic/robot/query_agv_status` |
| 5.7 | `.uploadStatus` | `/internal/shopee/automation/basic/robot/queryAgvStatus.uploadStatus` | `/api/v2/automation/tovendor/basic/robot/query_agv_status` |
| 5.8 | `.confirm` | `/internal/shopee/automation/basic/inventory/queryVendorUnitList.confirm` | `/api/v2/automation/tovendor/basic/inventory/query_vendor_unit_list` |
| 5.8 | `.get` | `/internal/shopee/automation/basic/inventory/queryVendorUnitList.get` | `/api/v2/automation/tovendor/basic/inventory/query_vendor_unit_list` |
| 5.8 | `.uploadStatus` | `/internal/shopee/automation/basic/inventory/queryVendorUnitList.uploadStatus` | `/api/v2/automation/tovendor/basic/inventory/query_vendor_unit_list` |

The previous one-call internal endpoint names are retained only as conceptual API names, not as the scheduled-job contract:

| Section | Internal endpoint | Shopee path |
| --- | --- | --- |
| 5.2 | `/internal/shopee/automation/basic/inventory/batchStatusUpdate` | `/api/v2/automation/tovendor/basic/inventory/batch_status_update` |
| 5.3 | `/internal/shopee/automation/basic/inventory/querySkuBatchInventory` | `/api/v2/automation/tovendor/basic/inventory/query_sku_batch_inventory` |
| 5.4 | `/internal/shopee/automation/basic/inventory/syncReconciliationResult` | `/api/v2/automation/tovendor/basic/inventory/sync_reconciliation_result` |
| 5.6 | `/internal/shopee/automation/basic/sku/syncSkuInfo` | `/api/v2/automation/tovendor/basic/sku/sync_sku_info` |
| 5.7 | `/internal/shopee/automation/basic/robot/queryAgvStatus` | `/api/v2/automation/tovendor/basic/robot/query_agv_status` |
| 5.8 | `/internal/shopee/automation/basic/inventory/queryVendorUnitList` | `/api/v2/automation/tovendor/basic/inventory/query_vendor_unit_list` |

These calls stay in `edi-shopee`. Scheduled jobs call `.confirm`, `.get`, then `.uploadStatus`; do not bind them to ad hoc WES business events in this phase.

- [ ] **Step 7: Compile transfer, API, inventory, and edi-shopee modules**

Run:

```powershell
cd C:\work\gitlab\shopee\wes-loghub
.\gradlew :wms-transfer:compileGroovy :wms-api:compileGroovy --no-problems-report
cd C:\work\gitlab\shopee\edi-shopee
.\gradlew compileGroovy
```

### Task 9: Add Sales Order and Demand Pool Interfaces

**Files:**
- Modify DTO file from Task 1.
- Modify `ShopeeInboundApiHandler.groovy`.
- Modify `C:\work\gitlab\shopee\wes-loghub\wms-api\src\main\groovy\com\ittx\wms\api\service\platform\shopee\ShopeeOutboundShipmentMappingService.groovy`.

- [ ] **Step 1: Implement sales order create/pull/revoke/lock**

Use the same handler pattern as Tasks 2-4.

Minimum mappings:

- Sales order create maps to `shipment_header` and `shipment_detail`, with `ticketType = 2`.
- Pull order queries eligible WES orders by Shopee identifiers and current state.
- Revoke order cancels or marks excluded, using existing shipment cancel/status services.
- Lock/unlock writes existing lock fields if present; otherwise reuse standard lock service.

- [ ] **Step 2: Implement RTS/MTO demand create/cancel/sync occupy**

Minimum mappings:

- RTS demand -> `shipmentType = RTS`, `ticketType = 3`.
- MTO demand -> `shipmentType = MTO`, `ticketType = 4`.
- MTO task -> `ticketType = 5`.
- RT -> `ticketType = 6`, does not use Shopee wave number.

- [ ] **Step 3: Guard one-wave-one-order types**

Ensure `WaveRuleMatchService` one-wave-one-order logic keeps:

- `ticketType = 1`
- `ticketType = 5`
- `ticketType = 6`

- [ ] **Step 4: Compile**

Run:

```powershell
cd C:\work\gitlab\shopee\wes-loghub
.\gradlew :wms-api:compileGroovy :wms-wave:compileGroovy --no-problems-report
```

### Task 10: Error Code and Response Contract Placeholder

**Files:**
- Modify: `ShopeeInboundApiHandler.groovy`
- Modify: `C:\work\gitlab\shopee\wes-loghub\wms-api\src\main\groovy\com\ittx\wms\api\service\platform\shopee\ShopeeOutboundShipmentMappingService.groovy`

- [ ] **Step 1: Keep retcode mapping as a placeholder**

WES standard `ResponseMessage` is acceptable inside current sync API. Final mapping from WES errors to Shopee protocol `retcode` is not decided yet and must not be implemented in this phase.

Boundary adapter behavior for now:

```groovy
[
    retcode: rsp.hasError() ? -1 : 0,
    message: rsp.hasError() ? sanitize(rsp.message) : 'success',
    data: rsp.data ?: [:]
]
```

Keep original WES error code/message in logs for later retcode mapping. Do not change `ResponseMessage` globally.

- [ ] **Step 2: Defer retry mapping**

Do not create a retry table or classify business `retcode` values yet.

Only transport-level failures such as timeout, connection refused, and upstream unavailable use existing retry behavior. Business retcode retry rules stay pending until the final error-code mapping is confirmed.

### Task 11: Verification and Documentation

**Files:**
- Modify: `C:\work\gitlab\shopee\docs\统一汇总.md`
- Modify or create: focused API implementation note under `C:\work\gitlab\shopee\docs\`

- [ ] **Step 1: Run WES compile**

Run:

```powershell
cd C:\work\gitlab\shopee\wes-loghub
.\gradlew :wms-api:compileGroovy :wms-wave:compileGroovy :wms-shipping:compileGroovy --no-problems-report
```

- [ ] **Step 2: Run edi-shopee compile**

Run:

```powershell
cd C:\work\gitlab\shopee\edi-shopee
.\gradlew compileGroovy
```

- [ ] **Step 3: Manual Shopee-facing smoke tests**

Use one JSON body per implemented interface through `edi-shopee`, which then forwards to WES:

```powershell
Invoke-RestMethod `
  -Method Post `
  -Uri 'http://localhost:9210/api/v2/automation/toshopee/outbound/salesorder/create_sub_picking_task' `
  -ContentType 'application/json' `
  -Headers @{ Authorization = '<valid-shopee-jwt>' } `
  -Body '<json body from protocol>'
```

Expected:

- Response is Shopee shape with `retcode = 0`.
- `shipment_header` has Shopee fields.
- `shipment_detail` rows exist.
- Duplicate request is idempotent.

- [ ] **Step 4: Manual 4.1-5.8 smoke tests**

Trigger or post representative `edi-shopee` endpoints from each added module.

4.1 WMS-to-Vendor rack-transfer body:

```powershell
Invoke-RestMethod `
  -Method Post `
  -Uri 'http://localhost:9210/internal/shopee/automation/rackTransfer/createRtOrder' `
  -ContentType 'application/json' `
  -Body '{
    "whs_id":"SGL",
    "rt_order_id":"RTSGL00020250102001",
    "attr_list":[{"attr_key":"urgent_flag","attr_value_id":"1","attr_value_name":"1"}],
    "ctime":1780000000,
    "operator":"xxx@shopee.com",
    "ticket_type":6,
    "sku_info_list":[{"sku_id":"18377383_9999","block_type":0,"qty":2,"sku_quality":0}]
  }'
```

Expected:

- `edi-shopee` calls `/api/v2/automation/tovendor/inventory/racktransfer/create_rt_order`.
- JWT header is present.
- Shopee success maps to CBT success.
- Retryable `-10013099` remains retryable.

Vendor-to-WES samples:

```powershell
Invoke-RestMethod `
  -Method Post `
  -Uri 'http://localhost:9210/api/v2/automation/toshopee/inventory/racktransfer/occupy_device' `
  -ContentType 'application/json' `
  -Headers @{ Authorization = '<valid-shopee-jwt>' } `
  -Body '<4.2 json body from protocol>'

Invoke-RestMethod `
  -Method Post `
  -Uri 'http://localhost:9210/api/v2/automation/toshopee/basic/user/query_user_exist' `
  -ContentType 'application/json' `
  -Headers @{ Authorization = '<valid-shopee-jwt>' } `
  -Body '<5.1 json body from protocol>'
```

WMS-to-Vendor basic-data samples:

```powershell
Invoke-RestMethod `
  -Method Post `
  -Uri 'http://localhost:9210/internal/shopee/automation/basic/sku/syncSkuInfo.confirm' `
  -ContentType 'application/json' `
  -Body '<5.6 confirm json body>'

Invoke-RestMethod `
  -Method Post `
  -Uri 'http://localhost:9210/internal/shopee/automation/basic/sku/syncSkuInfo.get' `
  -ContentType 'application/json' `
  -Body '<5.6 get json body from protocol>'

Invoke-RestMethod `
  -Method Post `
  -Uri 'http://localhost:9210/internal/shopee/automation/basic/sku/syncSkuInfo.uploadStatus' `
  -ContentType 'application/json' `
  -Body '<5.6 uploadStatus json body>'
```

- [ ] **Step 5: Manual internal WES smoke tests**

Use the WES sync API only as an internal verification target:

```powershell
Invoke-RestMethod `
  -Method Post `
  -Uri 'http://localhost:9001/api/wms/api/sync/in?api=shopee.outbound.salesorder.createSubPickingTask&format=json' `
  -ContentType 'application/json' `
  -Body '<json body from protocol>'
```

- [ ] **Step 6: Manual wave tests**

Check:

- Low-water scheduler calls `edi-shopee` and saves `shopee_wave`.
- Wave binding calls `edi-shopee`.
- Failed bind marks `kickOutWave`.
- Successful bind writes `shipment_header.shopeeWave`.

- [ ] **Step 7: Update docs**

Update:

- `docs/统一汇总.md`: move implemented items from "待实现" to "已落地".
- Add a concise implementation note linking `ShopeeInboundApiHandler`, `ShopeeWaveService`, `WaveRuleMatchService`, and `edi-shopee` JWT service.

## Self-Review

- Spec coverage: Covers `edi-shopee` as Shopee-facing entry, WES field mapping, wave number request, wave binding, all concrete interfaces from `3.1`-`3.4`, `4.1`-`4.7`, and `5.1`-`5.8`, JWT ownership in `edi-shopee`, and verification.
- Scope control: Excludes inbound module interfaces; includes outbound, inventory/rack-transfer, and basic-data interfaces from this protocol file; Shopee extension fields do not add WES entity columns and are persisted through dictionary group types.
- Type consistency: DTO names and handler method names match task references.
- Confirmed entry contract: Shopee 调 `edi-shopee`，`edi-shopee` 再转发到 WES `/api/wms/api/sync/in?api=shopee...`，WES 不直接作为 Shopee 外部入口。
