@vibedout/commerce 0.0.0-stage → 0.2.4

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.
@@ -0,0 +1,503 @@
1
+ //#region ../money/src/index.d.ts
2
+ /**
3
+ * `@vibedout/money`: one money type, and the conversions around it done once.
4
+ *
5
+ * An amount is an integer count of minor units (centavos, cents) plus an uppercase ISO 4217 currency.
6
+ * No float ever crosses a package's API: a provider that speaks decimals ("185.20", `62.5`) is converted
7
+ * at its HTTP boundary with {@link toMinor}, and shown to people with {@link formatMoney}.
8
+ *
9
+ * This file imports nothing, so it can be copied into a project as it is. It needs only `Intl`
10
+ * (for {@link formatMoney}), which every runtime provides.
11
+ */
12
+ interface Money {
13
+ /** Whole minor units, never a float. */
14
+ amountMinor: number;
15
+ /** Uppercase ISO 4217, for example `"MXN"`. */
16
+ currency: string;
17
+ }
18
+ //#endregion
19
+ //#region src/types.d.ts
20
+ /** The platform's two languages. `es-MX` is the default. */
21
+ type CommerceLocale = "es-MX" | "en";
22
+ /**
23
+ * What a port needs from the request it runs in. The same shape as the event bus's `SubscriberContext` with a clock, so a test can fix the
24
+ * time and a hold expires when the test says.
25
+ */
26
+ interface PortContext<Env = unknown> {
27
+ env: Env;
28
+ db: D1Database;
29
+ /** Milliseconds since the epoch. Use this and not `Date.now()`, so expiry is testable. */
30
+ now: number;
31
+ /** The language of the strings a port returns (titles, rate names). Default `es-MX`. */
32
+ locale?: CommerceLocale;
33
+ /** The signed-in person, when there is one. A guest is `null` or left out. */
34
+ userId?: string | null;
35
+ }
36
+ declare const LINE_AVAILABILITIES: readonly ["available", "sold_out", "not_orderable", "not_found"];
37
+ /**
38
+ * - `available`: can be bought now, in the quantity asked up to `maxQuantity`.
39
+ * - `sold_out`: a real item with no stock now ("agotado hoy"). It keeps its title and price so a cart can still show it.
40
+ * - `not_orderable`: a real item that is not for sale (hidden, draft, ordering switched off).
41
+ * - `not_found`: the `ref` is not (or no longer) sold. `title` may be the ref and the price is 0; it is never charged.
42
+ */
43
+ type LineAvailability = (typeof LINE_AVAILABILITIES)[number];
44
+ declare const FULFILMENTS: readonly ["ship", "pickup", "digital", "service", "none"];
45
+ /** What happens after payment, so a cart knows whether it needs an address (`ship`) and a receipt knows what it sold. */
46
+ type Fulfilment = (typeof FULFILMENTS)[number];
47
+ /** A value that survives `JSON.stringify` and back unchanged. */
48
+ type SnapshotValue = string | number | boolean | null | readonly SnapshotValue[] | {
49
+ readonly [key: string]: SnapshotValue;
50
+ };
51
+ /** What is frozen into the order line (a variant's name, its sku, an image id, the chosen options). At most 2 KiB as JSON. */
52
+ interface LineSnapshot {
53
+ readonly [key: string]: SnapshotValue;
54
+ }
55
+ /** What the browser says: which thing and how many. Never a price. */
56
+ interface LineAsk {
57
+ /** The source's own id for the thing (`var_1`, a sku, a service id). Opaque to everyone but the source. */
58
+ ref: string;
59
+ /** A whole number of at least 1. */
60
+ quantity: number;
61
+ /** Choices the buyer made (`{ size: "m" }`), for a source that has them. Strings only. */
62
+ options?: Readonly<Record<string, string>>;
63
+ }
64
+ /** The ids inventory knows an item by. The owner of the thing says it in two strings and never imports inventory. */
65
+ interface StockSubject {
66
+ /** `catalog.variant`, `menu.item`, `shop.sku`. */
67
+ subjectType: string;
68
+ subjectId: string;
69
+ }
70
+ /** What a line source answers for one {@link LineAsk}: the server's price and everything that depends on the thing. */
71
+ interface ResolvedLine {
72
+ /** The ask's `ref`, echoed. */
73
+ ref: string;
74
+ /** What the buyer sees and what the receipt keeps, in the context's language. */
75
+ title: string;
76
+ /** The ask's quantity, echoed. A source never changes it; `maxQuantity` says what is allowed. */
77
+ quantity: number;
78
+ /** The price of one, in minor units. 0 is allowed (a free item). */
79
+ unitAmountMinor: number;
80
+ /** Uppercase ISO 4217. */
81
+ currency: string;
82
+ availability: LineAvailability;
83
+ /** The most of this line one order may take: the stock left, or a per-order cap. At least 1 when given. */
84
+ maxQuantity?: number;
85
+ /** The SAT product and service key (ClaveProdServ) for the invoice. */
86
+ taxCode?: string;
87
+ /** Whether `unitAmountMinor` already includes tax (Mexican shelf prices do). */
88
+ taxIncluded: boolean;
89
+ fulfilment: Fulfilment;
90
+ /** Present when inventory should hold this line. */
91
+ stock?: StockSubject;
92
+ /** What to freeze into the order line. */
93
+ snapshot: LineSnapshot;
94
+ }
95
+ /**
96
+ * Prices what a buyer asks for. A feature registers one under a dotted key in `block.json` (`worker-ports`, port `line-source`, key
97
+ * `catalog.variant`); a checkout or a payment finds it by that key. The result has **one line per ask, in the order asked**, even for a
98
+ * ref that does not exist (`availability: "not_found"`); it never throws for a bad ref and never trusts a price from the ask.
99
+ *
100
+ * A source whose things have a capacity of their own, which inventory does not count (a booking slot and its seats, a class and its places),
101
+ * also holds them: `hold`, `commit` and `release`, optional (0.2.0) and always the three together. They follow the stock port's rules, by
102
+ * the ask instead of a stock subject: a hold is every ask or none and idempotent by `holdId` (a later `expiresAt` extends it, never
103
+ * shortens it; a hold that is over answers `hold_closed`), the last place goes to one buyer, the clock is `ctx.now`, the places come back by
104
+ * themselves at `expiresAt`, a payment that arrives after that still commits and says `late` (and `oversold` when the place went to someone
105
+ * else meanwhile), and a committed hold cannot be released. While a hold lasts, `resolve` counts its places as taken (`maxQuantity`, and
106
+ * `sold_out` at none). A checkout holds such a source's asks with the order's id, beside the stock port's hold, and commits or releases
107
+ * both. `runLineHoldConformance` in `@vibedout/commerce/testing` is the suite such a source passes.
108
+ */
109
+ interface LineSource {
110
+ resolve(ctx: PortContext, asks: readonly LineAsk[]): Promise<readonly ResolvedLine[]>;
111
+ /** Optional (0.2.0), with `commit` and `release`: takes the places the asks name until `expiresAt`. */
112
+ hold?(ctx: PortContext, request: LineHoldRequest): Promise<LineHoldResult>;
113
+ /** Optional (0.2.0): the payment succeeded, the places are sold. A late commit still commits. */
114
+ commit?(ctx: PortContext, holdId: string): Promise<LineSettleResult>;
115
+ /** Optional (0.2.0): the payment failed for good, was cancelled or expired: the places go back. */
116
+ release?(ctx: PortContext, holdId: string): Promise<LineSettleResult>;
117
+ }
118
+ /** One line of a cart, which names the line source that sells it: a cart holds things of several features. */
119
+ interface CartAsk extends LineAsk {
120
+ /** The key the source is registered under (`catalog.variant`, `booking.service`). */
121
+ source: string;
122
+ }
123
+ /** What `resolveCart` answers for a {@link CartAsk}: its source's line, and the key of that source. */
124
+ interface CartLine extends ResolvedLine {
125
+ source: string;
126
+ }
127
+ /**
128
+ * The line source registered under a key, or nothing. With the pattern pack's registry:
129
+ * `(key) => { const found = linePort<LineSource>(key); return found.ok ? found.port : undefined; }`.
130
+ */
131
+ type FindLineSource = (key: string) => LineSource | undefined;
132
+ /** A line source that holds its own places: `hold`, `commit` and `release` are there (`holdsCapacity` tells). */
133
+ type HoldingLineSource = LineSource & Required<Pick<LineSource, "hold" | "commit" | "release">>;
134
+ declare const STOCK_TRACKINGS: readonly ["none", "toggle", "count"];
135
+ /** `none`: always available. `toggle`: in or out by hand ("agotado hoy"). `count`: numbers, a ledger and holds. */
136
+ type StockTracking = (typeof STOCK_TRACKINGS)[number];
137
+ interface StockLevel {
138
+ subject: StockSubject;
139
+ tracking: StockTracking;
140
+ /** Whether one more can be taken now. */
141
+ inStock: boolean;
142
+ /** Units that can still be held. `null` when there is no number (`none`, and `toggle` while in); 0 for a `toggle` that is out. Never negative. */
143
+ available: number | null;
144
+ /** For `toggle` while out: when it comes back (milliseconds), or `null` for "until someone changes it". */
145
+ outUntil?: number | null;
146
+ }
147
+ interface HoldLine {
148
+ subject: StockSubject;
149
+ /** A whole number of at least 1. */
150
+ quantity: number;
151
+ }
152
+ interface HoldRequest {
153
+ /**
154
+ * The checkout's own id for this hold (the order id). The same id asked again with the same lines is the same hold: it holds nothing twice,
155
+ * and a later `expiresAt` extends it (a voucher's date is known only after the payment starts) but never shortens it. The hold the answer
156
+ * carries has the `expiresAt` that applies. An id whose hold is over answers `hold_closed`.
157
+ */
158
+ holdId: string;
159
+ /** Every line or none: a hold that cannot take all of them takes nothing. Two lines of one subject are held as their sum. */
160
+ lines: readonly HoldLine[];
161
+ /**
162
+ * Milliseconds since the epoch, after `ctx.now` and at most {@link MAX_HOLD_MS} after it (anything else is `invalid_request`: a time in the
163
+ * wrong unit would hold stock for years). The units go back by themselves at this time.
164
+ */
165
+ expiresAt: number;
166
+ }
167
+ /** The longest a hold may last: 14 days, more than the longest voucher a payment method gives. */
168
+ declare const MAX_HOLD_MS: number;
169
+ declare const HOLD_STATUSES: readonly ["held", "committed", "released", "expired"];
170
+ type HoldStatus = (typeof HOLD_STATUSES)[number];
171
+ /** A hold of the stock port, by stock subject; with `LineAsk` lines, a hold of a line source (`LineHold`). */
172
+ interface Hold<Line = HoldLine> {
173
+ holdId: string;
174
+ status: HoldStatus;
175
+ /** Milliseconds since the epoch. */
176
+ expiresAt: number;
177
+ lines: readonly Line[];
178
+ /**
179
+ * When the committed units left (`fulfil`), in milliseconds since the epoch. Absent while they have not; the status stays `committed`,
180
+ * because fulfilling moves the units on hand, not whether they are sold.
181
+ */
182
+ fulfilledAt?: number;
183
+ }
184
+ interface ShortLine {
185
+ subject: StockSubject;
186
+ requested: number;
187
+ /** What could have been held. */
188
+ available: number;
189
+ }
190
+ type HoldResult<Line = HoldLine, Short = ShortLine> = {
191
+ ok: true;
192
+ hold: Hold<Line>;
193
+ } | {
194
+ ok: false;
195
+ code: "insufficient_stock";
196
+ short: readonly Short[];
197
+ } |
198
+ /** The `holdId` exists with other lines. */
199
+ {
200
+ ok: false;
201
+ code: "hold_conflict";
202
+ } |
203
+ /** The `holdId` is a hold that is over (committed, released or expired): a new attempt takes a new id. */
204
+ {
205
+ ok: false;
206
+ code: "hold_closed";
207
+ status: Exclude<HoldStatus, "held">;
208
+ } | {
209
+ ok: false;
210
+ code: "invalid_request";
211
+ message: string;
212
+ };
213
+ type SettleResult<Line = HoldLine> = {
214
+ ok: true;
215
+ hold: Hold<Line>;
216
+ /** False when the hold was already in the state asked for (the call is idempotent). */
217
+ changed: boolean;
218
+ /** Commit only: the hold had expired or been released before the payment arrived. The units were taken anyway: the money is in. */
219
+ late?: boolean;
220
+ /** Commit only: taking them put the stock below zero, for an item that does not allow overselling. A person should look at the order. */
221
+ oversold?: boolean;
222
+ } | {
223
+ ok: false;
224
+ code: "unknown_hold";
225
+ } |
226
+ /** `release` of a hold that was already committed, `fulfil` of one that is not committed, or `uncommit` of one that is still held or was fulfilled. */
227
+ {
228
+ ok: false;
229
+ code: "invalid_state";
230
+ status: HoldStatus;
231
+ };
232
+ /**
233
+ * Stock, provided by the `inventory` feature (one per project). Holds are atomic and idempotent by `holdId`, the last unit goes to exactly
234
+ * one of two buyers, and every decision about time (a hold that is over, a toggle that comes back) uses `ctx.now`, never the database's clock.
235
+ * `runStockConformance` in `@vibedout/commerce/testing` is the suite a provider must pass.
236
+ */
237
+ interface StockPort {
238
+ availability(ctx: PortContext, subjects: readonly StockSubject[]): Promise<readonly StockLevel[]>;
239
+ /** Takes the units off the shelf until `expiresAt`. */
240
+ hold(ctx: PortContext, request: HoldRequest): Promise<HoldResult>;
241
+ /** The payment succeeded: the units are sold. A late commit still commits. */
242
+ commit(ctx: PortContext, holdId: string): Promise<SettleResult>;
243
+ /** The payment failed, was cancelled or expired: the units go back. */
244
+ release(ctx: PortContext, holdId: string): Promise<SettleResult>;
245
+ /**
246
+ * Optional (0.1.1): the units of a committed hold left the shelf (shipped, handed over, delivered). They stop being counted twice, as sold
247
+ * and as on hand: both go down by the hold's lines, so what can be bought does not move. Once per hold: a second call answers
248
+ * `changed: false`. A hold that is not committed is `invalid_state` with its status, and nothing moves. A checkout calls it when it
249
+ * fulfils an order; a provider that does not track units on hand may leave it out (`runStockConformance` then takes `skipFulfil`).
250
+ */
251
+ fulfil?(ctx: PortContext, holdId: string): Promise<SettleResult>;
252
+ /**
253
+ * Optional (0.2.1): the sale was undone before the units left the shelf (the order was refunded in full and never fulfilled), so a committed
254
+ * hold gives its units back: they can be bought again, `on hand` does not move, and the hold ends as `released`. Once per hold: a second
255
+ * call answers `changed: false`, as does a hold that is already over. A hold that is still `held` (not paid) is `invalid_state` with its
256
+ * status, because that is what `release` is for; a hold that was fulfilled is `invalid_state` `committed`, because its units left. The id
257
+ * stays closed to a new hold, and a payment that still arrived for it would commit it again as a late one: a caller uncommits an order
258
+ * that cannot be paid any more. A provider that does not count units may leave it out (`runStockConformance` then takes `skipUncommit`).
259
+ */
260
+ uncommit?(ctx: PortContext, holdId: string): Promise<SettleResult>;
261
+ }
262
+ /** What a line source that holds its own places takes: the asks it priced (`ref`, `quantity`, `options`), by the checkout's hold id. */
263
+ interface LineHoldRequest {
264
+ /** The checkout's id for this hold (the order id), as in the stock port's {@link HoldRequest}. */
265
+ holdId: string;
266
+ /** Every ask or none. Two asks for the same ref with the same options are held as their sum. */
267
+ lines: readonly LineAsk[];
268
+ /** Milliseconds since the epoch, after `ctx.now` and at most {@link MAX_HOLD_MS} after it; anything else is `invalid_request`. */
269
+ expiresAt: number;
270
+ }
271
+ /** An ask a line source could not hold in full: what was asked and how many places there were. A ref the source does not know has 0. */
272
+ interface ShortAsk {
273
+ ref: string;
274
+ requested: number;
275
+ available: number;
276
+ }
277
+ type LineHold = Hold<LineAsk>;
278
+ type LineHoldResult = HoldResult<LineAsk, ShortAsk>;
279
+ type LineSettleResult = SettleResult<LineAsk>;
280
+ declare const PAYMENT_METHODS: readonly ["card", "oxxo", "spei"];
281
+ type PaymentMethod = (typeof PAYMENT_METHODS)[number];
282
+ /** One thing paid for, as the provider's page lists it. */
283
+ interface PaymentLine {
284
+ ref?: string;
285
+ title: string;
286
+ quantity: number;
287
+ unitAmountMinor: number;
288
+ currency: string;
289
+ taxCode?: string;
290
+ }
291
+ declare const ADJUSTMENT_KINDS: readonly ["shipping", "tax", "discount"];
292
+ type AdjustmentKind = (typeof ADJUSTMENT_KINDS)[number];
293
+ /** How much of a discount came off one line: `index` is the position of the line in the list the discount is about. */
294
+ interface LineDiscountPart {
295
+ index: number;
296
+ amountMinor: number;
297
+ }
298
+ /** A charge on top of the lines (`shipping`, `tax`) or a reduction (`discount`). `amountMinor` is never negative: the kind says the sign. */
299
+ interface PaymentAdjustment {
300
+ kind: AdjustmentKind;
301
+ title: string;
302
+ amountMinor: number;
303
+ currency: string;
304
+ /**
305
+ * Optional (0.2.4), `discount` only: which of the request's `lines` it came off (`index` is the position in `StartPaymentInput.lines`), so
306
+ * the provider's page shows each line discounted by what the invoice states for it. The amounts add up to `amountMinor` and no line gives
307
+ * more than it costs ({@link checkLineDiscounts}). A discount that leaves it out is spread over what the other discounts left of the lines
308
+ * (the payments feature's own rule). `paymentRequestFrom` fills it in from `Totals.discounts`.
309
+ */
310
+ lines?: readonly LineDiscountPart[];
311
+ }
312
+ interface StartPaymentInput {
313
+ /** The order's id, set by the server. The provider's idempotency key derives from it: the same reference is the same payment. */
314
+ reference: string;
315
+ lines: readonly PaymentLine[];
316
+ adjustments?: readonly PaymentAdjustment[];
317
+ /**
318
+ * What is charged: the lines plus `shipping` and `tax`, minus `discount`. Must be greater than zero (a free order calls no provider). The
319
+ * discounts together are at most the lines: they are taken off the items, never off the shipping or the tax.
320
+ */
321
+ totalMinor: number;
322
+ currency: string;
323
+ /** Who pays. Both optional: a guest gives an email at most. */
324
+ payer?: {
325
+ userId?: string;
326
+ email?: string;
327
+ };
328
+ methods?: readonly PaymentMethod[];
329
+ successUrl: string;
330
+ cancelUrl: string;
331
+ /** Milliseconds since the epoch; the provider's page stops accepting payment then. */
332
+ expiresAt?: number;
333
+ metadata?: Readonly<Record<string, string>>;
334
+ }
335
+ type StartPaymentResult = {
336
+ ok: true;
337
+ paymentId: string;
338
+ redirectUrl: string;
339
+ expiresAt?: number;
340
+ } | {
341
+ ok: false;
342
+ code: "invalid_request" | "not_configured" | "provider_error";
343
+ message: string;
344
+ /** Whether the same call may be tried again. */
345
+ retryable?: boolean;
346
+ };
347
+ /** Why a payment could not be cancelled. */
348
+ type CancelPaymentFailure =
349
+ /** No payment was started for this reference. */
350
+ "unknown_payment" |
351
+ /** It was paid (or refunded, or disputed): refund it instead. */
352
+ "already_paid" |
353
+ /** The provider cannot stop it now. `message` says why. */
354
+ "not_cancellable" | "not_configured" | "provider_error";
355
+ type CancelPaymentResult =
356
+ /** The payment takes no money from now on. `changed` is false when it already could not (cancelled, expired, failed for good). */
357
+ {
358
+ ok: true;
359
+ changed: boolean;
360
+ } | {
361
+ ok: false;
362
+ code: CancelPaymentFailure;
363
+ message: string;
364
+ retryable?: boolean;
365
+ };
366
+ /**
367
+ * Money moves here, provided by the `payments` feature (one per project). `runPaymentsConformance` in `@vibedout/commerce/testing` is the
368
+ * suite a provider passes.
369
+ */
370
+ interface PaymentsPort {
371
+ start(ctx: PortContext, input: StartPaymentInput): Promise<StartPaymentResult>;
372
+ /**
373
+ * Optional (0.2.0): the order behind `reference` will not be paid (staff cancelled it, or its time ran out), so the provider's page stops
374
+ * taking money and a voucher stops being payable. Once per payment: a second call answers `changed: false`, and starting the reference
375
+ * again is refused. A payment already on its way (a payer who pressed "pay" at the same moment, a voucher being paid at the counter) can
376
+ * still arrive as `payment.succeeded`: the caller handles it as the late payment it is. A caller checks the method is there
377
+ * (`payments.cancel?.(...)`); without it the page stays payable until its `expiresAt`.
378
+ */
379
+ cancel?(ctx: PortContext, reference: string): Promise<CancelPaymentResult>;
380
+ }
381
+ /** Where something goes. Country is ISO 3166-1 alpha-2 (`MX`); the rest is the little a rate or a tax needs. */
382
+ interface Destination {
383
+ country: string;
384
+ region?: string;
385
+ postalCode?: string;
386
+ }
387
+ interface TaxInput {
388
+ currency: string;
389
+ /** The chargeable lines. */
390
+ lines: readonly ResolvedLine[];
391
+ /** What the discounts take off the subtotal, so a tax is worked on what is paid. */
392
+ discountMinor: number;
393
+ /**
394
+ * Optional (0.2.2): what the discounts took off each line, in the order of `lines` (see `lineDiscountsFrom`), adding up to `discountMinor`. A
395
+ * discount that applies to one line only lowers the tax of that line only, which matters when the lines are taxed at different rates. Without
396
+ * it a provider spreads `discountMinor` over the lines in proportion to their totals (`spreadDiscount`).
397
+ */
398
+ lineDiscountsMinor?: readonly number[];
399
+ shippingMinor: number;
400
+ destination?: Destination;
401
+ }
402
+ interface TaxBreakdown {
403
+ /** `IVA`, `IEPS`. */
404
+ code: string;
405
+ title: string;
406
+ /** Basis points: 16% is 1600. */
407
+ rateBp?: number;
408
+ amountMinor: number;
409
+ /** True when the amount is already inside the line prices (`taxIncluded`), so it is not added to the total. */
410
+ included: boolean;
411
+ }
412
+ interface TaxResult {
413
+ /** The currency of every amount here: it must be the order's. */
414
+ currency: string;
415
+ /** What the order adds on top of the lines. */
416
+ addedMinor: number;
417
+ /** The tax already inside the prices, for the invoice. It does not change the total. */
418
+ includedMinor: number;
419
+ /** When present, the amounts that are not `included` add up to `addedMinor` and those that are to `includedMinor`. */
420
+ breakdown?: readonly TaxBreakdown[];
421
+ }
422
+ /** Provided by a tax feature (`tax-mx`): at most one per project. Optional for checkout: without it the order adds no tax. */
423
+ interface TaxPort {
424
+ compute(ctx: PortContext, input: TaxInput): Promise<TaxResult>;
425
+ }
426
+ interface ShippingRatesInput {
427
+ currency: string;
428
+ lines: readonly ResolvedLine[];
429
+ subtotalMinor: number;
430
+ destination: Destination;
431
+ }
432
+ interface ShippingRate {
433
+ /** Stable for a quote: the buyer sends the id back and the server quotes again before it charges, so the amount is never the browser's. */
434
+ id: string;
435
+ title: string;
436
+ amountMinor: number;
437
+ currency: string;
438
+ carrier?: string;
439
+ estimatedDays?: {
440
+ min: number;
441
+ max: number;
442
+ };
443
+ }
444
+ /** Provided by a shipping feature: at most one per project. Optional for checkout. */
445
+ interface ShippingRatesPort {
446
+ quote(ctx: PortContext, input: ShippingRatesInput): Promise<readonly ShippingRate[]>;
447
+ }
448
+ interface DiscountInput {
449
+ currency: string;
450
+ lines: readonly ResolvedLine[];
451
+ subtotalMinor: number;
452
+ /** Codes the buyer typed. */
453
+ codes: readonly string[];
454
+ userId?: string | null;
455
+ }
456
+ interface AppliedDiscount {
457
+ code?: string;
458
+ title: string;
459
+ amountMinor: number;
460
+ /** Must be the order's currency. */
461
+ currency: string;
462
+ /**
463
+ * Optional (0.2.2): which lines it came off, so a tax and an invoice can state each line's discount. `index` is the line's position in
464
+ * `DiscountInput.lines`; the amounts add up to `amountMinor` and no line gives more than its total. A discount of the whole order leaves it out
465
+ * (`attributeDiscounts`, 0.2.4, spreads it over what the others left of the lines).
466
+ */
467
+ lines?: readonly LineDiscountPart[];
468
+ }
469
+ declare const DISCOUNT_REJECTIONS: readonly ["unknown", "expired", "not_applicable", "used_up", "minimum_not_met"];
470
+ type DiscountRejection = (typeof DISCOUNT_REJECTIONS)[number];
471
+ interface DiscountResult {
472
+ applied: readonly AppliedDiscount[];
473
+ rejected: readonly {
474
+ code: string;
475
+ reason: DiscountRejection;
476
+ }[];
477
+ }
478
+ /** Provided by a pricing feature: at most one per project. Optional for checkout. */
479
+ interface DiscountPort {
480
+ apply(ctx: PortContext, input: DiscountInput): Promise<DiscountResult>;
481
+ }
482
+ /**
483
+ * The port names and what each provides. `line-source` is registered under many keys, every other port once. The names are the engine's
484
+ * `WORKER_PORTS` (`worker-ports` in `block.json`) and the pattern pack's `PORT_NAMES`; a test in the engine keeps the three equal.
485
+ *
486
+ * ```ts
487
+ * export const fxStock: PortProviders["stock"] = { availability, hold, commit, release };
488
+ * ```
489
+ *
490
+ * The engine checks each provider of the generated `registerPorts` call against its entry here (`satisfies PortProviders["stock"]`) when
491
+ * the project lists this package.
492
+ */
493
+ interface PortProviders {
494
+ "line-source": LineSource;
495
+ stock: StockPort;
496
+ payments: PaymentsPort;
497
+ tax: TaxPort;
498
+ "shipping-rates": ShippingRatesPort;
499
+ discount: DiscountPort;
500
+ }
501
+ type PortName = keyof PortProviders;
502
+ //#endregion
503
+ export { StartPaymentInput as $, LineHoldRequest as A, PaymentsPort as B, HoldStatus as C, LineAvailability as D, LineAsk as E, MAX_HOLD_MS as F, STOCK_TRACKINGS as G, PortName as H, PAYMENT_METHODS as I, ShippingRatesInput as J, SettleResult as K, PaymentAdjustment as L, LineSettleResult as M, LineSnapshot as N, LineDiscountPart as O, LineSource as P, SnapshotValue as Q, PaymentLine as R, HoldResult as S, LINE_AVAILABILITIES as T, PortProviders as U, PortContext as V, ResolvedLine as W, ShortAsk as X, ShippingRatesPort as Y, ShortLine as Z, Fulfilment as _, CancelPaymentResult as a, TaxBreakdown as at, HoldLine as b, CommerceLocale as c, TaxResult as ct, DiscountInput as d, StartPaymentResult as et, DiscountPort as f, FindLineSource as g, FULFILMENTS as h, CancelPaymentFailure as i, StockTracking as it, LineHoldResult as j, LineHold as k, DISCOUNT_REJECTIONS as l, Money as lt, DiscountResult as m, AdjustmentKind as n, StockPort as nt, CartAsk as o, TaxInput as ot, DiscountRejection as p, ShippingRate as q, AppliedDiscount as r, StockSubject as rt, CartLine as s, TaxPort as st, ADJUSTMENT_KINDS as t, StockLevel as tt, Destination as u, HOLD_STATUSES as v, HoldingLineSource as w, HoldRequest as x, Hold as y, PaymentMethod as z };
@@ -0,0 +1,2 @@
1
+ import { $ as StartPaymentInput, A as LineHoldRequest, B as PaymentsPort, C as HoldStatus, D as LineAvailability, E as LineAsk, F as MAX_HOLD_MS, G as STOCK_TRACKINGS, H as PortName, I as PAYMENT_METHODS, J as ShippingRatesInput, K as SettleResult, L as PaymentAdjustment, M as LineSettleResult, N as LineSnapshot, O as LineDiscountPart, P as LineSource, Q as SnapshotValue, R as PaymentLine, S as HoldResult, T as LINE_AVAILABILITIES, U as PortProviders, V as PortContext, W as ResolvedLine, X as ShortAsk, Y as ShippingRatesPort, Z as ShortLine, _ as Fulfilment, a as CancelPaymentResult, at as TaxBreakdown, b as HoldLine, c as CommerceLocale, ct as TaxResult, d as DiscountInput, et as StartPaymentResult, f as DiscountPort, g as FindLineSource, h as FULFILMENTS, i as CancelPaymentFailure, it as StockTracking, j as LineHoldResult, k as LineHold, l as DISCOUNT_REJECTIONS, lt as Money, m as DiscountResult, n as AdjustmentKind, nt as StockPort, o as CartAsk, ot as TaxInput, p as DiscountRejection, q as ShippingRate, r as AppliedDiscount, rt as StockSubject, s as CartLine, st as TaxPort, t as ADJUSTMENT_KINDS, tt as StockLevel, u as Destination, v as HOLD_STATUSES, w as HoldingLineSource, x as HoldRequest, y as Hold, z as PaymentMethod } from "./types-BUyLunnB.mjs";
2
+ export { ADJUSTMENT_KINDS, AdjustmentKind, AppliedDiscount, CancelPaymentFailure, CancelPaymentResult, CartAsk, CartLine, CommerceLocale, DISCOUNT_REJECTIONS, Destination, DiscountInput, DiscountPort, DiscountRejection, DiscountResult, FULFILMENTS, FindLineSource, Fulfilment, HOLD_STATUSES, Hold, HoldLine, HoldRequest, HoldResult, HoldStatus, HoldingLineSource, LINE_AVAILABILITIES, LineAsk, LineAvailability, LineDiscountPart, LineHold, LineHoldRequest, LineHoldResult, LineSettleResult, LineSnapshot, LineSource, MAX_HOLD_MS, type Money, PAYMENT_METHODS, PaymentAdjustment, PaymentLine, PaymentMethod, PaymentsPort, PortContext, PortName, PortProviders, ResolvedLine, STOCK_TRACKINGS, SettleResult, ShippingRate, ShippingRatesInput, ShippingRatesPort, ShortAsk, ShortLine, SnapshotValue, StartPaymentInput, StartPaymentResult, StockLevel, StockPort, StockSubject, StockTracking, TaxBreakdown, TaxInput, TaxPort, TaxResult };
package/dist/types.mjs ADDED
@@ -0,0 +1,46 @@
1
+ //#region src/types.ts
2
+ const LINE_AVAILABILITIES = [
3
+ "available",
4
+ "sold_out",
5
+ "not_orderable",
6
+ "not_found"
7
+ ];
8
+ const FULFILMENTS = [
9
+ "ship",
10
+ "pickup",
11
+ "digital",
12
+ "service",
13
+ "none"
14
+ ];
15
+ const STOCK_TRACKINGS = [
16
+ "none",
17
+ "toggle",
18
+ "count"
19
+ ];
20
+ /** The longest a hold may last: 14 days, more than the longest voucher a payment method gives. */
21
+ const MAX_HOLD_MS = 12096e5;
22
+ const HOLD_STATUSES = [
23
+ "held",
24
+ "committed",
25
+ "released",
26
+ "expired"
27
+ ];
28
+ const PAYMENT_METHODS = [
29
+ "card",
30
+ "oxxo",
31
+ "spei"
32
+ ];
33
+ const ADJUSTMENT_KINDS = [
34
+ "shipping",
35
+ "tax",
36
+ "discount"
37
+ ];
38
+ const DISCOUNT_REJECTIONS = [
39
+ "unknown",
40
+ "expired",
41
+ "not_applicable",
42
+ "used_up",
43
+ "minimum_not_met"
44
+ ];
45
+ //#endregion
46
+ export { ADJUSTMENT_KINDS, DISCOUNT_REJECTIONS, FULFILMENTS, HOLD_STATUSES, LINE_AVAILABILITIES, MAX_HOLD_MS, PAYMENT_METHODS, STOCK_TRACKINGS };
package/package.json CHANGED
@@ -1,6 +1,94 @@
1
1
  {
2
2
  "name": "@vibedout/commerce",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.4",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "The contracts a store is built from on Cloudflare Workers, as types and pure functions: line sources that price what a buyer asks for on the server (ResolvedLine in integer minor units with its availability, maximum quantity and a snapshot) and may hold places of their own, the stock port (hold with an expiry, commit, release, fulfil, uncommit), the payments port (start, cancel), and the tax, shipping-rates and discount ports; checks for buyer input and for what a source returns; a cart priced across sources; totals in minor units that refuse a mixed currency; a discount split over lines to the centavo; and test helpers (a line-source harness, an in-memory stock and slots, a fake payments port, and the suites a stock, a holding source and a payments provider pass). No floats, no runtime dependencies.",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.mts",
10
+ "default": "./dist/index.mjs"
11
+ },
12
+ "./discounts": {
13
+ "types": "./dist/discounts.d.mts",
14
+ "default": "./dist/discounts.mjs"
15
+ },
16
+ "./errors": {
17
+ "types": "./dist/errors.d.mts",
18
+ "default": "./dist/errors.mjs"
19
+ },
20
+ "./lines": {
21
+ "types": "./dist/lines.d.mts",
22
+ "default": "./dist/lines.mjs"
23
+ },
24
+ "./money": {
25
+ "types": "./dist/money.d.mts",
26
+ "default": "./dist/money.mjs"
27
+ },
28
+ "./payments": {
29
+ "types": "./dist/payments.d.mts",
30
+ "default": "./dist/payments.mjs"
31
+ },
32
+ "./ports": {
33
+ "types": "./dist/ports.d.mts",
34
+ "default": "./dist/ports.mjs"
35
+ },
36
+ "./snapshot": {
37
+ "types": "./dist/snapshot.d.mts",
38
+ "default": "./dist/snapshot.mjs"
39
+ },
40
+ "./testing": {
41
+ "types": "./dist/testing.d.mts",
42
+ "default": "./dist/testing.mjs"
43
+ },
44
+ "./totals": {
45
+ "types": "./dist/totals.d.mts",
46
+ "default": "./dist/totals.mjs"
47
+ },
48
+ "./types": {
49
+ "types": "./dist/types.d.mts",
50
+ "default": "./dist/types.mjs"
51
+ },
52
+ "./package.json": "./package.json"
53
+ },
54
+ "devDependencies": {
55
+ "@cloudflare/workers-types": "^4.20260702.1",
56
+ "@types/node": "24.13.3",
57
+ "tsdown": "^0.22.14",
58
+ "typescript": "^5.9.3",
59
+ "vitest": "^2.1.0",
60
+ "@vibedout/money": "0.1.0"
61
+ },
62
+ "license": "MIT",
63
+ "sideEffects": false,
64
+ "files": [
65
+ "dist",
66
+ "README.md"
67
+ ],
68
+ "publishConfig": {
69
+ "access": "public"
70
+ },
71
+ "repository": {
72
+ "type": "git",
73
+ "url": "https://github.com/vibedoutapps/vibedout-platform.git",
74
+ "directory": "packages/commerce"
75
+ },
76
+ "keywords": [
77
+ "commerce",
78
+ "checkout",
79
+ "cart",
80
+ "inventory",
81
+ "stock",
82
+ "minor-units",
83
+ "cloudflare",
84
+ "workers"
85
+ ],
86
+ "scripts": {
87
+ "typecheck": "tsc --noEmit",
88
+ "build": "tsdown",
89
+ "test": "vitest run"
90
+ },
91
+ "main": "./dist/index.mjs",
92
+ "module": "./dist/index.mjs",
93
+ "types": "./dist/index.d.mts"
6
94
  }