@rytass/wms-module-graphql 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +19 -0
  2. package/docs/README.md +5 -2
  3. package/docs/api-reference.md +10 -8
  4. package/docs/integration-guide.md +34 -17
  5. package/docs/inventory-count.md +372 -0
  6. package/index.cjs.js +10 -0
  7. package/lib/inputs/inventory-count-order.args.d.ts +6 -0
  8. package/package.json +12 -11
  9. package/adapters/event-emitter.js +0 -41
  10. package/adapters/member-base.js +0 -63
  11. package/index.js +0 -12
  12. package/lib/auth/host-skip.constant.js +0 -29
  13. package/lib/auth/wms-auth.guard.js +0 -41
  14. package/lib/auth/wms-auth.types.js +0 -6
  15. package/lib/auth/wms-operation.decorator.js +0 -29
  16. package/lib/dto/allocate-inventory-order-item.dto.js +0 -36
  17. package/lib/dto/allocate-inventory-order.dto.js +0 -31
  18. package/lib/dto/batch.dto.js +0 -26
  19. package/lib/dto/consumption-order-item.dto.js +0 -43
  20. package/lib/dto/consumption-order.dto.js +0 -52
  21. package/lib/dto/hold-inventory-order-item.dto.js +0 -37
  22. package/lib/dto/hold-inventory-order.dto.js +0 -47
  23. package/lib/dto/inventory-count-order.dto.js +0 -207
  24. package/lib/dto/loader-type.dto.js +0 -46
  25. package/lib/dto/loader-unit.dto.js +0 -21
  26. package/lib/dto/loader.dto.js +0 -71
  27. package/lib/dto/location.dto.js +0 -36
  28. package/lib/dto/material-unit.dto.js +0 -46
  29. package/lib/dto/material.dto.js +0 -36
  30. package/lib/dto/merge-batch-order.dto.js +0 -50
  31. package/lib/dto/order-reference.dto.js +0 -73
  32. package/lib/dto/pagination.dto.js +0 -51
  33. package/lib/dto/quality-inspection-order-item.dto.js +0 -50
  34. package/lib/dto/quality-inspection-order.dto.js +0 -42
  35. package/lib/dto/receive-inventory-order-item.dto.js +0 -60
  36. package/lib/dto/receive-inventory-order.dto.js +0 -58
  37. package/lib/dto/reclassify-inventory-order.dto.js +0 -55
  38. package/lib/dto/sale-order.dto.js +0 -20
  39. package/lib/dto/scrape-order-item.dto.js +0 -56
  40. package/lib/dto/scrape-order.dto.js +0 -37
  41. package/lib/dto/ship-inventory-order-item.dto.js +0 -56
  42. package/lib/dto/ship-inventory-order-reference.dto.js +0 -48
  43. package/lib/dto/ship-inventory-order.dto.js +0 -58
  44. package/lib/dto/split-batch-order.dto.js +0 -50
  45. package/lib/dto/stock.dto.js +0 -166
  46. package/lib/dto/transfer-customer-order-item.dto.js +0 -61
  47. package/lib/dto/transfer-customer-order.dto.js +0 -37
  48. package/lib/dto/transfer-order-item.dto.js +0 -41
  49. package/lib/dto/transfer-order.dto.js +0 -57
  50. package/lib/dto/transfer-vendor-order-item.dto.js +0 -61
  51. package/lib/dto/transfer-vendor-order.dto.js +0 -37
  52. package/lib/dto/unit.dto.js +0 -20
  53. package/lib/dto/warehouse-map.dto.js +0 -165
  54. package/lib/events/wms-mutation-event.constants.js +0 -4
  55. package/lib/events/wms-mutation-event.decorator.js +0 -20
  56. package/lib/events/wms-mutation.interceptor.js +0 -64
  57. package/lib/graphql.module.js +0 -183
  58. package/lib/helpers/create-wms-loaders.js +0 -11
  59. package/lib/helpers/helper.module.js +0 -15
  60. package/lib/helpers/loader.service.js +0 -230
  61. package/lib/helpers/query-path.js +0 -19
  62. package/lib/inputs/cancel-ship-order.args.js +0 -20
  63. package/lib/inputs/cancel-transfer-order.args.js +0 -23
  64. package/lib/inputs/consumption-order.args.js +0 -182
  65. package/lib/inputs/create-allocate-inventory-order-item.args.js +0 -50
  66. package/lib/inputs/create-allocate-inventory-order.args.js +0 -36
  67. package/lib/inputs/create-hold-inventory-order-item.args.js +0 -40
  68. package/lib/inputs/create-hold-inventory-order.args.js +0 -41
  69. package/lib/inputs/create-loader-type.args.js +0 -35
  70. package/lib/inputs/create-loader.args.js +0 -130
  71. package/lib/inputs/create-location.args.js +0 -50
  72. package/lib/inputs/create-material-unit.args.js +0 -25
  73. package/lib/inputs/create-material.args.js +0 -51
  74. package/lib/inputs/create-quality-inspection-order.args.js +0 -50
  75. package/lib/inputs/create-receive-inventory-order-item.args.js +0 -70
  76. package/lib/inputs/create-receive-inventory-order.args.js +0 -51
  77. package/lib/inputs/create-ship-inventory-order-item.args.js +0 -45
  78. package/lib/inputs/create-ship-inventory-order-reference.args.js +0 -65
  79. package/lib/inputs/create-ship-inventory-order.args.js +0 -57
  80. package/lib/inputs/create-transfer-customer-order-item.args.js +0 -40
  81. package/lib/inputs/create-transfer-customer-order.args.js +0 -51
  82. package/lib/inputs/create-transfer-order-item.args.js +0 -40
  83. package/lib/inputs/create-transfer-order.args.js +0 -61
  84. package/lib/inputs/create-transfer-vendor-order-item.args.js +0 -40
  85. package/lib/inputs/create-transfer-vendor-order.args.js +0 -51
  86. package/lib/inputs/date-range.args.js +0 -20
  87. package/lib/inputs/deallocate-order.args.js +0 -25
  88. package/lib/inputs/find-allocate-inventory-order.args.js +0 -42
  89. package/lib/inputs/find-batch.args.js +0 -21
  90. package/lib/inputs/find-hold-inventory-order.args.js +0 -42
  91. package/lib/inputs/find-loader-type.args.js +0 -32
  92. package/lib/inputs/find-loader.args.js +0 -72
  93. package/lib/inputs/find-location.args.js +0 -26
  94. package/lib/inputs/find-material.args.js +0 -21
  95. package/lib/inputs/find-receive-inventory-order.args.js +0 -71
  96. package/lib/inputs/find-sale-order.args.js +0 -16
  97. package/lib/inputs/find-ship-inventory-order.args.js +0 -71
  98. package/lib/inputs/find-stock.args.js +0 -102
  99. package/lib/inputs/find-transfer-order.args.js +0 -82
  100. package/lib/inputs/inventory-count-order.args.js +0 -257
  101. package/lib/inputs/merge-batch.args.js +0 -40
  102. package/lib/inputs/order-reference.args.js +0 -103
  103. package/lib/inputs/pagination.args.js +0 -20
  104. package/lib/inputs/receive-order.args.js +0 -40
  105. package/lib/inputs/reclassify-inventory-order.args.js +0 -40
  106. package/lib/inputs/release-order.args.js +0 -25
  107. package/lib/inputs/return-transfer-customer-order.args.js +0 -35
  108. package/lib/inputs/return-transfer-vendor-order.args.js +0 -35
  109. package/lib/inputs/scrape-order.args.js +0 -35
  110. package/lib/inputs/ship-order.args.js +0 -20
  111. package/lib/inputs/split-batch.args.js +0 -51
  112. package/lib/inputs/transfer-order.args.js +0 -20
  113. package/lib/inputs/update-warehouse-map.args.js +0 -121
  114. package/lib/mutations/allocate-inventory-order.mutation.js +0 -91
  115. package/lib/mutations/consumption-order.mutation.js +0 -90
  116. package/lib/mutations/hold-inventory-order.mutation.js +0 -91
  117. package/lib/mutations/inventory-count-order.mutation.js +0 -138
  118. package/lib/mutations/loader.mutation.js +0 -123
  119. package/lib/mutations/location.mutation.js +0 -47
  120. package/lib/mutations/material.mutation.js +0 -74
  121. package/lib/mutations/merge-batch.mutation.js +0 -50
  122. package/lib/mutations/order-reference.mutation.js +0 -57
  123. package/lib/mutations/quality-inspection-order.mutation.js +0 -50
  124. package/lib/mutations/receive-inventory-order.mutation.js +0 -106
  125. package/lib/mutations/reclassify-inventory-order.mutation.js +0 -51
  126. package/lib/mutations/scrape-order.mutation.js +0 -44
  127. package/lib/mutations/ship-inventory-order.mutation.js +0 -123
  128. package/lib/mutations/split-batch.mutation.js +0 -44
  129. package/lib/mutations/transfer-customer-order.mutation.js +0 -91
  130. package/lib/mutations/transfer-order.mutation.js +0 -96
  131. package/lib/mutations/transfer-vendor-order.mutation.js +0 -91
  132. package/lib/mutations/warehouse-map.mutation.js +0 -60
  133. package/lib/queries/allocate-inventory-order.query.js +0 -33
  134. package/lib/queries/batch.query.js +0 -36
  135. package/lib/queries/consumption-order.query.js +0 -45
  136. package/lib/queries/hold-inventory-order.query.js +0 -33
  137. package/lib/queries/inventory-count-order.query.js +0 -63
  138. package/lib/queries/loader.query.js +0 -167
  139. package/lib/queries/location.query.js +0 -51
  140. package/lib/queries/material.query.js +0 -79
  141. package/lib/queries/order-reference.query.js +0 -34
  142. package/lib/queries/recieve-inventory-order.query.js +0 -77
  143. package/lib/queries/sale-order.query.js +0 -57
  144. package/lib/queries/ship-inventory-order.query.js +0 -77
  145. package/lib/queries/stock.query.js +0 -116
  146. package/lib/queries/transfer-order.query.js +0 -46
  147. package/lib/queries/unit.query.js +0 -30
  148. package/lib/queries/warehouse-map.query.js +0 -32
  149. package/lib/registry/operation-registry.js +0 -136
  150. package/lib/resolvers/aggregated-stock.resolver.js +0 -119
  151. package/lib/resolvers/allocate-inventory-order.resolver.js +0 -27
  152. package/lib/resolvers/change-log.resolver.js +0 -99
  153. package/lib/resolvers/consumption-order.resolver.js +0 -238
  154. package/lib/resolvers/hold-inventory-order.resolver.js +0 -27
  155. package/lib/resolvers/inventory-count-order.resolver.js +0 -148
  156. package/lib/resolvers/loader-type.resolver.js +0 -54
  157. package/lib/resolvers/loader.resolver.js +0 -99
  158. package/lib/resolvers/location.resolver.js +0 -80
  159. package/lib/resolvers/material.resolver.js +0 -87
  160. package/lib/resolvers/merge-batch.resolver.js +0 -26
  161. package/lib/resolvers/receive-inventory-order.resolver.js +0 -156
  162. package/lib/resolvers/ship-inventory-order.resolver.js +0 -301
  163. package/lib/resolvers/split-batch.resolver.js +0 -26
  164. package/lib/resolvers/stock.resolver.js +0 -99
  165. package/lib/resolvers/transfer-customer-order.resolver.js +0 -27
  166. package/lib/resolvers/transfer-order.resolver.js +0 -246
  167. package/lib/resolvers/transfer-vendor-order.resolver.js +0 -27
  168. package/lib/resolvers/warehouse-map.resolver.js +0 -41
  169. package/lib/scalars/json-object.scalar.js +0 -62
