platform-types 1.10.714 → 1.10.716
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/delivery.js +48 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/order.js +19 -0
- package/dist/cjs/orderCompletionAttempt.js +94 -0
- package/dist/delivery.d.ts +135 -2
- package/dist/delivery.js +47 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/order.d.ts +17 -1
- package/dist/order.js +19 -0
- package/dist/orderCompletionAttempt.d.ts +132 -0
- package/dist/orderCompletionAttempt.js +91 -0
- package/package.json +1 -1
package/dist/cjs/delivery.js
CHANGED
|
@@ -1,2 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DELIVERY_ERROR_CODES = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Every refusal code the delivery handlers (`POST /deliveries`,
|
|
6
|
+
* `POST /deliveries/{id}/events`, `GET /deliveries`, `GET /deliveries/{id}`)
|
|
7
|
+
* put on the wire, beyond the platform-wide set (`VALIDATION_FAILED`,
|
|
8
|
+
* `ROLE_FORBIDDEN`, `tryCatch`'s generic `UNHANDLED_ERROR`). Pinned against
|
|
9
|
+
* the handler sources by `tests/unit/deliveryErrorCodesPublished.test.ts`.
|
|
10
|
+
*
|
|
11
|
+
* - `INVALID_DELIVERY_ID` · 400 — no delivery id in the path.
|
|
12
|
+
* - `DELIVERY_NOT_FOUND` · 404 — unknown id, or another tenant's.
|
|
13
|
+
* - `DELIVERY_NOT_ASSIGNED` · 403 — the delivery names a platform courier
|
|
14
|
+
* (`courier.userId`) and the caller is neither that user nor ADMIN-tier.
|
|
15
|
+
* - `DELIVERY_TERMINAL` · 409 — the delivery is already `'delivered'` or
|
|
16
|
+
* `'failed'`; a failed delivery is re-dispatched through `POST /deliveries`.
|
|
17
|
+
* - `INVALID_IDEMPOTENCY_KEY` · 400 — the key is not ≤200 chars of
|
|
18
|
+
* `[A-Za-z0-9._:-]`.
|
|
19
|
+
* - `IDEMPOTENCY_KEY_REUSE` · 422 — the key was used with a different body.
|
|
20
|
+
* - `IDEMPOTENCY_KEY_FAILED` · 409 — the attempt under the key failed after it
|
|
21
|
+
* may have written proof, or the attempt lookup fenced the key; ask the
|
|
22
|
+
* lookup, then use a new key.
|
|
23
|
+
* - `IDEMPOTENCY_IN_PROGRESS` · 409 — the same key is still running.
|
|
24
|
+
* - `IDEMPOTENCY_LOCK_CONTENTION` · 409 — the key's marker could not be
|
|
25
|
+
* claimed; retry under the SAME key.
|
|
26
|
+
* - `INVALID_CURSOR` · 400 — a `?mine=true` `startKey` that is malformed or
|
|
27
|
+
* names another store's partition.
|
|
28
|
+
* - `IMPERSONATION_READONLY` · 403 — the attempt lookup, under a support
|
|
29
|
+
* session, could answer only by fencing the key (`op: 'delivery.attemptFence'`).
|
|
30
|
+
* - `INVALID_BASE64` · 400, `CONTENT_TOO_LARGE` · 413,
|
|
31
|
+
* `UNSUPPORTED_MEDIA_TYPE` · 415 — the inline proof payload was refused
|
|
32
|
+
* before anything was stored.
|
|
33
|
+
*/
|
|
34
|
+
exports.DELIVERY_ERROR_CODES = [
|
|
35
|
+
'DELIVERY_NOT_FOUND',
|
|
36
|
+
'DELIVERY_NOT_ASSIGNED',
|
|
37
|
+
'DELIVERY_TERMINAL',
|
|
38
|
+
'INVALID_DELIVERY_ID',
|
|
39
|
+
'INVALID_IDEMPOTENCY_KEY',
|
|
40
|
+
'IDEMPOTENCY_KEY_REUSE',
|
|
41
|
+
'IDEMPOTENCY_KEY_FAILED',
|
|
42
|
+
'IDEMPOTENCY_IN_PROGRESS',
|
|
43
|
+
'IDEMPOTENCY_LOCK_CONTENTION',
|
|
44
|
+
'INVALID_CURSOR',
|
|
45
|
+
'IMPERSONATION_READONLY',
|
|
46
|
+
// The proof payload gate (`deliveryAttachmentValidation.ts`).
|
|
47
|
+
'INVALID_BASE64',
|
|
48
|
+
'CONTENT_TOO_LARGE',
|
|
49
|
+
'UNSUPPORTED_MEDIA_TYPE',
|
|
50
|
+
];
|
package/dist/cjs/index.js
CHANGED
|
@@ -74,6 +74,7 @@ __exportStar(require("./mercadopago.js"), exports);
|
|
|
74
74
|
__exportStar(require("./notification.js"), exports);
|
|
75
75
|
__exportStar(require("./order.js"), exports);
|
|
76
76
|
__exportStar(require("./orderCollectionAttempt.js"), exports);
|
|
77
|
+
__exportStar(require("./orderCompletionAttempt.js"), exports);
|
|
77
78
|
__exportStar(require("./orderCreateAttempt.js"), exports);
|
|
78
79
|
__exportStar(require("./payment.js"), exports);
|
|
79
80
|
__exportStar(require("./platform.js"), exports);
|
package/dist/cjs/order.js
CHANGED
|
@@ -464,6 +464,22 @@ exports.orderFulfilmentProgress = orderFulfilmentProgress;
|
|
|
464
464
|
* `IDEMPOTENCY_KEY_FAILED`, ask `POST /orders { mode: 'collection-attempt' }`
|
|
465
465
|
* (`OrderCollectionAttemptVerdict`) — never resend it under a new key first.
|
|
466
466
|
*
|
|
467
|
+
* Completion-attempt refusals, on `mode: 'delivery'` under an `oc1~`
|
|
468
|
+
* `Idempotency-Key` (`ORDER_COMPLETION_ATTEMPT_KEY_PATTERN`). A legacy key never
|
|
469
|
+
* gets either; the key refusals are the create's own `ORDER_ATTEMPT_KEY_EXPIRED`
|
|
470
|
+
* and `ORDER_ATTEMPT_KEY_SCOPE_MISMATCH` above. ⚠️ On a delivery their remedy is
|
|
471
|
+
* NOT "mint a new key and resend": the original may have applied, so run
|
|
472
|
+
* `mode: 'completion-attempt'` on the old key first and send a new attempt only
|
|
473
|
+
* on `not-written`. Nothing was written on any of them:
|
|
474
|
+
* - `ORDER_COMPLETION_ATTEMPT_COMMITTED` · 409 — this key's original attempt
|
|
475
|
+
* already applied; `data` is the `committed` `OrderCompletionAttemptVerdict`.
|
|
476
|
+
* Do not resend.
|
|
477
|
+
* - `ORDER_COMPLETION_ATTEMPT_ABANDONED` · 409 — a completion-attempt lookup
|
|
478
|
+
* fenced this key and answered `not-written`. Re-read the order and send a NEW
|
|
479
|
+
* attempt under a NEW key.
|
|
480
|
+
* - `IDEMPOTENCY_KEY_REUSE` · 422 — the key's recorded attempt carried a
|
|
481
|
+
* different `orderId`, `delivered` or `sendSms`.
|
|
482
|
+
*
|
|
467
483
|
* Idempotency answers, all six from `withIdempotency`:
|
|
468
484
|
* - `INVALID_IDEMPOTENCY_KEY` · 400 · `IDEMPOTENCY_KEY_REUSE` · 422 (the same
|
|
469
485
|
* key with a different body)
|
|
@@ -613,4 +629,7 @@ exports.ORDER_ERROR_CODES = [
|
|
|
613
629
|
'ORDER_ATTEMPT_ABANDONED',
|
|
614
630
|
'ORDER_ATTEMPT_KEY_EXPIRED',
|
|
615
631
|
'ORDER_ATTEMPT_KEY_SCOPE_MISMATCH',
|
|
632
|
+
// `mode: 'delivery'` under an `oc1~` key — see `ORDER_COMPLETION_ATTEMPT_KEY_PATTERN`.
|
|
633
|
+
'ORDER_COMPLETION_ATTEMPT_COMMITTED',
|
|
634
|
+
'ORDER_COMPLETION_ATTEMPT_ABANDONED',
|
|
616
635
|
];
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Staff completion attempt evidence — the recovery protocol for
|
|
4
|
+
* `POST /orders { mode: 'delivery' }` (pickup completion and its same-day
|
|
5
|
+
* reversal) when the client lost the reply and has to decide whether the
|
|
6
|
+
* delivery it sent landed before it sends anything again.
|
|
7
|
+
*
|
|
8
|
+
* The protocol is opt-in and rides on the `Idempotency-Key` value. A key
|
|
9
|
+
* matching {@link ORDER_COMPLETION_ATTEMPT_KEY_PATTERN} makes the delivery
|
|
10
|
+
* write a durable `committed` record IN THE SAME TRANSACTION as the order, the
|
|
11
|
+
* customer balance and the receivable, bound to the store, the user, the key
|
|
12
|
+
* and the request (`orderId`, `delivered`, `sendSms`). That record — not the
|
|
13
|
+
* 24-hour `Idempotency-Key` replay cache — is what keeps the attempt from
|
|
14
|
+
* applying twice: a same-key resend after the cache is gone, after the order was
|
|
15
|
+
* un-delivered on another device, or after a crash between the commit and the
|
|
16
|
+
* reply is refused `409 ORDER_COMPLETION_ATTEMPT_COMMITTED` and writes nothing.
|
|
17
|
+
* Any other key is a legacy key and behaves exactly as before.
|
|
18
|
+
*
|
|
19
|
+
* `POST /orders { mode: 'completion-attempt', attemptKey }` answers for a
|
|
20
|
+
* protocol key ({@link OrderCompletionAttemptVerdict}).
|
|
21
|
+
*
|
|
22
|
+
* The key carries the same scope tag, mint window, clock skew and retention as
|
|
23
|
+
* the order-create attempt key: `ORDER_ATTEMPT_SCOPE_TAG_LENGTH`,
|
|
24
|
+
* `ORDER_ATTEMPT_CREATE_WINDOW_MS`, `ORDER_ATTEMPT_CLOCK_SKEW_MS` and
|
|
25
|
+
* `ORDER_ATTEMPT_RETENTION_MS`. A delivery under a key older than the window,
|
|
26
|
+
* or minted for another store or user session, is refused before anything is
|
|
27
|
+
* read or written (`400 ORDER_ATTEMPT_KEY_EXPIRED` /
|
|
28
|
+
* `400 ORDER_ATTEMPT_KEY_SCOPE_MISMATCH`). ⚠️ Neither says the original did not
|
|
29
|
+
* apply: before sending again under a new key, look the OLD key up — the
|
|
30
|
+
* lookup answers from the record for the whole retention, past the window —
|
|
31
|
+
* and resend only on `not-written`.
|
|
32
|
+
*
|
|
33
|
+
* What a client does with each outcome:
|
|
34
|
+
*
|
|
35
|
+
* - `200` — applied now. Done.
|
|
36
|
+
* - `409 ORDER_COMPLETION_ATTEMPT_COMMITTED` or a `committed` verdict — the
|
|
37
|
+
* ORIGINAL attempt applied; `data` names the order, the leg (`delivered`) and
|
|
38
|
+
* `committedAt`, which is also the `updatedAt` that write stamped. Done:
|
|
39
|
+
* refresh the order, never resend. The order may have moved since (an
|
|
40
|
+
* un-delivery on the web is a later, separate write).
|
|
41
|
+
* - `pending` — the original is still running. Ask again after `retryAfterMs`.
|
|
42
|
+
* - `not-written` or `409 ORDER_COMPLETION_ATTEMPT_ABANDONED` — the attempt
|
|
43
|
+
* never applied and never will. A resend is allowed only as a NEW attempt:
|
|
44
|
+
* re-read the order, then send under a NEW key.
|
|
45
|
+
* - `unknown` — the server cannot decide. Never treat it as `not-written` and
|
|
46
|
+
* never resend automatically; the operator verifies the order first.
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ `409 ORDER_DELIVERY_STATE_UNCHANGED`, a missing list row or an absent
|
|
49
|
+
* replay are NOT evidence of either outcome; only the codes and verdicts above
|
|
50
|
+
* are.
|
|
51
|
+
*
|
|
52
|
+
* ⚠️ A backend that predates this protocol refuses a protocol key with
|
|
53
|
+
* `400 INVALID_IDEMPOTENCY_KEY` before writing anything (`~` is outside the
|
|
54
|
+
* legacy key grammar). Do not mint protocol keys against a backend that has not
|
|
55
|
+
* shipped it: every delivery would fail, safely but entirely.
|
|
56
|
+
*
|
|
57
|
+
* The staff READY mark is a field edit (`POST /orders` with `readyAt` and
|
|
58
|
+
* `expectedUpdatedAt`), not this protocol: its `expectedUpdatedAt` is a frozen
|
|
59
|
+
* version precondition, so a resend of the same frozen body can never apply
|
|
60
|
+
* over a later change of the order — it is refused `409 ORDER_VERSION_CONFLICT`.
|
|
61
|
+
* ⚠️ Only when the token is sent: it is optional, and a ready mark without it
|
|
62
|
+
* is pinned to the server's own read at resend time, so a token-less resend
|
|
63
|
+
* after a later un-ready marks the order ready again. Send `expectedUpdatedAt`
|
|
64
|
+
* with every ready mark a client may resend.
|
|
65
|
+
*/
|
|
66
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
67
|
+
exports.ORDER_COMPLETION_ATTEMPT_UNKNOWN_REASONS = exports.ORDER_COMPLETION_ATTEMPT_VERDICTS = exports.ORDER_COMPLETION_ATTEMPT_KEY_PREFIX = exports.ORDER_COMPLETION_ATTEMPT_KEY_PATTERN = void 0;
|
|
68
|
+
/**
|
|
69
|
+
* The completion attempt key: `oc1~<mintMs>~<scopeTag>~<nonce>` — the
|
|
70
|
+
* order-create grammar under its own version tag, so neither route accepts the
|
|
71
|
+
* other's key. `mintMs` is 13 digits of epoch milliseconds, `scopeTag` is
|
|
72
|
+
* `ORDER_ATTEMPT_SCOPE_TAG_LENGTH` base64url characters computed exactly as for
|
|
73
|
+
* the create key, and `nonce` is 16 to 64 base64url characters.
|
|
74
|
+
*
|
|
75
|
+
* Mint ONE key per completion attempt and keep it, with the same body, for
|
|
76
|
+
* every resend of that attempt; a new key is a new attempt.
|
|
77
|
+
*
|
|
78
|
+
* A string, not a `RegExp`, and it carries no flags.
|
|
79
|
+
*/
|
|
80
|
+
exports.ORDER_COMPLETION_ATTEMPT_KEY_PATTERN = '^oc1~\\d{13}~[A-Za-z0-9_-]{12}~[A-Za-z0-9_-]{16,64}$';
|
|
81
|
+
/** The version tag every completion attempt key starts with. */
|
|
82
|
+
exports.ORDER_COMPLETION_ATTEMPT_KEY_PREFIX = 'oc1~';
|
|
83
|
+
/** Every `verdict` a completion-attempt lookup answers — see `OrderCompletionAttemptVerdict`. */
|
|
84
|
+
exports.ORDER_COMPLETION_ATTEMPT_VERDICTS = ['committed', 'pending', 'not-written', 'unknown'];
|
|
85
|
+
/**
|
|
86
|
+
* Every `reason` on an `unknown` verdict — the same four, with the same
|
|
87
|
+
* meaning, as the order-create lookup's.
|
|
88
|
+
*/
|
|
89
|
+
exports.ORDER_COMPLETION_ATTEMPT_UNKNOWN_REASONS = [
|
|
90
|
+
'LEGACY_KEY',
|
|
91
|
+
'SCOPE_MISMATCH',
|
|
92
|
+
'EVIDENCE_EXPIRED',
|
|
93
|
+
'KEY_CLOCK_AHEAD',
|
|
94
|
+
];
|
package/dist/delivery.d.ts
CHANGED
|
@@ -84,8 +84,9 @@ declare global {
|
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
86
|
* One append-only entry in a delivery's timeline. Never overwritten and never
|
|
87
|
-
* deleted
|
|
88
|
-
*
|
|
87
|
+
* deleted. `'delivered'` and `'failed'` are TERMINAL: nothing is appended
|
|
88
|
+
* after either (`409 DELIVERY_TERMINAL`), and a failed delivery is
|
|
89
|
+
* re-dispatched as a new delivery through `POST /deliveries`.
|
|
89
90
|
*
|
|
90
91
|
* PK `DELIVERY#{storeId}#{deliveryId}`, SK `EVENT#{createdAt}#{eventId}` —
|
|
91
92
|
* the storeId segment is load-bearing, exactly as it is for `CashEvent`:
|
|
@@ -108,6 +109,18 @@ declare global {
|
|
|
108
109
|
proof?: DeliveryProofAsset[];
|
|
109
110
|
/** Set only on a `'failed'` event. */
|
|
110
111
|
failureReason?: DeliveryFailureReason;
|
|
112
|
+
/**
|
|
113
|
+
* The `Idempotency-Key` the actor appended this event under — what lets a
|
|
114
|
+
* client match its own attempt exactly instead of guessing from type, time
|
|
115
|
+
* or photo count.
|
|
116
|
+
*
|
|
117
|
+
* ⚠️ Actor-scoped on the wire: `GET /deliveries/{id}` returns it only when
|
|
118
|
+
* the reader is this event's `actorUserId` or an ADMIN-tier role, and omits
|
|
119
|
+
* it otherwise. Absent on an event appended WITHOUT a key (still accepted —
|
|
120
|
+
* see `DeliveryEventAppendRequest`), on one written before keys existed, and
|
|
121
|
+
* on the opening `'assigned'` event `POST /deliveries` writes.
|
|
122
|
+
*/
|
|
123
|
+
idempotencyKey?: string;
|
|
111
124
|
}
|
|
112
125
|
/**
|
|
113
126
|
* A `DeliveryProofAsset` carrying the read-time-only `downloadUrl` minted by
|
|
@@ -126,5 +139,125 @@ declare global {
|
|
|
126
139
|
type DeliveryEventWithProofUrls = Omit<DeliveryEvent, 'proof'> & {
|
|
127
140
|
proof?: DeliveryProofAssetWithUrl[];
|
|
128
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* Body of `POST /deliveries/{id}/events`. The `Idempotency-Key` header is
|
|
144
|
+
* OPTIONAL for now and strongly recommended — the key, not the body, is what
|
|
145
|
+
* makes a retry safe. It becomes REQUIRED in a later change, once the courier
|
|
146
|
+
* app sends it.
|
|
147
|
+
*
|
|
148
|
+
* ⚠️ WITHOUT a key the append is NOT idempotent: every call writes a new
|
|
149
|
+
* event, so a resend after a lost response records the event twice. Same
|
|
150
|
+
* validation, assignee scoping and terminal refusal; no replay protection and
|
|
151
|
+
* no attempt lookup.
|
|
152
|
+
*
|
|
153
|
+
* WITH a key, resend the SAME key with the byte-identical body after a lost
|
|
154
|
+
* response: a committed attempt answers its original event
|
|
155
|
+
* (`Idempotent-Replayed: true`), a changed body under that key is
|
|
156
|
+
* `422 IDEMPOTENCY_KEY_REUSE`.
|
|
157
|
+
*
|
|
158
|
+
* A transient fault answers `tryCatch`'s generic `400 UNHANDLED_ERROR`. A
|
|
159
|
+
* same-key resend then replays the attempt if it committed, RE-RUNS only if
|
|
160
|
+
* DynamoDB cancelled its transaction (nothing applied), and otherwise answers
|
|
161
|
+
* `409 IDEMPOTENCY_KEY_FAILED` — the attempt may have stored proof or
|
|
162
|
+
* committed, so the key is sealed. On that 409, ask
|
|
163
|
+
* `GET /deliveries/{id}?idempotencyKey=` (`DeliveryEventAttemptLookup`) and
|
|
164
|
+
* mint a new key only when it answers `'not-committed'`.
|
|
165
|
+
*
|
|
166
|
+
* The server sets `actorUserId`, `createdAt`, `gps.receivedAt` and the proof
|
|
167
|
+
* object keys; none of them is accepted here.
|
|
168
|
+
*
|
|
169
|
+
* Held to its schema by `AssertDeliveryEventAppendRequestMatchesSchema`.
|
|
170
|
+
*/
|
|
171
|
+
interface DeliveryEventAppendRequest {
|
|
172
|
+
type: DeliveryEventType;
|
|
173
|
+
/** ≤ 1000 characters. */
|
|
174
|
+
note?: string;
|
|
175
|
+
gps?: Omit<DeliveryGpsFix, 'receivedAt'>;
|
|
176
|
+
/**
|
|
177
|
+
* Inline base64 proof, at most 5 assets (`MAX_PROOF_ASSETS_PER_EVENT`). Stored
|
|
178
|
+
* under a name derived from the delivery, the key, the asset's position and
|
|
179
|
+
* its bytes, written create-only — a retry can never replace an object.
|
|
180
|
+
*/
|
|
181
|
+
proof?: Array<{
|
|
182
|
+
kind: DeliveryProofAsset['kind'];
|
|
183
|
+
contentType: string;
|
|
184
|
+
data: string;
|
|
185
|
+
}>;
|
|
186
|
+
/** Only on a `'failed'` event — anything else is `400 VALIDATION_FAILED`. */
|
|
187
|
+
failureReason?: DeliveryFailureReason;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* What `GET /deliveries/{id}?idempotencyKey=<key>` answers about the
|
|
191
|
+
* CALLER'S OWN attempt under that key on THAT delivery — the authoritative
|
|
192
|
+
* recovery read, unlike the bounded (`truncated`) timeline, which can never
|
|
193
|
+
* prove absence.
|
|
194
|
+
*
|
|
195
|
+
* - `'committed'` — the attempt's event exists; `event` is that exact
|
|
196
|
+
* original. Never append again for it.
|
|
197
|
+
* - `'pending'` — a request under the key is still in flight. Re-ask after
|
|
198
|
+
* `retryAfterMs`; nothing was fenced.
|
|
199
|
+
* - `'not-committed'` — nothing was appended under the key, and the key is
|
|
200
|
+
* FENCED: a durable tombstone (written by this read, or an earlier one, at
|
|
201
|
+
* `fencedAt`) makes every later commit under it refuse. Only now may the
|
|
202
|
+
* client mint a NEW key for the same intent; the old key answers
|
|
203
|
+
* `409 IDEMPOTENCY_KEY_FAILED` forever.
|
|
204
|
+
*
|
|
205
|
+
* ⚠️ This read can WRITE: when the key is neither committed nor in flight it
|
|
206
|
+
* writes the tombstone before answering `'not-committed'`, so asking is itself
|
|
207
|
+
* a decision to give the key up. A client that would rather keep it resends
|
|
208
|
+
* under the same key instead.
|
|
209
|
+
*
|
|
210
|
+
* Attempt evidence is durable (no TTL), so both `'committed'` and
|
|
211
|
+
* `'not-committed'` stay answerable for the life of the delivery.
|
|
212
|
+
*
|
|
213
|
+
* The lookup takes the append's own authority bar: on a delivery that names
|
|
214
|
+
* a platform courier, a caller who is neither that courier nor ADMIN-tier is
|
|
215
|
+
* `403 DELIVERY_NOT_ASSIGNED`, before anything is read or written. Under an
|
|
216
|
+
* impersonated support session it answers only from stored state, and is
|
|
217
|
+
* `403 IMPERSONATION_READONLY` (`op: 'delivery.attemptFence'`) exactly where
|
|
218
|
+
* answering would need the tombstone.
|
|
219
|
+
*/
|
|
220
|
+
interface DeliveryEventAttemptLookup {
|
|
221
|
+
idempotencyKey: string;
|
|
222
|
+
verdict: 'committed' | 'pending' | 'not-committed';
|
|
223
|
+
/** Present only on `'committed'`. */
|
|
224
|
+
event?: DeliveryEventWithProofUrls;
|
|
225
|
+
/** Present only on `'pending'`. */
|
|
226
|
+
retryAfterMs?: number;
|
|
227
|
+
/** Present only on `'not-committed'` — ms epoch the tombstone was written. */
|
|
228
|
+
fencedAt?: number;
|
|
229
|
+
}
|
|
129
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Every refusal code the delivery handlers (`POST /deliveries`,
|
|
233
|
+
* `POST /deliveries/{id}/events`, `GET /deliveries`, `GET /deliveries/{id}`)
|
|
234
|
+
* put on the wire, beyond the platform-wide set (`VALIDATION_FAILED`,
|
|
235
|
+
* `ROLE_FORBIDDEN`, `tryCatch`'s generic `UNHANDLED_ERROR`). Pinned against
|
|
236
|
+
* the handler sources by `tests/unit/deliveryErrorCodesPublished.test.ts`.
|
|
237
|
+
*
|
|
238
|
+
* - `INVALID_DELIVERY_ID` · 400 — no delivery id in the path.
|
|
239
|
+
* - `DELIVERY_NOT_FOUND` · 404 — unknown id, or another tenant's.
|
|
240
|
+
* - `DELIVERY_NOT_ASSIGNED` · 403 — the delivery names a platform courier
|
|
241
|
+
* (`courier.userId`) and the caller is neither that user nor ADMIN-tier.
|
|
242
|
+
* - `DELIVERY_TERMINAL` · 409 — the delivery is already `'delivered'` or
|
|
243
|
+
* `'failed'`; a failed delivery is re-dispatched through `POST /deliveries`.
|
|
244
|
+
* - `INVALID_IDEMPOTENCY_KEY` · 400 — the key is not ≤200 chars of
|
|
245
|
+
* `[A-Za-z0-9._:-]`.
|
|
246
|
+
* - `IDEMPOTENCY_KEY_REUSE` · 422 — the key was used with a different body.
|
|
247
|
+
* - `IDEMPOTENCY_KEY_FAILED` · 409 — the attempt under the key failed after it
|
|
248
|
+
* may have written proof, or the attempt lookup fenced the key; ask the
|
|
249
|
+
* lookup, then use a new key.
|
|
250
|
+
* - `IDEMPOTENCY_IN_PROGRESS` · 409 — the same key is still running.
|
|
251
|
+
* - `IDEMPOTENCY_LOCK_CONTENTION` · 409 — the key's marker could not be
|
|
252
|
+
* claimed; retry under the SAME key.
|
|
253
|
+
* - `INVALID_CURSOR` · 400 — a `?mine=true` `startKey` that is malformed or
|
|
254
|
+
* names another store's partition.
|
|
255
|
+
* - `IMPERSONATION_READONLY` · 403 — the attempt lookup, under a support
|
|
256
|
+
* session, could answer only by fencing the key (`op: 'delivery.attemptFence'`).
|
|
257
|
+
* - `INVALID_BASE64` · 400, `CONTENT_TOO_LARGE` · 413,
|
|
258
|
+
* `UNSUPPORTED_MEDIA_TYPE` · 415 — the inline proof payload was refused
|
|
259
|
+
* before anything was stored.
|
|
260
|
+
*/
|
|
261
|
+
export declare const DELIVERY_ERROR_CODES: readonly ["DELIVERY_NOT_FOUND", "DELIVERY_NOT_ASSIGNED", "DELIVERY_TERMINAL", "INVALID_DELIVERY_ID", "INVALID_IDEMPOTENCY_KEY", "IDEMPOTENCY_KEY_REUSE", "IDEMPOTENCY_KEY_FAILED", "IDEMPOTENCY_IN_PROGRESS", "IDEMPOTENCY_LOCK_CONTENTION", "INVALID_CURSOR", "IMPERSONATION_READONLY", "INVALID_BASE64", "CONTENT_TOO_LARGE", "UNSUPPORTED_MEDIA_TYPE"];
|
|
262
|
+
export type DeliveryErrorCode = (typeof DELIVERY_ERROR_CODES)[number];
|
|
130
263
|
export {};
|
package/dist/delivery.js
CHANGED
|
@@ -1 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Every refusal code the delivery handlers (`POST /deliveries`,
|
|
3
|
+
* `POST /deliveries/{id}/events`, `GET /deliveries`, `GET /deliveries/{id}`)
|
|
4
|
+
* put on the wire, beyond the platform-wide set (`VALIDATION_FAILED`,
|
|
5
|
+
* `ROLE_FORBIDDEN`, `tryCatch`'s generic `UNHANDLED_ERROR`). Pinned against
|
|
6
|
+
* the handler sources by `tests/unit/deliveryErrorCodesPublished.test.ts`.
|
|
7
|
+
*
|
|
8
|
+
* - `INVALID_DELIVERY_ID` · 400 — no delivery id in the path.
|
|
9
|
+
* - `DELIVERY_NOT_FOUND` · 404 — unknown id, or another tenant's.
|
|
10
|
+
* - `DELIVERY_NOT_ASSIGNED` · 403 — the delivery names a platform courier
|
|
11
|
+
* (`courier.userId`) and the caller is neither that user nor ADMIN-tier.
|
|
12
|
+
* - `DELIVERY_TERMINAL` · 409 — the delivery is already `'delivered'` or
|
|
13
|
+
* `'failed'`; a failed delivery is re-dispatched through `POST /deliveries`.
|
|
14
|
+
* - `INVALID_IDEMPOTENCY_KEY` · 400 — the key is not ≤200 chars of
|
|
15
|
+
* `[A-Za-z0-9._:-]`.
|
|
16
|
+
* - `IDEMPOTENCY_KEY_REUSE` · 422 — the key was used with a different body.
|
|
17
|
+
* - `IDEMPOTENCY_KEY_FAILED` · 409 — the attempt under the key failed after it
|
|
18
|
+
* may have written proof, or the attempt lookup fenced the key; ask the
|
|
19
|
+
* lookup, then use a new key.
|
|
20
|
+
* - `IDEMPOTENCY_IN_PROGRESS` · 409 — the same key is still running.
|
|
21
|
+
* - `IDEMPOTENCY_LOCK_CONTENTION` · 409 — the key's marker could not be
|
|
22
|
+
* claimed; retry under the SAME key.
|
|
23
|
+
* - `INVALID_CURSOR` · 400 — a `?mine=true` `startKey` that is malformed or
|
|
24
|
+
* names another store's partition.
|
|
25
|
+
* - `IMPERSONATION_READONLY` · 403 — the attempt lookup, under a support
|
|
26
|
+
* session, could answer only by fencing the key (`op: 'delivery.attemptFence'`).
|
|
27
|
+
* - `INVALID_BASE64` · 400, `CONTENT_TOO_LARGE` · 413,
|
|
28
|
+
* `UNSUPPORTED_MEDIA_TYPE` · 415 — the inline proof payload was refused
|
|
29
|
+
* before anything was stored.
|
|
30
|
+
*/
|
|
31
|
+
export const DELIVERY_ERROR_CODES = [
|
|
32
|
+
'DELIVERY_NOT_FOUND',
|
|
33
|
+
'DELIVERY_NOT_ASSIGNED',
|
|
34
|
+
'DELIVERY_TERMINAL',
|
|
35
|
+
'INVALID_DELIVERY_ID',
|
|
36
|
+
'INVALID_IDEMPOTENCY_KEY',
|
|
37
|
+
'IDEMPOTENCY_KEY_REUSE',
|
|
38
|
+
'IDEMPOTENCY_KEY_FAILED',
|
|
39
|
+
'IDEMPOTENCY_IN_PROGRESS',
|
|
40
|
+
'IDEMPOTENCY_LOCK_CONTENTION',
|
|
41
|
+
'INVALID_CURSOR',
|
|
42
|
+
'IMPERSONATION_READONLY',
|
|
43
|
+
// The proof payload gate (`deliveryAttachmentValidation.ts`).
|
|
44
|
+
'INVALID_BASE64',
|
|
45
|
+
'CONTENT_TOO_LARGE',
|
|
46
|
+
'UNSUPPORTED_MEDIA_TYPE',
|
|
47
|
+
];
|
package/dist/index.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ export * from './mercadopago.ts';
|
|
|
50
50
|
export * from './notification.ts';
|
|
51
51
|
export * from './order.ts';
|
|
52
52
|
export * from './orderCollectionAttempt.ts';
|
|
53
|
+
export * from './orderCompletionAttempt.ts';
|
|
53
54
|
export * from './orderCreateAttempt.ts';
|
|
54
55
|
export * from './payment.ts';
|
|
55
56
|
export * from './platform.ts';
|
package/dist/index.js
CHANGED
|
@@ -58,6 +58,7 @@ export * from "./mercadopago.js";
|
|
|
58
58
|
export * from "./notification.js";
|
|
59
59
|
export * from "./order.js";
|
|
60
60
|
export * from "./orderCollectionAttempt.js";
|
|
61
|
+
export * from "./orderCompletionAttempt.js";
|
|
61
62
|
export * from "./orderCreateAttempt.js";
|
|
62
63
|
export * from "./payment.js";
|
|
63
64
|
export * from "./platform.js";
|
package/dist/order.d.ts
CHANGED
|
@@ -2702,6 +2702,22 @@ export declare const orderFulfilmentProgress: (order: OrderFulfilmentProgressInp
|
|
|
2702
2702
|
* `IDEMPOTENCY_KEY_FAILED`, ask `POST /orders { mode: 'collection-attempt' }`
|
|
2703
2703
|
* (`OrderCollectionAttemptVerdict`) — never resend it under a new key first.
|
|
2704
2704
|
*
|
|
2705
|
+
* Completion-attempt refusals, on `mode: 'delivery'` under an `oc1~`
|
|
2706
|
+
* `Idempotency-Key` (`ORDER_COMPLETION_ATTEMPT_KEY_PATTERN`). A legacy key never
|
|
2707
|
+
* gets either; the key refusals are the create's own `ORDER_ATTEMPT_KEY_EXPIRED`
|
|
2708
|
+
* and `ORDER_ATTEMPT_KEY_SCOPE_MISMATCH` above. ⚠️ On a delivery their remedy is
|
|
2709
|
+
* NOT "mint a new key and resend": the original may have applied, so run
|
|
2710
|
+
* `mode: 'completion-attempt'` on the old key first and send a new attempt only
|
|
2711
|
+
* on `not-written`. Nothing was written on any of them:
|
|
2712
|
+
* - `ORDER_COMPLETION_ATTEMPT_COMMITTED` · 409 — this key's original attempt
|
|
2713
|
+
* already applied; `data` is the `committed` `OrderCompletionAttemptVerdict`.
|
|
2714
|
+
* Do not resend.
|
|
2715
|
+
* - `ORDER_COMPLETION_ATTEMPT_ABANDONED` · 409 — a completion-attempt lookup
|
|
2716
|
+
* fenced this key and answered `not-written`. Re-read the order and send a NEW
|
|
2717
|
+
* attempt under a NEW key.
|
|
2718
|
+
* - `IDEMPOTENCY_KEY_REUSE` · 422 — the key's recorded attempt carried a
|
|
2719
|
+
* different `orderId`, `delivered` or `sendSms`.
|
|
2720
|
+
*
|
|
2705
2721
|
* Idempotency answers, all six from `withIdempotency`:
|
|
2706
2722
|
* - `INVALID_IDEMPOTENCY_KEY` · 400 · `IDEMPOTENCY_KEY_REUSE` · 422 (the same
|
|
2707
2723
|
* key with a different body)
|
|
@@ -2723,6 +2739,6 @@ export declare const orderFulfilmentProgress: (order: OrderFulfilmentProgressInp
|
|
|
2723
2739
|
* `SUBSCRIPTION_READONLY`) sit on the order CREATE leg only: an edit is not a new
|
|
2724
2740
|
* order, so `bareOrderPostHandler` routes past `requireMetered` entirely.
|
|
2725
2741
|
*/
|
|
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"];
|
|
2742
|
+
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", "ORDER_COMPLETION_ATTEMPT_COMMITTED", "ORDER_COMPLETION_ATTEMPT_ABANDONED"];
|
|
2727
2743
|
export type OrderErrorCode = (typeof ORDER_ERROR_CODES)[number];
|
|
2728
2744
|
export {};
|
package/dist/order.js
CHANGED
|
@@ -457,6 +457,22 @@ export const orderFulfilmentProgress = (order) => {
|
|
|
457
457
|
* `IDEMPOTENCY_KEY_FAILED`, ask `POST /orders { mode: 'collection-attempt' }`
|
|
458
458
|
* (`OrderCollectionAttemptVerdict`) — never resend it under a new key first.
|
|
459
459
|
*
|
|
460
|
+
* Completion-attempt refusals, on `mode: 'delivery'` under an `oc1~`
|
|
461
|
+
* `Idempotency-Key` (`ORDER_COMPLETION_ATTEMPT_KEY_PATTERN`). A legacy key never
|
|
462
|
+
* gets either; the key refusals are the create's own `ORDER_ATTEMPT_KEY_EXPIRED`
|
|
463
|
+
* and `ORDER_ATTEMPT_KEY_SCOPE_MISMATCH` above. ⚠️ On a delivery their remedy is
|
|
464
|
+
* NOT "mint a new key and resend": the original may have applied, so run
|
|
465
|
+
* `mode: 'completion-attempt'` on the old key first and send a new attempt only
|
|
466
|
+
* on `not-written`. Nothing was written on any of them:
|
|
467
|
+
* - `ORDER_COMPLETION_ATTEMPT_COMMITTED` · 409 — this key's original attempt
|
|
468
|
+
* already applied; `data` is the `committed` `OrderCompletionAttemptVerdict`.
|
|
469
|
+
* Do not resend.
|
|
470
|
+
* - `ORDER_COMPLETION_ATTEMPT_ABANDONED` · 409 — a completion-attempt lookup
|
|
471
|
+
* fenced this key and answered `not-written`. Re-read the order and send a NEW
|
|
472
|
+
* attempt under a NEW key.
|
|
473
|
+
* - `IDEMPOTENCY_KEY_REUSE` · 422 — the key's recorded attempt carried a
|
|
474
|
+
* different `orderId`, `delivered` or `sendSms`.
|
|
475
|
+
*
|
|
460
476
|
* Idempotency answers, all six from `withIdempotency`:
|
|
461
477
|
* - `INVALID_IDEMPOTENCY_KEY` · 400 · `IDEMPOTENCY_KEY_REUSE` · 422 (the same
|
|
462
478
|
* key with a different body)
|
|
@@ -606,4 +622,7 @@ export const ORDER_ERROR_CODES = [
|
|
|
606
622
|
'ORDER_ATTEMPT_ABANDONED',
|
|
607
623
|
'ORDER_ATTEMPT_KEY_EXPIRED',
|
|
608
624
|
'ORDER_ATTEMPT_KEY_SCOPE_MISMATCH',
|
|
625
|
+
// `mode: 'delivery'` under an `oc1~` key — see `ORDER_COMPLETION_ATTEMPT_KEY_PATTERN`.
|
|
626
|
+
'ORDER_COMPLETION_ATTEMPT_COMMITTED',
|
|
627
|
+
'ORDER_COMPLETION_ATTEMPT_ABANDONED',
|
|
609
628
|
];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staff completion attempt evidence — the recovery protocol for
|
|
3
|
+
* `POST /orders { mode: 'delivery' }` (pickup completion and its same-day
|
|
4
|
+
* reversal) when the client lost the reply and has to decide whether the
|
|
5
|
+
* delivery it sent landed before it sends anything again.
|
|
6
|
+
*
|
|
7
|
+
* The protocol is opt-in and rides on the `Idempotency-Key` value. A key
|
|
8
|
+
* matching {@link ORDER_COMPLETION_ATTEMPT_KEY_PATTERN} makes the delivery
|
|
9
|
+
* write a durable `committed` record IN THE SAME TRANSACTION as the order, the
|
|
10
|
+
* customer balance and the receivable, bound to the store, the user, the key
|
|
11
|
+
* and the request (`orderId`, `delivered`, `sendSms`). That record — not the
|
|
12
|
+
* 24-hour `Idempotency-Key` replay cache — is what keeps the attempt from
|
|
13
|
+
* applying twice: a same-key resend after the cache is gone, after the order was
|
|
14
|
+
* un-delivered on another device, or after a crash between the commit and the
|
|
15
|
+
* reply is refused `409 ORDER_COMPLETION_ATTEMPT_COMMITTED` and writes nothing.
|
|
16
|
+
* Any other key is a legacy key and behaves exactly as before.
|
|
17
|
+
*
|
|
18
|
+
* `POST /orders { mode: 'completion-attempt', attemptKey }` answers for a
|
|
19
|
+
* protocol key ({@link OrderCompletionAttemptVerdict}).
|
|
20
|
+
*
|
|
21
|
+
* The key carries the same scope tag, mint window, clock skew and retention as
|
|
22
|
+
* the order-create attempt key: `ORDER_ATTEMPT_SCOPE_TAG_LENGTH`,
|
|
23
|
+
* `ORDER_ATTEMPT_CREATE_WINDOW_MS`, `ORDER_ATTEMPT_CLOCK_SKEW_MS` and
|
|
24
|
+
* `ORDER_ATTEMPT_RETENTION_MS`. A delivery under a key older than the window,
|
|
25
|
+
* or minted for another store or user session, is refused before anything is
|
|
26
|
+
* read or written (`400 ORDER_ATTEMPT_KEY_EXPIRED` /
|
|
27
|
+
* `400 ORDER_ATTEMPT_KEY_SCOPE_MISMATCH`). ⚠️ Neither says the original did not
|
|
28
|
+
* apply: before sending again under a new key, look the OLD key up — the
|
|
29
|
+
* lookup answers from the record for the whole retention, past the window —
|
|
30
|
+
* and resend only on `not-written`.
|
|
31
|
+
*
|
|
32
|
+
* What a client does with each outcome:
|
|
33
|
+
*
|
|
34
|
+
* - `200` — applied now. Done.
|
|
35
|
+
* - `409 ORDER_COMPLETION_ATTEMPT_COMMITTED` or a `committed` verdict — the
|
|
36
|
+
* ORIGINAL attempt applied; `data` names the order, the leg (`delivered`) and
|
|
37
|
+
* `committedAt`, which is also the `updatedAt` that write stamped. Done:
|
|
38
|
+
* refresh the order, never resend. The order may have moved since (an
|
|
39
|
+
* un-delivery on the web is a later, separate write).
|
|
40
|
+
* - `pending` — the original is still running. Ask again after `retryAfterMs`.
|
|
41
|
+
* - `not-written` or `409 ORDER_COMPLETION_ATTEMPT_ABANDONED` — the attempt
|
|
42
|
+
* never applied and never will. A resend is allowed only as a NEW attempt:
|
|
43
|
+
* re-read the order, then send under a NEW key.
|
|
44
|
+
* - `unknown` — the server cannot decide. Never treat it as `not-written` and
|
|
45
|
+
* never resend automatically; the operator verifies the order first.
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ `409 ORDER_DELIVERY_STATE_UNCHANGED`, a missing list row or an absent
|
|
48
|
+
* replay are NOT evidence of either outcome; only the codes and verdicts above
|
|
49
|
+
* are.
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ A backend that predates this protocol refuses a protocol key with
|
|
52
|
+
* `400 INVALID_IDEMPOTENCY_KEY` before writing anything (`~` is outside the
|
|
53
|
+
* legacy key grammar). Do not mint protocol keys against a backend that has not
|
|
54
|
+
* shipped it: every delivery would fail, safely but entirely.
|
|
55
|
+
*
|
|
56
|
+
* The staff READY mark is a field edit (`POST /orders` with `readyAt` and
|
|
57
|
+
* `expectedUpdatedAt`), not this protocol: its `expectedUpdatedAt` is a frozen
|
|
58
|
+
* version precondition, so a resend of the same frozen body can never apply
|
|
59
|
+
* over a later change of the order — it is refused `409 ORDER_VERSION_CONFLICT`.
|
|
60
|
+
* ⚠️ Only when the token is sent: it is optional, and a ready mark without it
|
|
61
|
+
* is pinned to the server's own read at resend time, so a token-less resend
|
|
62
|
+
* after a later un-ready marks the order ready again. Send `expectedUpdatedAt`
|
|
63
|
+
* with every ready mark a client may resend.
|
|
64
|
+
*/
|
|
65
|
+
declare global {
|
|
66
|
+
/** One of {@link ORDER_COMPLETION_ATTEMPT_VERDICTS}. */
|
|
67
|
+
type OrderCompletionAttemptVerdictKind = (typeof ORDER_COMPLETION_ATTEMPT_VERDICTS)[number];
|
|
68
|
+
/** Why a lookup could not decide — one of {@link ORDER_COMPLETION_ATTEMPT_UNKNOWN_REASONS}. */
|
|
69
|
+
type OrderCompletionAttemptUnknownReason = (typeof ORDER_COMPLETION_ATTEMPT_UNKNOWN_REASONS)[number];
|
|
70
|
+
/**
|
|
71
|
+
* `POST /orders` body for the recovery lookup. `attemptKey` is the exact
|
|
72
|
+
* `Idempotency-Key` the delivery was sent with; the store and user are the
|
|
73
|
+
* caller's session. Safe to repeat, but `pending` is a moment in time: ask
|
|
74
|
+
* again after `retryAfterMs` and never cache it. `committed`, `not-written`
|
|
75
|
+
* and every `unknown` reason but `KEY_CLOCK_AHEAD` are final.
|
|
76
|
+
*/
|
|
77
|
+
interface OrderCompletionAttemptLookupRequest {
|
|
78
|
+
mode: 'completion-attempt';
|
|
79
|
+
attemptKey: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* `data` of a `200` from `POST /orders { mode: 'completion-attempt' }`, and
|
|
83
|
+
* `data` of a `409 ORDER_COMPLETION_ATTEMPT_COMMITTED` (always the
|
|
84
|
+
* `committed` member there).
|
|
85
|
+
*
|
|
86
|
+
* - `committed` — the delivery (`delivered: true`) or un-delivery
|
|
87
|
+
* (`delivered: false`) of `orderId` applied under this key at
|
|
88
|
+
* `committedAt` (epoch ms, the `updatedAt` it stamped on the order).
|
|
89
|
+
* - `pending` — still running; ask again after `retryAfterMs`.
|
|
90
|
+
* - `not-written` — nothing applied under this key and nothing ever will:
|
|
91
|
+
* the lookup fenced it at `fencedAt`. Resend under a NEW key.
|
|
92
|
+
* - `unknown` — undecidable; `reason` says why. Intervention, never a resend.
|
|
93
|
+
*/
|
|
94
|
+
type OrderCompletionAttemptVerdict = {
|
|
95
|
+
verdict: Extract<OrderCompletionAttemptVerdictKind, 'committed'>;
|
|
96
|
+
orderId: string;
|
|
97
|
+
delivered: boolean;
|
|
98
|
+
committedAt: number;
|
|
99
|
+
} | {
|
|
100
|
+
verdict: Extract<OrderCompletionAttemptVerdictKind, 'pending'>;
|
|
101
|
+
retryAfterMs: number;
|
|
102
|
+
} | {
|
|
103
|
+
verdict: Extract<OrderCompletionAttemptVerdictKind, 'not-written'>;
|
|
104
|
+
fencedAt: number;
|
|
105
|
+
} | {
|
|
106
|
+
verdict: Extract<OrderCompletionAttemptVerdictKind, 'unknown'>;
|
|
107
|
+
reason: OrderCompletionAttemptUnknownReason;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The completion attempt key: `oc1~<mintMs>~<scopeTag>~<nonce>` — the
|
|
112
|
+
* order-create grammar under its own version tag, so neither route accepts the
|
|
113
|
+
* other's key. `mintMs` is 13 digits of epoch milliseconds, `scopeTag` is
|
|
114
|
+
* `ORDER_ATTEMPT_SCOPE_TAG_LENGTH` base64url characters computed exactly as for
|
|
115
|
+
* the create key, and `nonce` is 16 to 64 base64url characters.
|
|
116
|
+
*
|
|
117
|
+
* Mint ONE key per completion attempt and keep it, with the same body, for
|
|
118
|
+
* every resend of that attempt; a new key is a new attempt.
|
|
119
|
+
*
|
|
120
|
+
* A string, not a `RegExp`, and it carries no flags.
|
|
121
|
+
*/
|
|
122
|
+
export declare const ORDER_COMPLETION_ATTEMPT_KEY_PATTERN = "^oc1~\\d{13}~[A-Za-z0-9_-]{12}~[A-Za-z0-9_-]{16,64}$";
|
|
123
|
+
/** The version tag every completion attempt key starts with. */
|
|
124
|
+
export declare const ORDER_COMPLETION_ATTEMPT_KEY_PREFIX = "oc1~";
|
|
125
|
+
/** Every `verdict` a completion-attempt lookup answers — see `OrderCompletionAttemptVerdict`. */
|
|
126
|
+
export declare const ORDER_COMPLETION_ATTEMPT_VERDICTS: readonly ["committed", "pending", "not-written", "unknown"];
|
|
127
|
+
/**
|
|
128
|
+
* Every `reason` on an `unknown` verdict — the same four, with the same
|
|
129
|
+
* meaning, as the order-create lookup's.
|
|
130
|
+
*/
|
|
131
|
+
export declare const ORDER_COMPLETION_ATTEMPT_UNKNOWN_REASONS: readonly ["LEGACY_KEY", "SCOPE_MISMATCH", "EVIDENCE_EXPIRED", "KEY_CLOCK_AHEAD"];
|
|
132
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staff completion attempt evidence — the recovery protocol for
|
|
3
|
+
* `POST /orders { mode: 'delivery' }` (pickup completion and its same-day
|
|
4
|
+
* reversal) when the client lost the reply and has to decide whether the
|
|
5
|
+
* delivery it sent landed before it sends anything again.
|
|
6
|
+
*
|
|
7
|
+
* The protocol is opt-in and rides on the `Idempotency-Key` value. A key
|
|
8
|
+
* matching {@link ORDER_COMPLETION_ATTEMPT_KEY_PATTERN} makes the delivery
|
|
9
|
+
* write a durable `committed` record IN THE SAME TRANSACTION as the order, the
|
|
10
|
+
* customer balance and the receivable, bound to the store, the user, the key
|
|
11
|
+
* and the request (`orderId`, `delivered`, `sendSms`). That record — not the
|
|
12
|
+
* 24-hour `Idempotency-Key` replay cache — is what keeps the attempt from
|
|
13
|
+
* applying twice: a same-key resend after the cache is gone, after the order was
|
|
14
|
+
* un-delivered on another device, or after a crash between the commit and the
|
|
15
|
+
* reply is refused `409 ORDER_COMPLETION_ATTEMPT_COMMITTED` and writes nothing.
|
|
16
|
+
* Any other key is a legacy key and behaves exactly as before.
|
|
17
|
+
*
|
|
18
|
+
* `POST /orders { mode: 'completion-attempt', attemptKey }` answers for a
|
|
19
|
+
* protocol key ({@link OrderCompletionAttemptVerdict}).
|
|
20
|
+
*
|
|
21
|
+
* The key carries the same scope tag, mint window, clock skew and retention as
|
|
22
|
+
* the order-create attempt key: `ORDER_ATTEMPT_SCOPE_TAG_LENGTH`,
|
|
23
|
+
* `ORDER_ATTEMPT_CREATE_WINDOW_MS`, `ORDER_ATTEMPT_CLOCK_SKEW_MS` and
|
|
24
|
+
* `ORDER_ATTEMPT_RETENTION_MS`. A delivery under a key older than the window,
|
|
25
|
+
* or minted for another store or user session, is refused before anything is
|
|
26
|
+
* read or written (`400 ORDER_ATTEMPT_KEY_EXPIRED` /
|
|
27
|
+
* `400 ORDER_ATTEMPT_KEY_SCOPE_MISMATCH`). ⚠️ Neither says the original did not
|
|
28
|
+
* apply: before sending again under a new key, look the OLD key up — the
|
|
29
|
+
* lookup answers from the record for the whole retention, past the window —
|
|
30
|
+
* and resend only on `not-written`.
|
|
31
|
+
*
|
|
32
|
+
* What a client does with each outcome:
|
|
33
|
+
*
|
|
34
|
+
* - `200` — applied now. Done.
|
|
35
|
+
* - `409 ORDER_COMPLETION_ATTEMPT_COMMITTED` or a `committed` verdict — the
|
|
36
|
+
* ORIGINAL attempt applied; `data` names the order, the leg (`delivered`) and
|
|
37
|
+
* `committedAt`, which is also the `updatedAt` that write stamped. Done:
|
|
38
|
+
* refresh the order, never resend. The order may have moved since (an
|
|
39
|
+
* un-delivery on the web is a later, separate write).
|
|
40
|
+
* - `pending` — the original is still running. Ask again after `retryAfterMs`.
|
|
41
|
+
* - `not-written` or `409 ORDER_COMPLETION_ATTEMPT_ABANDONED` — the attempt
|
|
42
|
+
* never applied and never will. A resend is allowed only as a NEW attempt:
|
|
43
|
+
* re-read the order, then send under a NEW key.
|
|
44
|
+
* - `unknown` — the server cannot decide. Never treat it as `not-written` and
|
|
45
|
+
* never resend automatically; the operator verifies the order first.
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ `409 ORDER_DELIVERY_STATE_UNCHANGED`, a missing list row or an absent
|
|
48
|
+
* replay are NOT evidence of either outcome; only the codes and verdicts above
|
|
49
|
+
* are.
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ A backend that predates this protocol refuses a protocol key with
|
|
52
|
+
* `400 INVALID_IDEMPOTENCY_KEY` before writing anything (`~` is outside the
|
|
53
|
+
* legacy key grammar). Do not mint protocol keys against a backend that has not
|
|
54
|
+
* shipped it: every delivery would fail, safely but entirely.
|
|
55
|
+
*
|
|
56
|
+
* The staff READY mark is a field edit (`POST /orders` with `readyAt` and
|
|
57
|
+
* `expectedUpdatedAt`), not this protocol: its `expectedUpdatedAt` is a frozen
|
|
58
|
+
* version precondition, so a resend of the same frozen body can never apply
|
|
59
|
+
* over a later change of the order — it is refused `409 ORDER_VERSION_CONFLICT`.
|
|
60
|
+
* ⚠️ Only when the token is sent: it is optional, and a ready mark without it
|
|
61
|
+
* is pinned to the server's own read at resend time, so a token-less resend
|
|
62
|
+
* after a later un-ready marks the order ready again. Send `expectedUpdatedAt`
|
|
63
|
+
* with every ready mark a client may resend.
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* The completion attempt key: `oc1~<mintMs>~<scopeTag>~<nonce>` — the
|
|
67
|
+
* order-create grammar under its own version tag, so neither route accepts the
|
|
68
|
+
* other's key. `mintMs` is 13 digits of epoch milliseconds, `scopeTag` is
|
|
69
|
+
* `ORDER_ATTEMPT_SCOPE_TAG_LENGTH` base64url characters computed exactly as for
|
|
70
|
+
* the create key, and `nonce` is 16 to 64 base64url characters.
|
|
71
|
+
*
|
|
72
|
+
* Mint ONE key per completion attempt and keep it, with the same body, for
|
|
73
|
+
* every resend of that attempt; a new key is a new attempt.
|
|
74
|
+
*
|
|
75
|
+
* A string, not a `RegExp`, and it carries no flags.
|
|
76
|
+
*/
|
|
77
|
+
export const ORDER_COMPLETION_ATTEMPT_KEY_PATTERN = '^oc1~\\d{13}~[A-Za-z0-9_-]{12}~[A-Za-z0-9_-]{16,64}$';
|
|
78
|
+
/** The version tag every completion attempt key starts with. */
|
|
79
|
+
export const ORDER_COMPLETION_ATTEMPT_KEY_PREFIX = 'oc1~';
|
|
80
|
+
/** Every `verdict` a completion-attempt lookup answers — see `OrderCompletionAttemptVerdict`. */
|
|
81
|
+
export const ORDER_COMPLETION_ATTEMPT_VERDICTS = ['committed', 'pending', 'not-written', 'unknown'];
|
|
82
|
+
/**
|
|
83
|
+
* Every `reason` on an `unknown` verdict — the same four, with the same
|
|
84
|
+
* meaning, as the order-create lookup's.
|
|
85
|
+
*/
|
|
86
|
+
export const ORDER_COMPLETION_ATTEMPT_UNKNOWN_REASONS = [
|
|
87
|
+
'LEGACY_KEY',
|
|
88
|
+
'SCOPE_MISMATCH',
|
|
89
|
+
'EVIDENCE_EXPIRED',
|
|
90
|
+
'KEY_CLOCK_AHEAD',
|
|
91
|
+
];
|