platform-types 1.10.709 → 1.10.711

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.
@@ -124,7 +124,16 @@ exports.isOverCycleCountToleranceRule = isOverCycleCountToleranceRule;
124
124
  * - `CYCLE_COUNT_VERSION_CONFLICT` · 409 — a cancel or finalise that lost the
125
125
  * compare-and-set on the session row. ⚠️ On finalise it is CONDITIONAL: the
126
126
  * handler re-reads consistently, and a winner that FINALISED the session makes
127
- * it a `200` replay instead; only a concurrent CANCEL surfaces as this.
127
+ * it a `200` replay instead; only a concurrent CANCEL surfaces as this. Never
128
+ * answered to `mode: 'edit'`.
129
+ * - `CYCLE_COUNT_CONTENT_CONFLICT` · 409 — the session is still `open` but no
130
+ * longer at the revision the request was composed against; top-level
131
+ * `currentUpdatedAt` is the revision to re-send after re-reviewing, and
132
+ * nothing was written. ONE meaning in both modes: a finalise whose lines or
133
+ * tolerance moved (or whose `expectedUpdatedAt` was stale on arrival), and an
134
+ * edit whose `expectedUpdatedAt` is stale — on arrival or overtaken before
135
+ * the write — or a token-less edit overtaken on both of its attempts. On an
136
+ * edit `data` is {@link CycleCountEditConflict}.
128
137
  * - `CYCLE_COUNT_TOLERANCE_EXCEEDED` · 403 — a finalise with at least one
129
138
  * over-tolerance line, from a caller who is neither ADMIN nor MANAGER. The
130
139
  * session stays `open` and NO stock moved.
