@openlup/platform 0.18.0 → 0.20.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.
- package/CHANGELOG.md +146 -0
- package/README.md +34 -11
- package/dist/catalog/adminBundleReadHandler.d.ts +2 -1
- package/dist/catalog/adminBundleReadHandler.js +4 -4
- package/dist/catalog/bundleCatalogReadPort.d.ts +8 -1
- package/dist/catalog/bundleCatalogStore.d.ts +3 -1
- package/dist/catalog/bundleCatalogStore.js +5 -1
- package/dist/catalog/bundlePriceSelection.d.ts +13 -0
- package/dist/catalog/bundlePriceSelection.js +26 -0
- package/dist/catalog/contracts/bundleRead.d.ts +18 -0
- package/dist/catalog/contracts/draft.js +1 -1
- package/dist/catalog/effectiveBundleCatalogQueries.d.ts +8 -0
- package/dist/catalog/effectiveBundleCatalogQueries.js +79 -0
- package/dist/catalog/effectiveBundleCatalogStore.d.ts +4 -0
- package/dist/catalog/effectiveBundleCatalogStore.js +156 -0
- package/dist/catalog/index.d.ts +1 -0
- package/dist/catalog/index.js +1 -0
- package/dist/composition/index.d.ts +2 -0
- package/dist/composition/index.js +2 -0
- package/dist/composition/inventoryContribution.d.ts +16 -0
- package/dist/composition/inventoryContribution.js +17 -0
- package/dist/composition/shippingContribution.d.ts +16 -0
- package/dist/composition/shippingContribution.js +17 -0
- package/dist/inventory/bindings.d.ts +35 -0
- package/dist/inventory/bindings.js +45 -0
- package/dist/inventory/index.d.ts +1 -0
- package/dist/inventory/index.js +1 -0
- package/dist/pricing/commercePriceAuthority.d.ts +2 -0
- package/dist/pricing/commercePriceAuthority.js +61 -0
- package/dist/pricing/commerceQuoteHandler.d.ts +7 -3
- package/dist/pricing/commerceQuoteHandler.js +16 -4
- package/dist/pricing/dbBackedCommerceQuotePort.d.ts +8 -0
- package/dist/pricing/dbBackedCommerceQuotePort.js +46 -29
- package/dist/pricing/index.d.ts +2 -1
- package/dist/pricing/index.js +1 -1
- package/dist/pricing/offerPolicySchemas.d.ts +2 -2
- package/dist/pricing/offerPricingSchemas.d.ts +2 -2
- package/dist/pricing/pricingPolicyToken.d.ts +2 -2
- package/dist/pricing/quoteSchemas.d.ts +18 -18
- package/dist/pricing/quoteShippingPromotions.d.ts +20 -0
- package/dist/pricing/quoteShippingPromotions.js +1 -0
- package/dist/promotions/adminPromotionCodesContracts.d.ts +7 -7
- package/dist/promotions/commerceQuoteReadPreparation.d.ts +3 -0
- package/dist/promotions/commerceQuoteReadPreparation.js +27 -0
- package/dist/promotions/index.d.ts +3 -0
- package/dist/promotions/index.js +1 -0
- package/dist/promotions/promotionQuoteAcceptance.d.ts +5 -0
- package/dist/promotions/promotionQuoteAcceptance.js +12 -12
- package/dist/promotions/promotionQuoteBinding.d.ts +4 -2
- package/dist/promotions/promotionQuoteBinding.js +5 -3
- package/dist/promotions/quoteAcquisitionBinding.d.ts +6 -0
- package/dist/promotions/quoteAcquisitionBinding.js +1 -0
- package/dist/promotions/quoteAutomaticPromotions.d.ts +3 -1
- package/dist/promotions/quoteAutomaticPromotions.js +28 -4
- package/dist/promotions/quotePromoDiscounts.d.ts +5 -1
- package/dist/promotions/quotePromoDiscounts.js +8 -2
- package/dist/promotions/quotePromotionV2.d.ts +7 -6
- package/dist/promotions/quotePromotionV2.js +15 -3
- package/dist/promotions/quoteShippingPolicy.d.ts +23 -0
- package/dist/promotions/quoteShippingPolicy.js +47 -0
- package/dist/shipping/bindings.d.ts +62 -0
- package/dist/shipping/bindings.js +56 -0
- package/dist/shipping/contracts/index.d.ts +1 -0
- package/dist/shipping/contracts/threshold.d.ts +22 -0
- package/dist/shipping/contracts/threshold.js +1 -0
- package/dist/shipping/index.d.ts +17 -0
- package/dist/shipping/index.js +7 -0
- package/dist/shipping/thresholdPolicy.d.ts +42 -0
- package/dist/shipping/thresholdPolicy.js +60 -0
- package/package.json +2 -2
- package/release-gates.json +136 -134
- package/sql/20261021200000_shipping_flat_rate_seed.sql +1 -0
- package/src/catalog/adminBundleReadHandler.ts +6 -4
- package/src/catalog/bundleCatalogReadPort.ts +6 -2
- package/src/catalog/bundleCatalogStore.ts +5 -0
- package/src/catalog/bundlePriceSelection.ts +25 -0
- package/src/catalog/contracts/bundleRead.ts +2 -2
- package/src/catalog/contracts/draft.ts +1 -1
- package/src/catalog/effectiveBundleCatalogQueries.ts +80 -0
- package/src/catalog/effectiveBundleCatalogStore.ts +208 -0
- package/src/catalog/index.ts +1 -0
- package/src/composition/index.ts +2 -0
- package/src/composition/inventoryContribution.ts +24 -0
- package/src/composition/shippingContribution.ts +24 -0
- package/src/inventory/bindings.ts +61 -0
- package/src/inventory/index.ts +1 -0
- package/src/pricing/commercePriceAuthority.ts +65 -0
- package/src/pricing/commerceQuoteHandler.ts +20 -5
- package/src/pricing/dbBackedCommerceQuotePort.ts +50 -30
- package/src/pricing/index.ts +2 -1
- package/src/pricing/quoteShippingPromotions.ts +21 -0
- package/src/promotions/commerceQuoteReadPreparation.ts +29 -0
- package/src/promotions/index.ts +3 -0
- package/src/promotions/promotionQuoteAcceptance.ts +17 -10
- package/src/promotions/promotionQuoteBinding.ts +7 -2
- package/src/promotions/quoteAcquisitionBinding.ts +7 -0
- package/src/promotions/quoteAutomaticPromotions.ts +30 -4
- package/src/promotions/quotePromoDiscounts.ts +11 -2
- package/src/promotions/quotePromotionV2.ts +20 -4
- package/src/promotions/quoteShippingPolicy.ts +61 -0
- package/src/shipping/bindings.ts +90 -0
- package/src/shipping/contracts/index.ts +1 -0
- package/src/shipping/contracts/threshold.ts +22 -0
- package/src/shipping/index.ts +9 -0
- package/src/shipping/thresholdPolicy.ts +65 -0
|
@@ -1,12 +1,30 @@
|
|
|
1
|
+
import { selectAcquisitionTerms, assertAcquisitionTermsCapability, ACQUISITION_TERMS_CAPABILITY } from "../pricing/index.js";
|
|
1
2
|
import { PROMOTION_ENGINE_V2 } from "../pricing/index.js";
|
|
2
|
-
export function createQuoteAutomaticPromotions({ resolveEmailEligibility, evaluateOrderTotalDiscounts, evaluateAutomaticPromotionV2, legacyPromotionDataPort }) {
|
|
3
|
+
export function createQuoteAutomaticPromotions({ resolveEmailEligibility, evaluateOrderTotalDiscounts, evaluateAutomaticPromotionV2, legacyPromotionDataPort, acquisition }) {
|
|
3
4
|
async function resolveAutomaticQuotePromotions(input) {
|
|
4
5
|
if (!input.promoDataPort)
|
|
5
6
|
return { discounts: [], codeRejections: [] };
|
|
7
|
+
let promoDataPort = input.promoDataPort;
|
|
8
|
+
let selection = null;
|
|
9
|
+
let acquisitionCapability;
|
|
10
|
+
if (acquisition) {
|
|
11
|
+
const clientId = input.options?.clientId ?? null;
|
|
12
|
+
const [paid, allRows] = await Promise.all([
|
|
13
|
+
clientId ? promoDataPort.countPaidOrdersByMode(clientId) : Promise.resolve({ oneTime: 0, subscription: 0 }),
|
|
14
|
+
promoDataPort.listActivePromotions(),
|
|
15
|
+
]);
|
|
16
|
+
const existingSubscriber = input.request.mode === "subscription" && paid.subscription > 0;
|
|
17
|
+
const rows = existingSubscriber ? allRows.filter((row) => !acquisition.financialPolicy.facts(row).metadataPresent && row.eligibility.first_subscription_purchase !== true) : allRows;
|
|
18
|
+
promoDataPort = { ...promoDataPort, listActivePromotions: async () => rows, countPaidOrdersByMode: async () => paid };
|
|
19
|
+
selection = input.request.mode === "subscription" && !existingSubscriber
|
|
20
|
+
? selectAcquisitionTerms(rows, input.atTime, input.regionCode, acquisition.financialPolicy) : null;
|
|
21
|
+
acquisitionCapability = acquisition.capability(input.request);
|
|
22
|
+
assertAcquisitionTermsCapability(selection, acquisitionCapability, input.options?.pricingPolicy?.promotionEngineVersion);
|
|
23
|
+
}
|
|
6
24
|
const emailEligibilityConfirmed = resolveEmailEligibility(input.request);
|
|
7
25
|
if (input.options?.pricingPolicy?.promotionEngineVersion === PROMOTION_ENGINE_V2) {
|
|
8
26
|
return evaluateAutomaticPromotionV2({
|
|
9
|
-
promoDataPort
|
|
27
|
+
promoDataPort,
|
|
10
28
|
clientId: input.options.clientId ?? null,
|
|
11
29
|
visitorId: input.request.visitorId ?? null,
|
|
12
30
|
mode: input.request.mode,
|
|
@@ -17,10 +35,11 @@ export function createQuoteAutomaticPromotions({ resolveEmailEligibility, evalua
|
|
|
17
35
|
shippingMinor: input.shippingGrossMinor,
|
|
18
36
|
atTime: input.atTime,
|
|
19
37
|
emailEligibilityConfirmed,
|
|
38
|
+
...(acquisition ? { acquisitionCapability } : {}),
|
|
20
39
|
});
|
|
21
40
|
}
|
|
22
|
-
|
|
23
|
-
promoDataPort: legacyPromotionDataPort(
|
|
41
|
+
const pending = evaluateOrderTotalDiscounts({
|
|
42
|
+
promoDataPort: legacyPromotionDataPort(promoDataPort),
|
|
24
43
|
clientId: input.options?.clientId ?? null,
|
|
25
44
|
visitorId: input.request.visitorId ?? null,
|
|
26
45
|
mode: input.request.mode,
|
|
@@ -31,6 +50,11 @@ export function createQuoteAutomaticPromotions({ resolveEmailEligibility, evalua
|
|
|
31
50
|
atTime: input.atTime,
|
|
32
51
|
emailEligibilityConfirmed,
|
|
33
52
|
});
|
|
53
|
+
if (!acquisition)
|
|
54
|
+
return pending;
|
|
55
|
+
const result = await pending;
|
|
56
|
+
return selection && acquisitionCapability === ACQUISITION_TERMS_CAPABILITY
|
|
57
|
+
? { ...result, acquisitionTerms: selection.terms } : result;
|
|
34
58
|
}
|
|
35
59
|
return { resolveAutomaticQuotePromotions };
|
|
36
60
|
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import type { ResolvedAcquisitionTerms } from "../pricing/index.js";
|
|
2
|
+
import type { QuoteAcquisitionBinding } from "./quoteAcquisitionBinding.js";
|
|
1
3
|
import type { CreateQuoteRequest } from "../pricing/index.js";
|
|
2
4
|
import type { CommerceCodeRejection, CommerceQuoteDiscount } from "../pricing/index.js";
|
|
3
5
|
import type { PromotionRow } from "@openlup/core/promo";
|
|
4
6
|
import type { CommercePromoDataPort } from "./promoDataPort.js";
|
|
5
7
|
export interface QuoteDiscountResult {
|
|
8
|
+
acquisitionTerms?: ResolvedAcquisitionTerms;
|
|
6
9
|
/** Applied discounts: order_total (clamped to subtotal) + at most one shipping discount. */
|
|
7
10
|
discounts: CommerceQuoteDiscount[];
|
|
8
11
|
/** Submitted coupon codes that produced no discount, with the reason. */
|
|
9
12
|
codeRejections: CommerceCodeRejection[];
|
|
10
13
|
}
|
|
11
|
-
export declare function createQuotePromoDiscounts({ promotionCustomerSemantic }: {
|
|
14
|
+
export declare function createQuotePromoDiscounts({ promotionCustomerSemantic, acquisition }: {
|
|
15
|
+
acquisition?: Pick<QuoteAcquisitionBinding, "financialPolicy">;
|
|
12
16
|
promotionCustomerSemantic: (row: PromotionRow) => CommerceQuoteDiscount["customerSemantic"];
|
|
13
17
|
}): {
|
|
14
18
|
evaluateOrderTotalDiscounts: (input: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { filterAcquisitionPromotions } from "../pricing/index.js";
|
|
1
2
|
import { evaluatePromos, promoEligibilityFailure } from "@openlup/core/promo";
|
|
2
|
-
export function createQuotePromoDiscounts({ promotionCustomerSemantic }) {
|
|
3
|
+
export function createQuotePromoDiscounts({ promotionCustomerSemantic, acquisition }) {
|
|
3
4
|
/**
|
|
4
5
|
* Evaluate active promotions for this cart and map them to quote discounts. Two lanes:
|
|
5
6
|
* `order_total` promos are clamped to the subtotal (so `discountTotal <= subtotal`), and
|
|
@@ -23,7 +24,12 @@ export function createQuotePromoDiscounts({ promotionCustomerSemantic }) {
|
|
|
23
24
|
input.promoDataPort.redemptionCounts(input.clientId),
|
|
24
25
|
input.promoDataPort.deviceFirstOrderRedeemed(input.visitorId ?? null),
|
|
25
26
|
]);
|
|
26
|
-
const
|
|
27
|
+
const customerCandidates = acquisition && input.mode === "subscription" && byMode.subscription > 0
|
|
28
|
+
? allCandidates.filter((row) => !acquisition.financialPolicy.facts(row).metadataPresent && row.eligibility.first_subscription_purchase !== true)
|
|
29
|
+
: allCandidates;
|
|
30
|
+
const effectiveCandidates = acquisition && input.mode === "subscription"
|
|
31
|
+
? filterAcquisitionPromotions(customerCandidates, input.atTime, input.regionCode, acquisition.financialPolicy) : customerCandidates;
|
|
32
|
+
const candidates = effectiveCandidates.filter((promo) => !capReached(promo, redemptionCounts.get(promo.id), input.clientId));
|
|
27
33
|
const cart = {
|
|
28
34
|
region_code: input.regionCode,
|
|
29
35
|
cart_mode: input.mode === "subscription" ? "subscription" : "one_time",
|
|
@@ -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 {
|
|
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
|
-
|
|
25
|
-
|
|
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 =
|
|
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(
|
|
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,62 @@
|
|
|
1
|
+
import type { ReadinessReport, SchemaObservation } from "@openlup/core/readiness";
|
|
2
|
+
import type { BffResponder } from "../bff/index.js";
|
|
3
|
+
import type { CustomerAddressBookPort, CustomerAddressMutationPort, CustomerAddressesDeps } from "./addressPorts.js";
|
|
4
|
+
import type { CustomerAddressSchemas } from "./addressSchemas.js";
|
|
5
|
+
import type { DeliverySelectionPort } from "./deliverySelectionPort.js";
|
|
6
|
+
import type { DeliverySelectionSchemas } from "./handlerSchemas.js";
|
|
7
|
+
import type { PickupPointSearchPort } from "./pickupPointSearchPort.js";
|
|
8
|
+
import type { AdminShippingRateHandlerDeps, ShippingRateDataPort, ShippingRateHandlerSchemas } from "./shippingRatePorts.js";
|
|
9
|
+
import { createCustomerAddressesHandler } from "./customerAddressesHandler.js";
|
|
10
|
+
import { createAdminShippingRateHandler } from "./adminShippingRateHandler.js";
|
|
11
|
+
import { createDeliveryOptionsHandler, createPickupPointSearchHandler, createPickupPointValidationHandler } from "./deliverySelectionHandler.js";
|
|
12
|
+
type Store = {
|
|
13
|
+
addressBookPort?: CustomerAddressBookPort;
|
|
14
|
+
addressMutationPort?: CustomerAddressMutationPort;
|
|
15
|
+
shippingRateDataPort?: ShippingRateDataPort;
|
|
16
|
+
};
|
|
17
|
+
export interface ShippingBindingDependencies {
|
|
18
|
+
dataKind: string;
|
|
19
|
+
createManagedStore?: () => Store | null;
|
|
20
|
+
/** Core report for the selected shipping contribution, observed by the host. */
|
|
21
|
+
nativeRateReadiness?: ReadinessReport;
|
|
22
|
+
/** Same-database Core observation of the applied shipping seed and required objects. */
|
|
23
|
+
nativeRateSchema?: SchemaObservation;
|
|
24
|
+
nativeStore?: Omit<Store, "addressMutationPort"> | null;
|
|
25
|
+
responder: BffResponder;
|
|
26
|
+
selection?: {
|
|
27
|
+
port: DeliverySelectionPort;
|
|
28
|
+
searchPort: PickupPointSearchPort;
|
|
29
|
+
schemas: DeliverySelectionSchemas;
|
|
30
|
+
};
|
|
31
|
+
addresses?: {
|
|
32
|
+
schemas: CustomerAddressSchemas;
|
|
33
|
+
authenticateUser: CustomerAddressesDeps["authenticateUser"];
|
|
34
|
+
requireWrites: boolean;
|
|
35
|
+
};
|
|
36
|
+
rate?: {
|
|
37
|
+
schemas: ShippingRateHandlerSchemas;
|
|
38
|
+
authorizeAdmin: AdminShippingRateHandlerDeps["authorizeAdmin"];
|
|
39
|
+
resolveAdmin: AdminShippingRateHandlerDeps["resolveAdmin"];
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface ShippingBindings {
|
|
43
|
+
store: Store;
|
|
44
|
+
handlers: {
|
|
45
|
+
options: ReturnType<typeof createDeliveryOptionsHandler> | null;
|
|
46
|
+
search: ReturnType<typeof createPickupPointSearchHandler> | null;
|
|
47
|
+
validate: ReturnType<typeof createPickupPointValidationHandler> | null;
|
|
48
|
+
addresses: ReturnType<typeof createCustomerAddressesHandler> | null;
|
|
49
|
+
rate: ReturnType<typeof createAdminShippingRateHandler> | null;
|
|
50
|
+
};
|
|
51
|
+
readiness: {
|
|
52
|
+
ready: boolean;
|
|
53
|
+
reason: string | null;
|
|
54
|
+
};
|
|
55
|
+
unsupported: {
|
|
56
|
+
addressWrites: string | null;
|
|
57
|
+
flatRateSave: string | null;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Explicit selected stores and host validation; importing this module activates nothing. */
|
|
61
|
+
export declare function createShippingBindings(deps: ShippingBindingDependencies): ShippingBindings;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
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 nativeRateReady = typeof deps.nativeStore?.shippingRateDataPort?.getShippingFlatMinor === "function"
|
|
9
|
+
&& typeof deps.nativeStore?.shippingRateDataPort?.setShippingFlatMinor === "function"
|
|
10
|
+
&& typeof deps.nativeStore?.shippingRateDataPort?.recordPromotionAudit === "function"
|
|
11
|
+
&& deps.nativeRateReadiness?.state === "ready"
|
|
12
|
+
&& typeof deps.nativeRateReadiness.candidate?.database === "string"
|
|
13
|
+
&& deps.nativeRateReadiness.candidate.database.length > 0
|
|
14
|
+
&& deps.nativeRateSchema?.database === deps.nativeRateReadiness.candidate.database
|
|
15
|
+
&& deps.nativeRateSchema.version === "20261021200000_shipping_flat_rate_seed"
|
|
16
|
+
&& ["public.commerce_settings", "public.commerce_settings.value_minor", "public.commerce_settings.updated_at"].every(name => deps.nativeRateSchema?.objects?.some(entry => entry.present === true && entry.object?.name === name
|
|
17
|
+
&& entry.object.kind === (name === "public.commerce_settings" ? "table" : "column")));
|
|
18
|
+
const flatRateSave = native && !nativeRateReady ? "shipping_native_flat_rate_save_unsupported" : null;
|
|
19
|
+
const refused = (reason) => ({
|
|
20
|
+
store: {}, handlers: { options: null, search: null, validate: null, addresses: null, rate: null },
|
|
21
|
+
readiness: { ready: false, reason }, unsupported: { addressWrites, flatRateSave },
|
|
22
|
+
});
|
|
23
|
+
if (!native && deps.dataKind !== "supabase")
|
|
24
|
+
return refused("shipping_store_incompatible");
|
|
25
|
+
if (native && deps.addresses?.requireWrites)
|
|
26
|
+
return refused(addressWrites);
|
|
27
|
+
if (native && deps.rate && flatRateSave)
|
|
28
|
+
return refused(flatRateSave);
|
|
29
|
+
const selected = native ? deps.nativeStore : deps.createManagedStore?.();
|
|
30
|
+
if ((deps.addresses || deps.rate) && !selected)
|
|
31
|
+
return refused("shipping_store_required");
|
|
32
|
+
// Native address mutations remain unavailable even if an untyped host plants a writer.
|
|
33
|
+
const store = { addressBookPort: selected?.addressBookPort, shippingRateDataPort: selected?.shippingRateDataPort,
|
|
34
|
+
addressMutationPort: native ? undefined : selected?.addressMutationPort };
|
|
35
|
+
if (deps.addresses && (typeof store.addressBookPort?.getAddressBook !== "function"
|
|
36
|
+
|| typeof deps.addresses.authenticateUser !== "function"
|
|
37
|
+
|| (deps.addresses.requireWrites && ![store.addressMutationPort?.upsertAddress, store.addressMutationPort?.deleteAddress].every(fn => typeof fn === "function"))))
|
|
38
|
+
return refused("shipping_address_store_incompatible");
|
|
39
|
+
if (deps.rate && (![store.shippingRateDataPort?.getShippingFlatMinor, store.shippingRateDataPort?.setShippingFlatMinor,
|
|
40
|
+
store.shippingRateDataPort?.recordPromotionAudit, deps.rate.authorizeAdmin, deps.rate.resolveAdmin].every(fn => typeof fn === "function")))
|
|
41
|
+
return refused("shipping_rate_store_incompatible");
|
|
42
|
+
if (deps.selection && ![deps.selection.port?.listOptions, deps.selection.port?.validatePickupPoint, deps.selection.searchPort?.searchPickupPoints].every(fn => typeof fn === "function"))
|
|
43
|
+
return refused("shipping_selection_incompatible");
|
|
44
|
+
return {
|
|
45
|
+
store,
|
|
46
|
+
handlers: {
|
|
47
|
+
options: deps.selection ? createDeliveryOptionsHandler(deps.selection.port, deps.selection.schemas, deps.responder) : null,
|
|
48
|
+
search: deps.selection ? createPickupPointSearchHandler(deps.selection.searchPort, deps.selection.schemas, deps.responder) : null,
|
|
49
|
+
validate: deps.selection ? createPickupPointValidationHandler(deps.selection.port, deps.selection.schemas, deps.responder) : null,
|
|
50
|
+
addresses: deps.addresses ? createCustomerAddressesHandler({ addressBookPort: store.addressBookPort, addressMutationPort: store.addressMutationPort,
|
|
51
|
+
authenticateUser: deps.addresses.authenticateUser }, deps.addresses.schemas, deps.responder) : null,
|
|
52
|
+
rate: deps.rate ? createAdminShippingRateHandler({ dataPort: store.shippingRateDataPort, authorizeAdmin: deps.rate.authorizeAdmin,
|
|
53
|
+
resolveAdmin: deps.rate.resolveAdmin }, deps.rate.schemas, deps.responder) : null,
|
|
54
|
+
}, readiness: { ready: true, reason: null }, unsupported: { addressWrites, flatRateSave },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -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 {};
|
package/dist/shipping/index.d.ts
CHANGED
|
@@ -15,3 +15,20 @@ 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";
|
|
21
|
+
/** Sparse canonical projections, never an install chain. */
|
|
22
|
+
export declare const SHIPPING_MIGRATIONS: readonly [{
|
|
23
|
+
readonly id: "20260807140000_read_parity_catalog";
|
|
24
|
+
readonly catalogPath: "db/platform/migrations/20260807140000_read_parity_catalog.sql";
|
|
25
|
+
readonly position: 1;
|
|
26
|
+
readonly sha256: "fcf3827028c1651ae42a7c7d7472153825a3121c936d58a3d105befc7dece9ea";
|
|
27
|
+
readonly url: URL;
|
|
28
|
+
}, {
|
|
29
|
+
readonly id: "20261021200000_shipping_flat_rate_seed";
|
|
30
|
+
readonly catalogPath: "db/platform/migrations/20261021200000_shipping_flat_rate_seed.sql";
|
|
31
|
+
readonly position: 72;
|
|
32
|
+
readonly sha256: "34f8696f515a2cc072395db5a3de2d9b09643d13236ee3851d0d78123f907eae";
|
|
33
|
+
readonly url: URL;
|
|
34
|
+
}];
|
package/dist/shipping/index.js
CHANGED
|
@@ -6,3 +6,10 @@ 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";
|
|
11
|
+
/** Sparse canonical projections, never an install chain. */
|
|
12
|
+
export const SHIPPING_MIGRATIONS = [
|
|
13
|
+
{ id: "20260807140000_read_parity_catalog", catalogPath: "db/platform/migrations/20260807140000_read_parity_catalog.sql", position: 1, sha256: "fcf3827028c1651ae42a7c7d7472153825a3121c936d58a3d105befc7dece9ea", url: new URL("../../sql/20260807140000_read_parity_catalog.sql", import.meta.url) },
|
|
14
|
+
{ id: "20261021200000_shipping_flat_rate_seed", catalogPath: "db/platform/migrations/20261021200000_shipping_flat_rate_seed.sql", position: 72, sha256: "34f8696f515a2cc072395db5a3de2d9b09643d13236ee3851d0d78123f907eae", url: new URL("../../sql/20261021200000_shipping_flat_rate_seed.sql", import.meta.url) },
|
|
15
|
+
];
|
|
@@ -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.
|
|
3
|
+
"version": "0.20.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.
|
|
34
|
+
"@openlup/core": "0.20.0",
|
|
35
35
|
"pg": "^8.22.0",
|
|
36
36
|
"@aws-sdk/client-s3": "^3.1144.0",
|
|
37
37
|
"@types/pg": "^8.20.0"
|