@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.
- package/CHANGELOG.md +47 -0
- package/README.md +24 -9
- 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 +57 -0
- package/dist/shipping/bindings.js +46 -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 +3 -0
- package/dist/shipping/index.js +2 -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/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 +74 -0
- package/src/shipping/contracts/index.ts +1 -0
- package/src/shipping/contracts/threshold.ts +22 -0
- package/src/shipping/index.ts +3 -0
- 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 {
|
|
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,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 {};
|
package/dist/shipping/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/shipping/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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"
|