@@ -187,12 +196,13 @@ exports.CYCLE_COUNT_ERROR_CODES = [
187
196
  'CYCLE_COUNT_NOT_OPEN',
188
197
  'CYCLE_COUNT_ILLEGAL_TRANSITION',
189
198
  'CYCLE_COUNT_VERSION_CONFLICT',
190
- // Finalise refuses a session whose COUNTED CONTENT moved since the caller's
191
- // read, distinct from the row-version conflict above: the caller's revision
192
- // is stale, and committing it would finalise a count nobody took. Answered
193
- // by `cycle-counts/_finalize.ts`; landed one PR after this list was
194
- // published, and the guard test that keeps this list honest caught it on
195
- // `main` rather than in either PR's own gate.
199
+ // The session is still open but its revision moved since the caller's read,
200
+ // distinct from the row-version conflict above (the session went terminal):
201
+ // committing would finalise a count nobody took, or overwrite lines nobody
202
+ // reviewed. Answered by `cycle-counts/_finalize.ts` and `_edit.ts`. The
203
+ // finalise case landed one PR after this list was published, and the guard
204
+ // test that keeps this list honest caught it on `main` rather than in either
205
+ // PR's own gate.
196
206
  'CYCLE_COUNT_CONTENT_CONFLICT',
197
207
  'CYCLE_COUNT_TOLERANCE_EXCEEDED',
198
208
  // Finalise refuses a line that is neither counted nor skipped rather than
@@ -40,10 +40,13 @@ exports.PURCHASE_RECEIPT_MAX_LINES_DEPOSITOS = 32;
40
40
  * - `STOCK_INCOME_RACED` · 409 — the ledger-key retry budget ran out. Nothing moved.
41
41
  * - `PRODUCT_NOT_FOUND` · 404 — a line's product no longer exists. Nothing moved.
42
42
  * - `PURCHASE_RECEIPT_NOT_FOUND` · 404 — `mode=receipt` addressed nothing.
43
+ * - `PURCHASE_RECEIPT_KEY_CONFLICT` · 409 — the `receiptKey` already booked a
44
+ * receipt from a different body. Nothing moved; a new delivery needs a new key.
43
45
  * - `INVALID_CURSOR` · 400
44
46
  *
45
- * A replay of a `receiptKey` that already committed is NOT a refusal: it answers
46
- * 200 with the stored receipt and `replayed: true`.
47
+ * A replay of a `receiptKey` that already committed with the SAME body is NOT a
48
+ * refusal: it answers 200 with the stored receipt and `replayed: true`. A receipt
49
+ * stored with no `requestFingerprint` replays whatever the body.
47
50
  *
48
51
  * ⚠️ Cross-cutting refusals are NOT repeated here — check `GATING_ERROR_CODES`
49
52
  * as well.
@@ -187,8 +187,9 @@ declare global {
187
187
  /** Unix ms the session was opened. */
188
188
  createdAt: number;
189
189
  /**
190
- * Unix ms of the last write. ABSENT on a session never revised since it
191
- * was opened — this platform is forward-only and nothing backfills it.
190
+ * Unix ms of the last write. Stamped at creation too, so ABSENT only on a
191
+ * legacy row written before that — this platform is forward-only and
192
+ * nothing backfills it.
192
193
  *
193
194
  * ⚠️ **`updatedAt ?? createdAt` is the session's VERSION TOKEN**, and
194
195
  * `POST /cycle-counts { mode: 'finalize' }` accepts it as the OPTIONAL
@@ -211,8 +212,28 @@ declare global {
211
212
  * an already-`finalized` session, deliberately: a consumer that collapses
212
213
  * the three cannot tell "re-confirm" from "it is over" from "it already
213
214
  * landed".
215
+ *
216
+ * ⚠️ `mode: 'edit'` accepts the same token as its OPTIONAL
217
+ * `expectedUpdatedAt` ({@link CycleCountEditRequest}), refusing a stale one on
218
+ * a still-`open` session with the SAME `409 CYCLE_COUNT_CONTENT_CONFLICT` and
219
+ * top-level `currentUpdatedAt`, plus the current row as `data.current` — so
220
+ * one client handler serves both modes.
214
221
  */
215
222
  updatedAt?: number;
223
+ /**
224
+ * The operator whose `mode: 'edit'` wrote the current revision, stamped by
225
+ * the server on EVERY edit. ABSENT on a session never edited since it was
226
+ * opened (and on every edit made before this field existed — nothing is
227
+ * backfilled). Only `mode: 'edit'` writes it: a finalise records
228
+ * `finalizedBy`, and a cancel leaves it as it was.
229
+ *
230
+ * Read it on an edit's `409 CYCLE_COUNT_CONTENT_CONFLICT` `data.current`: own
231
+ * actor plus every field the caller sent being identical is CONSISTENT
232
+ * with the caller's own earlier edit having landed — never proof of it,
233
+ * since this is a user id and the same operator on a second device reads
234
+ * the same.
235
+ */
236
+ updatedBy?: string;
216
237
  /** The operator who opened it. */
217
238
  userId: string;
218
239
  status: CycleCountStatus;
@@ -472,6 +493,55 @@ declare global {
472
493
  /** Why the line was skipped. Only with `skippedAt`. ⚠️ Never personal data — same un-scrubbed, TTL-less row as `note`. */
473
494
  skippedReason?: string;
474
495
  }
496
+ /**
497
+ * `POST /cycle-counts { mode: 'edit' }` — revise an OPEN session. At least one
498
+ * of `items`, `dated`, `name`, `zone`, `notes`, `tolerancePercent` is
499
+ * required; `expectedUpdatedAt` alone is refused 400.
500
+ *
501
+ * ⚠️ `items` is a WHOLE-ARRAY replace, never a per-line merge.
502
+ */
503
+ interface CycleCountEditRequest {
504
+ mode: 'edit';
505
+ cycleCountId: string;
506
+ items?: CycleCountItem[];
507
+ /** `YYYYMMDD`, as a string or a number. */
508
+ dated?: string | number;
509
+ name?: string;
510
+ zone?: string;
511
+ notes?: string;
512
+ /** `null` CLEARS the per-count rung, returning the session to the store default. */
513
+ tolerancePercent?: number | null;
514
+ /**
515
+ * The revision the operator REVIEWED — {@link CycleCount.updatedAt} `??`
516
+ * {@link CycleCount.createdAt} of the row the screen shows, read from
517
+ * `GET /cycle-counts?cycleCountId=` (strongly consistent).
518
+ *
519
+ * When sent, the write is fenced on it: a session still `open` but revised
520
+ * since is refused `409 CYCLE_COUNT_CONTENT_CONFLICT` — the code a finalise
521
+ * answers for the same case — with the revision to re-send as top-level
522
+ * `currentUpdatedAt` and the current {@link CycleCount} as `data.current`,
523
+ * and NOTHING is written. Compare `current.updatedBy` and EVERY field that
524
+ * was sent: a match is consistent with an own edit whose response was lost
525
+ * (not proof — same user, another device, reads the same); anything else is
526
+ * another write. Never retry automatically. A session that went terminal
527
+ * underneath stays `409 CYCLE_COUNT_NOT_OPEN`.
528
+ *
529
+ * Optional for compatibility: omitted, the edit is NOT fenced on a
530
+ * revision — last writer wins, as before. The server still stamps the new
531
+ * revision strictly past the stored one, re-reading and retrying once when
532
+ * a concurrent edit took that value first, so it never re-issues a token
533
+ * another client holds; a session that churns through both attempts is
534
+ * answered the same `409 CYCLE_COUNT_CONTENT_CONFLICT`.
535
+ */
536
+ expectedUpdatedAt?: number;
537
+ }
538
+ /**
539
+ * `data` of a `409 CYCLE_COUNT_CONTENT_CONFLICT` answered to `mode: 'edit'`
540
+ * (a finalise's carries no `data`; both carry top-level `currentUpdatedAt`).
541
+ */
542
+ interface CycleCountEditConflict {
543
+ current: CycleCount;
544
+ }
475
545
  }
476
546
  export {};
477
547
  /**
@@ -557,7 +627,16 @@ export declare const isOverCycleCountToleranceRule: (input: {
557
627
  * - `CYCLE_COUNT_VERSION_CONFLICT` · 409 — a cancel or finalise that lost the
558
628
  * compare-and-set on the session row. ⚠️ On finalise it is CONDITIONAL: the
559
629
  * handler re-reads consistently, and a winner that FINALISED the session makes
560
- * it a `200` replay instead; only a concurrent CANCEL surfaces as this.
630
+ * it a `200` replay instead; only a concurrent CANCEL surfaces as this. Never
631
+ * answered to `mode: 'edit'`.
632
+ * - `CYCLE_COUNT_CONTENT_CONFLICT` · 409 — the session is still `open` but no
633
+ * longer at the revision the request was composed against; top-level
634
+ * `currentUpdatedAt` is the revision to re-send after re-reviewing, and
635
+ * nothing was written. ONE meaning in both modes: a finalise whose lines or
636
+ * tolerance moved (or whose `expectedUpdatedAt` was stale on arrival), and an
637
+ * edit whose `expectedUpdatedAt` is stale — on arrival or overtaken before
638
+ * the write — or a token-less edit overtaken on both of its attempts. On an
639
+ * edit `data` is {@link CycleCountEditConflict}.
561
640
  * - `CYCLE_COUNT_TOLERANCE_EXCEEDED` · 403 — a finalise with at least one
562
641
  * over-tolerance line, from a caller who is neither ADMIN nor MANAGER. The
563
642
  * session stays `open` and NO stock moved.
@@ -119,7 +119,16 @@ export const isOverCycleCountToleranceRule = (input, tolerancePercent) => {
119
119
  * - `CYCLE_COUNT_VERSION_CONFLICT` · 409 — a cancel or finalise that lost the
120
120
  * compare-and-set on the session row. ⚠️ On finalise it is CONDITIONAL: the
121
121
  * handler re-reads consistently, and a winner that FINALISED the session makes
122
- * it a `200` replay instead; only a concurrent CANCEL surfaces as this.
122
+ * it a `200` replay instead; only a concurrent CANCEL surfaces as this. Never
123
+ * answered to `mode: 'edit'`.
124
+ * - `CYCLE_COUNT_CONTENT_CONFLICT` · 409 — the session is still `open` but no
125
+ * longer at the revision the request was composed against; top-level
126
+ * `currentUpdatedAt` is the revision to re-send after re-reviewing, and
127
+ * nothing was written. ONE meaning in both modes: a finalise whose lines or
128
+ * tolerance moved (or whose `expectedUpdatedAt` was stale on arrival), and an
129
+ * edit whose `expectedUpdatedAt` is stale — on arrival or overtaken before
130
+ * the write — or a token-less edit overtaken on both of its attempts. On an
131
+ * edit `data` is {@link CycleCountEditConflict}.
123
132
  * - `CYCLE_COUNT_TOLERANCE_EXCEEDED` · 403 — a finalise with at least one
124
133
  * over-tolerance line, from a caller who is neither ADMIN nor MANAGER. The
125
134
  * session stays `open` and NO stock moved.
@@ -182,12 +191,13 @@ export const CYCLE_COUNT_ERROR_CODES = [
182
191
  'CYCLE_COUNT_NOT_OPEN',
183
192
  'CYCLE_COUNT_ILLEGAL_TRANSITION',
184
193
  'CYCLE_COUNT_VERSION_CONFLICT',
185
- // Finalise refuses a session whose COUNTED CONTENT moved since the caller's
186
- // read, distinct from the row-version conflict above: the caller's revision
187
- // is stale, and committing it would finalise a count nobody took. Answered
188
- // by `cycle-counts/_finalize.ts`; landed one PR after this list was
189
- // published, and the guard test that keeps this list honest caught it on
190
- // `main` rather than in either PR's own gate.
194
+ // The session is still open but its revision moved since the caller's read,
195
+ // distinct from the row-version conflict above (the session went terminal):
196
+ // committing would finalise a count nobody took, or overwrite lines nobody
197
+ // reviewed. Answered by `cycle-counts/_finalize.ts` and `_edit.ts`. The
198
+ // finalise case landed one PR after this list was published, and the guard
199
+ // test that keeps this list honest caught it on `main` rather than in either
200
+ // PR's own gate.
191
201
  'CYCLE_COUNT_CONTENT_CONFLICT',
192
202
  'CYCLE_COUNT_TOLERANCE_EXCEEDED',
193
203
  // Finalise refuses a line that is neither counted nor skipped rather than
@@ -75,7 +75,10 @@ declare global {
75
75
  /** Original order FX stamp retained for receipt-linked valuation reversals. */
76
76
  currencyValue?: number;
77
77
  currencyValueAt?: number;
78
- /** Internal request identity for safe receipt-key retries. */
78
+ /**
79
+ * Internal request identity for safe receipt-key retries, written on every
80
+ * receipt booked by `mode: 'receive'`. Opaque: a client must ignore it.
81
+ */
79
82
  requestFingerprint?: string;
80
83
  updatedAt?: number;
81
84
  /** Optimistic revision for cumulative receipt-linked reversals. */
@@ -99,6 +102,78 @@ declare global {
99
102
  notes?: string;
100
103
  items: PurchaseReceiptItem[];
101
104
  }
105
+ /** The fields every receipt line carries, whichever quantity and cost it names. */
106
+ interface PurchaseReceiptLineBaseInput {
107
+ /** Index into the ordering `PurchaseOrder.items`; at most once per receipt. */
108
+ purchaseOrderItemIndex: number;
109
+ /** Batch the units arrived as — recorded as sent, see `PurchaseReceiptItem.lotId`. */
110
+ lotId?: string;
111
+ }
112
+ /**
113
+ * How much arrived: EXACTLY one of the line's base-unit `quantity` or its
114
+ * `purchaseQuantity` in the order line's frozen purchase unit. Naming both, or
115
+ * neither, is `400 VALIDATION_FAILED`.
116
+ */
117
+ type PurchaseReceiptLineQuantityInput = {
118
+ quantity: number;
119
+ purchaseQuantity?: never;
120
+ } | {
121
+ purchaseQuantity: number;
122
+ quantity?: never;
123
+ };
124
+ /**
125
+ * What it cost: AT MOST one of `cost` (per base unit) or `purchaseUnitCost` (per
126
+ * purchase unit), both in the purchase order's currency. Naming neither values the
127
+ * line at the order line's own frozen `purchaseUnitCost` when it has one, else
128
+ * at its `unitCost`; naming both is `400 VALIDATION_FAILED`.
129
+ */
130
+ type PurchaseReceiptLineCostInput = {
131
+ cost?: number;
132
+ purchaseUnitCost?: never;
133
+ } | {
134
+ purchaseUnitCost?: number;
135
+ cost?: never;
136
+ };
137
+ /** One line of a `PurchaseReceiptPostRequest`. */
138
+ type PurchaseReceiptLineInput = PurchaseReceiptLineBaseInput & PurchaseReceiptLineQuantityInput & PurchaseReceiptLineCostInput;
139
+ /**
140
+ * `POST /purchase-orders { mode: 'receive' }` — books one physical delivery
141
+ * against a purchase order, ADMIN or MANAGER only (`403 ADMIN_REQUIRED`).
142
+ * Refusals are `PURCHASE_RECEIPT_ERROR_CODES`.
143
+ *
144
+ * Idempotent on `receiptKey`: the same key with the same body answers the stored
145
+ * receipt with `replayed: true`; the same key with a different body is
146
+ * `409 PURCHASE_RECEIPT_KEY_CONFLICT`. EVERY body field binds the key, including
147
+ * one the server ignores (`warehouseId` on a store whose `stockAuthority` is not
148
+ * `'depositos'`), so a retry must resend the identical body; only `dated` sent as
149
+ * a string or a number, and an empty `notes` or none, count as the same. A
150
+ * receipt stored with no `requestFingerprint` replays whatever the body.
151
+ *
152
+ * Tied to its validator by `AssertPurchaseReceiptPostRequestMatchesSchema` in
153
+ * the api's `purchaseOrders/_receive.ts`.
154
+ */
155
+ interface PurchaseReceiptPostRequest {
156
+ mode: 'receive';
157
+ /** `PO` + 6–10 digits. */
158
+ purchaseOrderId: string;
159
+ /** Client-minted UUID v4 — mint it once per delivery and reuse it on every retry. */
160
+ receiptKey: string;
161
+ /**
162
+ * The depósito the goods arrived at. Read only on a store whose
163
+ * `stockAuthority` is `'depositos'`; absent there means the primary.
164
+ * `WH` + 6–10 digits.
165
+ */
166
+ warehouseId?: string;
167
+ /** `YYYYMMDD` day the goods arrived; absent means today. */
168
+ dated?: string | number;
169
+ /** At most 1000 characters. ⚠️ Never personal data. */
170
+ notes?: string;
171
+ /**
172
+ * 1..`PURCHASE_RECEIPT_MAX_LINES` lines (`PURCHASE_RECEIPT_MAX_LINES_DEPOSITOS`
173
+ * on a depósitos store), each naming a distinct `purchaseOrderItemIndex`.
174
+ */
175
+ lines: PurchaseReceiptLineInput[];
176
+ }
102
177
  }