package/README.md CHANGED
@@ -181,6 +181,14 @@ back the flow:
181
181
  - **`startInventoryCount` snapshots eight attributes per line** — `held`, `inShipment`,
182
182
  `inspectionResult`, `transferId`, `vendorId`, `customerId`, `saleId`, `saleSerialId` —
183
183
  so the sheet keeps showing the world the variance is measured against, not today's.
184
+ - **`createInventoryCountOrder` takes an optional `createdBy`**, like the other order
185
+ families; it is what the list shows as 建立者 and what its `createdBy` filter matches.
186
+ - **Every count mutation is one transaction.** Creating, updating, starting, counting,
187
+ adding or removing a line, resetting, cancelling and settling each commit or roll back
188
+ as a whole, and each accepts an `EntityManager` to join a transaction you already hold.
189
+ - **`settleInventoryCount` re-checks the snapshot first.** If a scoped line's
190
+ `bookQuantity` no longer matches the ledger — something wrote around the lock — it
191
+ refuses with a `409 Conflict` instead of posting a variance against stale books.
184
192
 
185
193
  ### ⚠️ Starting a count locks its locations
186
194
 
@@ -198,8 +206,17 @@ stock events use — so it covers resolvers **and** direct service calls, includ
198
206
  integration code. Without it the book snapshot the variance is measured against would go
199
207
  stale and settlement would silently write off somebody else's legitimate movement.
200
208
 
209
+ Both ends of a movement count. A transfer item names a source and a destination, and the
210
+ lock refuses a change on either, so a transfer **into** a counted location is refused as
211
+ well — at reservation, completion and cancellation alike, because a change row cannot tell
212
+ those apart.
213
+
201
214
  Settling or cancelling the count releases the lock.
202
215
 
216
+ ➡️ **Per-operation usage (繁體中文): [`docs/inventory-count.md`](./docs/inventory-count.md)** —
217
+ every query and mutation with arguments, preconditions, errors and examples.
218
+
219
+
203
220
  ## Events & Hooks
204
221
 
205
222
  Intervene in and get notified about mutations. Every root mutation carries a typed **operation event** (`material.created`, `ship-inventory-order.shipped`, `inventory-count-order.settled`, …) and fans out **stock domain events** (`stock.allocated`, `stock.shipped`, `stock.count-gained`, …) driven by the `ChangeType` state machine. Wire two opt-in hook points via `forRootAsync`:
