@openlup/platform 0.18.0 → 0.19.0

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.
Files changed (104) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +24 -9
  3. package/dist/catalog/adminBundleReadHandler.d.ts +2 -1
  4. package/dist/catalog/adminBundleReadHandler.js +4 -4
  5. package/dist/catalog/bundleCatalogReadPort.d.ts +8 -1
  6. package/dist/catalog/bundleCatalogStore.d.ts +3 -1
  7. package/dist/catalog/bundleCatalogStore.js +5 -1
  8. package/dist/catalog/bundlePriceSelection.d.ts +13 -0
  9. package/dist/catalog/bundlePriceSelection.js +26 -0
  10. package/dist/catalog/contracts/bundleRead.d.ts +18 -0
  11. package/dist/catalog/contracts/draft.js +1 -1
  12. package/dist/catalog/effectiveBundleCatalogQueries.d.ts +8 -0
  13. package/dist/catalog/effectiveBundleCatalogQueries.js +79 -0
  14. package/dist/catalog/effectiveBundleCatalogStore.d.ts +4 -0
  15. package/dist/catalog/effectiveBundleCatalogStore.js +156 -0
  16. package/dist/catalog/index.d.ts +1 -0
  17. package/dist/catalog/index.js +1 -0
  18. package/dist/composition/index.d.ts +2 -0
  19. package/dist/composition/index.js +2 -0
  20. package/dist/composition/inventoryContribution.d.ts +16 -0
  21. package/dist/composition/inventoryContribution.js +17 -0
  22. package/dist/composition/shippingContribution.d.ts +16 -0
  23. package/dist/composition/shippingContribution.js +17 -0
  24. package/dist/inventory/bindings.d.ts +35 -0
  25. package/dist/inventory/bindings.js +45 -0
  26. package/dist/inventory/index.d.ts +1 -0
  27. package/dist/inventory/index.js +1 -0
  28. package/dist/pricing/commercePriceAuthority.d.ts +2 -0
  29. package/dist/pricing/commercePriceAuthority.js +61 -0
  30. package/dist/pricing/commerceQuoteHandler.d.ts +7 -3
  31. package/dist/pricing/commerceQuoteHandler.js +16 -4
  32. package/dist/pricing/dbBackedCommerceQuotePort.d.ts +8 -0
  33. package/dist/pricing/dbBackedCommerceQuotePort.js +46 -29
  34. package/dist/pricing/index.d.ts +2 -1
  35. package/dist/pricing/index.js +1 -1
  36. package/dist/pricing/offerPolicySchemas.d.ts +2 -2
  37. package/dist/pricing/offerPricingSchemas.d.ts +2 -2
  38. package/dist/pricing/pricingPolicyToken.d.ts +2 -2
  39. package/dist/pricing/quoteSchemas.d.ts +18 -18
  40. package/dist/pricing/quoteShippingPromotions.d.ts +20 -0
  41. package/dist/pricing/quoteShippingPromotions.js +1 -0
  42. package/dist/promotions/adminPromotionCodesContracts.d.ts +7 -7
  43. package/dist/promotions/commerceQuoteReadPreparation.d.ts +3 -0
  44. package/dist/promotions/commerceQuoteReadPreparation.js +27 -0
  45. package/dist/promotions/index.d.ts +3 -0
  46. package/dist/promotions/index.js +1 -0
  47. package/dist/promotions/promotionQuoteAcceptance.d.ts +5 -0
  48. package/dist/promotions/promotionQuoteAcceptance.js +12 -12
  49. package/dist/promotions/promotionQuoteBinding.d.ts +4 -2
  50. package/dist/promotions/promotionQuoteBinding.js +5 -3
  51. package/dist/promotions/quoteAcquisitionBinding.d.ts +6 -0
  52. package/dist/promotions/quoteAcquisitionBinding.js +1 -0
  53. package/dist/promotions/quoteAutomaticPromotions.d.ts +3 -1
  54. package/dist/promotions/quoteAutomaticPromotions.js +28 -4
  55. package/dist/promotions/quotePromoDiscounts.d.ts +5 -1
  56. package/dist/promotions/quotePromoDiscounts.js +8 -2
  57. package/dist/promotions/quotePromotionV2.d.ts +7 -6
  58. package/dist/promotions/quotePromotionV2.js +15 -3
  59. package/dist/promotions/quoteShippingPolicy.d.ts +23 -0
  60. package/dist/promotions/quoteShippingPolicy.js +47 -0
  61. package/dist/shipping/bindings.d.ts +57 -0
  62. package/dist/shipping/bindings.js +46 -0
  63. package/dist/shipping/contracts/index.d.ts +1 -0
  64. package/dist/shipping/contracts/threshold.d.ts +22 -0
  65. package/dist/shipping/contracts/threshold.js +1 -0
  66. package/dist/shipping/index.d.ts +3 -0
  67. package/dist/shipping/index.js +2 -0
  68. package/dist/shipping/thresholdPolicy.d.ts +42 -0
  69. package/dist/shipping/thresholdPolicy.js +60 -0
  70. package/package.json +2 -2
  71. package/release-gates.json +136 -134
  72. package/src/catalog/adminBundleReadHandler.ts +6 -4
  73. package/src/catalog/bundleCatalogReadPort.ts +6 -2
  74. package/src/catalog/bundleCatalogStore.ts +5 -0
  75. package/src/catalog/bundlePriceSelection.ts +25 -0
  76. package/src/catalog/contracts/bundleRead.ts +2 -2
  77. package/src/catalog/contracts/draft.ts +1 -1
  78. package/src/catalog/effectiveBundleCatalogQueries.ts +80 -0
  79. package/src/catalog/effectiveBundleCatalogStore.ts +208 -0
  80. package/src/catalog/index.ts +1 -0
  81. package/src/composition/index.ts +2 -0
  82. package/src/composition/inventoryContribution.ts +24 -0
  83. package/src/composition/shippingContribution.ts +24 -0
  84. package/src/inventory/bindings.ts +61 -0
  85. package/src/inventory/index.ts +1 -0
  86. package/src/pricing/commercePriceAuthority.ts +65 -0
  87. package/src/pricing/commerceQuoteHandler.ts +20 -5
  88. package/src/pricing/dbBackedCommerceQuotePort.ts +50 -30
  89. package/src/pricing/index.ts +2 -1
  90. package/src/pricing/quoteShippingPromotions.ts +21 -0
  91. package/src/promotions/commerceQuoteReadPreparation.ts +29 -0
  92. package/src/promotions/index.ts +3 -0
  93. package/src/promotions/promotionQuoteAcceptance.ts +17 -10
  94. package/src/promotions/promotionQuoteBinding.ts +7 -2
  95. package/src/promotions/quoteAcquisitionBinding.ts +7 -0
  96. package/src/promotions/quoteAutomaticPromotions.ts +30 -4
  97. package/src/promotions/quotePromoDiscounts.ts +11 -2
  98. package/src/promotions/quotePromotionV2.ts +20 -4
  99. package/src/promotions/quoteShippingPolicy.ts +61 -0
  100. package/src/shipping/bindings.ts +74 -0
  101. package/src/shipping/contracts/index.ts +1 -0
  102. package/src/shipping/contracts/threshold.ts +22 -0
  103. package/src/shipping/index.ts +3 -0
  104. package/src/shipping/thresholdPolicy.ts +65 -0
