sinfactura-types 1.10.310 → 1.10.312
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/cart.d.ts +125 -1
- package/dist/cjs/marketing.js +29 -1
- package/dist/cjs/notification.js +29 -3
- package/dist/cjs/webhook.js +11 -2
- package/dist/marketing.d.ts +157 -8
- package/dist/marketing.js +28 -0
- package/dist/notification.d.ts +21 -0
- package/dist/notification.js +29 -3
- package/dist/store.d.ts +27 -0
- package/dist/userActivity.d.ts +21 -3
- package/dist/webhook.d.ts +108 -3
- package/dist/webhook.js +11 -2
- package/package.json +1 -1
package/dist/cart.d.ts
CHANGED
|
@@ -67,6 +67,18 @@ declare global {
|
|
|
67
67
|
* redemption. See `CartCoupon`.
|
|
68
68
|
*/
|
|
69
69
|
coupon?: CartCoupon;
|
|
70
|
+
/**
|
|
71
|
+
* The promotion this cart carries, if any — at most one. Applied by
|
|
72
|
+
* `applyPromotion`, cleared by `removePromotion`, and, like the coupon,
|
|
73
|
+
* holding it is not a redemption. See {@link CartPromotion}.
|
|
74
|
+
*/
|
|
75
|
+
promotion?: CartPromotion;
|
|
76
|
+
/**
|
|
77
|
+
* What `promotion` came to on this cart's current lines, re-derived on
|
|
78
|
+
* every write. Absent when no promotion is applied. See
|
|
79
|
+
* {@link CartPromotionEffect}.
|
|
80
|
+
*/
|
|
81
|
+
promotionEffect?: CartPromotionEffect;
|
|
70
82
|
/**
|
|
71
83
|
* The CART-LEVEL cut this coupon produced, re-derived on every write from
|
|
72
84
|
* `coupon`'s frozen grant against the current subtotal. Absent when no
|
|
@@ -410,6 +422,89 @@ declare global {
|
|
|
410
422
|
*/
|
|
411
423
|
maxDiscountAmount?: number;
|
|
412
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* The promotion a cart currently carries — the granted TERMS, frozen at
|
|
427
|
+
* apply time, exactly as {@link CartCoupon} freezes a coupon's.
|
|
428
|
+
*
|
|
429
|
+
* ⚠️ The terms are frozen, not a reference to the `Promotion` row, and
|
|
430
|
+
* for the same reason the coupon's are: a promotion the merchant edits or
|
|
431
|
+
* ends after a shopper applied it keeps working for that cart until checkout
|
|
432
|
+
* re-validates. Re-reading the row on every cart write would reprice a
|
|
433
|
+
* shopper mid-session from a change they never saw.
|
|
434
|
+
*
|
|
435
|
+
* ⚠️ A cart carries at most ONE of these, the same single slot the
|
|
436
|
+
* coupon has. Stacking two promotions of the same type on one cart is a
|
|
437
|
+
* question nobody has answered — what a second buy-2-get-1 does to units
|
|
438
|
+
* the first already rewarded has no obvious right answer — so applying one
|
|
439
|
+
* over another REPLACES it rather than quietly compounding a cut. When that
|
|
440
|
+
* question is answered, this becomes a list and the replace becomes an
|
|
441
|
+
* append; until then a single slot is the shape that cannot pay out twice
|
|
442
|
+
* for the same unit.
|
|
443
|
+
*
|
|
444
|
+
* ⚠️ `'coupon'` is absent from the union on purpose. A coupon promotion's
|
|
445
|
+
* money lives in the `Coupon` row, reaches the cart through `applyCoupon`,
|
|
446
|
+
* and sits in `Cart.coupon` — a cart that could hold it here as well would
|
|
447
|
+
* have two places to disagree about one cut.
|
|
448
|
+
*/
|
|
449
|
+
type CartPromotion = {
|
|
450
|
+
promotionId: string;
|
|
451
|
+
/** The promotion's name at apply time, so a receipt can say what was given. */
|
|
452
|
+
name: string;
|
|
453
|
+
/** When the shopper applied it. */
|
|
454
|
+
appliedAt: number;
|
|
455
|
+
} & ({
|
|
456
|
+
type: 'buyXGetY';
|
|
457
|
+
buyXGetY: BuyXGetYTerms;
|
|
458
|
+
} | {
|
|
459
|
+
type: 'freeShipping';
|
|
460
|
+
freeShipping: FreeShippingTerms;
|
|
461
|
+
} | {
|
|
462
|
+
type: 'bundle';
|
|
463
|
+
bundle: BundleTerms;
|
|
464
|
+
});
|
|
465
|
+
/**
|
|
466
|
+
* What the frozen {@link CartPromotion} actually came to on THIS cart —
|
|
467
|
+
* re-derived on every write, never stored as the answer.
|
|
468
|
+
*
|
|
469
|
+
* ⚠️ Server-owned, like {@link CartDiscount}'s `amount`. The client may
|
|
470
|
+
* read it and must never send it: it is a function of the cart's current
|
|
471
|
+
* lines and their current prices, so a client-supplied value is a claim about
|
|
472
|
+
* money the server is about to recompute anyway.
|
|
473
|
+
*/
|
|
474
|
+
interface CartPromotionEffect {
|
|
475
|
+
promotionId: string;
|
|
476
|
+
type: 'buyXGetY' | 'freeShipping' | 'bundle';
|
|
477
|
+
/**
|
|
478
|
+
* The DERIVED cut, in currency units, already included in
|
|
479
|
+
* `CartTotals.discount`.
|
|
480
|
+
*
|
|
481
|
+
* ⚠️ ALWAYS `0` for `'freeShipping'`, and that is the invariant, not
|
|
482
|
+
* an accident of the current numbers. Free shipping is not a discount —
|
|
483
|
+
* it zeroes `CartTotals.shipping`, which is a different term of the
|
|
484
|
+
* `grandTotal` formula. A free-shipping promotion that ever contributed
|
|
485
|
+
* here would be cutting the merchandise price for a delivery the merchant
|
|
486
|
+
* agreed to absorb, and the comprobante would discriminate a discount the
|
|
487
|
+
* customer was never given on the goods.
|
|
488
|
+
*/
|
|
489
|
+
amount: number;
|
|
490
|
+
/**
|
|
491
|
+
* How many times the promotion applied. `1` for a promotion that has no
|
|
492
|
+
* repeat notion (`'freeShipping'`), and `0` when the cart carries the
|
|
493
|
+
* promotion but no longer qualifies — a shopper who applied a buy-2-get-1
|
|
494
|
+
* and then removed a line keeps the frozen grant and earns nothing by it.
|
|
495
|
+
*/
|
|
496
|
+
applications: number;
|
|
497
|
+
/**
|
|
498
|
+
* Whether this promotion waives the cart's shipping.
|
|
499
|
+
*
|
|
500
|
+
* ⚠️ `CartTotals.shipping` is structurally `0` today — nothing in the
|
|
501
|
+
* api computes a shipping cost yet, by design, because it is chosen at
|
|
502
|
+
* checkout. So the waiver currently changes no money, and this flag is the
|
|
503
|
+
* only place the GRANT survives to be read by whoever lands the shipping
|
|
504
|
+
* quote. Do not read its absence as "shipping was charged".
|
|
505
|
+
*/
|
|
506
|
+
shippingWaived?: boolean;
|
|
507
|
+
}
|
|
413
508
|
/**
|
|
414
509
|
* A redeemable coupon. `PK: COUPON#{storeId}`, `SK: <normalized code>`.
|
|
415
510
|
*
|
|
@@ -622,7 +717,7 @@ declare global {
|
|
|
622
717
|
* at the DynamoDB marshaller instead of at validation.
|
|
623
718
|
* - `merge.items` is `min(1).max(50)`.
|
|
624
719
|
*/
|
|
625
|
-
type CartActionRequest = CartActionAddLine | CartActionChangeQuantity | CartActionRemoveLine | CartActionClear | CartActionMerge | CartActionSaveLine | CartActionRestoreLine | CartActionRemoveSavedLine | CartActionApplyCoupon | CartActionRemoveCoupon | CartActionSetLineDiscount;
|
|
720
|
+
type CartActionRequest = CartActionAddLine | CartActionChangeQuantity | CartActionRemoveLine | CartActionClear | CartActionMerge | CartActionSaveLine | CartActionRestoreLine | CartActionRemoveSavedLine | CartActionApplyCoupon | CartActionRemoveCoupon | CartActionApplyPromotion | CartActionRemovePromotion | CartActionSetLineDiscount;
|
|
626
721
|
/**
|
|
627
722
|
* Fields common to every action.
|
|
628
723
|
*
|
|
@@ -897,6 +992,35 @@ declare global {
|
|
|
897
992
|
interface CartActionRemoveCoupon extends CartActionBase {
|
|
898
993
|
mode: 'removeCoupon';
|
|
899
994
|
}
|
|
995
|
+
/**
|
|
996
|
+
* Applies a promotion to the cart by id.
|
|
997
|
+
*
|
|
998
|
+
* ⚠️ A cart holds AT MOST ONE promotion. Applying a second REPLACES the
|
|
999
|
+
* first rather than stacking — the same rule, and the same reason, as
|
|
1000
|
+
* {@link CartActionApplyCoupon}: nothing in the request says what a second
|
|
1001
|
+
* buy-2-get-1 does to units the first already rewarded.
|
|
1002
|
+
*
|
|
1003
|
+
* ⚠️ The action names an ID and nothing else. Every term comes off the
|
|
1004
|
+
* `Promotion` row, so a client cannot name its own cut.
|
|
1005
|
+
*
|
|
1006
|
+
* ⚠️ A promotion and a coupon can BOTH be on one cart. They are separate
|
|
1007
|
+
* slots and both cuts land in `CartTotals.discount`, which is clamped at the
|
|
1008
|
+
* subtotal — so the two together can take the cart to zero but never below.
|
|
1009
|
+
*/
|
|
1010
|
+
interface CartActionApplyPromotion extends CartActionBase {
|
|
1011
|
+
mode: 'applyPromotion';
|
|
1012
|
+
promotionId: string;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Clears the cart's promotion and its derived effect.
|
|
1016
|
+
*
|
|
1017
|
+
* A cart carrying no promotion is a no-op `200`, matching every other removal
|
|
1018
|
+
* verb here. Nothing is released, because a promotion has no redemption
|
|
1019
|
+
* counter to consume in the first place.
|
|
1020
|
+
*/
|
|
1021
|
+
interface CartActionRemovePromotion extends CartActionBase {
|
|
1022
|
+
mode: 'removePromotion';
|
|
1023
|
+
}
|
|
900
1024
|
/**
|
|
901
1025
|
* Sets or clears ONE line's discount — the operator's per-line cut.
|
|
902
1026
|
*
|
package/dist/cjs/marketing.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AUTOMATION_RULE_STATUSES = exports.AUTOMATION_SEND_STATUSES = exports.ATTRIBUTION_WINDOW_HOURS = exports.RFM_SEGMENT_LABELS = exports.AUTOMATION_TRIGGERS = void 0;
|
|
3
|
+
exports.CART_APPLICABLE_PROMOTION_TYPES = exports.PROMOTION_TYPES = exports.AUTOMATION_RULE_STATUSES = exports.AUTOMATION_SEND_STATUSES = exports.ATTRIBUTION_WINDOW_HOURS = exports.RFM_SEGMENT_LABELS = exports.AUTOMATION_TRIGGERS = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* The {@link AutomationTrigger} vocabulary as a RUNTIME value, so the api's Zod
|
|
6
6
|
* enum, the configuration screen's picker and the published union all derive
|
|
@@ -70,3 +70,31 @@ exports.AUTOMATION_RULE_STATUSES = [
|
|
|
70
70
|
'active',
|
|
71
71
|
'archived',
|
|
72
72
|
];
|
|
73
|
+
/**
|
|
74
|
+
* {@link PromotionType} as a runtime value — see {@link AUTOMATION_TRIGGERS}.
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ `'coupon'` is in the tuple even though a row may express it by carrying
|
|
77
|
+
* no `type` at all. The tuple is the vocabulary a writer may DECLARE; the absent
|
|
78
|
+
* spelling is a reader's concern, and conflating the two is how a CRUD schema
|
|
79
|
+
* comes to reject the one value every legacy row means.
|
|
80
|
+
*/
|
|
81
|
+
exports.PROMOTION_TYPES = [
|
|
82
|
+
'coupon',
|
|
83
|
+
'buyXGetY',
|
|
84
|
+
'freeShipping',
|
|
85
|
+
'bundle',
|
|
86
|
+
];
|
|
87
|
+
/**
|
|
88
|
+
* The promotion types a CART can freeze onto itself — every type but `'coupon'`.
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ `'coupon'` is excluded on purpose, and not for tidiness. A coupon
|
|
91
|
+
* promotion's money lives in the `Coupon` row its `couponCode` names, and that
|
|
92
|
+
* row already reaches the cart through `applyCoupon` and sits in `Cart.coupon`.
|
|
93
|
+
* Letting it also occupy the promotion slot would give one cart two places to
|
|
94
|
+
* disagree about the same cut.
|
|
95
|
+
*/
|
|
96
|
+
exports.CART_APPLICABLE_PROMOTION_TYPES = [
|
|
97
|
+
'buyXGetY',
|
|
98
|
+
'freeShipping',
|
|
99
|
+
'bundle',
|
|
100
|
+
];
|
package/dist/cjs/notification.js
CHANGED
|
@@ -7,9 +7,14 @@ exports.NotificationTypeEnum = void 0;
|
|
|
7
7
|
// Stripe hook, propagate-fx). Exported as a real enum so `api`
|
|
8
8
|
// (stacks/helpers/notificationType.ts) and `app`
|
|
9
9
|
// (src/domain/notificationType.ts) can drop their hand-mirrored copies
|
|
10
|
-
// in follow-ups. DOLARBNA / ERROR /
|
|
11
|
-
// read path — enum members only
|
|
12
|
-
// alert type)
|
|
10
|
+
// in follow-ups. DOLARBNA / ERROR / ML_FACTURADOR_COLLISION have no
|
|
11
|
+
// User-row read path — enum members only. AFIP_CERT_EXPIRY (the
|
|
12
|
+
// cert-expiry alert type) was in that list and no longer belongs to it:
|
|
13
|
+
// `createStore` seeds it on the founding user and `afipCertMonitor`
|
|
14
|
+
// reads `getNotificationSubscribers` for it, so it is an ordinary opt-in
|
|
15
|
+
// key. Which members are opt-in is not cosmetic — it decides whether a
|
|
16
|
+
// producer may read subscribers at all, and a member seeded nowhere that
|
|
17
|
+
// does so fans out to an empty list on every store.
|
|
13
18
|
var NotificationTypeEnum;
|
|
14
19
|
(function (NotificationTypeEnum) {
|
|
15
20
|
NotificationTypeEnum["ORDER"] = "ORDER";
|
|
@@ -23,6 +28,27 @@ var NotificationTypeEnum;
|
|
|
23
28
|
// ML order-ingestion fanout — User-row read path
|
|
24
29
|
// added by the orders_v2 worker.
|
|
25
30
|
NotificationTypeEnum["MERCADOLIBRE"] = "MERCADOLIBRE";
|
|
31
|
+
/**
|
|
32
|
+
* A fiscal-document upload that failed with a signal saying the pack
|
|
33
|
+
* already carries one — the tenant's own Mercado Libre Facturador is
|
|
34
|
+
* probably active and issuing invoices against the same orders we are.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ Additive, on the same grounds as `ABANDONED_CART`:
|
|
37
|
+
* `UserNotifications` is `Partial<Record<NotificationTypeEnum, boolean>>`,
|
|
38
|
+
* so every existing preferences row stays valid and an absent key already
|
|
39
|
+
* reads as "not opted in". No consumer migration is owed.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ **No User-row read path, deliberately — the `ERROR` / `DOLARBNA`
|
|
42
|
+
* category, not the opt-in one.** Two independent reasons, and the second
|
|
43
|
+
* is the one that bites: a collision means invoices may be issued twice
|
|
44
|
+
* against one order, which no tenant would sensibly hold an opt-out for;
|
|
45
|
+
* and a NEW key is seeded nowhere, so an opt-in read path would deliver it
|
|
46
|
+
* to nobody. `createStore`'s `FOUNDING_USER_NOTIFICATIONS` is a fixed
|
|
47
|
+
* seven-key literal written to the founding user alone, and this repo is
|
|
48
|
+
* forward-only — no backfill will ever add this key to an existing row.
|
|
49
|
+
* The producer therefore fans out to every ADMIN of the store.
|
|
50
|
+
*/
|
|
51
|
+
NotificationTypeEnum["ML_FACTURADOR_COLLISION"] = "ML_FACTURADOR_COLLISION";
|
|
26
52
|
// Stock alerts — fired when a sale crosses a product's stock
|
|
27
53
|
// threshold. LOW_STOCK at stock <= `Product.minStock`; OUT_OF_STOCK at
|
|
28
54
|
// stock <= 0. Both have User-row opt-in read paths.
|
package/dist/cjs/webhook.js
CHANGED
|
@@ -70,6 +70,7 @@ exports.WEBHOOK_EVENT_TYPES = [
|
|
|
70
70
|
'product.stock_low',
|
|
71
71
|
'product.stock_out',
|
|
72
72
|
'order.created',
|
|
73
|
+
'order.status_changed',
|
|
73
74
|
'invoice.generated',
|
|
74
75
|
];
|
|
75
76
|
/** Terminal outcomes for a print job. Closed union — a job printed, or it failed. */
|
|
@@ -80,8 +81,16 @@ exports.PRINT_JOB_SETTLED_OUTCOMES = ['printed', 'failed'];
|
|
|
80
81
|
* ⚠️ A CLOSED union, and rule 1 above applies to it with full force: a fifth
|
|
81
82
|
* order-minting path cannot be added here without breaking every subscriber
|
|
82
83
|
* that switches exhaustively — it mints `order.created.v2` instead. The four
|
|
83
|
-
* members are the
|
|
84
|
-
*
|
|
84
|
+
* members are the paths that EMIT — which is no longer the same thing as the
|
|
85
|
+
* paths that WRITE an ORDER row.
|
|
86
|
+
*
|
|
87
|
+
* ⚠️ Converting an approved presupuesto (`convertPresupuestoToOrder`) mints an
|
|
88
|
+
* ordinary order through the same `createOrder` every member above uses, and
|
|
89
|
+
* emits nothing here — precisely BECAUSE adding a fifth member is the breaking
|
|
90
|
+
* change the paragraph above forbids. The gap is known and deliberate, not an
|
|
91
|
+
* oversight to be tidied: do not close it by quietly widening this tuple. The
|
|
92
|
+
* rest of that path's fan-out (stock webhooks, notifications, the ML push) does
|
|
93
|
+
* run; it is only this event that waits on the v2 decision.
|
|
85
94
|
*
|
|
86
95
|
* `service_delivery` is the repair-shop handover: a `ready` → `delivered`
|
|
87
96
|
* transition mints an ordinary sale out of the counter lines the operator
|
package/dist/marketing.d.ts
CHANGED
|
@@ -221,25 +221,155 @@ declare global {
|
|
|
221
221
|
updatedAt?: number;
|
|
222
222
|
}
|
|
223
223
|
/**
|
|
224
|
-
*
|
|
224
|
+
* How a promotion cuts money — the discriminant on {@link Promotion}.
|
|
225
225
|
*
|
|
226
|
-
* ⚠️
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
226
|
+
* ⚠️ `'coupon'` is the ABSENT value, not merely the first one. Every row
|
|
227
|
+
* written before this union existed carries no `type` at all, and
|
|
228
|
+
* forward-only means none of them is ever rewritten — so a reader that asks
|
|
229
|
+
* `promotion.type === 'coupon'` gets `false` on every one of them.
|
|
230
|
+
*
|
|
231
|
+
* ⚠️ The move, for EVERY consumer of this package, is `promotion.type ??
|
|
232
|
+
* 'coupon'` — or a `switch` whose `default` arm is the coupon arm. The api
|
|
233
|
+
* has a `promotionType()` (`stacks/services/promotions.ts`) that does exactly
|
|
234
|
+
* this, but it lives under `stacks/` and nothing importing these types can
|
|
235
|
+
* reach it: it is named here so a reader stops looking for it, not so one
|
|
236
|
+
* goes hunting.
|
|
230
237
|
*/
|
|
231
|
-
|
|
238
|
+
type PromotionType = 'coupon' | 'buyXGetY' | 'freeShipping' | 'bundle';
|
|
239
|
+
/** What every promotion carries, whatever it does to the money. */
|
|
240
|
+
interface PromotionBase {
|
|
232
241
|
storeId: string;
|
|
233
242
|
promotionId: string;
|
|
234
243
|
name: string;
|
|
235
|
-
/** FK into the existing `Coupon` entity. */
|
|
236
|
-
couponCode?: string;
|
|
237
244
|
status: 'draft' | 'active' | 'ended';
|
|
238
245
|
startsAt?: number;
|
|
239
246
|
endsAt?: number;
|
|
240
247
|
createdAt: number;
|
|
241
248
|
updatedAt?: number;
|
|
242
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* A marketing wrapper around a coupon that already exists — the original
|
|
252
|
+
* shape, and the one every unmigrated row still has.
|
|
253
|
+
*
|
|
254
|
+
* ⚠️ It carries NO discount mechanics of its own. `couponCode` is the only
|
|
255
|
+
* link, and every money field — type, value, minimum subtotal, cap, currency
|
|
256
|
+
* — stays owned by `Coupon`. A second place that can disagree with the
|
|
257
|
+
* coupon on terms is a second answer to "what did the customer actually get".
|
|
258
|
+
*
|
|
259
|
+
* ⚠️ This variant's discriminant is OPTIONAL, and it is the only one that
|
|
260
|
+
* is. That is what keeps every pre-union row readable with no migration, and
|
|
261
|
+
* it is why narrowing reaches this arm through a `switch`'s `default` rather
|
|
262
|
+
* than through `case 'coupon'`.
|
|
263
|
+
*/
|
|
264
|
+
interface CouponPromotion extends PromotionBase {
|
|
265
|
+
type?: 'coupon';
|
|
266
|
+
/** FK into the existing `Coupon` entity. */
|
|
267
|
+
couponCode?: string;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* "Buy X, get Y" — a quantity of one product arms a cut on another.
|
|
271
|
+
*
|
|
272
|
+
* ⚠️ The reward is a PERCENT, never money, for the same reason
|
|
273
|
+
* {@link CartDiscount} carries the grant and not the cut: it is re-derived on
|
|
274
|
+
* every cart write against the unit price the rewarded line actually has at
|
|
275
|
+
* that moment, which moves whenever a quantity break re-resolves `basePrice`.
|
|
276
|
+
* A frozen money amount would keep paying out the price the cart had when the
|
|
277
|
+
* shopper applied it.
|
|
278
|
+
*/
|
|
279
|
+
interface BuyXGetYTerms {
|
|
280
|
+
/** The product whose quantity ARMS the promotion. */
|
|
281
|
+
buyProductId: string;
|
|
282
|
+
/** How many units of `buyProductId` one application consumes. At least 1. */
|
|
283
|
+
buyQuantity: number;
|
|
284
|
+
/** The product the reward is taken on. May be the same as `buyProductId`. */
|
|
285
|
+
getProductId: string;
|
|
286
|
+
/** How many units of `getProductId` one application rewards. At least 1. */
|
|
287
|
+
getQuantity: number;
|
|
288
|
+
/**
|
|
289
|
+
* The cut on the rewarded units, as a percent of their line's unit price.
|
|
290
|
+
* `100` is the plain "get one free" shape; `50` is "get one half price".
|
|
291
|
+
*/
|
|
292
|
+
getDiscountPercent: number;
|
|
293
|
+
/**
|
|
294
|
+
* How many times one cart may apply this promotion. Absent means as often
|
|
295
|
+
* as the cart's own quantities allow.
|
|
296
|
+
*/
|
|
297
|
+
maxApplications?: number;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* What a bundle charges once its members are all present.
|
|
301
|
+
*
|
|
302
|
+
* ⚠️ A discriminated pair rather than two optional fields, because "both
|
|
303
|
+
* set" and "neither set" are both unanswerable at money time and neither
|
|
304
|
+
* should be representable. The CRUD schema does not have to refuse a shape
|
|
305
|
+
* the type cannot express.
|
|
306
|
+
*/
|
|
307
|
+
type BundlePricing = {
|
|
308
|
+
mode: 'fixedPrice';
|
|
309
|
+
/** What the whole bundle costs, in the cart's currency. */
|
|
310
|
+
amount: number;
|
|
311
|
+
} | {
|
|
312
|
+
mode: 'percentOff';
|
|
313
|
+
/** The cut on the summed member lines, as a percent. */
|
|
314
|
+
percent: number;
|
|
315
|
+
};
|
|
316
|
+
/** A set of products that, bought together, price as one. */
|
|
317
|
+
interface BundleTerms {
|
|
318
|
+
/**
|
|
319
|
+
* Every product that must be present, at one unit each, for a single
|
|
320
|
+
* application. Order is not significant.
|
|
321
|
+
*/
|
|
322
|
+
productIds: readonly string[];
|
|
323
|
+
pricing: BundlePricing;
|
|
324
|
+
/**
|
|
325
|
+
* How many times one cart may apply this bundle. Absent means as often as
|
|
326
|
+
* the cart's own quantities allow.
|
|
327
|
+
*/
|
|
328
|
+
maxApplications?: number;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* When shipping is waived.
|
|
332
|
+
*
|
|
333
|
+
* ⚠️ Eligibility ONLY — there is deliberately no money field here.
|
|
334
|
+
* Free shipping does not grant a discount, it zeroes a total the cart already
|
|
335
|
+
* carries, so a value on this object would be a second, disagreeing answer to
|
|
336
|
+
* what the shipping costs.
|
|
337
|
+
*/
|
|
338
|
+
interface FreeShippingTerms {
|
|
339
|
+
/** The cart subtotal that qualifies. Absent means any subtotal does. */
|
|
340
|
+
minSubtotal?: number;
|
|
341
|
+
/**
|
|
342
|
+
* At least one of these products must be on the cart. Absent means any
|
|
343
|
+
* cart qualifies.
|
|
344
|
+
*/
|
|
345
|
+
productIds?: readonly string[];
|
|
346
|
+
}
|
|
347
|
+
interface BuyXGetYPromotion extends PromotionBase {
|
|
348
|
+
type: 'buyXGetY';
|
|
349
|
+
buyXGetY: BuyXGetYTerms;
|
|
350
|
+
}
|
|
351
|
+
interface FreeShippingPromotion extends PromotionBase {
|
|
352
|
+
type: 'freeShipping';
|
|
353
|
+
freeShipping: FreeShippingTerms;
|
|
354
|
+
}
|
|
355
|
+
interface BundlePromotion extends PromotionBase {
|
|
356
|
+
type: 'bundle';
|
|
357
|
+
bundle: BundleTerms;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* A promotion a store runs — one of four mechanics, discriminated on `type`.
|
|
361
|
+
*
|
|
362
|
+
* ⚠️ Each variant carries its terms under a key NAMED FOR ITS TYPE
|
|
363
|
+
* (`buyXGetY`, `freeShipping`, `bundle`) rather than a shared `terms` key.
|
|
364
|
+
* That is what lets the union narrow: a shared key would have to be the union
|
|
365
|
+
* of every terms shape, so a `bundle` row could typecheck while carrying
|
|
366
|
+
* buy-X-get-Y terms and nothing but a runtime check would object.
|
|
367
|
+
*
|
|
368
|
+
* ⚠️ The `'coupon'` variant is the ONLY one whose money lives elsewhere
|
|
369
|
+
* — in the `Coupon` row its `couponCode` names. The other three carry their
|
|
370
|
+
* own mechanics, and are the only three a cart can freeze onto itself.
|
|
371
|
+
*/
|
|
372
|
+
type Promotion = CouponPromotion | BuyXGetYPromotion | FreeShippingPromotion | BundlePromotion;
|
|
243
373
|
/**
|
|
244
374
|
* What arms an {@link AutomationRule} — the SIGNAL a rule listens for.
|
|
245
375
|
*
|
|
@@ -724,4 +854,23 @@ export declare const ATTRIBUTION_WINDOW_HOURS = 72;
|
|
|
724
854
|
export declare const AUTOMATION_SEND_STATUSES: readonly ["queued", "sent", "failed", "skipped"];
|
|
725
855
|
/** {@link AutomationRuleStatus} as a runtime value — see {@link AUTOMATION_TRIGGERS}. */
|
|
726
856
|
export declare const AUTOMATION_RULE_STATUSES: readonly ["paused", "active", "archived"];
|
|
857
|
+
/**
|
|
858
|
+
* {@link PromotionType} as a runtime value — see {@link AUTOMATION_TRIGGERS}.
|
|
859
|
+
*
|
|
860
|
+
* ⚠️ `'coupon'` is in the tuple even though a row may express it by carrying
|
|
861
|
+
* no `type` at all. The tuple is the vocabulary a writer may DECLARE; the absent
|
|
862
|
+
* spelling is a reader's concern, and conflating the two is how a CRUD schema
|
|
863
|
+
* comes to reject the one value every legacy row means.
|
|
864
|
+
*/
|
|
865
|
+
export declare const PROMOTION_TYPES: readonly ["coupon", "buyXGetY", "freeShipping", "bundle"];
|
|
866
|
+
/**
|
|
867
|
+
* The promotion types a CART can freeze onto itself — every type but `'coupon'`.
|
|
868
|
+
*
|
|
869
|
+
* ⚠️ `'coupon'` is excluded on purpose, and not for tidiness. A coupon
|
|
870
|
+
* promotion's money lives in the `Coupon` row its `couponCode` names, and that
|
|
871
|
+
* row already reaches the cart through `applyCoupon` and sits in `Cart.coupon`.
|
|
872
|
+
* Letting it also occupy the promotion slot would give one cart two places to
|
|
873
|
+
* disagree about the same cut.
|
|
874
|
+
*/
|
|
875
|
+
export declare const CART_APPLICABLE_PROMOTION_TYPES: readonly ["buyXGetY", "freeShipping", "bundle"];
|
|
727
876
|
export {};
|
package/dist/marketing.js
CHANGED
|
@@ -67,3 +67,31 @@ export const AUTOMATION_RULE_STATUSES = [
|
|
|
67
67
|
'active',
|
|
68
68
|
'archived',
|
|
69
69
|
];
|
|
70
|
+
/**
|
|
71
|
+
* {@link PromotionType} as a runtime value — see {@link AUTOMATION_TRIGGERS}.
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ `'coupon'` is in the tuple even though a row may express it by carrying
|
|
74
|
+
* no `type` at all. The tuple is the vocabulary a writer may DECLARE; the absent
|
|
75
|
+
* spelling is a reader's concern, and conflating the two is how a CRUD schema
|
|
76
|
+
* comes to reject the one value every legacy row means.
|
|
77
|
+
*/
|
|
78
|
+
export const PROMOTION_TYPES = [
|
|
79
|
+
'coupon',
|
|
80
|
+
'buyXGetY',
|
|
81
|
+
'freeShipping',
|
|
82
|
+
'bundle',
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* The promotion types a CART can freeze onto itself — every type but `'coupon'`.
|
|
86
|
+
*
|
|
87
|
+
* ⚠️ `'coupon'` is excluded on purpose, and not for tidiness. A coupon
|
|
88
|
+
* promotion's money lives in the `Coupon` row its `couponCode` names, and that
|
|
89
|
+
* row already reaches the cart through `applyCoupon` and sits in `Cart.coupon`.
|
|
90
|
+
* Letting it also occupy the promotion slot would give one cart two places to
|
|
91
|
+
* disagree about the same cut.
|
|
92
|
+
*/
|
|
93
|
+
export const CART_APPLICABLE_PROMOTION_TYPES = [
|
|
94
|
+
'buyXGetY',
|
|
95
|
+
'freeShipping',
|
|
96
|
+
'bundle',
|
|
97
|
+
];
|
package/dist/notification.d.ts
CHANGED
|
@@ -8,6 +8,27 @@ export declare enum NotificationTypeEnum {
|
|
|
8
8
|
ERROR = "ERROR",
|
|
9
9
|
AFIP_CERT_EXPIRY = "AFIP_CERT_EXPIRY",
|
|
10
10
|
MERCADOLIBRE = "MERCADOLIBRE",
|
|
11
|
+
/**
|
|
12
|
+
* A fiscal-document upload that failed with a signal saying the pack
|
|
13
|
+
* already carries one — the tenant's own Mercado Libre Facturador is
|
|
14
|
+
* probably active and issuing invoices against the same orders we are.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ Additive, on the same grounds as `ABANDONED_CART`:
|
|
17
|
+
* `UserNotifications` is `Partial<Record<NotificationTypeEnum, boolean>>`,
|
|
18
|
+
* so every existing preferences row stays valid and an absent key already
|
|
19
|
+
* reads as "not opted in". No consumer migration is owed.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ **No User-row read path, deliberately — the `ERROR` / `DOLARBNA`
|
|
22
|
+
* category, not the opt-in one.** Two independent reasons, and the second
|
|
23
|
+
* is the one that bites: a collision means invoices may be issued twice
|
|
24
|
+
* against one order, which no tenant would sensibly hold an opt-out for;
|
|
25
|
+
* and a NEW key is seeded nowhere, so an opt-in read path would deliver it
|
|
26
|
+
* to nobody. `createStore`'s `FOUNDING_USER_NOTIFICATIONS` is a fixed
|
|
27
|
+
* seven-key literal written to the founding user alone, and this repo is
|
|
28
|
+
* forward-only — no backfill will ever add this key to an existing row.
|
|
29
|
+
* The producer therefore fans out to every ADMIN of the store.
|
|
30
|
+
*/
|
|
31
|
+
ML_FACTURADOR_COLLISION = "ML_FACTURADOR_COLLISION",
|
|
11
32
|
LOW_STOCK = "LOW_STOCK",
|
|
12
33
|
OUT_OF_STOCK = "OUT_OF_STOCK",
|
|
13
34
|
SUPPORT = "SUPPORT",
|
package/dist/notification.js
CHANGED
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
// Stripe hook, propagate-fx). Exported as a real enum so `api`
|
|
5
5
|
// (stacks/helpers/notificationType.ts) and `app`
|
|
6
6
|
// (src/domain/notificationType.ts) can drop their hand-mirrored copies
|
|
7
|
-
// in follow-ups. DOLARBNA / ERROR /
|
|
8
|
-
// read path — enum members only
|
|
9
|
-
// alert type)
|
|
7
|
+
// in follow-ups. DOLARBNA / ERROR / ML_FACTURADOR_COLLISION have no
|
|
8
|
+
// User-row read path — enum members only. AFIP_CERT_EXPIRY (the
|
|
9
|
+
// cert-expiry alert type) was in that list and no longer belongs to it:
|
|
10
|
+
// `createStore` seeds it on the founding user and `afipCertMonitor`
|
|
11
|
+
// reads `getNotificationSubscribers` for it, so it is an ordinary opt-in
|
|
12
|
+
// key. Which members are opt-in is not cosmetic — it decides whether a
|
|
13
|
+
// producer may read subscribers at all, and a member seeded nowhere that
|
|
14
|
+
// does so fans out to an empty list on every store.
|
|
10
15
|
export var NotificationTypeEnum;
|
|
11
16
|
(function (NotificationTypeEnum) {
|
|
12
17
|
NotificationTypeEnum["ORDER"] = "ORDER";
|
|
@@ -20,6 +25,27 @@ export var NotificationTypeEnum;
|
|
|
20
25
|
// ML order-ingestion fanout — User-row read path
|
|
21
26
|
// added by the orders_v2 worker.
|
|
22
27
|
NotificationTypeEnum["MERCADOLIBRE"] = "MERCADOLIBRE";
|
|
28
|
+
/**
|
|
29
|
+
* A fiscal-document upload that failed with a signal saying the pack
|
|
30
|
+
* already carries one — the tenant's own Mercado Libre Facturador is
|
|
31
|
+
* probably active and issuing invoices against the same orders we are.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ Additive, on the same grounds as `ABANDONED_CART`:
|
|
34
|
+
* `UserNotifications` is `Partial<Record<NotificationTypeEnum, boolean>>`,
|
|
35
|
+
* so every existing preferences row stays valid and an absent key already
|
|
36
|
+
* reads as "not opted in". No consumer migration is owed.
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ **No User-row read path, deliberately — the `ERROR` / `DOLARBNA`
|
|
39
|
+
* category, not the opt-in one.** Two independent reasons, and the second
|
|
40
|
+
* is the one that bites: a collision means invoices may be issued twice
|
|
41
|
+
* against one order, which no tenant would sensibly hold an opt-out for;
|
|
42
|
+
* and a NEW key is seeded nowhere, so an opt-in read path would deliver it
|
|
43
|
+
* to nobody. `createStore`'s `FOUNDING_USER_NOTIFICATIONS` is a fixed
|
|
44
|
+
* seven-key literal written to the founding user alone, and this repo is
|
|
45
|
+
* forward-only — no backfill will ever add this key to an existing row.
|
|
46
|
+
* The producer therefore fans out to every ADMIN of the store.
|
|
47
|
+
*/
|
|
48
|
+
NotificationTypeEnum["ML_FACTURADOR_COLLISION"] = "ML_FACTURADOR_COLLISION";
|
|
23
49
|
// Stock alerts — fired when a sale crosses a product's stock
|
|
24
50
|
// threshold. LOW_STOCK at stock <= `Product.minStock`; OUT_OF_STOCK at
|
|
25
51
|
// stock <= 0. Both have User-row opt-in read paths.
|
package/dist/store.d.ts
CHANGED
|
@@ -713,6 +713,33 @@ declare global {
|
|
|
713
713
|
* `Mercadopago.expiresAt` is.
|
|
714
714
|
*/
|
|
715
715
|
accessTokenExpiresAt?: number;
|
|
716
|
+
/**
|
|
717
|
+
* unix ms of the last SUCCESSFUL connect-time send verification
|
|
718
|
+
* (`POST /gmail/test-send`) — the tenant asked us to prove the send path
|
|
719
|
+
* works, and it did.
|
|
720
|
+
*
|
|
721
|
+
* ⚠️ **Written on SUCCESS ONLY, and there is deliberately no
|
|
722
|
+
* `lastFailedAt` sibling.** A failure is a transient condition the
|
|
723
|
+
* operator is already looking at; persisting it invites a screen that
|
|
724
|
+
* renders stale alarm about a problem reconnecting fixed five minutes
|
|
725
|
+
* later. This stays a FRESHNESS signal — `status` already carries status,
|
|
726
|
+
* and two representations of status is how they drift.
|
|
727
|
+
*
|
|
728
|
+
* ⚠️ **Absent means "never verified", NEVER "verification failed".** The
|
|
729
|
+
* platform is forward-only and never backfills, so every tenant who
|
|
730
|
+
* connected before this shipped reads as absent. A consumer rendering
|
|
731
|
+
* absence as an error state puts a red badge on all of them.
|
|
732
|
+
*
|
|
733
|
+
* ⚠️ **Staleness is the consumer's call.** There is no server-computed
|
|
734
|
+
* "verified recently" boolean and there must not be one — the threshold
|
|
735
|
+
* is a product decision that will change, and a boolean freezes it into
|
|
736
|
+
* the wire where every consumer inherits it.
|
|
737
|
+
*
|
|
738
|
+
* Distinct from `lastTokenRefreshAt`, which is token-level truth: a
|
|
739
|
+
* token can refresh perfectly while delivery is broken, and that state
|
|
740
|
+
* is exactly what the verification endpoint exists to detect.
|
|
741
|
+
*/
|
|
742
|
+
gmailLastVerifiedAt?: number;
|
|
716
743
|
}
|
|
717
744
|
type FxAutoUpdateStrategy = 'overwrite' | 'overwrite-if-stale' | 'notify-only';
|
|
718
745
|
interface FxAutoUpdateBinding {
|
package/dist/userActivity.d.ts
CHANGED
|
@@ -494,10 +494,18 @@ declare global {
|
|
|
494
494
|
interface BasketDiscountGrantedEvent extends UserActivityEventBase {
|
|
495
495
|
event: 'Basket Discount Granted';
|
|
496
496
|
/** Which verb — a withdrawal is as worth auditing as a grant. */
|
|
497
|
-
verb: 'setLineDiscount' | 'applyCoupon' | 'removeCoupon';
|
|
497
|
+
verb: 'setLineDiscount' | 'applyCoupon' | 'removeCoupon' | 'applyPromotion' | 'removePromotion';
|
|
498
498
|
/**
|
|
499
|
-
* The grant's unit. Absent on `removeCoupon`, which
|
|
500
|
-
*
|
|
499
|
+
* The grant's unit. Absent on `removeCoupon` and `removePromotion`, which
|
|
500
|
+
* withdraw rather than grant and therefore have no terms of their own.
|
|
501
|
+
*
|
|
502
|
+
* ⚠️ Also absent on `applyPromotion`, which GRANTS — and that is the
|
|
503
|
+
* one case where absence does not mean withdrawal. A promotion's grant is a
|
|
504
|
+
* whole terms object (a trigger, a reward, a repeat cap); there is no
|
|
505
|
+
* `(type, value)` pair that states it without lying by omission, so `code`
|
|
506
|
+
* carries the promotion id and the row holds the terms. A review reading
|
|
507
|
+
* this trail for abuse gets `amount` — the money — either way, which is
|
|
508
|
+
* the figure the review is actually made of.
|
|
501
509
|
*/
|
|
502
510
|
type?: 'percent' | 'amount';
|
|
503
511
|
/** The GRANT, in the unit `type` names — NOT money. See `amount`. */
|
|
@@ -515,6 +523,16 @@ declare global {
|
|
|
515
523
|
amount?: number;
|
|
516
524
|
/** The coupon code, on the two coupon verbs. Absent for a line discount. */
|
|
517
525
|
code?: string;
|
|
526
|
+
/**
|
|
527
|
+
* The promotion, on `applyPromotion`.
|
|
528
|
+
*
|
|
529
|
+
* ⚠️ Its OWN field rather than a promotion id shipped in `code`. They
|
|
530
|
+
* are different identifiers into different entities, and a trail that put
|
|
531
|
+
* both in one column would make "which coupon was granted most often"
|
|
532
|
+
* unanswerable without knowing each row's verb — which is exactly the kind
|
|
533
|
+
* of join a review does not do before drawing a conclusion.
|
|
534
|
+
*/
|
|
535
|
+
promotion_id?: string;
|
|
518
536
|
/** The line the cut was applied to, on `setLineDiscount`. */
|
|
519
537
|
line_id?: string;
|
|
520
538
|
/** ⚠️ Optional for the same reason as `BasketUpdatedEvent.customer_id`: a
|
package/dist/webhook.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* immediately subscribable — and a store that subscribes to an event with no
|
|
11
11
|
* emit site gets silence it cannot distinguish from "nothing happened".
|
|
12
12
|
*/
|
|
13
|
-
export declare const WEBHOOK_EVENT_TYPES: readonly ["print.queued", "print.received", "print.printed", "print.failed", "print.agent.connected", "print.agent.disconnected", "print.printer.online", "print.printer.offline", "print.job.settled", "payment.received", "product.stock_low", "product.stock_out", "order.created", "invoice.generated"];
|
|
13
|
+
export declare const WEBHOOK_EVENT_TYPES: readonly ["print.queued", "print.received", "print.printed", "print.failed", "print.agent.connected", "print.agent.disconnected", "print.printer.online", "print.printer.offline", "print.job.settled", "payment.received", "product.stock_low", "product.stock_out", "order.created", "order.status_changed", "invoice.generated"];
|
|
14
14
|
export type WebhookEventType = (typeof WEBHOOK_EVENT_TYPES)[number];
|
|
15
15
|
/** Terminal outcomes for a print job. Closed union — a job printed, or it failed. */
|
|
16
16
|
export declare const PRINT_JOB_SETTLED_OUTCOMES: readonly ["printed", "failed"];
|
|
@@ -194,8 +194,16 @@ export interface ProductStockOutPayload {
|
|
|
194
194
|
* ⚠️ A CLOSED union, and rule 1 above applies to it with full force: a fifth
|
|
195
195
|
* order-minting path cannot be added here without breaking every subscriber
|
|
196
196
|
* that switches exhaustively — it mints `order.created.v2` instead. The four
|
|
197
|
-
* members are the
|
|
198
|
-
*
|
|
197
|
+
* members are the paths that EMIT — which is no longer the same thing as the
|
|
198
|
+
* paths that WRITE an ORDER row.
|
|
199
|
+
*
|
|
200
|
+
* ⚠️ Converting an approved presupuesto (`convertPresupuestoToOrder`) mints an
|
|
201
|
+
* ordinary order through the same `createOrder` every member above uses, and
|
|
202
|
+
* emits nothing here — precisely BECAUSE adding a fifth member is the breaking
|
|
203
|
+
* change the paragraph above forbids. The gap is known and deliberate, not an
|
|
204
|
+
* oversight to be tidied: do not close it by quietly widening this tuple. The
|
|
205
|
+
* rest of that path's fan-out (stock webhooks, notifications, the ML push) does
|
|
206
|
+
* run; it is only this event that waits on the v2 decision.
|
|
199
207
|
*
|
|
200
208
|
* `service_delivery` is the repair-shop handover: a `ready` → `delivered`
|
|
201
209
|
* transition mints an ordinary sale out of the counter lines the operator
|
|
@@ -277,6 +285,103 @@ export interface OrderCreatedPayload {
|
|
|
277
285
|
*/
|
|
278
286
|
customerId?: string;
|
|
279
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Payload for `order.status_changed` — an order MOVED along one of its status
|
|
290
|
+
* axes, and the move is durable.
|
|
291
|
+
*
|
|
292
|
+
* ── WHY THERE IS AN `axis` FIELD AT ALL ──────────────────────────────────
|
|
293
|
+
*
|
|
294
|
+
* `Order` has **no single `status` field**. It carries `fulfilmentStatus`
|
|
295
|
+
* (`pending | ready | delivered | not_delivered`) and `financialStatus`
|
|
296
|
+
* (`pending | partial | paid`) as independent axes, plus a third cancellation
|
|
297
|
+
* axis (`cancelledAt`/`cancelledBy`/`cancellationSource`) that moves neither.
|
|
298
|
+
*
|
|
299
|
+
* ⚠️ **`'pending'` is a member of BOTH unions.** A payload carrying only
|
|
300
|
+
* `from`/`to` is therefore genuinely ambiguous — `to: 'pending'` alone cannot
|
|
301
|
+
* tell a subscriber whether an order became unfulfilled or became unpaid, and
|
|
302
|
+
* those are opposite operational signals. The axis is what disambiguates them,
|
|
303
|
+
* so it is REQUIRED and is never inferred from the status spelling.
|
|
304
|
+
*
|
|
305
|
+
* ── WHY `axis` HAS EXACTLY ONE MEMBER TODAY ──────────────────────────────
|
|
306
|
+
*
|
|
307
|
+
* Only the FULFILMENT axis is shipped. `'financial'` is deliberately NOT
|
|
308
|
+
* declared here, and this is the line most likely to be "helpfully" widened by
|
|
309
|
+
* someone who reads the three-axis description above and assumes the union was
|
|
310
|
+
* simply left unfinished.
|
|
311
|
+
*
|
|
312
|
+
* Rule 3 above is the reason: a declared value means "this can occur on a real
|
|
313
|
+
* event", never "we intend to emit this later". Declaring `'financial'` before
|
|
314
|
+
* a financial seam emits would publish a value no subscriber can ever observe
|
|
315
|
+
* — the union equivalent of publishing an event nothing emits, and it would
|
|
316
|
+
* read to an integrator as a branch worth writing and testing against.
|
|
317
|
+
*
|
|
318
|
+
* ⚠️ Adding `'financial'` LATER is a BREAKING change under rule 1 (it widens a
|
|
319
|
+
* union an integrator may exhaustively switch on) and it is breaking whether or
|
|
320
|
+
* not it was pre-announced here — pre-declaring buys nothing and costs honesty.
|
|
321
|
+
* The financial axis ships behind rule 2 instead: either an announced widen, or
|
|
322
|
+
* a new `order.status_changed.v2`. That decision belongs to whoever builds the
|
|
323
|
+
* financial seam, with the settlement and payment-linking writers in front of
|
|
324
|
+
* them; it is not owed in advance.
|
|
325
|
+
*
|
|
326
|
+
* ── WHICH MOVES REACH THE WIRE ───────────────────────────────────────────
|
|
327
|
+
*
|
|
328
|
+
* REAL moves only, from the two single-order fulfilment seams: the operator
|
|
329
|
+
* delivery/un-delivery toggle, and the MercadoLibre shipment sync. Two writers
|
|
330
|
+
* touch `fulfilmentStatus` and are deliberately SILENT, because neither is a
|
|
331
|
+
* transition:
|
|
332
|
+
*
|
|
333
|
+
* - the soft-delete hide path, which rewrites the field wholesale and
|
|
334
|
+
* deliberately writes no `statusHistory` entry either;
|
|
335
|
+
* - the order-edit heal branch, which writes a status where none was stored.
|
|
336
|
+
* A back-catalogue row acquiring the field it always implied is a data
|
|
337
|
+
* repair, and announcing it as a status change would tell an integrator an
|
|
338
|
+
* order moved on a day nobody touched it.
|
|
339
|
+
*
|
|
340
|
+
* A self-edge never emits. Both seams gate on the stored value differing from
|
|
341
|
+
* the computed one, so a redundant write — a repeated delivery toggle, a
|
|
342
|
+
* shipment webhook replayed at the same state — is silence, not a duplicate.
|
|
343
|
+
*
|
|
344
|
+
* ⚠️ **No `total` and no `currency`**, for the same denomination reason set out
|
|
345
|
+
* on {@link OrderCreatedPayload}: `Order.currency` is a catalogId, not ISO
|
|
346
|
+
* 4217. A fulfilment move does not change either figure in any case.
|
|
347
|
+
*
|
|
348
|
+
* ⚠️ **Enumerated, never spread from the row.** `getOrderById` does not delete
|
|
349
|
+
* `search`, which embeds the customer's `fullName` (Ley 25.326).
|
|
350
|
+
*/
|
|
351
|
+
export interface OrderStatusChangedPayload {
|
|
352
|
+
orderId: string;
|
|
353
|
+
/**
|
|
354
|
+
* Which of the order's independent status axes moved.
|
|
355
|
+
*
|
|
356
|
+
* Single-member today. See the block above before widening it — the widen
|
|
357
|
+
* is the breaking change, not the pre-declaration.
|
|
358
|
+
*/
|
|
359
|
+
axis: 'fulfilment';
|
|
360
|
+
/**
|
|
361
|
+
* The state moved FROM.
|
|
362
|
+
*
|
|
363
|
+
* ⚠️ **Genuinely optional, and it is not a "we might add it later".** Every
|
|
364
|
+
* order written before `fulfilmentStatus` existed carries no such field, and
|
|
365
|
+
* this platform is forward-only — nothing backfills. So the first real move
|
|
366
|
+
* on a back-catalogue order has no prior state to name, and a subscriber
|
|
367
|
+
* must handle its absence rather than treating it as a malformed event.
|
|
368
|
+
*
|
|
369
|
+
* Absent is never the same as equal to {@link to}: a self-edge does not
|
|
370
|
+
* emit at all.
|
|
371
|
+
*/
|
|
372
|
+
from?: OrderFulfilmentStatus;
|
|
373
|
+
/** The state moved TO. Always the value the committed row now carries. */
|
|
374
|
+
to: OrderFulfilmentStatus;
|
|
375
|
+
/**
|
|
376
|
+
* Server-stamped Unix milliseconds, taken at the post-commit emit — the
|
|
377
|
+
* moment the api ANNOUNCED the move, on the same convention and for the same
|
|
378
|
+
* reason as {@link OrderCreatedPayload.occurredAt}.
|
|
379
|
+
*
|
|
380
|
+
* ⚠️ NOT the `statusHistory` entry's own `timestamp`. The two are within
|
|
381
|
+
* milliseconds of each other and they are not the same clock reading.
|
|
382
|
+
*/
|
|
383
|
+
occurredAt: number;
|
|
384
|
+
}
|
|
280
385
|
/**
|
|
281
386
|
* Payload for `invoice.generated` — ARCA authorised a fiscal voucher and its
|
|
282
387
|
* `INVOICE#{storeId}` row is durable.
|
package/dist/webhook.js
CHANGED
|
@@ -67,6 +67,7 @@ export const WEBHOOK_EVENT_TYPES = [
|
|
|
67
67
|
'product.stock_low',
|
|
68
68
|
'product.stock_out',
|
|
69
69
|
'order.created',
|
|
70
|
+
'order.status_changed',
|
|
70
71
|
'invoice.generated',
|
|
71
72
|
];
|
|
72
73
|
/** Terminal outcomes for a print job. Closed union — a job printed, or it failed. */
|
|
@@ -77,8 +78,16 @@ export const PRINT_JOB_SETTLED_OUTCOMES = ['printed', 'failed'];
|
|
|
77
78
|
* ⚠️ A CLOSED union, and rule 1 above applies to it with full force: a fifth
|
|
78
79
|
* order-minting path cannot be added here without breaking every subscriber
|
|
79
80
|
* that switches exhaustively — it mints `order.created.v2` instead. The four
|
|
80
|
-
* members are the
|
|
81
|
-
*
|
|
81
|
+
* members are the paths that EMIT — which is no longer the same thing as the
|
|
82
|
+
* paths that WRITE an ORDER row.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ Converting an approved presupuesto (`convertPresupuestoToOrder`) mints an
|
|
85
|
+
* ordinary order through the same `createOrder` every member above uses, and
|
|
86
|
+
* emits nothing here — precisely BECAUSE adding a fifth member is the breaking
|
|
87
|
+
* change the paragraph above forbids. The gap is known and deliberate, not an
|
|
88
|
+
* oversight to be tidied: do not close it by quietly widening this tuple. The
|
|
89
|
+
* rest of that path's fan-out (stock webhooks, notifications, the ML push) does
|
|
90
|
+
* run; it is only this event that waits on the v2 decision.
|
|
82
91
|
*
|
|
83
92
|
* `service_delivery` is the repair-shop handover: a `ready` → `delivered`
|
|
84
93
|
* transition mints an ordinary sale out of the counter lines the operator
|