@stacksjs/commerce 0.70.45 → 0.70.54

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.
@@ -1,3 +1,4 @@
1
+ export type { CouponRedemptionResult } from './update';
1
2
  export {
2
3
  deleteCoupon,
3
4
  deleteCoupons,
@@ -21,5 +22,6 @@ export {
21
22
  store,
22
23
  } from './store';
23
24
  export {
25
+ redeem,
24
26
  update,
25
27
  } from './update';
@@ -6,5 +6,41 @@
6
6
  * @returns The updated coupon record
7
7
  */
8
8
  export declare function update(id: number, data: Omit<CouponUpdate, 'id'>): Promise<CouponJsonResponse | undefined>;
9
+ /**
10
+ * Atomically redeem a coupon (stacksjs/stacks#1879 Co-5).
11
+ *
12
+ * Pre-fix: callers fetched the coupon, checked `usage_count <
13
+ * max_uses`, then bumped `usage_count` via `update()`. Two concurrent
14
+ * redemption requests against a coupon with `max_uses=1` both saw
15
+ * `usage_count=0`, both incremented, both succeeded. The coupon
16
+ * was redeemed twice — direct revenue loss equal to `discount_value`
17
+ * times the over-redemption count.
18
+ *
19
+ * Post-fix: single conditional UPDATE that bumps `usage_count` and
20
+ * enforces `max_uses` / `is_active` / expiry in the WHERE clause.
21
+ * The database guarantees only one writer wins the race. Returns a
22
+ * discriminated result so callers can surface a useful reason
23
+ * ("limit reached" vs "expired" vs "inactive") instead of a generic
24
+ * failure.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const result = await redeem(couponId)
29
+ * if (!result.ok) {
30
+ * throw new HttpError(400, `Coupon cannot be redeemed: ${result.reason}`)
31
+ * }
32
+ * // Use result.coupon — it reflects the post-redemption state.
33
+ * ```
34
+ */
35
+ export declare function redeem(id: number): Promise<CouponRedemptionResult>;
9
36
  declare type CouponJsonResponse = ModelRow<typeof Coupon>;
10
37
  declare type CouponUpdate = UpdateModelData<typeof Coupon>;
38
+ /**
39
+ * Discriminated result for `redeem` — `ok: true` on a successful
40
+ * atomic redemption (the row's `usage_count` was bumped and the
41
+ * limit wasn't already hit); `ok: false` with a `reason` when the
42
+ * redemption failed (out of uses, expired, inactive, missing).
43
+ * Callers branch on `reason` to surface a useful error to the user.
44
+ */
45
+ export type CouponRedemptionResult = | { ok: true, coupon: CouponJsonResponse }
46
+ | { ok: false, reason: 'not-found' | 'inactive' | 'expired' | 'limit-reached' }
@@ -7,7 +7,19 @@
7
7
  */
8
8
  export declare function update(id: number, data: Omit<GiftCardUpdate, 'id'>): Promise<GiftCardJsonResponse | undefined>;
9
9
  /**
10
- * Update a gift card's balance
10
+ * Update a gift card's balance atomically (stacksjs/stacks#1879 Co-8).
11
+ *
12
+ * Pre-fix: read balance → compute new → write. Two parallel $50
13
+ * redemptions of a $100 card both saw `current_balance = 100`,
14
+ * both wrote `50`, leaving `50` instead of `0`. The post-read
15
+ * negative-balance guard only caught single-threaded misuse.
16
+ *
17
+ * Post-fix: single conditional UPDATE that does the arithmetic and
18
+ * enforces every precondition in the WHERE clause. The database
19
+ * guarantees only one writer wins the race. Throws when the row
20
+ * was found but the precondition failed (insufficient balance,
21
+ * inactive, missing) so the caller can distinguish from a
22
+ * not-found row.
11
23
  *
12
24
  * @param id The ID of the gift card
13
25
  * @param amount The amount to adjust (positive to add, negative to deduct)
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Check whether `from → to` is a legal status transition
3
+ * (stacksjs/stacks#1879 Co-4). Use in `updateStatus` to reject
4
+ * illegal jumps like SHIPPED → PENDING before they hit the database.
5
+ */
6
+ export declare function canTransition(from: OrderStatus, to: OrderStatus): boolean;
7
+ /** Emit `order:created` after a successful placeOrder / store call. */
8
+ export declare function emitOrderCreated(order: Record<string, unknown>): Promise<void>;
9
+ /** Emit `order:paid` after payment confirmation lands. */
10
+ export declare function emitOrderPaid(order: Record<string, unknown>, payment?: Record<string, unknown>): Promise<void>;
11
+ /** Emit `order:shipped` after fulfillment marks the order shipped. */
12
+ export declare function emitOrderShipped(order: Record<string, unknown>): Promise<void>;
13
+ /** Emit `order:delivered` after the carrier confirms delivery. */
14
+ export declare function emitOrderDelivered(order: Record<string, unknown>): Promise<void>;
15
+ /** Emit `order:cancelled` on cancellation (pre- or post-payment). */
16
+ export declare function emitOrderCancelled(order: Record<string, unknown>, reason?: string): Promise<void>;
17
+ /** Emit `order:refunded` after a refund settles. */
18
+ export declare function emitOrderRefunded(order: Record<string, unknown>, refundAmount?: number): Promise<void>;
19
+ /**
20
+ * Convenience dispatch keyed by status. Used from `updateStatus`
21
+ * so a single function fires the right event for whichever status
22
+ * we just transitioned into.
23
+ */
24
+ export declare function emitForStatus(status: OrderStatus, order: Record<string, unknown>): Promise<void>;
25
+ /**
26
+ * Commerce → events bus integration (stacksjs/stacks#1879 Co-18).
27
+ *
28
+ * Background: orders moved through created → paid → shipped →
29
+ * delivered states with no `events.dispatch(...)` calls. Every
30
+ * downstream service (email confirmation, inventory replenishment,
31
+ * analytics, fraud detection) had to poll the orders table because
32
+ * there was no subscription path. The framework's event bus from
33
+ * `@stacksjs/events` has been ready since #1878 — commerce just
34
+ * wasn't wired into it.
35
+ *
36
+ * This module wraps the dispatch calls so they:
37
+ * - Lazy-import the events package so commerce stays usable in
38
+ * environments that don't load it (CLI scripts, migrations)
39
+ * - Silently no-op when the events package isn't installed
40
+ * - Use a structured payload shape callers can subscribe with type safety
41
+ * - Never throw — emission failures shouldn't abort the order write
42
+ *
43
+ * Status-transition guard (stacksjs/stacks#1879 Co-4) lives here
44
+ * too so the rules are co-located with the events they trigger.
45
+ */
46
+ export type OrderStatus = 'PENDING' | 'PROCESSING' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED' | 'REFUNDED';
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Validate that a set of coupon ids satisfies the stacking policy
3
+ * (stacksjs/stacks#1879 Co-7). Pre-fix there was no enforcement
4
+ * anywhere — frontend could submit two coupons and double-discount
5
+ * the order with no signal. Default policy is single-coupon-only;
6
+ * apps that intentionally support stacking opt in.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const stack = validateCouponStacking(cart.couponIds)
11
+ * if (!stack.ok)
12
+ * throw new HttpError(400, `Coupon stacking violation: ${stack.reason}`)
13
+ * ```
14
+ */
15
+ export declare function validateCouponStacking(couponIds: ReadonlyArray<number>, policy?: CouponStackingPolicy): CouponStackingResult;
16
+ /**
17
+ * Validate that a cart line-item's quantity respects the product's
18
+ * `min_order_qty` / `max_order_qty` fields
19
+ * (stacksjs/stacks#1879 Co-14). Looks up the product server-side
20
+ * so the client can't bypass by lying about the bounds.
21
+ *
22
+ * Products without explicit bounds default to "any positive int."
23
+ * A quantity of 0 or negative always fails — the cart's UI is
24
+ * responsible for letting users remove items, not for passing
25
+ * sentinel zeros through the API.
26
+ */
27
+ export declare function validateQuantityBounds(productId: number, quantity: number): Promise<QuantityBoundsResult>;
28
+ /**
29
+ * Batch variant — short-circuits on the first violation so the
30
+ * caller can surface a single useful "item X has min qty Y"
31
+ * error rather than a list of every failed line.
32
+ */
33
+ export declare function validateCartQuantities(items: ReadonlyArray<{ productId: number, quantity: number }>): Promise<QuantityBoundsResult>;
34
+ /**
35
+ * Delete carts that have been untouched for more than
36
+ * `olderThanDays` days (stacksjs/stacks#1879 Co-15). Default 30.
37
+ * Capped at `limit` rows per call (default 1000) so the delete
38
+ * doesn't hold a long write lock on a busy database. Returns the
39
+ * count + cutoff so callers can log or loop.
40
+ *
41
+ * Schedule via `@stacksjs/scheduler` (now fully wired since
42
+ * #1877's scheduler audit closed) to run nightly:
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * // app/Scheduler.ts
47
+ * import { cleanupAbandonedCarts } from '@stacksjs/commerce'
48
+ *
49
+ * schedule.job(async () => {
50
+ * let total = 0
51
+ * while (true) {
52
+ * const r = await cleanupAbandonedCarts({ olderThanDays: 30, limit: 1000 })
53
+ * total += r.deleted
54
+ * if (r.deleted < 1000) break
55
+ * }
56
+ * log.info(`Cleaned up ${total} abandoned carts`)
57
+ * }).daily().setTimeZone('UTC').withName('CleanupAbandonedCarts')
58
+ * ```
59
+ */
60
+ export declare function cleanupAbandonedCarts(options?: CleanupAbandonedCartsOptions): Promise<CleanupAbandonedCartsResult>;
61
+ // ============================================================================
62
+ // Co-7: coupon stacking guard
63
+ // ============================================================================
64
+ export declare interface CouponStackingPolicy {
65
+ allowMultiple?: boolean
66
+ maxStacked?: number
67
+ }
68
+ export declare interface CouponStackingResult {
69
+ ok: boolean
70
+ count: number
71
+ reason?: 'multiple-not-allowed' | 'exceeds-max-stacked'
72
+ }
73
+ // ============================================================================
74
+ // Co-14: min/max order quantity bounds
75
+ // ============================================================================
76
+ export declare interface QuantityBoundsResult {
77
+ ok: boolean
78
+ productId: number
79
+ quantity: number
80
+ reason?: 'below-min' | 'above-max' | 'non-positive' | 'product-missing'
81
+ bound?: number
82
+ }
83
+ // ============================================================================
84
+ // Co-15: abandoned cart cleanup
85
+ // ============================================================================
86
+ export declare interface CleanupAbandonedCartsOptions {
87
+ olderThanDays?: number
88
+ limit?: number
89
+ }
90
+ export declare interface CleanupAbandonedCartsResult {
91
+ deleted: number
92
+ cutoffAt: string
93
+ }
@@ -1,3 +1,17 @@
1
+ export type { PlaceOrderInput, PlaceOrderResult } from './place-order';
2
+ export type { OrderStatus } from './events';
3
+ export type {
4
+ RecomputeLineItem,
5
+ RecomputeOrderInput,
6
+ RecomputeOrderResult,
7
+ } from './totals';
8
+ export type {
9
+ CleanupAbandonedCartsOptions,
10
+ CleanupAbandonedCartsResult,
11
+ CouponStackingPolicy,
12
+ CouponStackingResult,
13
+ QuantityBoundsResult,
14
+ } from './guards';
1
15
  export {
2
16
  bulkDestroy,
3
17
  bulkSoftDelete,
@@ -20,8 +34,49 @@ export {
20
34
  export {
21
35
  store,
22
36
  } from './store';
37
+ // Atomic order placement (stacksjs/stacks#1879 Co-1).
38
+ // Wraps order + payment + inventory decrement in a single
39
+ // transaction so any failure rolls back the rest.
40
+ export { placeOrder } from './place-order';
41
+ // Event bus integration + status-transition state machine
42
+ // (stacksjs/stacks#1879 Co-18, Co-4). Emit helpers fire
43
+ // order:created / order:paid / order:shipped / order:delivered /
44
+ // order:cancelled / order:refunded for downstream subscribers;
45
+ // `canTransition` rejects illegal state changes.
46
+ export {
47
+ canTransition,
48
+ emitForStatus,
49
+ emitOrderCancelled,
50
+ emitOrderCreated,
51
+ emitOrderDelivered,
52
+ emitOrderPaid,
53
+ emitOrderRefunded,
54
+ emitOrderShipped,
55
+ } from './events';
23
56
  export {
24
57
  update,
25
58
  updateDeliveryInfo,
26
59
  updateStatus,
27
60
  } from './update';
61
+ // Stripe webhook handlers (stacksjs/stacks#1879 Co-17). Call
62
+ // registerCommerceWebhookHandlers() once at boot; the payments
63
+ // package's processWebhook() verifies signatures + dispatches.
64
+ export {
65
+ handleChargeRefunded,
66
+ handlePaymentIntentFailed,
67
+ handlePaymentIntentSucceeded,
68
+ registerCommerceWebhookHandlers,
69
+ } from './webhook';
70
+ // Server-side cart-to-order total recompute (stacksjs/stacks#1879 Co-13).
71
+ // Apps pre-flight the cart through this before placeOrder to detect
72
+ // client-supplied drift and reject "price changed" cases.
73
+ export { recomputeOrderTotals } from './totals';
74
+ // Cart / order policy guards (stacksjs/stacks#1879 Co-7, Co-14, Co-15).
75
+ // Pre-flight the cart through these before placeOrder; abandoned-
76
+ // cart cleanup runs as a scheduled job.
77
+ export {
78
+ cleanupAbandonedCarts,
79
+ validateCartQuantities,
80
+ validateCouponStacking,
81
+ validateQuantityBounds,
82
+ } from './guards';
@@ -0,0 +1,18 @@
1
+ export declare function placeOrder(input: PlaceOrderInput): Promise<PlaceOrderResult>;
2
+ export declare interface PlaceOrderInput {
3
+ order: NewOrder
4
+ payment?: NewPayment
5
+ inventory?: ReadonlyArray<{ id: number, delta: number }>
6
+ idempotencyKey?: string
7
+ }
8
+ declare type OrderJsonResponse = ModelRow<typeof Order>;
9
+ declare type NewOrder = NewModelData<typeof Order>;
10
+ declare type PaymentJsonResponse = ModelRow<typeof Payment>;
11
+ declare type NewPayment = NewModelData<typeof Payment>;
12
+ /**
13
+ * Discriminated result. `ok: true` means the order, payment, and
14
+ * inventory all committed; `ok: false` carries a `reason` so the
15
+ * caller can surface a specific error to the user.
16
+ */
17
+ export type PlaceOrderResult = | { ok: true, order: OrderJsonResponse, payment?: PaymentJsonResponse }
18
+ | { ok: false, reason: 'out-of-stock' | 'duplicate-payment' | 'duplicate-idempotency-key' | 'unknown', failedAt?: string, error?: unknown }
@@ -0,0 +1,24 @@
1
+ export declare function recomputeOrderTotals(input: RecomputeOrderInput): Promise<RecomputeOrderResult>;
2
+ export declare interface RecomputeLineItem {
3
+ productId: number
4
+ quantity: number
5
+ unitPriceCents?: number
6
+ }
7
+ export declare interface RecomputeOrderInput {
8
+ items: ReadonlyArray<RecomputeLineItem>
9
+ taxRateId?: number
10
+ shippingCents?: number
11
+ discountCents?: number
12
+ clientTotalCents?: number
13
+ maxDriftCents?: number
14
+ }
15
+ export declare interface RecomputeOrderResult {
16
+ subtotalCents: number
17
+ discountCents: number
18
+ taxCents: number
19
+ shippingCents: number
20
+ totalCents: number
21
+ resolvedItems: Array<{ productId: number, quantity: number, unitPriceCents: number, lineCents: number }>
22
+ priceChanged: boolean
23
+ diffCents: number
24
+ }
@@ -1,3 +1,4 @@
1
+ import type { OrderStatus } from './events';
1
2
  /**
2
3
  * Update an order by ID
3
4
  *
@@ -13,7 +14,7 @@ export declare function update(id: number, data: Omit<OrderUpdate, 'id'>): Promi
13
14
  * @param status The new order status
14
15
  * @returns The updated order with the new status
15
16
  */
16
- export declare function updateStatus(id: number, status: 'PENDING' | 'PROCESSING' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED' | 'REFUNDED'): Promise<OrderJsonResponse | undefined>;
17
+ export declare function updateStatus(id: number, status: OrderStatus): Promise<OrderJsonResponse | undefined>;
17
18
  /**
18
19
  * Update delivery information for an order
19
20
  *
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Handler for `payment_intent.succeeded`. Marks the linked payment
3
+ * row + order row as paid in a single transaction. Idempotent via
4
+ * `recordEventOrSkip`.
5
+ */
6
+ declare function handlePaymentIntentSucceeded(event: { id: string, data: { object: any } }): Promise<void>;
7
+ /**
8
+ * Handler for `payment_intent.payment_failed`. Marks the payment
9
+ * as failed and cancels the linked order. Records the failure
10
+ * reason on the payment row for ops triage.
11
+ */
12
+ declare function handlePaymentIntentFailed(event: { id: string, data: { object: any } }): Promise<void>;
13
+ /**
14
+ * Handler for `charge.refunded`. Marks the order REFUNDED and
15
+ * records the refund amount on the linked payment.
16
+ */
17
+ declare function handleChargeRefunded(event: { id: string, data: { object: any } }): Promise<void>;
18
+ /**
19
+ * Wire commerce's Stripe webhook handlers into the payments
20
+ * package's event router. Call once at boot.
21
+ *
22
+ * Returns an unregister callback for tests that want to swap the
23
+ * handler set between cases.
24
+ */
25
+ export declare function registerCommerceWebhookHandlers(): Promise<() => void>;
26
+ // Export the individual handlers for direct testing.
27
+ export {
28
+ handleChargeRefunded,
29
+ handlePaymentIntentFailed,
30
+ handlePaymentIntentSucceeded,
31
+ };
@@ -33,6 +33,34 @@ export declare function updateAvailability(id: number, isAvailable: boolean): Pr
33
33
  * semantics for admin tooling.
34
34
  */
35
35
  export declare function adjustInventory(id: number, delta: number): Promise<ProductJsonResponse | null>;
36
+ /**
37
+ * Atomic batch inventory adjustment (stacksjs/stacks#1879 Co-2).
38
+ *
39
+ * Pre-fix: a multi-item cart called `adjustInventory(id, delta)` N
40
+ * times sequentially. If item 2 failed (out of stock), item 1's
41
+ * decrement was already committed with no rollback — the order
42
+ * persisted with partial stock reservation and the customer saw
43
+ * a refund-on-next-page-load surprise.
44
+ *
45
+ * Post-fix: wraps the per-item loop in `db.transaction`. Any item
46
+ * that can't be adjusted (the existing `adjustInventory` returns
47
+ * null on insufficient stock) throws inside the transaction; the
48
+ * driver rolls every prior decrement back. Returns a discriminated
49
+ * result so the cart handler can surface "item X was out of stock"
50
+ * specifically instead of a generic transaction-failed error.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const result = await adjustInventoryMany([
55
+ * { id: cart.itemA, delta: -2 },
56
+ * { id: cart.itemB, delta: -1 },
57
+ * ])
58
+ * if (!result.ok) {
59
+ * throw new HttpError(409, `Item ${result.productId} is out of stock`)
60
+ * }
61
+ * ```
62
+ */
63
+ export declare function adjustInventoryMany(updates: ReadonlyArray<{ id: number, delta: number }>): Promise<InventoryBatchResult>;
36
64
  /**
37
65
  * Update inventory information for a product item
38
66
  *
@@ -43,3 +71,12 @@ export declare function adjustInventory(id: number, delta: number): Promise<Prod
43
71
  export declare function updateInventory(id: number, inventoryCount?: number): Promise<ProductJsonResponse | undefined>;
44
72
  declare type ProductJsonResponse = ModelRow<typeof Product>;
45
73
  declare type ProductUpdate = UpdateModelData<typeof Product>;
74
+ /**
75
+ * Discriminated result for `adjustInventoryMany` — `ok: true` when
76
+ * every per-item adjustment succeeded; `ok: false` with `failedAt`
77
+ * (the index in the input array that triggered the rollback) when
78
+ * any single item couldn't be adjusted (out of stock, missing,
79
+ * etc.). All adjustments roll back as a unit on failure.
80
+ */
81
+ export type InventoryBatchResult = | { ok: true, products: ProductJsonResponse[] }
82
+ | { ok: false, failedAt: number, productId: number, reason: 'out-of-stock' | 'not-found' }
@@ -14,13 +14,63 @@ export declare function fetchAll(): Promise<ShippingRateJsonResponse[]>;
14
14
  */
15
15
  export declare function getRatesByZone(zoneId: number): Promise<ShippingRateJsonResponse[]>;
16
16
  /**
17
- * Get shipping rate based on weight and zone
17
+ * Get shipping rate based on weight and zone.
18
+ *
19
+ * **⚠️ Caller-trusted zone (stacksjs/stacks#1879 Co-11).** This
20
+ * function does NOT verify that the `zoneId` actually serves the
21
+ * customer's declared address. A user in NYC can pass a Texas
22
+ * `zoneId` and get cheaper shipping. Use `getRateByWeightAndAddress`
23
+ * below when the request boundary involves untrusted input — it
24
+ * resolves the zone server-side from the address.
25
+ *
26
+ * Kept for back-compat with internal callers that have already
27
+ * resolved the zone (admin tooling, scheduled fulfillment jobs).
18
28
  *
19
29
  * @param weight Weight in the appropriate unit
20
- * @param zoneId Shipping zone identifier
30
+ * @param zoneId Shipping zone identifier (caller-trusted)
21
31
  * @returns Matching shipping rate or undefined
22
32
  */
23
33
  export declare function getRateByWeightAndZone(weight: number, zoneId: number): Promise<ShippingRateJsonResponse | undefined>;
34
+ /**
35
+ * Resolve the canonical shipping zone for a delivery address
36
+ * (stacksjs/stacks#1879 Co-11). Walks the zones table looking for
37
+ * the most specific match: postal-code → region → country-only.
38
+ * Returns null when no zone covers the address.
39
+ *
40
+ * The current zones schema stores `countries` as a comma-separated
41
+ * string (per `getZonesByCountry`'s LIKE-query); we use the same
42
+ * convention here. Apps with a normalized zone-country join table
43
+ * can override by replacing this function via the package facade.
44
+ */
45
+ export declare function resolveZoneForAddress(address: ShippingAddress): Promise<number | null>;
46
+ /**
47
+ * Safer counterpart to `getRateByWeightAndZone`. Resolves the zone
48
+ * server-side from the delivery address, then fetches the rate
49
+ * for that zone. Throws when the address doesn't match any active
50
+ * zone — caller surfaces "we don't ship to your area" to the user.
51
+ *
52
+ * Use this from any HTTP/API boundary where the address is
53
+ * caller-controlled. Internal callers that already have a verified
54
+ * zone can stay on `getRateByWeightAndZone`.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const rate = await getRateByWeightAndAddress(2.5, {
59
+ * countryCode: 'US',
60
+ * region: 'NY',
61
+ * postalCode: '10001',
62
+ * })
63
+ * if (!rate) throw new HttpError(404, 'No shipping rate for this address')
64
+ * ```
65
+ */
66
+ export declare function getRateByWeightAndAddress(weight: number, address: ShippingAddress): Promise<ShippingRateJsonResponse | undefined>;
67
+ /**
68
+ * Verify that a caller-supplied `zoneId` actually serves the given
69
+ * address. Returns true when the address resolves to the same zone.
70
+ * Use when a flow already has a `zoneId` from earlier and just
71
+ * wants to confirm it wasn't tampered with mid-checkout.
72
+ */
73
+ export declare function validateZoneMatchesAddress(zoneId: number, address: ShippingAddress): Promise<boolean>;
24
74
  /**
25
75
  * Format shipping rate options for dropdown menus or selectors
26
76
  *
@@ -34,4 +84,15 @@ export declare function formatShippingRateOptions(): Promise<{ id: number, shipp
34
84
  * @returns List of shipping rates for the specified method
35
85
  */
36
86
  export declare function getShippingRatesByMethod(methodId: number): Promise<ShippingRateJsonResponse[]>;
87
+ /**
88
+ * Shipping address shape used by the safe-by-default lookup helpers
89
+ * (stacksjs/stacks#1879 Co-11). All fields except `countryCode` are
90
+ * optional — zone matching tries them in order of specificity
91
+ * (postal → region → country) and returns the most specific match.
92
+ */
93
+ export declare interface ShippingAddress {
94
+ countryCode: string
95
+ region?: string
96
+ postalCode?: string
97
+ }
37
98
  declare type ShippingRateJsonResponse = ModelRow<typeof ShippingRate>;
@@ -1,3 +1,4 @@
1
+ export type { ShippingAddress } from './fetch';
1
2
  export {
2
3
  bulkDestroy,
3
4
  destroy,
@@ -9,9 +10,14 @@ export {
9
10
  fetchAll,
10
11
  fetchById,
11
12
  formatShippingRateOptions,
13
+ // Safe-by-default address-based lookups (stacksjs/stacks#1879 Co-11).
14
+ // Use these from request handlers where the address is caller-controlled.
15
+ getRateByWeightAndAddress,
12
16
  getRateByWeightAndZone,
13
17
  getRatesByZone,
14
18
  getShippingRatesByMethod,
19
+ resolveZoneForAddress,
20
+ validateZoneMatchesAddress,
15
21
  } from './fetch';
16
22
  // Functions from store.ts
17
23
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/commerce",
3
3
  "type": "module",
4
- "version": "0.70.45",
4
+ "version": "0.70.54",
5
5
  "description": "Stacks commerce utilities.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [