platform-types 1.10.711 → 1.10.713

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.
package/dist/cjs/index.js CHANGED
@@ -73,6 +73,7 @@ __exportStar(require("./mercadolibre.js"), exports);
73
73
  __exportStar(require("./mercadopago.js"), exports);
74
74
  __exportStar(require("./notification.js"), exports);
75
75
  __exportStar(require("./order.js"), exports);
76
+ __exportStar(require("./orderCollectionAttempt.js"), exports);
76
77
  __exportStar(require("./orderCreateAttempt.js"), exports);
77
78
  __exportStar(require("./payment.js"), exports);
78
79
  __exportStar(require("./platform.js"), exports);
@@ -15,7 +15,9 @@ exports.MP_DYNAMIC_QR_ERROR_CODES = void 0;
15
15
  * - 503 `PLATFORM_INTEGRATION_DISABLED`; 504 `MP_DYN_QR_UNCONFIRMED`, an unknown creation outcome.
16
16
  * A thrown error answers `400 UNHANDLED_ERROR`, never a 500. The re-sync read
17
17
  * (`GET /mercadopago/qr/dynamic?orderId=`) uses `VALIDATION_FAILED`, `PLAN_UPGRADE_REQUIRED`,
18
- * `CAPABILITY_REQUIRED`, `ORDER_NOT_FOUND` and `ORDER_CANCELLED` from this list.
18
+ * `CAPABILITY_REQUIRED`, `ORDER_NOT_FOUND` and `ORDER_CANCELLED` from this list; its
19
+ * `ORDER_CANCELLED` carries `data: MpDynamicQrCancelledDetails` when the dead order holds linked
20
+ * MercadoPago payments.
19
21
  *
20
22
  * An order-bound charge holds the order's QR fence while it mints, and another charge for the