103
178
  /**
104
179
  * Refusals from `POST /purchase-orders { mode: 'receive' }` and the receipt reads
@@ -124,10 +199,13 @@ declare global {
124
199
  * - `STOCK_INCOME_RACED` · 409 — the ledger-key retry budget ran out. Nothing moved.
125
200
  * - `PRODUCT_NOT_FOUND` · 404 — a line's product no longer exists. Nothing moved.
126
201
  * - `PURCHASE_RECEIPT_NOT_FOUND` · 404 — `mode=receipt` addressed nothing.
202
+ * - `PURCHASE_RECEIPT_KEY_CONFLICT` · 409 — the `receiptKey` already booked a
203
+ * receipt from a different body. Nothing moved; a new delivery needs a new key.
127
204
  * - `INVALID_CURSOR` · 400
128
205
  *
129
- * A replay of a `receiptKey` that already committed is NOT a refusal: it answers
130
- * 200 with the stored receipt and `replayed: true`.
206
+ * A replay of a `receiptKey` that already committed with the SAME body is NOT a
207
+ * refusal: it answers 200 with the stored receipt and `replayed: true`. A receipt
208
+ * stored with no `requestFingerprint` replays whatever the body.
131
209
  *
132
210
  * ⚠️ Cross-cutting refusals are NOT repeated here — check `GATING_ERROR_CODES`
133
211
  * as well.
@@ -37,10 +37,13 @@ export const PURCHASE_RECEIPT_MAX_LINES_DEPOSITOS = 32;
37
37
  * - `STOCK_INCOME_RACED` · 409 — the ledger-key retry budget ran out. Nothing moved.
38
38
  * - `PRODUCT_NOT_FOUND` · 404 — a line's product no longer exists. Nothing moved.
39
39
  * - `PURCHASE_RECEIPT_NOT_FOUND` · 404 — `mode=receipt` addressed nothing.
40
+ * - `PURCHASE_RECEIPT_KEY_CONFLICT` · 409 — the `receiptKey` already booked a
41
+ * receipt from a different body. Nothing moved; a new delivery needs a new key.
40
42
  * - `INVALID_CURSOR` · 400
41
43
  *
42
- * A replay of a `receiptKey` that already committed is NOT a refusal: it answers
43
- * 200 with the stored receipt and `replayed: true`.
44
+ * A replay of a `receiptKey` that already committed with the SAME body is NOT a
45
+ * refusal: it answers 200 with the stored receipt and `replayed: true`. A receipt
46
+ * stored with no `requestFingerprint` replays whatever the body.
44
47
  *
45
48
  * ⚠️ Cross-cutting refusals are NOT repeated here — check `GATING_ERROR_CODES`
46
49
  * as well.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "platform-types",
3
- "version": "1.10.709",
3
+ "version": "1.10.711",
4
4
  "description": "Shared wire and entity contracts for the platform.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",