@@ -1,9 +1,12 @@
1
+ import type { QuoteAcquisitionBinding } from "./quoteAcquisitionBinding.js";
2
+ import type { QuoteDiscountResult } from "./quotePromoDiscounts.js";
1
3
  import type { PromotionRow } from "@openlup/core/promo";
2
- import type { CommerceCodeRejection, CommerceQuoteDiscount } from "../pricing/index.js";
4
+ import type { CommerceQuoteDiscount } from "../pricing/index.js";
3
5
  import type { CreateQuoteRequest } from "../pricing/index.js";
4
6
  import type { CommercePromoDataPort } from "./promoDataPort.js";
5
7
  import type { createQuotePromoDiscounts } from "./quotePromoDiscounts.js";
6
- export declare function createQuotePromotionV2({ promotionCustomerSemantic, minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR, evaluateOrderTotalDiscounts }: {
8
+ export declare function createQuotePromotionV2({ promotionCustomerSemantic, minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR, evaluateOrderTotalDiscounts, acquisition }: {
9
+ acquisition?: QuoteAcquisitionBinding<CreateQuoteRequest>;
7
10
  promotionCustomerSemantic: (row: PromotionRow) => CommerceQuoteDiscount["customerSemantic"];
8
11
  minimumProductPayableMinor: number;
9
12
  evaluateOrderTotalDiscounts: ReturnType<typeof createQuotePromoDiscounts>["evaluateOrderTotalDiscounts"];
@@ -21,9 +24,7 @@ export declare function createQuotePromotionV2({ promotionCustomerSemantic, mini
21
24
  shippingMinor: number;
22
25
  atTime: string;
23
26
  emailEligibilityConfirmed?: boolean;
24
- }) => Promise<{
25
- discounts: CommerceQuoteDiscount[];
26
- codeRejections: CommerceCodeRejection[];
27
- }>;
27
+ acquisitionCapability?: unknown;
28
+ }) => Promise<QuoteDiscountResult>;
28
29
  legacyPromotionDataPort: (port: CommercePromoDataPort) => CommercePromoDataPort;
29
30
  };
@@ -1,6 +1,7 @@
1
+ import { filterAcquisitionPromotions, selectAcquisitionTerms, assertAcquisitionTermsCapability, ACQUISITION_TERMS_CAPABILITY } from "../pricing/index.js";
1
2
  import { evaluatePromotionAdjustmentsV2, promoEligibilityFailure } from "@openlup/core/promo";
2
3
  import { resolvePromotionBenefitDefinition } from "./promotionBenefitDefinition.js";
3
- export function createQuotePromotionV2({ promotionCustomerSemantic, minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR, evaluateOrderTotalDiscounts }) {
4
+ export function createQuotePromotionV2({ promotionCustomerSemantic, minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR, evaluateOrderTotalDiscounts, acquisition }) {
4
5
  /** Automatic v2 benefits. Code claims remain outside quote evaluation. */
5
6
  async function evaluateAutomaticPromotionV2(input) {
6
7
  const byMode = input.clientId
@@ -11,6 +12,14 @@ export function createQuotePromotionV2({ promotionCustomerSemantic, minimumProdu
11
12
  input.promoDataPort.redemptionCounts(input.clientId),
12
13
  input.promoDataPort.deviceFirstOrderRedeemed(input.visitorId ?? null),
13
14
  ]);
15
+ const existingSubscriber = input.mode === "subscription" && byMode.subscription > 0;
16
+ const candidatesForCustomer = acquisition && existingSubscriber ? rows.filter((row) => !acquisition.financialPolicy.facts(row).metadataPresent && row.eligibility.first_subscription_purchase !== true) : rows;
17
+ const selection = acquisition && input.mode === "subscription" && !existingSubscriber
18
+ ? selectAcquisitionTerms(candidatesForCustomer, input.atTime, input.regionCode, acquisition.financialPolicy) : null;
19
+ if (acquisition)
20
+ assertAcquisitionTermsCapability(selection, input.acquisitionCapability, "promotion-engine.v2");
21
+ const effectiveRows = acquisition && input.mode === "subscription"
22
+ ? filterAcquisitionPromotions(candidatesForCustomer, input.atTime, input.regionCode, acquisition.financialPolicy) : candidatesForCustomer;
14
23
  const cart = {
15
24
  region_code: input.regionCode,
16
25
  cart_mode: input.mode === "subscription" ? "subscription" : "one_time",
@@ -23,7 +32,7 @@ export function createQuotePromotionV2({ promotionCustomerSemantic, minimumProdu
23
32
  email_eligibility_confirmed: input.emailEligibilityConfirmed === true,
24
33
  applied_codes: [],
25
34
  };
26
- const eligibleRows = rows
35
+ const eligibleRows = effectiveRows
27
36
  .filter((row) => row.promotion_engine_version === "promotion-engine.v2")
28
37
  .filter((row) => row.trigger_type === "automatic")
29
38
  .filter((row) => promoEligibilityFailure(row, cart, input.atTime) === null)
@@ -54,7 +63,8 @@ export function createQuotePromotionV2({ promotionCustomerSemantic, minimumProdu
54
63
  };
55
64
  });
56
65
  const legacyFloor = await evaluateOrderTotalDiscounts({
57
- promoDataPort: legacyPromotionDataPort(input.promoDataPort),
66
+ promoDataPort: legacyPromotionDataPort(acquisition
67
+ ? { ...input.promoDataPort, listActivePromotions: async () => effectiveRows } : input.promoDataPort),
58
68
  clientId: input.clientId,
59
69
  visitorId: input.visitorId,
60
70
  mode: input.mode,
@@ -69,6 +79,8 @@ export function createQuotePromotionV2({ promotionCustomerSemantic, minimumProdu
69
79
  return {
70
80
  discounts,
71
81
  codeRejections: reconcileLegacyCodeOutcomes(input.promoCodes ?? [], discounts, legacyFloor),
82
+ ...(selection && input.acquisitionCapability === ACQUISITION_TERMS_CAPABILITY
83
+ ? { acquisitionTerms: selection.terms } : {}),
72
84
  };
73
85
  }
74
86
  function legacyPromotionDataPort(port) {
@@ -0,0 +1,23 @@
1
+ import type { CreateQuoteRequest, QuoteShippingPromotionInput, QuoteShippingPromotionResult } from "../pricing/index.js";
2
+ import type { ShippingSettings } from "../shipping/index.js";
3
+ import type { CommercePromoDataPort } from "./promoDataPort.js";
4
+ import type { PromotionCodeQuotePort } from "./promotionCodeQuotePort.js";
5
+ import type { createQuoteAutomaticPromotions } from "./quoteAutomaticPromotions.js";
6
+ import type { createQuotePromotionCodeAdjustments } from "./quotePromotionCodeAdjustments.js";
7
+ export interface QuoteShippingPromotionsDeps<Request extends CreateQuoteRequest & {
8
+ visitorId?: string;
9
+ }> {
10
+ promoDataPort?: CommercePromoDataPort;
11
+ promotionCodeQuotePort?: PromotionCodeQuotePort;
12
+ commerceSettingsPort?: {
13
+ getShippingSettings(): Promise<ShippingSettings>;
14
+ };
15
+ resolveAutomaticQuotePromotions: ReturnType<typeof createQuoteAutomaticPromotions<Request>>["resolveAutomaticQuotePromotions"];
16
+ applyPromotionCodesV2: ReturnType<typeof createQuotePromotionCodeAdjustments>["applyPromotionCodesV2"];
17
+ }
18
+ /** Evaluate both lanes again only when the post-product-discount threshold changes the fee. */
19
+ export declare function createQuoteShippingPromotions<Request extends CreateQuoteRequest & {
20
+ visitorId?: string;
21
+ }>(deps: QuoteShippingPromotionsDeps<Request>): {
22
+ resolveQuoteShippingPromotions: (input: QuoteShippingPromotionInput<Request>) => Promise<QuoteShippingPromotionResult>;
23
+ };
@@ -0,0 +1,47 @@
1
+ import { PROMOTION_ENGINE_V2 } from "../pricing/index.js";
2
+ import { resolveShippingPolicy, resolveThresholdShipping } from "../shipping/index.js";
3
+ import { memoizeCommercePromoReads } from "./commerceQuoteReadPreparation.js";
4
+ /** Evaluate both lanes again only when the post-product-discount threshold changes the fee. */
5
+ export function createQuoteShippingPromotions(deps) {
6
+ async function resolveQuoteShippingPromotions(input) {
7
+ const settings = deps.commerceSettingsPort
8
+ ? await deps.commerceSettingsPort.getShippingSettings()
9
+ : { legacyFlatMinor: 0, policy: null };
10
+ const shipping = resolveShippingPolicy(settings, input.atTime, input.currency);
11
+ const promoDataPort = deps.promoDataPort ? memoizeCommercePromoReads(deps.promoDataPort) : undefined;
12
+ let candidates;
13
+ const port = deps.promotionCodeQuotePort ? {
14
+ resolve: async (request) => structuredClone(await (candidates ??= deps.promotionCodeQuotePort.resolve(request))),
15
+ } : undefined;
16
+ async function evaluate(shippingGrossMinor) {
17
+ const automatic = await deps.resolveAutomaticQuotePromotions({ ...input, promoDataPort, shippingGrossMinor });
18
+ const adjusted = await deps.applyPromotionCodesV2({
19
+ port,
20
+ preserveLegacyDiscountsWhenNoCandidates: input.options?.pricingPolicy?.promotionEngineVersion !== PROMOTION_ENGINE_V2,
21
+ clientId: input.options?.clientId ?? null,
22
+ mode: input.request.mode,
23
+ promoCodes: input.request.promoCodes,
24
+ referenceProductMinor: input.referenceProductMinor,
25
+ subtotalGrossMinor: input.subtotalGrossMinor,
26
+ shippingGrossMinor,
27
+ discounts: automatic.discounts,
28
+ codeRejections: automatic.codeRejections,
29
+ atTime: input.atTime,
30
+ });
31
+ return { ...adjusted, acquisitionTerms: automatic.acquisitionTerms };
32
+ }
33
+ let shippingGrossMinor = shipping.flatMinor;
34
+ let result = await evaluate(shippingGrossMinor);
35
+ const productDiscount = result.discounts.filter((discount) => discount.appliesTo !== "shipping")
36
+ .reduce((total, discount) => total + discount.amountOffMinor, 0);
37
+ if (shipping.terms) {
38
+ const resolved = resolveThresholdShipping(shipping.terms, input.subtotalGrossMinor - productDiscount);
39
+ if (resolved.grossMinor !== shippingGrossMinor) {
40
+ shippingGrossMinor = resolved.grossMinor;
41
+ result = await evaluate(shippingGrossMinor);
42
+ }
43
+ }
44
+ return { ...result, shippingGrossMinor, shippingTerms: shipping.terms, hasShipping: deps.commerceSettingsPort != null };
45
+ }
46
+ return { resolveQuoteShippingPromotions };
47
+ }
@@ -0,0 +1,57 @@
1
+ import type { BffResponder } from "../bff/index.js";
2
+ import type { CustomerAddressBookPort, CustomerAddressMutationPort, CustomerAddressesDeps } from "./addressPorts.js";
3
+ import type { CustomerAddressSchemas } from "./addressSchemas.js";
4
+ import type { DeliverySelectionPort } from "./deliverySelectionPort.js";
5
+ import type { DeliverySelectionSchemas } from "./handlerSchemas.js";
6
+ import type { PickupPointSearchPort } from "./pickupPointSearchPort.js";
7
+ import type { AdminShippingRateHandlerDeps, ShippingRateDataPort, ShippingRateHandlerSchemas } from "./shippingRatePorts.js";
8
+ import { createCustomerAddressesHandler } from "./customerAddressesHandler.js";
9
+ import { createAdminShippingRateHandler } from "./adminShippingRateHandler.js";
10
+ import { createDeliveryOptionsHandler, createPickupPointSearchHandler, createPickupPointValidationHandler } from "./deliverySelectionHandler.js";
11
+ type Store = {
12
+ addressBookPort?: CustomerAddressBookPort;
13
+ addressMutationPort?: CustomerAddressMutationPort;
14
+ shippingRateDataPort?: ShippingRateDataPort;
15
+ };
16
+ export interface ShippingBindingDependencies {
17
+ dataKind: string;
18
+ createManagedStore?: () => Store | null;
19
+ nativeStore?: Omit<Store, "addressMutationPort"> | null;
20
+ responder: BffResponder;
21
+ selection?: {
22
+ port: DeliverySelectionPort;
23
+ searchPort: PickupPointSearchPort;
24
+ schemas: DeliverySelectionSchemas;
25
+ };
26
+ addresses?: {
27
+ schemas: CustomerAddressSchemas;
28
+ authenticateUser: CustomerAddressesDeps["authenticateUser"];
29
+ requireWrites: boolean;
30
+ };
31
+ rate?: {
32
+ schemas: ShippingRateHandlerSchemas;
33
+ authorizeAdmin: AdminShippingRateHandlerDeps["authorizeAdmin"];
34
+ resolveAdmin: AdminShippingRateHandlerDeps["resolveAdmin"];
35
+ };
36
+ }
37
+ export interface ShippingBindings {
38
+ store: Store;
39
+ handlers: {
40
+ options: ReturnType<typeof createDeliveryOptionsHandler> | null;
41
+ search: ReturnType<typeof createPickupPointSearchHandler> | null;
42
+ validate: ReturnType<typeof createPickupPointValidationHandler> | null;
43
+ addresses: ReturnType<typeof createCustomerAddressesHandler> | null;
44
+ rate: ReturnType<typeof createAdminShippingRateHandler> | null;
45
+ };
46
+ readiness: {
47
+ ready: boolean;
48
+ reason: string | null;
49
+ };
50
+ unsupported: {
51
+ addressWrites: string | null;
52
+ flatRateSave: string | null;
53
+ };
54
+ }
55
+ /** Explicit selected stores and host validation; importing this module activates nothing. */
56
+ export declare function createShippingBindings(deps: ShippingBindingDependencies): ShippingBindings;
57
+ export {};
@@ -0,0 +1,46 @@
1
+ import { createCustomerAddressesHandler } from "./customerAddressesHandler.js";
2
+ import { createAdminShippingRateHandler } from "./adminShippingRateHandler.js";
3
+ import { createDeliveryOptionsHandler, createPickupPointSearchHandler, createPickupPointValidationHandler } from "./deliverySelectionHandler.js";
4
+ /** Explicit selected stores and host validation; importing this module activates nothing. */
5
+ export function createShippingBindings(deps) {
6
+ const native = deps.dataKind === "postgres";
7
+ const addressWrites = native ? "shipping_native_address_writes_unsupported" : null;
8
+ const flatRateSave = native ? "shipping_native_flat_rate_save_unsupported" : null;
9
+ const refused = (reason) => ({
10
+ store: {}, handlers: { options: null, search: null, validate: null, addresses: null, rate: null },
11
+ readiness: { ready: false, reason }, unsupported: { addressWrites, flatRateSave },
12
+ });
13
+ if (!native && deps.dataKind !== "supabase")
14
+ return refused("shipping_store_incompatible");
15
+ if (native && deps.addresses?.requireWrites)
16
+ return refused(addressWrites);
17
+ if (native && deps.rate)
18
+ return refused(flatRateSave);
19
+ const selected = native ? deps.nativeStore : deps.createManagedStore?.();
20
+ if ((deps.addresses || deps.rate) && !selected)
21
+ return refused("shipping_store_required");
22
+ // Native address mutations remain unavailable even if an untyped host plants a writer.
23
+ const store = { addressBookPort: selected?.addressBookPort, shippingRateDataPort: selected?.shippingRateDataPort,
24
+ addressMutationPort: native ? undefined : selected?.addressMutationPort };
25
+ if (deps.addresses && (typeof store.addressBookPort?.getAddressBook !== "function"
26
+ || typeof deps.addresses.authenticateUser !== "function"
27
+ || (deps.addresses.requireWrites && ![store.addressMutationPort?.upsertAddress, store.addressMutationPort?.deleteAddress].every(fn => typeof fn === "function"))))
28
+ return refused("shipping_address_store_incompatible");
29
+ if (deps.rate && (![store.shippingRateDataPort?.getShippingFlatMinor, store.shippingRateDataPort?.setShippingFlatMinor,
30
+ store.shippingRateDataPort?.recordPromotionAudit, deps.rate.authorizeAdmin, deps.rate.resolveAdmin].every(fn => typeof fn === "function")))
31
+ return refused("shipping_rate_store_incompatible");
32
+ if (deps.selection && ![deps.selection.port?.listOptions, deps.selection.port?.validatePickupPoint, deps.selection.searchPort?.searchPickupPoints].every(fn => typeof fn === "function"))
33
+ return refused("shipping_selection_incompatible");
34
+ return {
35
+ store,
36
+ handlers: {
37
+ options: deps.selection ? createDeliveryOptionsHandler(deps.selection.port, deps.selection.schemas, deps.responder) : null,
38
+ search: deps.selection ? createPickupPointSearchHandler(deps.selection.searchPort, deps.selection.schemas, deps.responder) : null,
39
+ validate: deps.selection ? createPickupPointValidationHandler(deps.selection.port, deps.selection.schemas, deps.responder) : null,
40
+ addresses: deps.addresses ? createCustomerAddressesHandler({ addressBookPort: store.addressBookPort, addressMutationPort: store.addressMutationPort,
41
+ authenticateUser: deps.addresses.authenticateUser }, deps.addresses.schemas, deps.responder) : null,
42
+ rate: deps.rate ? createAdminShippingRateHandler({ dataPort: store.shippingRateDataPort, authorizeAdmin: deps.rate.authorizeAdmin,
43
+ resolveAdmin: deps.rate.resolveAdmin }, deps.rate.schemas, deps.responder) : null,
44
+ }, readiness: { ready: true, reason: null }, unsupported: { addressWrites, flatRateSave },
45
+ };
46
+ }
@@ -1,3 +1,4 @@
1
1
  export type { DeliveryKind, DeliveryProviderKind, DeliveryCarrierKind, DeliveryService, PickupPoint, DeliveryOption, DeliveryOptionsResponse, PickupPointValidationRequest, PickupPointValidationResponse, PickupPointSearchRequest, PickupPointSearchResult, PickupPointSearchResponse, } from "./delivery.js";
2
2
  export type { CanonicalAddressInput, AddressCanonSource, AddressCanonLocalityCandidate, AddressCanonStreet, AddressCanonPostalCodeLookupRequest, AddressCanonLocalitiesLookupRequest, AddressCanonStreetsLookupRequest, AddressCanonPostalCodeLookupResponse, AddressCanonLocalitiesLookupResponse, AddressCanonStreetsLookupResponse, CustomerAddressKind, CustomerAddress, CustomerOrdererProfile, CustomerAddressesRequest, CustomerAddressesResponse, CustomerAddressUpsertRequest, CustomerAddressDeleteRequest } from "./address.js";
3
3
  export type { AdminShippingRateResponse, AdminSetShippingRateRequest, AdminSetShippingRateResponse } from "./shippingRate.js";
4
+ export type { ShippingTerms, ShippingPolicyEntry, ShippingPolicy, ShippingSettings } from "./threshold.js";
@@ -0,0 +1,22 @@
1
+ /** Shipping promises selected by an opt-in host policy. Runtime validation is server-only. */
2
+ export interface ShippingTerms {
3
+ version: "shipping-threshold.v1";
4
+ currency: string;
5
+ flatMinor: number;
6
+ freeThresholdMinor: number;
7
+ }
8
+ export interface ShippingPolicyEntry {
9
+ enabled: boolean;
10
+ flatMinor: number;
11
+ freeThresholdMinor: number;
12
+ effectiveFrom: string;
13
+ }
14
+ export interface ShippingPolicy {
15
+ version: "shipping-policy.v1";
16
+ current: ShippingPolicyEntry | null;
17
+ scheduled?: ShippingPolicyEntry | null;
18
+ }
19
+ export interface ShippingSettings {
20
+ legacyFlatMinor: number;
21
+ policy: ShippingPolicy | null;
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -15,3 +15,6 @@ export { readAddresses, readOrdererProfiles, addressUsedByActiveSubscription, ad
15
15
  export type { AdminShippingRateResponse, AdminSetShippingRateRequest, AdminSetShippingRateResponse } from "./contracts/shippingRate.js";
16
16
  export { createAdminShippingRateHandler } from "./adminShippingRateHandler.js";
17
17
  export type { AdminShippingRateHandlerDeps, ShippingRateDataPort, ShippingRateHandlerSchemas } from "./shippingRatePorts.js";
18
+ export { createShippingBindings, type ShippingBindingDependencies, type ShippingBindings } from "./bindings.js";
19
+ export type { ShippingTerms, ShippingPolicyEntry, ShippingPolicy, ShippingSettings } from "./contracts/threshold.js";
20
+ export { shippingTermsSchema, shippingPolicySchema, parseShippingTerms, hasShippingTerms, normalizeShippingPolicy, resolveShippingPolicy, resolveThresholdShipping } from "./thresholdPolicy.js";
@@ -6,3 +6,5 @@ export { createCustomerAddressesHandler } from "./customerAddressesHandler.js";
6
6
  export { canonicalAddressKey } from "./addressCanonical.js";
7
7
  export { readAddresses, readOrdererProfiles, addressUsedByActiveSubscription, addressRow, findCanonicalAddressId, clearDefaultAddress } from "./customerSelfServiceAddressModels.js";
8
8
  export { createAdminShippingRateHandler } from "./adminShippingRateHandler.js";
9
+ export { createShippingBindings } from "./bindings.js";
10
+ export { shippingTermsSchema, shippingPolicySchema, parseShippingTerms, hasShippingTerms, normalizeShippingPolicy, resolveShippingPolicy, resolveThresholdShipping } from "./thresholdPolicy.js";
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ import type { ShippingPolicy, ShippingSettings, ShippingTerms } from "./contracts/threshold.js";
3
+ export declare const shippingTermsSchema: z.ZodObject<{
4
+ version: z.ZodLiteral<"shipping-threshold.v1">;
5
+ currency: z.ZodString;
6
+ flatMinor: z.ZodNumber;
7
+ freeThresholdMinor: z.ZodNumber;
8
+ }, z.core.$strict>;
9
+ export declare const shippingPolicySchema: z.ZodObject<{
10
+ version: z.ZodLiteral<"shipping-policy.v1">;
11
+ current: z.ZodNullable<z.ZodObject<{
12
+ enabled: z.ZodBoolean;
13
+ flatMinor: z.ZodNumber;
14
+ freeThresholdMinor: z.ZodNumber;
15
+ effectiveFrom: z.ZodString;
16
+ }, z.core.$strict>>;
17
+ scheduled: z.ZodOptional<z.ZodNullable<z.ZodObject<{
18
+ enabled: z.ZodBoolean;
19
+ flatMinor: z.ZodNumber;
20
+ freeThresholdMinor: z.ZodNumber;
21
+ effectiveFrom: z.ZodString;
22
+ }, z.core.$strict>>>;
23
+ }, z.core.$strict>;
24
+ /** Absence selects legacy behavior; malformed retained promises refuse. */
25
+ export declare function parseShippingTerms(value: unknown): ShippingTerms | undefined;
26
+ export declare function hasShippingTerms(quote: {
27
+ context?: {
28
+ shippingTerms?: unknown;
29
+ };
30
+ }): boolean;
31
+ /** Resolve effective entries on read, without a scheduled write. */
32
+ export declare function normalizeShippingPolicy(policy: ShippingPolicy, asOf: Date | string): ShippingPolicy;
33
+ export declare function resolveShippingPolicy(settings: ShippingSettings, asOf: Date | string, currency: string): {
34
+ configured: boolean;
35
+ flatMinor: number;
36
+ terms?: ShippingTerms;
37
+ };
38
+ /** Only product payable after discounts counts; equality receives free delivery. */
39
+ export declare function resolveThresholdShipping(terms: ShippingTerms, productPayableMinor: number): {
40
+ grossMinor: number;
41
+ remainingMinor: number;
42
+ };
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ const minorSchema = z.number().int().nonnegative().max(2_147_483_647);
3
+ const instantSchema = z.string().datetime({ offset: true }).refine((value) => {
4
+ const [year, month, day] = value.slice(0, 10).split("-").map(Number);
5
+ const days = new Date(Date.UTC(year, month, 0)).getUTCDate();
6
+ return day <= days && Number.isFinite(Date.parse(value));
7
+ }, "Invalid calendar date");
8
+ export const shippingTermsSchema = z.object({
9
+ version: z.literal("shipping-threshold.v1"),
10
+ currency: z.string().regex(/^[A-Z]{3}$/),
11
+ flatMinor: minorSchema,
12
+ freeThresholdMinor: minorSchema,
13
+ }).strict();
14
+ const entrySchema = z.object({
15
+ enabled: z.boolean(), flatMinor: minorSchema, freeThresholdMinor: minorSchema,
16
+ effectiveFrom: instantSchema,
17
+ }).strict();
18
+ export const shippingPolicySchema = z.object({
19
+ version: z.literal("shipping-policy.v1"),
20
+ current: entrySchema.nullable(), scheduled: entrySchema.nullable().optional(),
21
+ }).strict().refine((policy) => !policy.current || !policy.scheduled ||
22
+ Date.parse(policy.current.effectiveFrom) < Date.parse(policy.scheduled.effectiveFrom), "Scheduled policy must follow current policy");
23
+ /** Absence selects legacy behavior; malformed retained promises refuse. */
24
+ export function parseShippingTerms(value) {
25
+ return value === undefined ? undefined : shippingTermsSchema.parse(value);
26
+ }
27
+ export function hasShippingTerms(quote) {
28
+ return quote.context?.shippingTerms !== undefined;
29
+ }
30
+ /** Resolve effective entries on read, without a scheduled write. */
31
+ export function normalizeShippingPolicy(policy, asOf) {
32
+ const parsed = shippingPolicySchema.parse(policy);
33
+ const now = new Date(asOf).getTime();
34
+ if (!Number.isFinite(now))
35
+ throw new Error("shipping_policy_invalid_as_of");
36
+ const entries = [parsed.current, parsed.scheduled].filter((entry) => !!entry);
37
+ const effective = entries.filter((entry) => Date.parse(entry.effectiveFrom) <= now);
38
+ const future = entries.filter((entry) => Date.parse(entry.effectiveFrom) > now);
39
+ if (future.length > 1)
40
+ throw new Error("shipping_policy_multiple_scheduled_changes");
41
+ return { version: parsed.version, current: effective[effective.length - 1] ?? null, scheduled: future[0] ?? null };
42
+ }
43
+ export function resolveShippingPolicy(settings, asOf, currency) {
44
+ if (settings.policy === null)
45
+ return { configured: false, flatMinor: minorSchema.parse(settings.legacyFlatMinor) };
46
+ const { current } = normalizeShippingPolicy(settings.policy, asOf);
47
+ if (!current?.enabled)
48
+ return { configured: true, flatMinor: 0 };
49
+ const terms = shippingTermsSchema.parse({
50
+ version: "shipping-threshold.v1", currency,
51
+ flatMinor: current.flatMinor, freeThresholdMinor: current.freeThresholdMinor,
52
+ });
53
+ return { configured: true, flatMinor: terms.flatMinor, terms };
54
+ }
55
+ /** Only product payable after discounts counts; equality receives free delivery. */
56
+ export function resolveThresholdShipping(terms, productPayableMinor) {
57
+ const payable = minorSchema.parse(productPayableMinor);
58
+ const remainingMinor = Math.max(0, terms.freeThresholdMinor - payable);
59
+ return { grossMinor: remainingMinor > 0 ? terms.flatMinor : 0, remainingMinor };
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlup/platform",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "private": false,
5
5
  "description": "Runnable server substrate: Node HTTP runtime, BFF dispatch, scheduler, storage, data gateways and bundle composition; development preview.",
6
6
  "license": "Apache-2.0",
@@ -31,7 +31,7 @@
31
31
  "api:update": "npm run build && node --experimental-strip-types ./scripts/api-contract.ts --update"
32
32
  },
33
33
  "peerDependencies": {
34
- "@openlup/core": "0.18.0",
34
+ "@openlup/core": "0.19.0",
35
35
  "pg": "^8.22.0",
36
36
  "@aws-sdk/client-s3": "^3.1144.0",
37
37
  "@types/pg": "^8.20.0"