21
23
  * same order meanwhile answers `MP_DYN_QR_IN_PROGRESS`. A `contacting` attempt (MercadoPago may
package/dist/cjs/order.js CHANGED
@@ -235,6 +235,15 @@ exports.orderFulfilmentProgress = orderFulfilmentProgress;
235
235
  * is also `mode: 'edit'`'s and the update leg's answer to a re-price that
236
236
  * would leave the order's net señas above its new charged total. Nothing was
237
237
  * written on any of them.
238
+ * - `TILL_TENDER_REQUIRES_DEPOSIT` · 400 — `mode: 'tender'` adding till money
239
+ * (any leg whose `source` is not `account`) to an order that is NOT yet
240
+ * delivered and holds no seña: money taken before delivery is a seña and
241
+ * goes through `mode: 'deposit'`. Decided after replays are absorbed, so a
242
+ * pure repeat of legs already on the ledger still answers its 200; an
243
+ * account-only tender is never refused. Nothing was written and no
244
+ * idempotency marker remains (refused ahead of the claim, or the claim is
245
+ * released on the 400), so a collection lookup for it answers
246
+ * `unknown` / `NO_EVIDENCE`.
238
247
  * - `PAYMENT_REFERENCED_BY_DEPOSIT` · 409 — the payment link (fresh, over an
239
248
  * entry the order already holds, or a credit upgrade) of a payment whose id a
240
249
  * seña on the order already carries as its `reference`: the same money would
@@ -442,6 +451,19 @@ exports.orderFulfilmentProgress = orderFulfilmentProgress;
442
451
  * for another store or user session (`ORDER_ATTEMPT_SCOPE_TAG_LENGTH`). Mint
443
452
  * a new key in this session. Nothing was written.
444
453
  *
454
+ * ⚠️ **A money write's attempt identity is its `Idempotency-Key`, never its
455
+ * `reference`.** Mint one key per tender, seña or account payment and keep it
456
+ * for every retry. `reference` is a duplicate-money guard with its own
457
+ * horizon, not an outcome, and it runs across keys: a tender absorbs a repeat
458
+ * leg identical on money, method and source that carries the same `reference`
459
+ * indefinitely, or — neither side carrying one — was recorded by the same user
460
+ * within 30 seconds; a seña absorbs a same-user or same-`reference` repeat
461
+ * only inside its 30-second replay window, and an older lookalike is
462
+ * `409 DEPOSIT_DUPLICATE_REFERENCE`. To learn whether a keyed
463
+ * write landed after a lost reply, `IDEMPOTENCY_IN_PROGRESS` or
464
+ * `IDEMPOTENCY_KEY_FAILED`, ask `POST /orders { mode: 'collection-attempt' }`
465
+ * (`OrderCollectionAttemptVerdict`) — never resend it under a new key first.
466
+ *
445
467
  * Idempotency answers, all six from `withIdempotency`:
446
468
  * - `INVALID_IDEMPOTENCY_KEY` · 400 · `IDEMPOTENCY_KEY_REUSE` · 422 (the same
447
469
  * key with a different body)
@@ -521,6 +543,8 @@ exports.ORDER_ERROR_CODES = [
521
543
  'DEPOSIT_INVOICED_ORDER_MUST_FREEZE',
522
544
  'DEPOSIT_REFERENCE_IS_LINKED_PAYMENT',
523
545
  'DEPOSIT_DUPLICATE_REFERENCE',
546
+ // Till money on an undelivered order with no seña — `mode: 'deposit'` takes it.
547
+ 'TILL_TENDER_REQUIRES_DEPOSIT',
524
548
  // The payment link of a payment a seña on the order already references.
525
549
  'PAYMENT_REFERENCED_BY_DEPOSIT',
526
550
  // Customer cancellation of an order still holding a seña.
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ /**
3
+ * Order-collection attempt lookup — the read a client runs when a money write
4
+ * on an order (a till tender, a seña, an account payment) left it without an
5
+ * answer: a lost reply, a timeout, or one of the two uncertain refusals
6
+ * `409 IDEMPOTENCY_IN_PROGRESS` and `409 IDEMPOTENCY_KEY_FAILED`.
7
+ *
8
+ * ⚠️ **The attempt's identity is its `Idempotency-Key`, never its `reference`.**
9
+ * Mint ONE key per collection attempt and keep it for every retry of that
10
+ * attempt. `reference` keeps its own, narrower meaning as a duplicate-money
11
+ * guard, and that guard runs ACROSS keys: a tender absorbs a repeat leg
12
+ * identical on money, method and source that carries the same `reference`
13
+ * however much later it arrives, OR — neither side carrying a reference — was
14
+ * recorded by the same user within 30 seconds; a seña absorbs a same-user or
15
+ * same-`reference` repeat only inside its 30-second replay window (an older
16
+ * lookalike is `409 DEPOSIT_DUPLICATE_REFERENCE`). So a NEW key is a new
17
+ * attempt, not a guaranteed new leg: two genuine reference-less handoffs of the
18
+ * same amount by one cashier inside 30 s land as ONE leg, and the second key
19
+ * answers `committed` all the same. Give each physical payment its own
20
+ * `reference` when that distinction matters. Neither guard is an outcome
21
+ * protocol.
22
+ *
23
+ * `POST /orders { mode: 'collection-attempt', operation, orderId, idempotencyKey }`
24
+ * answers from the write's own idempotency marker, scoped to the CALLER's store
25
+ * and user. It never claims, replays, reclaims or seals the marker, never
26
+ * writes the marker or any business row, and calls no provider; a claim it
27
+ * finds stranded past 60 s logs one deduped operator row.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.ORDER_COLLECTION_ATTEMPT_RETENTION_MS = exports.ORDER_COLLECTION_ATTEMPT_UNKNOWN_REASONS = exports.ORDER_COLLECTION_ATTEMPT_VERDICTS = exports.ORDER_COLLECTION_ATTEMPT_OPERATIONS = void 0;
31
+ /** Every `operation` the lookup answers for — see `OrderCollectionAttemptLookupRequest`. */
32
+ exports.ORDER_COLLECTION_ATTEMPT_OPERATIONS = ['tender', 'deposit', 'account'];
33
+ /** Every `verdict` the lookup answers — see `OrderCollectionAttemptVerdict`. */
34
+ exports.ORDER_COLLECTION_ATTEMPT_VERDICTS = ['committed', 'in-progress', 'sealed', 'unknown'];
35
+ /**
36
+ * Every `reason` on an `unknown` verdict:
37
+ *
38
+ * - `NO_EVIDENCE` — no marker under this key for the caller: never sent, sent
39
+ * by another user or store, refused or released before it committed,
40
+ * already deleted by its retention expiry — or STILL IN FLIGHT and not yet
41
+ * claimed. A write holds no marker until its claim lands, and before that
42
+ * it can be queued at the gateway, in a cold start, or (a tender) running
43
+ * its pre-claim reads. So it is never "not collected": follow the
44
+ * `unknown` remedy on `OrderCollectionAttemptVerdict` — resend the same
45
+ * body under the same key first.
46
+ * - `EVIDENCE_EXPIRED` — the marker outlived
47
+ * {@link ORDER_COLLECTION_ATTEMPT_RETENTION_MS} and is awaiting deletion,
48
+ * which DynamoDB does lazily (up to days late). That is the LOOKUP's
49
+ * horizon, not the writer's: the write never reads the expiry, so while
50
+ * the row survives a same-key resend still replays a committed `200` (or
51
+ * refuses a sealed or stranded key) and does NOT run again. Once DynamoDB
52
+ * deletes the row the same resend runs as a new write — so past the
53
+ * horizon verify where the operation writes before resending at all.
54
+ * - `UNBOUND_ATTEMPT` — the key belongs to a write that is not a collection
55
+ * attempt (another order mode), or to one admitted before the server
56
+ * started recording which collection a key was for.
57
+ * - `ATTEMPT_MISMATCH` — the key was sent for a different `operation` or
58
+ * `orderId` than the lookup names.
59
+ * - `REFUSED` — the write's cached answer was a refusal (non-`2xx`): nothing
60
+ * moved under this key. No collection write caches a refusal today; this
61
+ * keeps one that ever does from reading as `committed`.
62
+ */
63
+ exports.ORDER_COLLECTION_ATTEMPT_UNKNOWN_REASONS = [
64
+ 'NO_EVIDENCE',
65
+ 'EVIDENCE_EXPIRED',
66
+ 'UNBOUND_ATTEMPT',
67
+ 'ATTEMPT_MISMATCH',
68
+ 'REFUSED',
69
+ ];
70
+ /**
71
+ * How long the server keeps a collection attempt's evidence: 24 hours — the
72
+ * write's own idempotency retention. Past it the lookup answers `unknown`.
73
+ */
74
+ exports.ORDER_COLLECTION_ATTEMPT_RETENTION_MS = 86_400_000;
package/dist/index.d.ts CHANGED
@@ -49,6 +49,7 @@ export * from './mercadolibre.ts';
49
49
  export * from './mercadopago.ts';
50
50
  export * from './notification.ts';
51
51
  export * from './order.ts';
52
+ export * from './orderCollectionAttempt.ts';
52
53
  export * from './orderCreateAttempt.ts';
53
54
  export * from './payment.ts';
54
55
  export * from './platform.ts';
package/dist/index.js CHANGED
@@ -57,6 +57,7 @@ export * from "./mercadolibre.js";
57
57
  export * from "./mercadopago.js";
58
58
  export * from "./notification.js";
59
59
  export * from "./order.js";
60
+ export * from "./orderCollectionAttempt.js";
60
61
  export * from "./orderCreateAttempt.js";
61
62
  export * from "./payment.js";
62
63
  export * from "./platform.js";
@@ -98,13 +98,73 @@ declare global {
98
98
  state: 'reserved' | 'contacting';
99
99
  since?: number;
100
100
  }
101
+ /**
102
+ * One MercadoPago payment linked to the order, as the `paid` arm of
103
+ * {@link MpDynamicQrStateResponse} reports it — read from the order's own
104
+ * `linkedPayments` entry, never matched by amount, customer or time.
105
+ *
106
+ * - `paymentId` — MercadoPago's payment id, the entry's key.
107
+ * - `amount` — what the store still holds for that payment, net of refunds
108
+ * (`LinkedPaymentEntry.total`), in `currency`.
109
+ * - `currency` — the payment's denomination as `LinkedPaymentEntry.currency`
110
+ * states it (an ISO code or a platform catalogId); for an entry naming
111
+ * none, the order's own currency, and `'ARS'` only when the order names
112
+ * none either.
113
+ * - `paidAt` — when the approved payment was linked to the order (epoch ms),
114
+ * which follows MercadoPago's approval, never precedes it.
115
+ */
116
+ interface MpDynamicQrPaidPayment {
117
+ paymentId: string;
118
+ amount: number;
119
+ currency: string;
120
+ paidAt: number;
121
+ }
122
+ /**
123
+ * `data` of the re-sync read's `409 ORDER_CANCELLED`
124
+ * (`GET /mercadopago/qr/dynamic?orderId=`), present only when the dead order
125
+ * still holds linked MercadoPago payments: the money that moved before (or
126
+ * despite) the cancellation, from the same consistent order read that
127
+ * refused. The `paid` arm of {@link MpDynamicQrStateResponse} without
128
+ * `pending` — a dead order's QR fence is not read. Absent when no
129
+ * MercadoPago payment is linked, which, exactly like `none`, is no evidence
130
+ * of non-payment. The POST's `409 ORDER_CANCELLED` never carries it.
131
+ */
132
+ interface MpDynamicQrCancelledDetails {
133
+ state: 'paid';
134
+ payments: MpDynamicQrPaidPayment[];
135
+ serverNow: number;
136
+ }
101
137
  /**
102
138
  * `data` of `GET /mercadopago/qr/dynamic?orderId=` — the re-sync read of the order's
103
- * dynamic QR. Never carries the attempt's key or owner. `pending` = an attempt
104
- * holds the order's fence (`since` = its reservation instant; no `qrData`); `none` =
105
- * nothing blocks the order.
139
+ * dynamic QR. Never carries the attempt's key or owner.
140
+ *
141
+ * - `paid` — the order holds at least one approved MercadoPago payment
142
+ * (`payments`, oldest link first), from a consistent read of the order.
143
+ * When another attempt ALSO holds the fence (a QR for the remaining
144
+ * balance), `pending` carries its `phase` and `since`; absent otherwise.
145
+ * - `pending` — an attempt holds the order's fence (`since` = its reservation
146
+ * instant; no `qrData`).
147
+ * - `none` — nothing blocks the order and no MercadoPago payment is linked
148
+ * YET. ⚠️ `none` is never evidence of non-payment: an approval may not be
149
+ * linked yet — a late webhook, a `409` the payment hook is waiting to
150
+ * retry, or MercadoPago processing switched off by the platform kill
151
+ * switch while a QR minted earlier stays payable at MercadoPago. A client
152
+ * must not collect again on `none`.
153
+ *
154
+ * ⚠️ Creating or showing a QR is never payment: only `paid` says money moved,
155
+ * and only its absence AFTER the operator has checked MercadoPago is a reason
156
+ * to collect again. A dead order answers `409 ORDER_CANCELLED` instead, with
157
+ * `data: MpDynamicQrCancelledDetails` when it holds linked payments.
106
158
  */
107
159
  type MpDynamicQrStateResponse = {
160
+ state: 'paid';
161
+ payments: MpDynamicQrPaidPayment[];
162
+ pending?: {
163
+ phase: 'reserved' | 'contacting';
164
+ since?: number;
165
+ };
166
+ serverNow: number;
167
+ } | {
108
168
  state: 'pending';
109
169
  phase: 'reserved' | 'contacting';
110
170
  since?: number;
@@ -191,7 +251,9 @@ declare global {
191
251
  * - 503 `PLATFORM_INTEGRATION_DISABLED`; 504 `MP_DYN_QR_UNCONFIRMED`, an unknown creation outcome.
192
252
  * A thrown error answers `400 UNHANDLED_ERROR`, never a 500. The re-sync read
193
253
  * (`GET /mercadopago/qr/dynamic?orderId=`) uses `VALIDATION_FAILED`, `PLAN_UPGRADE_REQUIRED`,
194
- * `CAPABILITY_REQUIRED`, `ORDER_NOT_FOUND` and `ORDER_CANCELLED` from this list.
254
+ * `CAPABILITY_REQUIRED`, `ORDER_NOT_FOUND` and `ORDER_CANCELLED` from this list; its
255
+ * `ORDER_CANCELLED` carries `data: MpDynamicQrCancelledDetails` when the dead order holds linked
256
+ * MercadoPago payments.
195
257
  *
196
258
  * An order-bound charge holds the order's QR fence while it mints, and another charge for the
197
259
  * same order meanwhile answers `MP_DYN_QR_IN_PROGRESS`. A `contacting` attempt (MercadoPago may
@@ -12,7 +12,9 @@
12
12
  * - 503 `PLATFORM_INTEGRATION_DISABLED`; 504 `MP_DYN_QR_UNCONFIRMED`, an unknown creation outcome.
13
13
  * A thrown error answers `400 UNHANDLED_ERROR`, never a 500. The re-sync read
14
14
  * (`GET /mercadopago/qr/dynamic?orderId=`) uses `VALIDATION_FAILED`, `PLAN_UPGRADE_REQUIRED`,
15
- * `CAPABILITY_REQUIRED`, `ORDER_NOT_FOUND` and `ORDER_CANCELLED` from this list.
15
+ * `CAPABILITY_REQUIRED`, `ORDER_NOT_FOUND` and `ORDER_CANCELLED` from this list; its
16
+ * `ORDER_CANCELLED` carries `data: MpDynamicQrCancelledDetails` when the dead order holds linked
17
+ * MercadoPago payments.
16
18
  *
17
19
  * An order-bound charge holds the order's QR fence while it mints, and another charge for the
18
20
  * same order meanwhile answers `MP_DYN_QR_IN_PROGRESS`. A `contacting` attempt (MercadoPago may
package/dist/order.d.ts CHANGED
@@ -2473,6 +2473,15 @@ export declare const orderFulfilmentProgress: (order: OrderFulfilmentProgressInp
2473
2473
  * is also `mode: 'edit'`'s and the update leg's answer to a re-price that
2474
2474
  * would leave the order's net señas above its new charged total. Nothing was
2475
2475
  * written on any of them.
2476
+ * - `TILL_TENDER_REQUIRES_DEPOSIT` · 400 — `mode: 'tender'` adding till money
2477
+ * (any leg whose `source` is not `account`) to an order that is NOT yet
2478
+ * delivered and holds no seña: money taken before delivery is a seña and
2479
+ * goes through `mode: 'deposit'`. Decided after replays are absorbed, so a
2480
+ * pure repeat of legs already on the ledger still answers its 200; an
2481
+ * account-only tender is never refused. Nothing was written and no
2482
+ * idempotency marker remains (refused ahead of the claim, or the claim is
2483
+ * released on the 400), so a collection lookup for it answers
2484
+ * `unknown` / `NO_EVIDENCE`.
2476
2485
  * - `PAYMENT_REFERENCED_BY_DEPOSIT` · 409 — the payment link (fresh, over an
2477
2486
  * entry the order already holds, or a credit upgrade) of a payment whose id a
2478
2487
  * seña on the order already carries as its `reference`: the same money would
@@ -2680,6 +2689,19 @@ export declare const orderFulfilmentProgress: (order: OrderFulfilmentProgressInp
2680
2689
  * for another store or user session (`ORDER_ATTEMPT_SCOPE_TAG_LENGTH`). Mint
2681
2690
  * a new key in this session. Nothing was written.
2682
2691
  *
2692
+ * ⚠️ **A money write's attempt identity is its `Idempotency-Key`, never its
2693
+ * `reference`.** Mint one key per tender, seña or account payment and keep it
2694
+ * for every retry. `reference` is a duplicate-money guard with its own
2695
+ * horizon, not an outcome, and it runs across keys: a tender absorbs a repeat
2696
+ * leg identical on money, method and source that carries the same `reference`
2697
+ * indefinitely, or — neither side carrying one — was recorded by the same user
2698
+ * within 30 seconds; a seña absorbs a same-user or same-`reference` repeat
2699
+ * only inside its 30-second replay window, and an older lookalike is
2700
+ * `409 DEPOSIT_DUPLICATE_REFERENCE`. To learn whether a keyed
2701
+ * write landed after a lost reply, `IDEMPOTENCY_IN_PROGRESS` or
2702
+ * `IDEMPOTENCY_KEY_FAILED`, ask `POST /orders { mode: 'collection-attempt' }`
2703
+ * (`OrderCollectionAttemptVerdict`) — never resend it under a new key first.
2704
+ *
2683
2705
  * Idempotency answers, all six from `withIdempotency`:
2684
2706
  * - `INVALID_IDEMPOTENCY_KEY` · 400 · `IDEMPOTENCY_KEY_REUSE` · 422 (the same
2685
2707
  * key with a different body)
@@ -2701,6 +2723,6 @@ export declare const orderFulfilmentProgress: (order: OrderFulfilmentProgressInp
2701
2723
  * `SUBSCRIPTION_READONLY`) sit on the order CREATE leg only: an edit is not a new
2702
2724
  * order, so `bareOrderPostHandler` routes past `requireMetered` entirely.
2703
2725
  */
2704
- export declare const ORDER_ERROR_CODES: readonly ["ORDER_COST_DENOMINATION_UNKNOWN", "SERVICE_ORDER_DELIVERY_MANAGED", "ORDER_NOT_FOUND", "ORDER_LOCKED", "ORDER_NOT_READY", "ORDER_DELIVERED_ANOTHER_DAY", "ORDER_DELIVERY_STATE_UNCHANGED", "ORDER_ILLEGAL_TRANSITION", "ORDER_VERSION_CONFLICT", "CUSTOMER_NOT_FOUND", "CUSTOMER_MERGED", "ORDER_HAS_NO_CUSTOMER", "CUSTOMER_ANONYMIZED", "FX_CONVERSION_UNAVAILABLE", "ROLE_FORBIDDEN", "ORDER_CANCELLED", "PAYMENT_SETTLED_ON_ANOTHER_ORDER", "ORDER_HAS_SHIPMENTS", "ORDER_HAS_RETURNS", "SHIPMENT_QUANTITY_EXCEEDED", "SHIPMENT_LINE_MISMATCH", "SHIPMENT_NOT_FOUND", "SHIPMENT_LIMIT_REACHED", "SHIPMENT_TOO_LARGE", "ORDER_FULFILMENT_EXTERNAL", "SHIPMENT_FAILED", "ADMIN_REQUIRED", "IDEMPOTENCY_KEY_REQUIRED", "INVALID_IDEMPOTENCY_KEY", "IDEMPOTENCY_KEY_REUSE", "IDEMPOTENCY_IN_PROGRESS", "IDEMPOTENCY_LOCK_CONTENTION", "IDEMPOTENCY_KEY_FAILED", "IDEMPOTENT_RESPONSE_NOT_CACHED", "ORDER_CANCELLATION_NOT_PROTECTED", "ORDER_CANCELLATION_COMMITTED", "ORDER_CANCELLATION_LOCKED", "ORDER_CANCELLATION_TOO_LARGE", "ORDER_CANCELLATION_FAILED", "SALE_LEDGER_RETRACTION_REFUSED", "SALE_LEDGER_ROW_NOT_OWNED", "PRODUCT_NOT_FOUND", "PRODUCT_SALE_LIMIT_EXCEEDED", "PRODUCT_SALE_LIMIT_INVALID", "DEPOSIT_SOURCE_NOT_ALLOWED", "DEPOSIT_CURRENCY_MISMATCH", "DEPOSIT_EXCEEDS_TOTAL", "DEPOSIT_FREEZE_REQUIRES_INVOICE", "DEPOSIT_FREEZE_INVOICE_PENDING", "DEPOSIT_CHARACTERIZATION_CONFLICT", "DEPOSIT_FREEZE_INVOICE_NOT_QUALIFYING", "DEPOSIT_INVOICED_ORDER_MUST_FREEZE", "DEPOSIT_REFERENCE_IS_LINKED_PAYMENT", "DEPOSIT_DUPLICATE_REFERENCE", "PAYMENT_REFERENCED_BY_DEPOSIT", "ORDER_HAS_UNREFUNDED_DEPOSIT", "DEPOSIT_NOT_FOUND", "DEPOSIT_ALREADY_REFUNDED", "UNIT_QUANTITY_NOT_WHOLE", "UNIT_REVISION_STALE", "UNIT_QUANTITY_UNREPRESENTABLE", "KIT_RECIPE_CHANGED", "CART_PROMOTION_UNAVAILABLE", "ORDER_HAS_KIT_LINES", "ORDER_TARGET_REQUIRED", "ORDER_SOURCE_CONFLICT", "ORDER_CUSTOMER_REQUIRED", "BASKET_EMPTY", "BASKET_CHANGED_DURING_CHECKOUT", "CART_VERSION_CREATE_ONLY", "CART_VERSION_REQUIRES_CART_ID", "CART_REVISION_STALE", "CART_CUSTOMER_MISMATCH", "COUNTER_SALE_CREATE_ONLY", "SOLD_AT_CREATE_ONLY", "SOLD_AT_IN_FUTURE", "ORDER_TENDER_CREATE_ONLY", "CREDIT_OVERRIDE_CREATE_ONLY", "ON_ACCOUNT_CREATE_ONLY", "ON_ACCOUNT_WITH_TENDERS", "TENDERS_REQUIRE_COUNTER_SALE", "LINES_REQUIRE_COUNTER_SALE", "PRODUCT_COST_UNKNOWN", "PRICE_CHANGE_DISABLED", "LINE_DISCOUNT_EXCEEDS_MAX", "CUSTOMER_DISABLED", "SHIFT_NOT_AVAILABLE", "SOLD_AT_OUTSIDE_SHIFT", "ORDER_CURRENCY_NOT_SUBSCRIBED", "PAYMENT_METHODS_NOT_CONFIGURED", "PAYMENT_METHOD_NOT_FOUND", "DELIVERY_METHOD_NOT_FOUND", "ORDER_BELOW_MINIMUM_PURCHASE", "ORDER_INVALID_CART", "EMAIL_VERIFICATION_REQUIRED", "ACCOUNT_DISABLED", "CURRENCY_MISMATCH", "SHIFT_NOT_OPEN", "ORDER_DISCOUNT_AFTER_INVOICE", "ORDER_DISCOUNT_ON_SERVICE_ORDER", "ORDER_DISCOUNT_ON_MARKETPLACE_ORDER", "ORDER_INVOICE_METHOD_OUTSTANDING_FACTURA", "ORDER_SERVICE_PERIOD_AFTER_INVOICE", "ORDER_ATTEMPT_COMMITTED", "ORDER_ATTEMPT_ABANDONED", "ORDER_ATTEMPT_KEY_EXPIRED", "ORDER_ATTEMPT_KEY_SCOPE_MISMATCH"];
2726
+ export declare const ORDER_ERROR_CODES: readonly ["ORDER_COST_DENOMINATION_UNKNOWN", "SERVICE_ORDER_DELIVERY_MANAGED", "ORDER_NOT_FOUND", "ORDER_LOCKED", "ORDER_NOT_READY", "ORDER_DELIVERED_ANOTHER_DAY", "ORDER_DELIVERY_STATE_UNCHANGED", "ORDER_ILLEGAL_TRANSITION", "ORDER_VERSION_CONFLICT", "CUSTOMER_NOT_FOUND", "CUSTOMER_MERGED", "ORDER_HAS_NO_CUSTOMER", "CUSTOMER_ANONYMIZED", "FX_CONVERSION_UNAVAILABLE", "ROLE_FORBIDDEN", "ORDER_CANCELLED", "PAYMENT_SETTLED_ON_ANOTHER_ORDER", "ORDER_HAS_SHIPMENTS", "ORDER_HAS_RETURNS", "SHIPMENT_QUANTITY_EXCEEDED", "SHIPMENT_LINE_MISMATCH", "SHIPMENT_NOT_FOUND", "SHIPMENT_LIMIT_REACHED", "SHIPMENT_TOO_LARGE", "ORDER_FULFILMENT_EXTERNAL", "SHIPMENT_FAILED", "ADMIN_REQUIRED", "IDEMPOTENCY_KEY_REQUIRED", "INVALID_IDEMPOTENCY_KEY", "IDEMPOTENCY_KEY_REUSE", "IDEMPOTENCY_IN_PROGRESS", "IDEMPOTENCY_LOCK_CONTENTION", "IDEMPOTENCY_KEY_FAILED", "IDEMPOTENT_RESPONSE_NOT_CACHED", "ORDER_CANCELLATION_NOT_PROTECTED", "ORDER_CANCELLATION_COMMITTED", "ORDER_CANCELLATION_LOCKED", "ORDER_CANCELLATION_TOO_LARGE", "ORDER_CANCELLATION_FAILED", "SALE_LEDGER_RETRACTION_REFUSED", "SALE_LEDGER_ROW_NOT_OWNED", "PRODUCT_NOT_FOUND", "PRODUCT_SALE_LIMIT_EXCEEDED", "PRODUCT_SALE_LIMIT_INVALID", "DEPOSIT_SOURCE_NOT_ALLOWED", "DEPOSIT_CURRENCY_MISMATCH", "DEPOSIT_EXCEEDS_TOTAL", "DEPOSIT_FREEZE_REQUIRES_INVOICE", "DEPOSIT_FREEZE_INVOICE_PENDING", "DEPOSIT_CHARACTERIZATION_CONFLICT", "DEPOSIT_FREEZE_INVOICE_NOT_QUALIFYING", "DEPOSIT_INVOICED_ORDER_MUST_FREEZE", "DEPOSIT_REFERENCE_IS_LINKED_PAYMENT", "DEPOSIT_DUPLICATE_REFERENCE", "TILL_TENDER_REQUIRES_DEPOSIT", "PAYMENT_REFERENCED_BY_DEPOSIT", "ORDER_HAS_UNREFUNDED_DEPOSIT", "DEPOSIT_NOT_FOUND", "DEPOSIT_ALREADY_REFUNDED", "UNIT_QUANTITY_NOT_WHOLE", "UNIT_REVISION_STALE", "UNIT_QUANTITY_UNREPRESENTABLE", "KIT_RECIPE_CHANGED", "CART_PROMOTION_UNAVAILABLE", "ORDER_HAS_KIT_LINES", "ORDER_TARGET_REQUIRED", "ORDER_SOURCE_CONFLICT", "ORDER_CUSTOMER_REQUIRED", "BASKET_EMPTY", "BASKET_CHANGED_DURING_CHECKOUT", "CART_VERSION_CREATE_ONLY", "CART_VERSION_REQUIRES_CART_ID", "CART_REVISION_STALE", "CART_CUSTOMER_MISMATCH", "COUNTER_SALE_CREATE_ONLY", "SOLD_AT_CREATE_ONLY", "SOLD_AT_IN_FUTURE", "ORDER_TENDER_CREATE_ONLY", "CREDIT_OVERRIDE_CREATE_ONLY", "ON_ACCOUNT_CREATE_ONLY", "ON_ACCOUNT_WITH_TENDERS", "TENDERS_REQUIRE_COUNTER_SALE", "LINES_REQUIRE_COUNTER_SALE", "PRODUCT_COST_UNKNOWN", "PRICE_CHANGE_DISABLED", "LINE_DISCOUNT_EXCEEDS_MAX", "CUSTOMER_DISABLED", "SHIFT_NOT_AVAILABLE", "SOLD_AT_OUTSIDE_SHIFT", "ORDER_CURRENCY_NOT_SUBSCRIBED", "PAYMENT_METHODS_NOT_CONFIGURED", "PAYMENT_METHOD_NOT_FOUND", "DELIVERY_METHOD_NOT_FOUND", "ORDER_BELOW_MINIMUM_PURCHASE", "ORDER_INVALID_CART", "EMAIL_VERIFICATION_REQUIRED", "ACCOUNT_DISABLED", "CURRENCY_MISMATCH", "SHIFT_NOT_OPEN", "ORDER_DISCOUNT_AFTER_INVOICE", "ORDER_DISCOUNT_ON_SERVICE_ORDER", "ORDER_DISCOUNT_ON_MARKETPLACE_ORDER", "ORDER_INVOICE_METHOD_OUTSTANDING_FACTURA", "ORDER_SERVICE_PERIOD_AFTER_INVOICE", "ORDER_ATTEMPT_COMMITTED", "ORDER_ATTEMPT_ABANDONED", "ORDER_ATTEMPT_KEY_EXPIRED", "ORDER_ATTEMPT_KEY_SCOPE_MISMATCH"];
2705
2727
  export type OrderErrorCode = (typeof ORDER_ERROR_CODES)[number];
2706
2728
  export {};
package/dist/order.js CHANGED
@@ -228,6 +228,15 @@ export const orderFulfilmentProgress = (order) => {
228
228
  * is also `mode: 'edit'`'s and the update leg's answer to a re-price that
229
229
  * would leave the order's net señas above its new charged total. Nothing was
230
230
  * written on any of them.
231
+ * - `TILL_TENDER_REQUIRES_DEPOSIT` · 400 — `mode: 'tender'` adding till money
232
+ * (any leg whose `source` is not `account`) to an order that is NOT yet
233
+ * delivered and holds no seña: money taken before delivery is a seña and
234
+ * goes through `mode: 'deposit'`. Decided after replays are absorbed, so a
235
+ * pure repeat of legs already on the ledger still answers its 200; an
236
+ * account-only tender is never refused. Nothing was written and no
237
+ * idempotency marker remains (refused ahead of the claim, or the claim is
238
+ * released on the 400), so a collection lookup for it answers
239
+ * `unknown` / `NO_EVIDENCE`.
231
240
  * - `PAYMENT_REFERENCED_BY_DEPOSIT` · 409 — the payment link (fresh, over an
232
241
  * entry the order already holds, or a credit upgrade) of a payment whose id a
233
242
  * seña on the order already carries as its `reference`: the same money would
@@ -435,6 +444,19 @@ export const orderFulfilmentProgress = (order) => {
435
444
  * for another store or user session (`ORDER_ATTEMPT_SCOPE_TAG_LENGTH`). Mint
436
445
  * a new key in this session. Nothing was written.
437
446
  *
447
+ * ⚠️ **A money write's attempt identity is its `Idempotency-Key`, never its
448
+ * `reference`.** Mint one key per tender, seña or account payment and keep it
449
+ * for every retry. `reference` is a duplicate-money guard with its own
450
+ * horizon, not an outcome, and it runs across keys: a tender absorbs a repeat
451
+ * leg identical on money, method and source that carries the same `reference`
452
+ * indefinitely, or — neither side carrying one — was recorded by the same user
453
+ * within 30 seconds; a seña absorbs a same-user or same-`reference` repeat
454
+ * only inside its 30-second replay window, and an older lookalike is
455
+ * `409 DEPOSIT_DUPLICATE_REFERENCE`. To learn whether a keyed
456
+ * write landed after a lost reply, `IDEMPOTENCY_IN_PROGRESS` or
457
+ * `IDEMPOTENCY_KEY_FAILED`, ask `POST /orders { mode: 'collection-attempt' }`
458
+ * (`OrderCollectionAttemptVerdict`) — never resend it under a new key first.
459
+ *
438
460
  * Idempotency answers, all six from `withIdempotency`:
439
461
  * - `INVALID_IDEMPOTENCY_KEY` · 400 · `IDEMPOTENCY_KEY_REUSE` · 422 (the same
440
462
  * key with a different body)
@@ -514,6 +536,8 @@ export const ORDER_ERROR_CODES = [
514
536
  'DEPOSIT_INVOICED_ORDER_MUST_FREEZE',
515
537
  'DEPOSIT_REFERENCE_IS_LINKED_PAYMENT',
516
538
  'DEPOSIT_DUPLICATE_REFERENCE',
539
+ // Till money on an undelivered order with no seña — `mode: 'deposit'` takes it.
540
+ 'TILL_TENDER_REQUIRES_DEPOSIT',
517
541
  // The payment link of a payment a seña on the order already references.
518
542
  'PAYMENT_REFERENCED_BY_DEPOSIT',
519
543
  // Customer cancellation of an order still holding a seña.
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Order-collection attempt lookup — the read a client runs when a money write
3
+ * on an order (a till tender, a seña, an account payment) left it without an
4
+ * answer: a lost reply, a timeout, or one of the two uncertain refusals
5
+ * `409 IDEMPOTENCY_IN_PROGRESS` and `409 IDEMPOTENCY_KEY_FAILED`.
6
+ *
7
+ * ⚠️ **The attempt's identity is its `Idempotency-Key`, never its `reference`.**
8
+ * Mint ONE key per collection attempt and keep it for every retry of that
9
+ * attempt. `reference` keeps its own, narrower meaning as a duplicate-money
10
+ * guard, and that guard runs ACROSS keys: a tender absorbs a repeat leg
11
+ * identical on money, method and source that carries the same `reference`
12
+ * however much later it arrives, OR — neither side carrying a reference — was
13
+ * recorded by the same user within 30 seconds; a seña absorbs a same-user or
14
+ * same-`reference` repeat only inside its 30-second replay window (an older
15
+ * lookalike is `409 DEPOSIT_DUPLICATE_REFERENCE`). So a NEW key is a new
16
+ * attempt, not a guaranteed new leg: two genuine reference-less handoffs of the
17
+ * same amount by one cashier inside 30 s land as ONE leg, and the second key
18
+ * answers `committed` all the same. Give each physical payment its own
19
+ * `reference` when that distinction matters. Neither guard is an outcome
20
+ * protocol.
21
+ *
22
+ * `POST /orders { mode: 'collection-attempt', operation, orderId, idempotencyKey }`
23
+ * answers from the write's own idempotency marker, scoped to the CALLER's store
24
+ * and user. It never claims, replays, reclaims or seals the marker, never
25
+ * writes the marker or any business row, and calls no provider; a claim it
26
+ * finds stranded past 60 s logs one deduped operator row.
27
+ */
28
+ declare global {
29
+ /** One of {@link ORDER_COLLECTION_ATTEMPT_OPERATIONS}. */
30
+ type OrderCollectionAttemptOperation = (typeof ORDER_COLLECTION_ATTEMPT_OPERATIONS)[number];
31
+ /** One of {@link ORDER_COLLECTION_ATTEMPT_VERDICTS}. */
32
+ type OrderCollectionAttemptVerdictKind = (typeof ORDER_COLLECTION_ATTEMPT_VERDICTS)[number];
33
+ /** Why a lookup could not decide — one of {@link ORDER_COLLECTION_ATTEMPT_UNKNOWN_REASONS}. */
34
+ type OrderCollectionAttemptUnknownReason = (typeof ORDER_COLLECTION_ATTEMPT_UNKNOWN_REASONS)[number];
35
+ /**
36
+ * `POST /orders` body for the lookup.
37
+ *
38
+ * - `operation` — which write the key was sent with: `tender`
39
+ * (`POST /orders { mode: 'tender' }`), `deposit`
40
+ * (`POST /orders { mode: 'deposit' }`) or `account` (`POST /account`, a
41
+ * customer-account movement sent with this `orderId`).
42
+ * - `orderId` — the order that write named.
43
+ * - `idempotencyKey` — the exact `Idempotency-Key` header the write carried.
44
+ *
45
+ * The store and user are the caller's session, never a body or header
46
+ * field. The lookup is not itself idempotency-keyed and is safe to repeat;
47
+ * an `in-progress` answer is a moment in time, so ask again after
48
+ * `retryAfterMs` and never cache it.
49
+ */
50
+ interface OrderCollectionAttemptLookupRequest {
51
+ mode: 'collection-attempt';
52
+ operation: OrderCollectionAttemptOperation;
53
+ orderId: string;
54
+ idempotencyKey: string;
55
+ }
56
+ /**
57
+ * `data` of a `200` from `POST /orders { mode: 'collection-attempt' }`.
58
+ *
59
+ * - `committed` — the write answered `2xx` (`statusCode`). `body` is the
60
+ * exact JSON envelope it answered — for a tender or a seña the order's
61
+ * post-commit echo with the updated settlement (a repeat the dedupe guard
62
+ * absorbed appended no leg, and the echo shows the ledger as it stands);
63
+ * for an account movement its ACCOUNT row. `body` is `null` when no usable
64
+ * envelope was cached (a body too large to keep, or not JSON);
65
+ * `statusCode` is `null` only on a marker that cached no response at all.
66
+ * `claimedAt` is the instant the write was admitted (epoch ms), at or
67
+ * before its commit. Do not collect again.
68
+ * - `in-progress` — the write is still running. Ask again after
69
+ * `retryAfterMs`.
70
+ * - `sealed` — the write may or may not have landed and can never be
71
+ * re-run under this key. `abandoned: false` — the key is sealed: by a
72
+ * throw after the transaction may have committed, or by a later same-key
73
+ * retry that found the claim stranded. `abandoned: true` — the claim is
74
+ * still stranded `in_progress` past 60 s: the invocation died mid-flight
75
+ * and nothing has sealed it yet. Show "unresolved — check", never
76
+ * "pay again", and verify where the operation writes before collecting
77
+ * under a NEW key: the order's `tenders` for `tender` and `deposit`; the
78
+ * customer's account movements carrying this `orderId` for `account`,
79
+ * which never writes the order.
80
+ * - `unknown` — the server holds no evidence for this attempt; `reason`
81
+ * says why. Never read it as "not written": a write released before it
82
+ * committed leaves no evidence, and so does one STILL IN FLIGHT that has
83
+ * not claimed its key yet (`NO_EVIDENCE`). The safe remedy, in this order:
84
+ * 1. FIRST resend the identical request — the same body under the SAME
85
+ * `Idempotency-Key`, as the SAME user. The write's claim is a
86
+ * conditional put (`attribute_not_exists`), so the resend serialises
87
+ * against any copy still in flight: it replays the cached `2xx` of a
88
+ * write that committed, or runs exactly once. A
89
+ * `409 IDEMPOTENCY_IN_PROGRESS` means a copy holds the claim — ask the
90
+ * lookup again after a pause; a `409 IDEMPOTENCY_KEY_FAILED` means the
91
+ * key is sealed — handle it as `sealed`. Neither is a second charge.
92
+ * "Identical" is what the write fingerprints: a changed body answers
93
+ * `422 IDEMPOTENCY_KEY_REUSE` (for `POST /account` a fresh
94
+ * `elevationToken` is not a change).
95
+ * 2. Collect under a NEW key only after the operator has verified where
96
+ * the operation writes (as for `sealed`) AND at least 30 seconds have
97
+ * passed since the last send of this key — API Gateway stops waiting at
98
+ * 29 s, and the writing Lambdas time out at 25 s (`tender`, `deposit`)
99
+ * and 20 s (`account`).
100
+ * This holds inside {@link ORDER_COLLECTION_ATTEMPT_RETENTION_MS} of the
101
+ * attempt's first send. Past it a same-key resend is no longer a safe
102
+ * probe (see `EVIDENCE_EXPIRED`): verify first.
103
+ *
104
+ * ⚠️ A verdict only ever describes the caller's OWN key: another user's or
105
+ * another store's key answers `unknown` / `NO_EVIDENCE`, never their verdict.
106
+ */
107
+ type OrderCollectionAttemptVerdict = {
108
+ verdict: Extract<OrderCollectionAttemptVerdictKind, 'committed'>;
109
+ claimedAt: number | null;
110
+ statusCode: number | null;
111
+ body: Record<string, unknown> | null;
112
+ } | {
113
+ verdict: Extract<OrderCollectionAttemptVerdictKind, 'in-progress'>;
114
+ retryAfterMs: number;
115
+ claimedAt: number;
116
+ } | {
117
+ verdict: Extract<OrderCollectionAttemptVerdictKind, 'sealed'>;
118
+ claimedAt: number | null;
119
+ abandoned: boolean;
120
+ } | {
121
+ verdict: Extract<OrderCollectionAttemptVerdictKind, 'unknown'>;
122
+ reason: OrderCollectionAttemptUnknownReason;
123
+ };
124
+ }
125
+ /** Every `operation` the lookup answers for — see `OrderCollectionAttemptLookupRequest`. */
126
+ export declare const ORDER_COLLECTION_ATTEMPT_OPERATIONS: readonly ["tender", "deposit", "account"];
127
+ /** Every `verdict` the lookup answers — see `OrderCollectionAttemptVerdict`. */
128
+ export declare const ORDER_COLLECTION_ATTEMPT_VERDICTS: readonly ["committed", "in-progress", "sealed", "unknown"];
129
+ /**
130
+ * Every `reason` on an `unknown` verdict:
131
+ *
132
+ * - `NO_EVIDENCE` — no marker under this key for the caller: never sent, sent
133
+ * by another user or store, refused or released before it committed,
134
+ * already deleted by its retention expiry — or STILL IN FLIGHT and not yet
135
+ * claimed. A write holds no marker until its claim lands, and before that
136
+ * it can be queued at the gateway, in a cold start, or (a tender) running
137
+ * its pre-claim reads. So it is never "not collected": follow the
138
+ * `unknown` remedy on `OrderCollectionAttemptVerdict` — resend the same
139
+ * body under the same key first.
140
+ * - `EVIDENCE_EXPIRED` — the marker outlived
141
+ * {@link ORDER_COLLECTION_ATTEMPT_RETENTION_MS} and is awaiting deletion,
142
+ * which DynamoDB does lazily (up to days late). That is the LOOKUP's
143
+ * horizon, not the writer's: the write never reads the expiry, so while
144
+ * the row survives a same-key resend still replays a committed `200` (or
145
+ * refuses a sealed or stranded key) and does NOT run again. Once DynamoDB
146
+ * deletes the row the same resend runs as a new write — so past the
147
+ * horizon verify where the operation writes before resending at all.
148
+ * - `UNBOUND_ATTEMPT` — the key belongs to a write that is not a collection
149
+ * attempt (another order mode), or to one admitted before the server
150
+ * started recording which collection a key was for.
151
+ * - `ATTEMPT_MISMATCH` — the key was sent for a different `operation` or
152
+ * `orderId` than the lookup names.
153
+ * - `REFUSED` — the write's cached answer was a refusal (non-`2xx`): nothing
154
+ * moved under this key. No collection write caches a refusal today; this
155
+ * keeps one that ever does from reading as `committed`.
156
+ */
157
+ export declare const ORDER_COLLECTION_ATTEMPT_UNKNOWN_REASONS: readonly ["NO_EVIDENCE", "EVIDENCE_EXPIRED", "UNBOUND_ATTEMPT", "ATTEMPT_MISMATCH", "REFUSED"];
158
+ /**
159
+ * How long the server keeps a collection attempt's evidence: 24 hours — the
160
+ * write's own idempotency retention. Past it the lookup answers `unknown`.
161
+ */
162
+ export declare const ORDER_COLLECTION_ATTEMPT_RETENTION_MS = 86400000;
163
+ export {};
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Order-collection attempt lookup — the read a client runs when a money write
3
+ * on an order (a till tender, a seña, an account payment) left it without an
4
+ * answer: a lost reply, a timeout, or one of the two uncertain refusals
5
+ * `409 IDEMPOTENCY_IN_PROGRESS` and `409 IDEMPOTENCY_KEY_FAILED`.
6
+ *
7
+ * ⚠️ **The attempt's identity is its `Idempotency-Key`, never its `reference`.**
8
+ * Mint ONE key per collection attempt and keep it for every retry of that
9
+ * attempt. `reference` keeps its own, narrower meaning as a duplicate-money
10
+ * guard, and that guard runs ACROSS keys: a tender absorbs a repeat leg
11
+ * identical on money, method and source that carries the same `reference`
12
+ * however much later it arrives, OR — neither side carrying a reference — was
13
+ * recorded by the same user within 30 seconds; a seña absorbs a same-user or
14
+ * same-`reference` repeat only inside its 30-second replay window (an older
15
+ * lookalike is `409 DEPOSIT_DUPLICATE_REFERENCE`). So a NEW key is a new
16
+ * attempt, not a guaranteed new leg: two genuine reference-less handoffs of the
17
+ * same amount by one cashier inside 30 s land as ONE leg, and the second key
18
+ * answers `committed` all the same. Give each physical payment its own
19
+ * `reference` when that distinction matters. Neither guard is an outcome
20
+ * protocol.
21
+ *
22
+ * `POST /orders { mode: 'collection-attempt', operation, orderId, idempotencyKey }`
23
+ * answers from the write's own idempotency marker, scoped to the CALLER's store
24
+ * and user. It never claims, replays, reclaims or seals the marker, never
25
+ * writes the marker or any business row, and calls no provider; a claim it
26
+ * finds stranded past 60 s logs one deduped operator row.
27
+ */
28
+ /** Every `operation` the lookup answers for — see `OrderCollectionAttemptLookupRequest`. */
29
+ export const ORDER_COLLECTION_ATTEMPT_OPERATIONS = ['tender', 'deposit', 'account'];
30
+ /** Every `verdict` the lookup answers — see `OrderCollectionAttemptVerdict`. */
31
+ export const ORDER_COLLECTION_ATTEMPT_VERDICTS = ['committed', 'in-progress', 'sealed', 'unknown'];
32
+ /**
33
+ * Every `reason` on an `unknown` verdict:
34
+ *
35
+ * - `NO_EVIDENCE` — no marker under this key for the caller: never sent, sent
36
+ * by another user or store, refused or released before it committed,
37
+ * already deleted by its retention expiry — or STILL IN FLIGHT and not yet
38
+ * claimed. A write holds no marker until its claim lands, and before that
39
+ * it can be queued at the gateway, in a cold start, or (a tender) running
40
+ * its pre-claim reads. So it is never "not collected": follow the
41
+ * `unknown` remedy on `OrderCollectionAttemptVerdict` — resend the same
42
+ * body under the same key first.
43
+ * - `EVIDENCE_EXPIRED` — the marker outlived
44
+ * {@link ORDER_COLLECTION_ATTEMPT_RETENTION_MS} and is awaiting deletion,
45
+ * which DynamoDB does lazily (up to days late). That is the LOOKUP's
46
+ * horizon, not the writer's: the write never reads the expiry, so while
47
+ * the row survives a same-key resend still replays a committed `200` (or
48
+ * refuses a sealed or stranded key) and does NOT run again. Once DynamoDB
49
+ * deletes the row the same resend runs as a new write — so past the
50
+ * horizon verify where the operation writes before resending at all.
51
+ * - `UNBOUND_ATTEMPT` — the key belongs to a write that is not a collection
52
+ * attempt (another order mode), or to one admitted before the server
53
+ * started recording which collection a key was for.
54
+ * - `ATTEMPT_MISMATCH` — the key was sent for a different `operation` or
55
+ * `orderId` than the lookup names.
56
+ * - `REFUSED` — the write's cached answer was a refusal (non-`2xx`): nothing
57
+ * moved under this key. No collection write caches a refusal today; this
58
+ * keeps one that ever does from reading as `committed`.
59
+ */
60
+ export const ORDER_COLLECTION_ATTEMPT_UNKNOWN_REASONS = [
61
+ 'NO_EVIDENCE',
62
+ 'EVIDENCE_EXPIRED',
63
+ 'UNBOUND_ATTEMPT',
64
+ 'ATTEMPT_MISMATCH',
65
+ 'REFUSED',
66
+ ];
67
+ /**
68
+ * How long the server keeps a collection attempt's evidence: 24 hours — the
69
+ * write's own idempotency retention. Past it the lookup answers `unknown`.
70
+ */
71
+ export const ORDER_COLLECTION_ATTEMPT_RETENTION_MS = 86_400_000;
package/dist/stock.d.ts CHANGED
@@ -506,6 +506,50 @@ declare global {
506
506
  /** Hydrated movement returned by readers after deriving ids from DynamoDB keys. */
507
507
  interface StockSale extends StockBase, StockSaleWrite {
508
508
  }
509
+ /**
510
+ * `data` of `GET /products?mode=stock&productId=…` — one product's ledger
511
+ * TOTALS, never its movement rows.
512
+ *
513
+ * - `incomes` — `Σ quantity` over the product's `INCOME#` rows.
514
+ * - `sales` — `Σ quantity` over the product's `SALE#` rows.
515
+ * - `stock` — on-hand, `incomes − sales`.
516
+ *
517
+ * Rows carrying `skip` are excluded from both sums.
518
+ *
519
+ * ⚠️ **`stock` is ABSENT exactly when the read truncated** — when either
520
+ * leg's paged read stopped short, which the envelope reports as top-level
521
+ * `truncated: true` ({@link ProductStockTotalsResponse}). It is never `0`
522
+ * or `null` in that case: a short read of either leg makes the difference
523
+ * WRONG, in either direction and by any amount, so absence is the signal.
524
+ * Read `stock === undefined` as "not computable from this read", never as
525
+ * zero. `incomes` and `sales` are still present then, but each is only the
526
+ * sum over the rows that were read — NOT a bound on the true leg total,
527
+ * because ledger quantities are signed (a service-part restore writes a
528
+ * negative-quantity `SALE#` row).
529
+ *
530
+ * A leg with no rows sums to `0`, never absent: a product with an empty
531
+ * ledger answers `incomes: 0, sales: 0, stock: 0` with `truncated: false`.
532
+ * The handler does not check that the product exists, so an unknown
533
+ * `productId` answers the same zeros rather than a 404.
534
+ */
535
+ interface ProductStockTotals {
536
+ incomes: number;
537
+ sales: number;
538
+ stock?: number;
539
+ }
540
+ /**
541
+ * The success envelope of `GET /products?mode=stock&productId=…`.
542
+ *
543
+ * `truncated` is always present: `true` exactly when `data.stock` is absent
544
+ * (see {@link ProductStockTotals}). There is no resume cursor — a truncated
545
+ * read means the on-hand figure is not computable from this endpoint, not
546
+ * that a next page exists.
547
+ */
548
+ interface ProductStockTotalsResponse {
549
+ message: string;
550
+ data: ProductStockTotals;
551
+ truncated: boolean;
552
+ }
509
553
  /**
510
554
  * The body of one nightly stock-drift reconciliation run
511
555
  * (`stacks/lambdas/stockReconciliationCron/index.ts`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "platform-types",
3
- "version": "1.10.711",
3
+ "version": "1.10.713",
4
4
  "description": "Shared wire and entity contracts for the platform.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",