@@ -288,6 +305,8 @@ Full documentation is shipped inside this package under the `docs/` directory (a
288
305
  | [`docs/integration-guide.md`](./docs/integration-guide.md) | Step-by-step wiring guide for NestJS + Next.js consumers |
289
306
  | [`docs/authorization.md`](./docs/authorization.md) | Complete resolver-authorization design & integration recipes (member-base / custom JWT / no-guard) |
290
307
  | [`docs/events.md`](./docs/events.md) | Mutation event/hook system — before-hooks (veto), post-commit after-listeners, complete event list, event-emitter bridge |
308
+ | [`docs/inventory-count.md`](./docs/inventory-count.md) | 盤點 API 使用說明(繁體中文)— 3 支 query、9 支 mutation 的參數、前置狀態、錯誤與範例 |
309
+
291
310
  | [`docs/api-reference.md`](./docs/api-reference.md) | Complete export catalog with signatures and usage examples |
292
311
 
293
312
  GitHub hosted links (always up-to-date):
package/docs/README.md CHANGED
@@ -9,7 +9,8 @@ Reference material for consumers of `@rytass/wms-module-{core,graphql,react}`.
9
9
  | [Integration Guide](./integration-guide.md) | Step-by-step instructions for wiring the three packages into a new consumer project (NestJS + Next.js + PostgreSQL). Includes ready-to-paste `_app.tsx`, `app.module.ts`, `next.config.ts`, the 19 Pages Router shell pattern, AuthGuard stub, seed script template, deployment notes, and known issues. |
10
10
  | [Authorization](./authorization.md) | Complete design of the resolver-level authorization protocol (`@WmsOperation` + `WmsAuthGuard` + `authChecker`). Explains the global deny-by-default guard pitfall, execution order, and integration recipes for member-base / Casbin, custom JWT / RBAC, and no-guard setups. Read this if any WMS query/mutation returns *forbidden*. |
11
11
  | [Events & Hooks](./events.md) | The mutation event/hook system — `beforeHooks` (veto before any DB work) and `afterListeners` (post-commit notify), the `AsyncLocalStorage` post-commit mechanism, the optional `@nestjs/event-emitter` bridge, and the complete list of 38 operation + 18 stock domain events with payloads. |
12
- | [API Reference](./api-reference.md) | Complete export catalog for all three packages — every NestJS module, TypeORM entity, GraphQL resolver, DTO, React component, hook, and utility, organized by role with source paths and usage examples. Mirrors the actual `src/index.ts` barrel exports of `@rytass/wms-module-graphql@0.3.0` and `@rytass/wms-module-{core,react}@0.1.6`. |
12
+ | [API Reference](./api-reference.md) | Complete export catalog for all three packages — every NestJS module, TypeORM entity, GraphQL resolver, DTO, React component, hook, and utility, organized by role with source paths and usage examples. Mirrors the actual `src/index.ts` barrel exports of the three packages. |
13
+ | [盤點 API 使用說明](./inventory-count.md) | Inventory counting, operation by operation (繁體中文): the 3 queries and 9 mutations with arguments, status preconditions, errors, examples, the location lock and the events each one emits. |
13
14
 
14
15
  ## Quick links
15
16
 
@@ -29,11 +30,13 @@ Reference material for consumers of `@rytass/wms-module-{core,graphql,react}`.
29
30
  | Fix "every WMS query/mutation is forbidden" | [Authorization section 1](./authorization.md#1-the-problem) |
30
31
  | Hook into / get notified about mutations | [Events & Hooks](./events.md) |
31
32
  | See every event the system emits | [Events & Hooks section 5](./events.md#5-complete-event-list) |
33
+ | Drive an inventory count (盤點) end to end | [盤點 API 使用說明](./inventory-count.md) |
34
+
32
35
  | Decide between PostgreSQL and SQLite | [Integration Guide section 9](./integration-guide.md#9-database-support-matrix) |
33
36
  | Build a custom UI on top of the lib's GraphQL operations| [Integration Guide section 7.3](./integration-guide.md#73-graphql-operations--typed-hooks) |
34
37
 
35
38
  ## Versioning
36
39
 
37
- These documents track the **latest published versions** — `@rytass/wms-module-graphql@0.3.0` and `@rytass/wms-module-{core,react}@0.1.6` as of this writing. Anything in `main` that has not yet been released is marked clearly inline; otherwise assume the documented signature matches the current npm-published library.
40
+ These documents track the **latest published versions** of the three packages. Anything in `main` that has not yet been released is marked clearly inline; otherwise assume the documented signature matches the current npm-published library.
38
41
 
39
42
  `@rytass/wms-module-graphql@0.3.0` introduced **breaking changes** to `WmsModuleGraphqlModule.forRootAsync` (single argument; consumer now registers their own `GraphQLModule.forRootAsync`). The integration guide and API reference reflect the post-0.3.0 surface — see each package's `CHANGELOG.md` for the full breaking change list when upgrading.
@@ -6,7 +6,7 @@ Complete export catalog for the three publishable libraries:
6
6
  - `@rytass/wms-module-graphql` — GraphQL resolvers, DTOs, DataLoader helpers
7
7
  - `@rytass/wms-module-react` — React components, hooks, Apollo client, GraphQL operations
8
8
 
9
- Current versions: **0.3.0** (`@rytass/wms-module-graphql`), **0.1.6** (`@rytass/wms-module-core`, `@rytass/wms-module-react`).
9
+ 本文件不另記版本號;各 package 的現行版本以其 `package.json` 與 `CHANGELOG.md` 為準。
10
10
 
11
11
  This document mirrors the actual `src/index.ts` barrel exports and is generated by static analysis of the lib sources. For runtime behavior changes between versions, see each package's `CHANGELOG.md`.
12
12
 
@@ -69,7 +69,7 @@ static forRootAsync(
69
69
 
70
70
  ## TypeORM Entities(Extensions — 訂單與作業)
71
71
 
72
- 同樣每個都有 Symbol + EntityClass 雙匯出,共 37 個:
72
+ 同樣每個都有 Symbol + EntityClass 雙匯出(依領域列出,並非逐一窮舉;完整清單見 core 的 `lib/models/extensions/index.ts`):
73
73
 
74
74
  **盤點單 (Inventory Count Order)**
75
75
  - `InventoryCountOrderEntity` / `InventoryCountOrderEntityClass` — 盤點單主檔(`countDate` 為盤差記帳基準日)
@@ -272,7 +272,7 @@ enum ChangeType {
272
272
  VENDOR_TRANSFERRED, VENDOR_RETURNED, HELD, RELEASED,
273
273
  CUSTOMER_HELD, CUSTOMER_RETURNED, SPLITTED, MERGED,
274
274
  ALLOCATED, DEALLOCATED, RECLASSIFIED, SCRAPED,
275
- COUNT_GAINED, COUNT_LOST
275
+ COUNT_GAINED, COUNT_LOST, ISSUED, RETURNED, CONSUMED
276
276
  }
277
277
  ```
278
278
  來源: `lib/constants/stock-status.ts`
@@ -287,7 +287,7 @@ enum InventoryCountStatus { PENDING, COUNTING, COMPLETED, CANCELLED }
287
287
 
288
288
  ### `WmsOperationName` (type) / `WMS_OPERATION_NAMES` (const)
289
289
  ```ts
290
- type WmsOperationName = 'material:read' | 'material:create' | /* …59 項… */;
290
+ type WmsOperationName = 'material:read' | 'material:create' | /* …68 項… */;
291
291
  const WMS_OPERATION_NAMES: readonly WmsOperationName[];
292
292
  ```
293
293
  本套件所有 root operation 的 `` `${resource}:${action}` `` 名稱。同一組字串有三個用途:
@@ -339,6 +339,10 @@ static forRootAsync(
339
339
  // defaultLoaderSerialId: string — 庫存作業預設載具序號
340
340
  // defaultLoaderTypeKey: string — 預設載具類型 key
341
341
  // defaultLocationKey: string — 預設儲存位置 key
342
+ // 選填:tablePrefix、sequenceStorage('sequence' | 'table')、seedDefaults、
343
+ // enrichStockEvents、customChangeTables、事件接線(beforeHooks / afterListeners /
344
+ // actorExtractor / eventEmitterBridge)— 完整說明見 core 的 docs/api-reference.md
345
+ // 「WmsModuleCoreModuleOptions」
342
346
  // + WmsAuthModuleOptions:
343
347
  // authChecker?: WmsAuthChecker — 選填;resolver 授權委派函式(見 Authorization)
344
348
  // + WmsOperationRegistrationOptions(since 0.9.0):
@@ -866,9 +870,7 @@ interface LoaderType<T> {
866
870
  export const createApolloClient: () => ApolloClient<NormalizedCacheObject>
867
871
  ```
868
872
 
869
- As of 0.1.2, this factory takes **no parameters** and returns a fresh instance with `/graphql`
870
- as the endpoint URI (the same as the default singleton). A URL/header override parameter is
871
- planned for 0.2.0 (semver minor, non-breaking addition).
873
+ 目前此工廠**不接受參數**,回傳一個新實例,endpoint 為 `/graphql`(與預設單例相同)。URL / header 覆寫參數尚未提供。
872
874
 
873
875
  #### `ApolloClient` (named export — singleton instance)
874
876
  - Purpose: The pre-built Apollo client singleton created by `createApolloClient()`. Re-exported
@@ -972,7 +974,7 @@ useRoutes(): Array<{
972
974
  }>
973
975
  ```
974
976
  Returns the full WMS sidebar navigation tree. Titles are hardcoded zh-TW strings; there is
975
- no i18n mechanism in 0.1.x (see §13.9 in the integration guide for workarounds).
977
+ no i18n mechanism (see §13.9 in the integration guide for workarounds).
976
978
 
977
979
  #### `useTablePageState(usePageQs?: boolean)`
978
980
  - Source: `hooks/useTablePagination.ts`
@@ -13,6 +13,7 @@ Target audience: engineers integrating WMS into a new product who want a working
13
13
  3. [Quick start (60 seconds)](#3-quick-start-60-seconds)
14
14
  4. [Backend setup (NestJS + GraphQL + TypeORM)](#4-backend-setup-nestjs--graphql--typeorm)
15
15
  - [4.6 Version compatibility matrix](#46-version-compatibility-matrix)
16
+ - [4.7 Module format: CommonJS only](#47-module-format-commonjs-only)
16
17
  5. [Frontend setup (Next.js Pages Router)](#5-frontend-setup-nextjs-pages-router)
17
18
  6. [Pages Router shell pattern (the 19 thin shells)](#6-pages-router-shell-pattern-the-19-thin-shells)
18
19
  7. [Apollo client wiring](#7-apollo-client-wiring)
@@ -296,8 +297,8 @@ WMS_DB_SCHEMA=wms_demo
296
297
  "@nestjs/graphql": "^13.0.0",
297
298
  "@nestjs/platform-express": "^11.0.0",
298
299
  "@nestjs/typeorm": "^11.0.0",
299
- "@rytass/wms-module-core": "^0.1.2",
300
- "@rytass/wms-module-graphql": "^0.1.2",
300
+ "@rytass/wms-module-core": "^0.9.1",
301
+ "@rytass/wms-module-graphql": "^0.11.1",
301
302
  "graphql": "^16.0.0",
302
303
  "pg": "^8.0.0",
303
304
  "reflect-metadata": "^0.2.0",
@@ -356,7 +357,7 @@ because it was exercised.
356
357
 
357
358
  | Package | Accepted (peer range) | Tested | Notes |
358
359
  |-----------------------------|---------------------------|-------------------|-----------------------------------------------------------------------------|
359
- | `@nestjs/common` / `core` | 10.x – 11.x | 11.1.x | **12.x is not accepted**: the whole NestJS 12 line is ESM-only, which this package (dual CJS+ESM) and a webpack-bundled host cannot consume yet |
360
+ | `@nestjs/common` / `core` | 10.x – 11.x | 11.1.x | **12.x is not accepted**: the NestJS 12 line is ESM-only, and this package ships CommonJS only and has not been tested against it (see §4.7) |
360
361
  | `@nestjs/graphql` | 13.x | 13.4.5 | 14.x requires NestJS 12 (ESM-only) |
361
362
  | `@nestjs/apollo` | 13.x | 13.4.5 | The lib itself imports neither `@nestjs/apollo` nor `@apollo/server`; the peer records that a driver is required |
362
363
  | `@apollo/server` | not a peer | 5.5.1 | Chosen by your driver, not by this package |
@@ -382,6 +383,19 @@ because it was exercised.
382
383
 
383
384
  ---
384
385
 
386
+ ### 4.7 Module format: CommonJS only
387
+
388
+ `@rytass/wms-module-graphql` ships one CommonJS file (`index.cjs.js`), and every condition in its
389
+ `exports` points at it. There is no ESM build, on purpose. The package exports DI tokens
390
+ (`Symbol`s) and entity classes, and both are identities: if one process loads it through two
391
+ module records, it gets two copies of each, and a provider registered under one copy's token is
392
+ invisible to a lookup holding the other. The same reasoning applies to `@rytass/wms-module-core`
393
+ (see its integration guide §4.7), which owns the tokens and entities this package consumes.
394
+
395
+ ESM consumers lose nothing. Node reads named exports out of CommonJS through `cjs-module-lexer`,
396
+ so named `import` statements work as written. The NestJS 12 line is ESM-only; it is outside the
397
+ accepted peer range (see §4.6).
398
+
385
399
  ## 5. Frontend setup (Next.js Pages Router)
386
400
 
387
401
  The frontend package targets **Next.js Pages Router**, not App Router. The pre-built page bodies use SSR-incompatible providers and must be loaded on the client.
@@ -522,7 +536,7 @@ be listed in `transpilePackages`. Install it as a regular production dependency:
522
536
  pnpm add @rytass/wms-map-react-components
523
537
  ```
524
538
 
525
- Current published version: `^0.1.0`. Registry: `https://registry.npmjs.org/`.
539
+ Check the latest published version with `npm view @rytass/wms-map-react-components version`. Registry: `https://registry.npmjs.org/`.
526
540
 
527
541
  ### 5.4 `apps/client/package.json` essentials
528
542
 
@@ -542,8 +556,8 @@ Current published version: `^0.1.0`. Registry: `https://registry.npmjs.org/`.
542
556
  "@mezzanine-ui/icons": "^1.0.0",
543
557
  "@mezzanine-ui/react": "^1.0.0",
544
558
  "@mezzanine-ui/system": "^1.0.0",
545
- "@rytass/wms-map-react-components": "^0.1.0",
546
- "@rytass/wms-module-react": "^0.1.2",
559
+ "@rytass/wms-map-react-components": "^0.1.10",
560
+ "@rytass/wms-module-react": "^0.5.1",
547
561
  "@xyflow/react": "^12.0.0",
548
562
  "graphql": "^16.0.0",
549
563
  "lodash": "^4.17.0",
@@ -670,10 +684,10 @@ import { createApolloClient } from '@rytass/wms-module-react';
670
684
  const client = createApolloClient();
671
685
  ```
672
686
 
673
- As of **0.1.2**, `createApolloClient()` takes **no parameters** and returns a fresh
687
+ `createApolloClient()` takes **no parameters** and returns a fresh
674
688
  `ApolloClient<NormalizedCacheObject>` instance configured with the same `/graphql` URI as
675
- the default singleton. A URL/header override parameter is planned for **0.2.0** (will be a
676
- non-breaking addition). See [API Reference - createApolloClient](./api-reference.md).
689
+ the default singleton. A URL/header override parameter is planned as a
690
+ non-breaking addition, and is not available yet. See [API Reference - createApolloClient](./api-reference.md).
677
691
 
678
692
  ### 7.3 GraphQL operations & DocumentNode constants
679
693
 
@@ -1005,7 +1019,10 @@ The entities are designed for PostgreSQL:
1005
1019
 
1006
1020
  - `timestamptz` columns map to native `timestamp with time zone`
1007
1021
  - `bool` maps to PG `boolean`
1008
- - Sequence-based serial IDs (e.g. loader.serialId) use PG `CURRVAL`
1022
+ - Serial IDs (order keys, loader serials) come from `WmsSequenceService`; `CURRVAL` is not used. `sequenceStorage` selects one of two modes:
1023
+ - `'sequence'` (default): one PostgreSQL sequence per counter. The first use of a counter in a process runs `CREATE SEQUENCE IF NOT EXISTS` on a separate connection in autocommit, so a rollback cannot drop it and other callers are not blocked. Consecutive runs (loader serials) hold a transaction-scoped advisory lock on the counter.
1024
+ - `'table'`: one row per counter in the `sequences` table, advanced by a single `INSERT … ON CONFLICT … RETURNING` statement. No DDL is run.
1025
+ - In both modes a `maxValue` bound refuses a reservation before the counter moves.
1009
1026
  - `synchronize: true` works for dev/demo; use proper migrations in production
1010
1027
 
1011
1028
  This is the only database where the lib runs without any consumer-side workaround.
@@ -1023,7 +1040,7 @@ If you must run against SQLite (e.g. for local prototyping without a PG server):
1023
1040
 
1024
1041
  At the disk layer SQLite is liberal — it accepts any column type string and gives it NUMERIC affinity — so once the validator is past, schema creation works.
1025
1042
 
1026
- 2. **`CURRVAL(seq_name)` PostgreSQL function** — used internally to compute loader serial IDs. SQLite has no equivalent. All `createLoader` mutations will fail with `no such function: CURRVAL`. There is no clean consumer-side workaround; running real loader creation flows on SQLite is not supported as of 0.1.2.
1043
+ 2. **Sequence reservation** — order keys and loader serials come from `WmsSequenceService`, which emits PostgreSQL-specific SQL in both storage modes. `sequenceStorage: 'sequence'` (default) uses `CREATE SEQUENCE`, `nextval` / `setval`, `to_regclass` and `pg_advisory_xact_lock`; `sequenceStorage: 'table'` avoids DDL but still uses the PostgreSQL `::bigint` cast. Neither mode runs on SQLite, so running `createLoader` or any order-creation flow on SQLite is not supported.
1027
1044
 
1028
1045
  3. **DateTime serialize round-trip** — SQLite returns datetime strings in `YYYY-MM-DD HH:MM:SS` format. The GraphQL DateTime scalar may emit `null` for some columns; the consumer-visible effect is blank `createdAt` fields in some payloads.
1029
1046
 
@@ -1545,7 +1562,7 @@ the navigation chrome with your own.
1545
1562
 
1546
1563
  ### 13.9 i18n: Current Limitations and Workarounds
1547
1564
 
1548
- **Status**: No i18n support in 0.1.x. All user-visible strings in the lib are hardcoded
1565
+ **Status**: No i18n support. All user-visible strings in the lib are hardcoded
1549
1566
  Traditional Chinese (zh-TW).
1550
1567
 
1551
1568
  Affected areas:
@@ -1596,13 +1613,13 @@ WMS page components (`MaterialsManagement`, `InventoryList`, etc.) are designed
1596
1613
  **full-viewport rendering**. They use `PageWrapper` and `SectionWrapper` internally which
1597
1614
  set `width: 100%` and `height: 100%` of the viewport.
1598
1615
 
1599
- **Embedding in a tab, panel, or drawer is not officially supported in 0.1.x.** Known issues
1616
+ **Embedding in a tab, panel, or drawer is not officially supported.** Known issues
1600
1617
  when embedded in a constrained container:
1601
1618
  - Mezzanine Table column-width calculations use the viewport width
1602
1619
  - Modal/dialog portals render at document body, not relative to the container
1603
1620
  - React Flow (used by Maps and LocationsManagement map tab) requires a fixed-height container
1604
1621
 
1605
- There is no clean workaround in 0.1.x. If you need sectional embedding, fork the lib and
1622
+ There is no clean workaround. If you need sectional embedding, fork the lib and
1606
1623
  modify the `PageWrapper`/`SectionWrapper` styles to be container-relative.
1607
1624
 
1608
1625
  ---
@@ -1734,7 +1751,7 @@ export default defineConfig({
1734
1751
  4. `@mezzanine-ui/react` imports `lodash/compact` without a `.js` extension, which may
1735
1752
  require `resolve.alias` configuration in Vite.
1736
1753
 
1737
- No official Vite setup guide exists for 0.1.x.
1754
+ No official Vite setup guide exists.
1738
1755
 
1739
1756
  ---
1740
1757
 
@@ -1788,7 +1805,7 @@ same `@nestjs/*` major version. Options:
1788
1805
  ```yaml
1789
1806
  # pnpm-workspace.yaml
1790
1807
  overrides:
1791
- '@rytass/wms-module-core': '0.1.4'
1808
+ '@rytass/wms-module-core': '0.9.1'
1792
1809
  ```
1793
1810
 
1794
1811
  3. Apply `public-hoist-pattern[]` for `@rytass/wms-module-*` in `.npmrc`
@@ -1817,7 +1834,7 @@ There is no name-prefixing or namespace configuration in the lib's DTOs. This co
1817
1834
  affects **all** GraphQL types in the WMS schema: ObjectTypes, InputTypes, Enums, and any
1818
1835
  union types.
1819
1836
 
1820
- The viable options are the same as those described in §13.1:
1837
+ The viable options are:
1821
1838
 
1822
1839
  1. **Run WMS as a separate NestJS process** (recommended) — isolates schemas entirely;
1823
1840
  no type name collisions possible.
@@ -0,0 +1,372 @@
1
+ # 盤點(Inventory Count)API 使用說明
2
+
3
+ > 對象:串接 `@rytass/wms-module-graphql` 的前後端開發者。本文逐一說明盤點的
4
+ > 3 支 query 與 9 支 mutation 的用途、參數、前置狀態、錯誤與範例;鎖定與事件的設計理由
5
+ > 請另見 [`README.md`「Inventory counting」](../README.md#inventory-counting) 與
6
+ > [`events.md`](./events.md)。
7
+
8
+ ## 1. 概觀
9
+
10
+ 盤點是「拿帳面對照現場、再把帳面修正成現場」的流程。三個前提決定了所有 API 的行為:
11
+
12
+ - **盤點不搬貨。** 只在原位調整數量,位置錯了是調撥(`createTransferOrder`)的事。
13
+ - **執行盤差之前都不動庫存。** 盤點數量只是暫存的對帳資料;唯一會寫庫存的是
14
+ `settleInventoryCount`,而且在單一 transaction 內完成。
15
+ - **開始盤點即鎖定範圍。** `startInventoryCount` 快照帳面並鎖住範圍內(含所有下層)
16
+ 儲位;期間任何碰到這些儲位的庫存異動(含調撥**進入**該儲位)都會被 `409 Conflict`
17
+ 擋下,直到結算或取消。
18
+
19
+ ### 1.1 狀態機
20
+
21
+ ```
22
+ createInventoryCountOrder ──▶ PENDING(尚未執行)
23
+ │ updateInventoryCountOrder(僅此狀態可改 Info)
24
+ ▼ startInventoryCount
25
+ COUNTING(盤點中)── countInventory / addInventoryCountItem /
26
+ │ removeInventoryCountItem / resetInventoryCount
27
+ ├── settleInventoryCount ──▶ COMPLETED(已完成)
28
+ └── cancelInventoryCount ──▶ CANCELLED(已取消) ◀── 也可從 PENDING 取消
29
+ ```
30
+
31
+ `COMPLETED` 與 `CANCELLED` 是終態,任何 mutation 都會回 `400`。
32
+
33
+ ### 1.2 每支 mutation 都是一個 transaction
34
+
35
+ 建立/更新/開始/暫存/新增列/移除列/清空/取消/結算各自整體成功或整體回滾,
36
+ 不會留下半張單。宿主直接呼叫 `InventoryCountOrderService` 時,每個方法最後一個參數
37
+ 都可傳入自己的 `EntityManager` 以併入既有 transaction(見 §6)。
38
+
39
+ ### 1.3 權限名稱與部署開關
40
+
41
+ 每支操作都掛 `@WmsOperation`,名稱即 `authChecker` 收到的字串,也是
42
+ `forRootAsync({ enabledOperations / disabledOperations })` 可開關的單位:
43
+
44
+ | 操作 | 名稱 |
45
+ | --- | --- |
46
+ | `inventoryCountOrders`、`inventoryCountOrder` | `inventory-count-order:read` |
47
+ | `inventoryCountOrderItems` | `inventory-count-order:read-items` |
48
+ | `createInventoryCountOrder` | `inventory-count-order:create` |
49
+ | `updateInventoryCountOrder` | `inventory-count-order:update` |
50
+ | `startInventoryCount` | `inventory-count-order:start` |
51
+ | `countInventory` | `inventory-count-order:count` |
52
+ | `addInventoryCountItem` | `inventory-count-order:add-item` |
53
+ | `removeInventoryCountItem` | `inventory-count-order:remove-item` |
54
+ | `resetInventoryCount` | `inventory-count-order:reset` |
55
+ | `cancelInventoryCount` | `inventory-count-order:cancel` |
56
+ | `settleInventoryCount` | `inventory-count-order:settle` |
57
+
58
+ ## 2. 型別
59
+
60
+ ### `InventoryCountOrderDto`(盤點單)
61
+
62
+ | 欄位 | 型別 | 說明 |
63
+ | --- | --- | --- |
64
+ | `id` | `ID!` | |
65
+ | `key` | `String!` | 盤點單號(系統流水號) |
66
+ | `name` | `String!` | 名稱 |
67
+ | `countDate` | `String!` | 盤點日期 `YYYY-MM-DD`,是**基準日**:盤差對外以此日期呈現,`createdAt` 保留真實建立時間 |
68
+ | `status` | `InventoryCountStatus!` | `PENDING` 尚未執行/`COUNTING` 盤點中/`COMPLETED` 已完成/`CANCELLED` 已取消 |
69
+ | `createdBy` | `String` | 建立者;由 `createInventoryCountOrder` 的 `createdBy` 寫入 |
70
+ | `startedAt` `settledAt` `cancelledAt` | `DateTime` | 對應狀態轉換的時間 |
71
+ | `createdAt` | `DateTime!` | |
72
+ | `locations` | `[LocationDto!]!` | 使用者**選的根儲位**;下層展開不在此欄(但會被鎖定) |
73
+ | `items` | `[InventoryCountOrderItemDto!]!` | **整張**盤點清單,不分頁。表格請改用 `inventoryCountOrderItems`;只有需要全量(匯出、上傳比對)時才選這個欄位 |
74
+
75
+ ### `InventoryCountOrderItemDto`(盤點清單的一列)
76
+
77
+ | 欄位 | 型別 | 說明 |
78
+ | --- | --- | --- |
79
+ | `id` `orderId` `materialId` `batchId` | `ID!` | |
80
+ | `locationId` `loaderId` | `ID` | 位置;`loaderId` 為 null 代表預設載具 |
81
+ | `unitId` | `ID` | 盤點數量使用的單位,是 **`MaterialUnitDto.id`**;null 代表基本單位 |
82
+ | `bookQuantity` | `Float!` | 帳面數量,**基本單位**,開始盤點時凍結 |
83
+ | `countedQuantity` | `Float` | 盤點數量,以 `unitId` 的單位計;null 代表尚未盤 |
84
+ | `isAddition` | `Boolean!` | `true` 為盤點中手動新增的盤盈列(`bookQuantity` 必為 0,且是唯一可刪的列) |
85
+ | `held` `inShipment` `inspectionResult` `requiresInspection` `transferId` | 旗標 | 開始盤點當下從 batch **快照**而來:凍結/出貨中/檢驗結果/是否需檢驗/調撥中。開始前為 null |
86
+ | `vendorId` `customerId` `saleId` `saleSerialId` | `String` | 同上,供應商/客戶/銷售訂單號碼/項次的快照 |
87
+ | `material` `batch` `location` `loader` | 關聯 | DataLoader 解析;`material.materialUnits` 可用來組單位下拉與換算 |
88
+
89
+ > 為什麼快照而不是即時解析:batch 在盤點結束後仍會變動,若即時解析,已完成的盤點單
90
+ > 會在幾個月後自己改寫。盤點中儲位已鎖,快照與即時值必然相同。
91
+
92
+ ### 分頁型別
93
+
94
+ `InventoryCountOrders` 與 `InventoryCountOrderItems` 都是 `{ id, items, total }`。
95
+
96
+ ## 3. Query
97
+
98
+ ### 3.1 `inventoryCountOrders` — 盤點列表
99
+
100
+ ```graphql
101
+ query {
102
+ inventoryCountOrders(
103
+ offset: 0
104
+ limit: 10
105
+ name: "例行盤點" # ILIKE 模糊比對
106
+ statuses: [PENDING, COUNTING]
107
+ createdBy: "Mark Wu" # 完全比對
108
+ countDateFrom: "2025-01-01" # 含
109
+ countDateTo: "2025-12-31" # 含
110
+ locationIds: ["<locationId>"] # 比對「展開後」的範圍:篩某層架會找到盤整條走道的單
111
+ ) {
112
+ total
113
+ items { id key name countDate status createdBy locations { id key } }
114
+ }
115
+ }
116
+ ```
117
+
118
+ - 排序固定 `countDate DESC, createdAt DESC`。
119
+ - 全部參數可省略;`offset < 0` 或 `limit <= 0` 視為不分頁。
120
+
121
+ ### 3.2 `inventoryCountOrder` — 單張盤點單
122
+
123
+ ```graphql
124
+ query {
125
+ inventoryCountOrder(id: "<orderId>") {
126
+ id key name countDate status createdBy startedAt settledAt cancelledAt
127
+ locations { id key }
128
+ # items { ... } ← 只有匯出/上傳比對需要整張清單時才選
129
+ }
130
+ }
131
+ ```
132
+
133
+ - 查無此單回 `404 NotFoundException`。
134
+
135
+ ### 3.3 `inventoryCountOrderItems` — 盤點清單(分頁+篩選)
136
+
137
+ ```graphql
138
+ query {
139
+ inventoryCountOrderItems(
140
+ id: "<orderId>"
141
+ offset: 0
142
+ limit: 20
143
+ materialKey: "WATER" # 以下五個為 ILIKE 模糊比對
144
+ materialDescription: "礦泉水"
145
+ batchKey: "2510"
146
+ locationKey: "1000A1B"
147
+ loaderSerialId: "BY00"
148
+ vendorId: "V001" # 以下四個為完全比對(比對列上的快照值)
149
+ customerId: "C001"
150
+ saleId: "SO240596827"
151
+ saleSerialId: "00100043"
152
+ onlyVariance: true # 只回「已暫存的盤點量 ≠ 帳面」的列
153
+ ) {
154
+ total
155
+ items {
156
+ id bookQuantity countedQuantity unitId isAddition
157
+ held inShipment inspectionResult requiresInspection transferId
158
+ vendorId customerId saleId saleSerialId
159
+ material { id key description baseUnit { id key } materialUnits { id unit conversionFactor } }
160
+ batch { id key }
161
+ location { id key }
162
+ loader { id serialId }
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ - 排序:新增的盤盈列優先,其後依料號、批號、id,分頁不會重複或漏列。
169
+ - `onlyVariance` 的比較會先把 `countedQuantity` 以 `unitId` 的換算係數換成基本單位,
170
+ 與結算用的是同一套換算;**只看已暫存(`countInventory`)的值**,前端尚未送出的輸入
171
+ 不在其中,切換前請先暫存。
172
+ - 需要整張清單(例如下載盤點清單)時,請用 `inventoryCountOrder.items`,不要用大
173
+ `limit` 翻頁拼湊。
174
+
175
+ ## 4. Mutation
176
+
177
+ 共通規則:
178
+
179
+ - 單據不存在 → `404`;狀態不符 → `400`,訊息指出目前狀態與要求的狀態。
180
+ - 每支都在一個 transaction 內;失敗即整體回滾。
181
+ - 每支都會發出對應的操作事件(§5),`beforeHooks` 丟出例外即可否決。
182
+
183
+ ### 4.1 `createInventoryCountOrder` — 建立盤點
184
+
185
+ ```graphql
186
+ mutation {
187
+ createInventoryCountOrder(
188
+ name: "2025-上半年度例行盤點"
189
+ countDate: "2025-12-25"
190
+ locationIds: ["<rootLocationId>", "..."] # 必填、至少一個;下層全部自動納入
191
+ createdBy: "Mark Wu" # 選填;列表「建立者」與其篩選即讀此欄
192
+ ) { id key status }
193
+ }
194
+ ```
195
+
196
+ - 事件:`inventory-count-order.created`。
197
+ - 建立時就把範圍展開落地,並依目前帳面產生第一版清單(`PENDING` 狀態下可重算)。
198
+ - `locationIds` 為空 → `400`。
199
+
200
+ ### 4.2 `updateInventoryCountOrder` — 修改 Info(僅尚未執行)
201
+
202
+ ```graphql
203
+ mutation {
204
+ updateInventoryCountOrder(
205
+ id: "<orderId>"
206
+ name: "新名稱" # 以下三個皆選填
207
+ countDate: "2025-12-26"
208
+ locationIds: ["<rootLocationId>"]
209
+ ) { id name countDate locations { key } }
210
+ }
211
+ ```
212
+
213
+ - 事件:`inventory-count-order.updated`。
214
+ - 僅 `PENDING`;開始盤點後 Info 唯讀。
215
+ - 傳了 `locationIds` 會重新展開範圍並**重建清單**(帳面列會依新範圍增減,盤盈列不動);
216
+ 傳空陣列 → `400`。
217
+
218
+ ### 4.3 `startInventoryCount` — 開始盤點
219
+
220
+ ```graphql
221
+ mutation { startInventoryCount(id: "<orderId>") { id status startedAt } }
222
+ ```
223
+
224
+ - 事件:`inventory-count-order.started`。
225
+ - `PENDING → COUNTING`。做兩件事,且在同一個 transaction 內:
226
+ 1. **快照**:重算每列 `bookQuantity`,並把 batch 的九個旗標寫到列上;
227
+ 2. **鎖定**:範圍內(含下層)儲位自此拒絕所有庫存異動,直到結算或取消。
228
+ - 建議前端先跳確認框(「系統將鎖定盤點範圍的庫存禁止異動行為…」),因為鎖定會影響
229
+ 其他正在作業的人。
230
+
231
+ ### 4.4 `countInventory` — 暫存結果
232
+
233
+ ```graphql
234
+ mutation {
235
+ countInventory(
236
+ id: "<orderId>"
237
+ items: [
238
+ { itemId: "<itemId>", countedQuantity: 94 } # 基本單位
239
+ { itemId: "<itemId>", countedQuantity: 8, unitId: "<materialUnitId>" } # 指定單位
240
+ { itemId: "<itemId>", countedQuantity: null } # 清空,回到「尚未盤」
241
+ ]
242
+ ) { id }
243
+ }
244
+ ```
245
+
246
+ - 事件:`inventory-count-order.counted`。
247
+ - 僅 `COUNTING`。**不動庫存**,只把數量與單位存到列上,可分多次、多人呼叫。
248
+ - `countedQuantity`:省略或 `null` 清空;負數或非數字 → `400`。
249
+ - `unitId`:省略=維持原單位;`null`=基本單位;給值=該料號的 `MaterialUnitDto.id`。
250
+ - `itemId` 不屬於此單 → `404`。空陣列為 no-op。
251
+
252
+ ### 4.5 `addInventoryCountItem` — 新增庫存(盤盈列)
253
+
254
+ ```graphql
255
+ mutation {
256
+ addInventoryCountItem(
257
+ id: "<orderId>"
258
+ materialId: "<materialId>"
259
+ batchKey: "2510310042" # 既有批號或新批號;不存在會立即建立
260
+ locationId: "<locationId>" # 必須在盤點範圍內
261
+ loaderId: "<loaderId>" # 選填;null 代表預設載具
262
+ countedQuantity: 100 # 必須 > 0
263
+ unitId: "<materialUnitId>" # 選填;null 代表基本單位
264
+ ) { id isAddition bookQuantity countedQuantity batch { key } }
265
+ }
266
+ ```
267
+
268
+ - 事件:`inventory-count-order.updated`(回傳的是**該列** `InventoryCountOrderItemDto`)。
269
+ - 僅 `COUNTING`。新列 `bookQuantity = 0`、`isAddition = true`,旗標從它的 batch 快照
270
+ (全新批號即預設值:未凍結、未調撥、未檢驗)。
271
+ - 錯誤(皆 `400`):`countedQuantity <= 0`;`locationId` 不在範圍內;該 position
272
+ (料號+批號+儲位+載具,載具以解析後的值比對)已經在清單上——請直接在那一列填數量,
273
+ 否則結算會對同一批庫存調整兩次。
274
+
275
+ ### 4.6 `removeInventoryCountItem` — 刪除盤盈列
276
+
277
+ ```graphql
278
+ mutation { removeInventoryCountItem(id: "<orderId>", itemId: "<itemId>") { id } }
279
+ ```
280
+
281
+ - 事件:`inventory-count-order.updated`。
282
+ - 僅 `COUNTING`,且只能刪 `isAddition = true` 的列;帳面列一律鎖定 → `400`。
283
+
284
+ ### 4.7 `resetInventoryCount` — 清空重填
285
+
286
+ ```graphql
287
+ mutation { resetInventoryCount(id: "<orderId>") { id status } }
288
+ ```
289
+
290
+ - 事件:`inventory-count-order.reset`。
291
+ - 僅 `COUNTING`。清空所有列的 `countedQuantity` 與 `unitId`,並刪除所有盤盈列;
292
+ **狀態維持 `COUNTING`、鎖定不解除、帳面快照不重取**(儲位鎖著,帳面本來就沒變)。
293
+ - 會丟掉現場已填資料,前端應加確認框。
294
+
295
+ ### 4.8 `cancelInventoryCount` — 取消盤點
296
+
297
+ ```graphql
298
+ mutation { cancelInventoryCount(id: "<orderId>") { id status cancelledAt } }
299
+ ```
300
+
301
+ - 事件:`inventory-count-order.cancelled`。
302
+ - `PENDING` 或 `COUNTING → CANCELLED`,**解除鎖定**;暫存的數量留在列上供查閱,但不會
303
+ 寫入庫存。
304
+
305
+ ### 4.9 `settleInventoryCount` — 執行盤差
306
+
307
+ ```graphql
308
+ mutation { settleInventoryCount(id: "<orderId>") { id status settledAt } }
309
+ ```
310
+
311
+ - 事件:`inventory-count-order.settled`;每一列有差異者另發 `stock.count-gained`
312
+ (盤盈)或 `stock.count-lost`(盤虧)。
313
+ - 僅 `COUNTING → COMPLETED`,單一 transaction,順序如下:
314
+ 1. **重驗快照**:每一帳面列的 `bookQuantity` 必須仍等於當下帳面。不等代表有東西繞過
315
+ 鎖寫了庫存(例如直接寫 `stocks`),此時回 `409 Conflict`、不寫任何東西;請取消後重盤。
316
+ 2. 逐列計算 `delta = countedQuantity × 換算係數 − bookQuantity`:
317
+ - `countedQuantity` 為 null(沒盤)→ **跳過,不當作 0**;
318
+ - `delta = 0` → 不寫;
319
+ - 否則寫一列 `inventory_count_order_changes` 與一列 `stocks`(`+` 盤盈/`−` 盤虧),
320
+ 位置為該列的儲位與載具(null 載具解析為預設載具)。
321
+ 3. 更新狀態與 `settledAt`,鎖定隨之解除。
322
+ - 前端送出前請先 `countInventory` 把畫面上的輸入全部暫存,結算只看已存的值;結算期間
323
+ 建議鎖住整頁(不可離開/取消)。
324
+
325
+ ## 5. 事件
326
+
327
+ | 事件 | 觸發 | `result` 型別 |
328
+ | --- | --- | --- |
329
+ | `inventory-count-order.created` | create | `InventoryCountOrderDto` |
330
+ | `inventory-count-order.updated` | update/add-item/remove-item | `InventoryCountOrderDto` 或 `InventoryCountOrderItemDto`(add-item) |
331
+ | `inventory-count-order.started` | start | `InventoryCountOrderDto` |
332
+ | `inventory-count-order.counted` | count | `InventoryCountOrderDto` |
333
+ | `inventory-count-order.reset` | reset | `InventoryCountOrderDto` |
334
+ | `inventory-count-order.cancelled` | cancel | `InventoryCountOrderDto` |
335
+ | `inventory-count-order.settled` | settle | `InventoryCountOrderDto`;另有 `stock.count-gained` / `stock.count-lost` |
336
+
337
+ 事件在最外層 transaction commit **之後**才送出;`beforeHooks` 在任何寫入之前執行,丟出
338
+ 例外即否決。詳見 [`events.md`](./events.md)。
339
+
340
+ ## 6. 從宿主程式直接呼叫
341
+
342
+ GraphQL 之外,宿主也可注入 `InventoryCountOrderService`(`@rytass/wms-module-core`):
343
+ 方法與 mutation 一一對應,最後一個參數可傳 `EntityManager` 併入既有 transaction。
344
+
345
+ ```ts
346
+ await dataSource.transaction(async (manager) => {
347
+ const order = await counts.create(
348
+ { name: '夜間盤點', countDate: '2026-01-31', locationIds: [aisleId], createdBy: 'batch-job' },
349
+ manager,
350
+ );
351
+ await counts.start(order.id, manager);
352
+ });
353
+
354
+ await counts.count(order.id, [{ itemId, countedQuantity: 94 }]);
355
+ await counts.settle(order.id); // 自己開 transaction
356
+ ```
357
+
358
+ 直接呼叫一樣受鎖定保護(鎖在 `*_order_change` 的 insert 上,不在 resolver),事件也一樣
359
+ 會發出(`WmsOperationRunner`)。
360
+
361
+ ## 7. 被鎖定時會看到什麼
362
+
363
+ 盤點中,對範圍內儲位的任何庫存異動——出貨、調撥(來源**或目的**在範圍內)、報廢、
364
+ 凍結、拆併批……——都會收到:
365
+
366
+ ```
367
+ 409 Conflict
368
+ Location <locationId> is locked by inventory count order <key>;
369
+ finish or cancel that count before moving this stock.
370
+ ```
371
+
372
+ 訊息指名單號,現場即可找到該單去結算或取消。