@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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Structural selected callables keep inventory and catalog declarations independent. */
|
|
2
|
+
export function createInventoryContribution(input) {
|
|
3
|
+
const contribution = {
|
|
4
|
+
manifest: {
|
|
5
|
+
name: "@openlup/platform", version: input.version, kind: "rail",
|
|
6
|
+
module: { id: "inventory", kind: "capability", provides: ["./inventory"], dependsOn: [] },
|
|
7
|
+
emits: [], handles: [], schedules: [], env: [], requiredPorts: Object.keys(input.inventory),
|
|
8
|
+
routes: input.routes.map(route => route.id), requiredSchema: input.requiredSchema,
|
|
9
|
+
},
|
|
10
|
+
handlers: [], schedules: [], routes: input.routes,
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
contributions: [contribution],
|
|
14
|
+
ports: Object.fromEntries(Object.entries(input.inventory).filter(([, fn]) => typeof fn === "function").map(([name, fn]) => [`@openlup/platform/inventory:${name}`, fn])),
|
|
15
|
+
routes: Object.fromEntries(input.routes.map(route => [`@openlup/platform/inventory:${route.id}`, route.handle])),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Contribution, RequiredSchema } from "@openlup/core/readiness";
|
|
2
|
+
/** Structural selected callables keep shipping declarations independent. */
|
|
3
|
+
export declare function createShippingContribution(input: {
|
|
4
|
+
version: string;
|
|
5
|
+
requiredSchema: RequiredSchema;
|
|
6
|
+
shipping: Readonly<Record<string, ((...args: never[]) => unknown) | null>>;
|
|
7
|
+
routes: Contribution["routes"];
|
|
8
|
+
}): {
|
|
9
|
+
contributions: Contribution[];
|
|
10
|
+
ports: {
|
|
11
|
+
[k: string]: ((...args: never[]) => unknown) | null;
|
|
12
|
+
};
|
|
13
|
+
routes: {
|
|
14
|
+
[k: string]: (...args: never[]) => unknown;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Structural selected callables keep shipping declarations independent. */
|
|
2
|
+
export function createShippingContribution(input) {
|
|
3
|
+
const contribution = {
|
|
4
|
+
manifest: {
|
|
5
|
+
name: "@openlup/platform", version: input.version, kind: "rail",
|
|
6
|
+
module: { id: "shipping", kind: "capability", provides: ["./shipping"], dependsOn: [] },
|
|
7
|
+
emits: [], handles: [], schedules: [], env: [], requiredPorts: Object.keys(input.shipping),
|
|
8
|
+
routes: input.routes.map(route => route.id), requiredSchema: input.requiredSchema,
|
|
9
|
+
},
|
|
10
|
+
handlers: [], schedules: [], routes: input.routes,
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
contributions: [contribution],
|
|
14
|
+
ports: Object.fromEntries(Object.entries(input.shipping).filter(([, fn]) => typeof fn === "function").map(([name, fn]) => [`@openlup/platform/shipping:${name}`, fn])),
|
|
15
|
+
routes: Object.fromEntries(input.routes.map(route => [`@openlup/platform/shipping:${route.id}`, route.handle])),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { InventoryMutationPort, InventoryReadPort } from "./ports.js";
|
|
3
|
+
import type { NewPurchaseStockReadPort } from "./newPurchaseStockAvailability.js";
|
|
4
|
+
export interface InventoryBindingDependencies {
|
|
5
|
+
dataKind: string;
|
|
6
|
+
provider: {
|
|
7
|
+
stockMasterProvider: string;
|
|
8
|
+
providerSchema: z.ZodType<string>;
|
|
9
|
+
};
|
|
10
|
+
executor?: {
|
|
11
|
+
query(text: string, values?: unknown[]): Promise<{
|
|
12
|
+
rows: Record<string, unknown>[];
|
|
13
|
+
}>;
|
|
14
|
+
} | null;
|
|
15
|
+
createManagedStore?: (provider: InventoryBindingDependencies["provider"]) => {
|
|
16
|
+
readPort: InventoryReadPort;
|
|
17
|
+
mutationPort: InventoryMutationPort;
|
|
18
|
+
} | null;
|
|
19
|
+
newPurchaseStockReadPort?: NewPurchaseStockReadPort | null;
|
|
20
|
+
now?: () => string;
|
|
21
|
+
}
|
|
22
|
+
export interface InventoryBindings {
|
|
23
|
+
readPort: InventoryReadPort | null;
|
|
24
|
+
mutationPort: InventoryMutationPort | null;
|
|
25
|
+
newPurchaseStockReadPort: NewPurchaseStockReadPort | null;
|
|
26
|
+
readiness: {
|
|
27
|
+
ready: boolean;
|
|
28
|
+
reason: string | null;
|
|
29
|
+
};
|
|
30
|
+
unsupported: {
|
|
31
|
+
newPurchaseStockRead: string | null;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Select one store only. Factories declare bindings, never discover or activate a host. */
|
|
35
|
+
export declare function createInventoryBindings(deps: InventoryBindingDependencies): InventoryBindings;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createPostgresAdminInventoryPort } from "./adminInventory.js";
|
|
2
|
+
import { createInventorySubscriptionForecastResponseSchema } from "./schemas.js";
|
|
3
|
+
/** Select one store only. Factories declare bindings, never discover or activate a host. */
|
|
4
|
+
export function createInventoryBindings(deps) {
|
|
5
|
+
const refused = (reason) => ({
|
|
6
|
+
readPort: null, mutationPort: null, newPurchaseStockReadPort: null,
|
|
7
|
+
readiness: { ready: false, reason }, unsupported: { newPurchaseStockRead: reason },
|
|
8
|
+
});
|
|
9
|
+
if (!deps.provider?.providerSchema?.safeParse(deps.provider.stockMasterProvider).success)
|
|
10
|
+
return refused("inventory_provider_mismatch");
|
|
11
|
+
let readPort, mutationPort;
|
|
12
|
+
if (deps.dataKind === "postgres") {
|
|
13
|
+
if (!deps.executor || typeof deps.executor.query !== "function")
|
|
14
|
+
return refused("inventory_executor_required");
|
|
15
|
+
readPort = mutationPort = createPostgresAdminInventoryPort(deps.executor, deps.provider, deps.now);
|
|
16
|
+
}
|
|
17
|
+
else if (deps.dataKind === "supabase") {
|
|
18
|
+
const store = deps.createManagedStore?.(deps.provider);
|
|
19
|
+
if (!store)
|
|
20
|
+
return refused("inventory_managed_store_required");
|
|
21
|
+
({ readPort, mutationPort } = store);
|
|
22
|
+
}
|
|
23
|
+
else
|
|
24
|
+
return refused("inventory_store_incompatible");
|
|
25
|
+
if (![readPort?.listStock, readPort?.listReservations, readPort?.checkAtp, readPort?.forecastSubscriptions, mutationPort?.adjustStock].every(fn => typeof fn === "function"))
|
|
26
|
+
return refused("inventory_store_incompatible");
|
|
27
|
+
const responseSchema = createInventorySubscriptionForecastResponseSchema(deps.provider.providerSchema);
|
|
28
|
+
const native = deps.dataKind === "postgres";
|
|
29
|
+
const stockRead = native ? null : deps.newPurchaseStockReadPort;
|
|
30
|
+
const newPurchaseStockReadPort = typeof stockRead?.read === "function" ? stockRead : null;
|
|
31
|
+
return {
|
|
32
|
+
readPort: {
|
|
33
|
+
listStock: request => readPort.listStock(request),
|
|
34
|
+
listReservations: request => readPort.listReservations(request),
|
|
35
|
+
checkAtp: request => readPort.checkAtp(request),
|
|
36
|
+
forecastSubscriptions: async (request) => responseSchema.parse(await readPort.forecastSubscriptions(request)),
|
|
37
|
+
},
|
|
38
|
+
mutationPort, newPurchaseStockReadPort,
|
|
39
|
+
readiness: { ready: true, reason: null },
|
|
40
|
+
unsupported: {
|
|
41
|
+
newPurchaseStockRead: native ? "inventory_native_new_purchase_stock_read_unsupported"
|
|
42
|
+
: newPurchaseStockReadPort ? null : "inventory_new_purchase_stock_reader_required",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -11,3 +11,4 @@ export { createPostgresRenewalReservationPort, type AutomaticRenewalInventoryPor
|
|
|
11
11
|
export { forecastSubscriptionInventoryShortages, buildSubscriptionInventoryForecast } from "./forecast.js";
|
|
12
12
|
export { calculateSubscriptionForecastSkus, calculateSubscriptionForecastTotals } from "./subscriptionForecastCalculation.js";
|
|
13
13
|
export { INVENTORY_MIGRATIONS } from "./sql.js";
|
|
14
|
+
export { createInventoryBindings, type InventoryBindingDependencies, type InventoryBindings } from "./bindings.js";
|
package/dist/inventory/index.js
CHANGED
|
@@ -8,3 +8,4 @@ export { createPostgresRenewalReservationPort } from "./renewalReservation.js";
|
|
|
8
8
|
export { forecastSubscriptionInventoryShortages, buildSubscriptionInventoryForecast } from "./forecast.js";
|
|
9
9
|
export { calculateSubscriptionForecastSkus, calculateSubscriptionForecastTotals } from "./subscriptionForecastCalculation.js";
|
|
10
10
|
export { INVENTORY_MIGRATIONS } from "./sql.js";
|
|
11
|
+
export { createInventoryBindings } from "./bindings.js";
|
|
@@ -22,5 +22,7 @@ export interface CommercePriceAuthorityReadPort {
|
|
|
22
22
|
}
|
|
23
23
|
/** Provider-neutral strict money algorithm; adapters own only persistence reads and row mapping. */
|
|
24
24
|
export declare function createCommercePriceAuthorityPort(reads: CommercePriceAuthorityReadPort): CommercePriceAuthorityPort;
|
|
25
|
+
/** Opt-in scheduling using the selected list for concurrent history reads. */
|
|
26
|
+
export declare function createConcurrentCommercePriceAuthorityPort(reads: CommercePriceAuthorityReadPort): CommercePriceAuthorityPort;
|
|
25
27
|
/** The current money context requires one list; creation order cannot resolve overlap. */
|
|
26
28
|
export declare function selectUniqueEffectivePriceList(priceLists: readonly TimedAuthorityValue<string>[], atTime: string): string;
|
|
@@ -60,6 +60,67 @@ export function createCommercePriceAuthorityPort(reads) {
|
|
|
60
60
|
},
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
/** Opt-in scheduling using the selected list for concurrent history reads. */
|
|
64
|
+
export function createConcurrentCommercePriceAuthorityPort(reads) {
|
|
65
|
+
return {
|
|
66
|
+
async resolvePrice(query) {
|
|
67
|
+
const atTime = query.atTime ?? new Date().toISOString();
|
|
68
|
+
const priceListId = selectUniqueEffectivePriceList(await reads.listPriceLists(query), atTime);
|
|
69
|
+
// Schedule history alongside base reads, retaining base-refusal precedence.
|
|
70
|
+
const policyHistoryRead = query.mode === "subscription" ? Promise.resolve().then(() => reads.listSubscriptionPolicyHistory({ priceListId, regionCode: query.regionCode,
|
|
71
|
+
currency: query.currency, channel: query.channel })).then((value) => ({ value }), (error) => ({ error })) : null;
|
|
72
|
+
const baseCandidates = effective(await reads.listExactBaseEntries({
|
|
73
|
+
variantId: query.variantId,
|
|
74
|
+
priceListId,
|
|
75
|
+
}), atTime);
|
|
76
|
+
if (baseCandidates.length === 0)
|
|
77
|
+
throw new CommercePriceAuthorityError("base_price_not_found");
|
|
78
|
+
if (baseCandidates.length !== 1)
|
|
79
|
+
throw new CommercePriceAuthorityError("base_price_ambiguous");
|
|
80
|
+
if (baseCandidates[0].value.amountKind !== "gross") {
|
|
81
|
+
throw new CommercePriceAuthorityError("base_price_amount_kind_invalid");
|
|
82
|
+
}
|
|
83
|
+
const base = { ...baseCandidates[0].value, resolvedAt: atTime };
|
|
84
|
+
if (query.mode === "one_time")
|
|
85
|
+
return { kind: "one_time", base, unitPriceMinor: base.unitPriceMinor };
|
|
86
|
+
const policyRead = await policyHistoryRead;
|
|
87
|
+
if ("error" in policyRead)
|
|
88
|
+
throw policyRead.error;
|
|
89
|
+
const policyHistory = policyRead.value;
|
|
90
|
+
if (policyHistory.length === 0) {
|
|
91
|
+
const legacyCandidates = effective(await reads.listExactLegacySubscriptionEntries({
|
|
92
|
+
variantId: query.variantId,
|
|
93
|
+
priceListId: base.priceListId,
|
|
94
|
+
}), atTime);
|
|
95
|
+
if (legacyCandidates.length === 0)
|
|
96
|
+
throw new CommercePriceAuthorityError("legacy_subscription_price_not_found");
|
|
97
|
+
if (legacyCandidates.length !== 1)
|
|
98
|
+
throw new CommercePriceAuthorityError("legacy_subscription_price_ambiguous");
|
|
99
|
+
const legacyResolved = { ...legacyCandidates[0].value, resolvedAt: atTime };
|
|
100
|
+
return { kind: "subscription_legacy", base, unitPriceMinor: legacyResolved.unitPriceMinor, legacyResolved };
|
|
101
|
+
}
|
|
102
|
+
const effectivePolicies = effective(policyHistory, atTime);
|
|
103
|
+
if (effectivePolicies.length === 0)
|
|
104
|
+
throw new CommercePriceAuthorityError("subscription_price_policy_not_effective");
|
|
105
|
+
if (effectivePolicies.length !== 1)
|
|
106
|
+
throw new CommercePriceAuthorityError("subscription_price_policy_ambiguous");
|
|
107
|
+
try {
|
|
108
|
+
const policy = await assertSubscriptionPricePolicyDigest(effectivePolicies[0].value);
|
|
109
|
+
return {
|
|
110
|
+
kind: "subscription_policy",
|
|
111
|
+
base,
|
|
112
|
+
unitPriceMinor: deriveSubscriptionUnitPrice(base.unitPriceMinor, policy),
|
|
113
|
+
policy,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
throw new CommercePriceAuthorityError(error instanceof SubscriptionPricePolicyDigestError
|
|
118
|
+
? "subscription_price_policy_digest_mismatch"
|
|
119
|
+
: "subscription_price_policy_invalid");
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
63
124
|
/** The current money context requires one list; creation order cannot resolve overlap. */
|
|
64
125
|
export function selectUniqueEffectivePriceList(priceLists, atTime) {
|
|
65
126
|
const candidates = effective(priceLists, atTime);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ShippingTerms } from "../shipping/index.js";
|
|
1
2
|
import type { ResolvedAcquisitionTerms } from "./contracts/acquisitionTerms.js";
|
|
2
3
|
import type { HttpRequest, HttpResponse } from "../http/index.js";
|
|
3
4
|
import type { BffResponder } from "../bff/index.js";
|
|
@@ -7,6 +8,9 @@ import { type CommerceQuotePort } from "./quotePorts.js";
|
|
|
7
8
|
import type { PricingPolicySnapshot } from "./contracts/offerPolicy.js";
|
|
8
9
|
export interface CommerceQuoteHandlerDeps<Request extends CreateQuoteRequest = CreateQuoteRequest, Response extends CreateQuoteResponse = CreateQuoteResponse> {
|
|
9
10
|
responder: BffResponder;
|
|
11
|
+
shipping?: {
|
|
12
|
+
terms(quote: Response["quote"]): ShippingTerms | undefined;
|
|
13
|
+
};
|
|
10
14
|
acquisition?: {
|
|
11
15
|
terms(quote: Response["quote"]): ResolvedAcquisitionTerms | undefined;
|
|
12
16
|
};
|
|
@@ -25,7 +29,7 @@ export interface CommerceQuoteHandlerDeps<Request extends CreateQuoteRequest = C
|
|
|
25
29
|
source: "anonymous" | "resolved_client";
|
|
26
30
|
}>;
|
|
27
31
|
resolvePricingPolicy?: (request: Request) => PricingPolicySnapshot | undefined | Promise<PricingPolicySnapshot | undefined>;
|
|
28
|
-
createPromotionAcceptance?: (request: Request, quote: Awaited<ReturnType<CommerceQuotePort<Request, Response>["createQuote"]>>["quote"], acquisitionTerms?: ResolvedAcquisitionTerms) => string | null;
|
|
29
|
-
honorPromotionAcceptance?: (token: string, request: Request, quote: Awaited<ReturnType<CommerceQuotePort<Request, Response>["createQuote"]>>["quote"], acquisitionTerms?: ResolvedAcquisitionTerms) => boolean;
|
|
32
|
+
createPromotionAcceptance?: (request: Request, quote: Awaited<ReturnType<CommerceQuotePort<Request, Response>["createQuote"]>>["quote"], acquisitionTerms?: ResolvedAcquisitionTerms, shippingTerms?: ShippingTerms) => string | null;
|
|
33
|
+
honorPromotionAcceptance?: (token: string, request: Request, quote: Awaited<ReturnType<CommerceQuotePort<Request, Response>["createQuote"]>>["quote"], acquisitionTerms?: ResolvedAcquisitionTerms, shippingTerms?: ShippingTerms) => boolean;
|
|
30
34
|
}
|
|
31
|
-
export declare function createCommerceQuoteHandler<Request extends CreateQuoteRequest, Response extends CreateQuoteResponse>({ responder, createQuoteRequestSchema, createQuoteResponseSchema, publicCreateQuoteRequestSchema, publicCreateQuoteResponseSchema, quotePort, acquisition, fallbackQuotePort, resolveCustomerEligibility, resolvePricingPolicy, createPromotionAcceptance, honorPromotionAcceptance, }: CommerceQuoteHandlerDeps<Request, Response>): (req: HttpRequest, res: HttpResponse) => Promise<void>;
|
|
35
|
+
export declare function createCommerceQuoteHandler<Request extends CreateQuoteRequest, Response extends CreateQuoteResponse>({ responder, createQuoteRequestSchema, createQuoteResponseSchema, publicCreateQuoteRequestSchema, publicCreateQuoteResponseSchema, quotePort, acquisition, shipping, fallbackQuotePort, resolveCustomerEligibility, resolvePricingPolicy, createPromotionAcceptance, honorPromotionAcceptance, }: CommerceQuoteHandlerDeps<Request, Response>): (req: HttpRequest, res: HttpResponse) => Promise<void>;
|
|
@@ -6,7 +6,7 @@ import { CommerceQuoteCatalogReadError } from "./commerceQuoteCatalogReadPort.js
|
|
|
6
6
|
function commerceQuoteFailureDetails(stage) {
|
|
7
7
|
return { reason: "quote_dependency_failed", stage };
|
|
8
8
|
}
|
|
9
|
-
export function createCommerceQuoteHandler({ responder, createQuoteRequestSchema, createQuoteResponseSchema, publicCreateQuoteRequestSchema, publicCreateQuoteResponseSchema, quotePort, acquisition, fallbackQuotePort, resolveCustomerEligibility, resolvePricingPolicy, createPromotionAcceptance, honorPromotionAcceptance, }) {
|
|
9
|
+
export function createCommerceQuoteHandler({ responder, createQuoteRequestSchema, createQuoteResponseSchema, publicCreateQuoteRequestSchema, publicCreateQuoteResponseSchema, quotePort, acquisition, shipping, fallbackQuotePort, resolveCustomerEligibility, resolvePricingPolicy, createPromotionAcceptance, honorPromotionAcceptance, }) {
|
|
10
10
|
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = responder;
|
|
11
11
|
return async function handler(req, res) {
|
|
12
12
|
if (req.method !== "POST") {
|
|
@@ -47,9 +47,21 @@ export function createCommerceQuoteHandler({ responder, createQuoteRequestSchema
|
|
|
47
47
|
let quote = canonicalQuote.data;
|
|
48
48
|
stage = "promotion_acceptance";
|
|
49
49
|
const acquisitionTerms = acquisition?.terms(quote.quote);
|
|
50
|
-
const
|
|
51
|
-
const
|
|
50
|
+
const shippingTerms = shipping?.terms(quote.quote);
|
|
51
|
+
const honoredToken = promotionAcceptanceToken && (shipping
|
|
52
|
+
? honorPromotionAcceptance?.(promotionAcceptanceToken, request.data, quote.quote, acquisitionTerms, shippingTerms)
|
|
53
|
+
: honorPromotionAcceptance?.(promotionAcceptanceToken, request.data, quote.quote, acquisitionTerms))
|
|
54
|
+
? promotionAcceptanceToken : undefined;
|
|
55
|
+
const issuedToken = honoredToken ?? (shipping
|
|
56
|
+
? createPromotionAcceptance?.(request.data, quote.quote, acquisitionTerms, shippingTerms)
|
|
57
|
+
: createPromotionAcceptance?.(request.data, quote.quote, acquisitionTerms)) ?? undefined;
|
|
52
58
|
const containsV2Promotion = quote.quote.discounts.some((discount) => discount.promotionEngineVersion === "promotion-engine.v2");
|
|
59
|
+
if (shippingTerms && !issuedToken) {
|
|
60
|
+
sendBffError(res, "CONFLICT", "Shipping quote must be refreshed", {
|
|
61
|
+
details: { feature: "quote", reason: "promotion_acceptance_refresh_required" },
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
53
65
|
if ((containsV2Promotion || acquisitionTerms) && !issuedToken) {
|
|
54
66
|
stage = "promotion_fallback";
|
|
55
67
|
const fallback = fallbackQuotePort
|
|
@@ -58,7 +70,7 @@ export function createCommerceQuoteHandler({ responder, createQuoteRequestSchema
|
|
|
58
70
|
...(pricingPolicy ? { pricingPolicy } : {}),
|
|
59
71
|
}))
|
|
60
72
|
: null;
|
|
61
|
-
if (!fallback?.success || acquisition?.terms(fallback.data.quote) || fallback.data.quote.discounts.some((discount) => discount.promotionEngineVersion === "promotion-engine.v2")) {
|
|
73
|
+
if (!fallback?.success || shipping?.terms(fallback.data.quote) || acquisition?.terms(fallback.data.quote) || fallback.data.quote.discounts.some((discount) => discount.promotionEngineVersion === "promotion-engine.v2")) {
|
|
62
74
|
sendBffError(res, "CONFLICT", "Promotion quote must be refreshed", {
|
|
63
75
|
details: { feature: "quote", reason: "promotion_acceptance_refresh_required" },
|
|
64
76
|
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { ShippingTerms } from "../shipping/index.js";
|
|
2
|
+
import type { ResolvedAcquisitionTerms } from "./contracts/acquisitionTerms.js";
|
|
3
|
+
import type { QuoteShippingPromotionInput, QuoteShippingPromotionResult } from "./quoteShippingPromotions.js";
|
|
1
4
|
import type { PricingResolverPort } from "@openlup/core/pricing";
|
|
2
5
|
import type { CreateQuoteResponse, PricingPolicySnapshot, CreateQuoteRequest, CommerceQuoteContext, CommerceQuoteLine, CommerceTaxProfile, CommerceQuoteDiscount, CommerceCodeRejection, CommerceCodeRejectionDetail, CartLineInput } from "./contracts/index.js";
|
|
3
6
|
import { type CreateQuoteOptions } from "./quotePorts.js";
|
|
@@ -21,6 +24,7 @@ export interface DbBackedCommerceQuotePortDeps<Request extends CreateQuoteReques
|
|
|
21
24
|
currency: string;
|
|
22
25
|
now?: () => string;
|
|
23
26
|
shippingFlatMinor?: () => Promise<number>;
|
|
27
|
+
resolveQuoteShippingPromotions?: (input: QuoteShippingPromotionInput<Request>) => Promise<QuoteShippingPromotionResult>;
|
|
24
28
|
hasPromotionCodePort: boolean;
|
|
25
29
|
lineContext(line: CartLineInput, item: Item): LineContext;
|
|
26
30
|
quoteContext(request: Request, lines: Array<LineContext & {
|
|
@@ -53,6 +57,8 @@ export declare function createDbBackedCommerceQuotePort<Request extends CreateQu
|
|
|
53
57
|
quote: Omit<CreateQuoteResponse["quote"], "context"> & {
|
|
54
58
|
context: Context & {
|
|
55
59
|
pricingPolicy?: PricingPolicySnapshot;
|
|
60
|
+
shippingTerms?: ShippingTerms;
|
|
61
|
+
acquisitionTerms?: ResolvedAcquisitionTerms;
|
|
56
62
|
};
|
|
57
63
|
};
|
|
58
64
|
}>;
|
|
@@ -60,6 +66,8 @@ export declare function createDbBackedCommerceQuotePort<Request extends CreateQu
|
|
|
60
66
|
quote: Omit<CreateQuoteResponse["quote"], "context"> & {
|
|
61
67
|
context: Context & {
|
|
62
68
|
pricingPolicy?: PricingPolicySnapshot;
|
|
69
|
+
shippingTerms?: ShippingTerms;
|
|
70
|
+
acquisitionTerms?: ResolvedAcquisitionTerms;
|
|
63
71
|
};
|
|
64
72
|
};
|
|
65
73
|
}>;
|
|
@@ -7,7 +7,7 @@ import { assertSupportedCommerceTaxProfile } from "./taxProfileGuard.js";
|
|
|
7
7
|
import { catalogFactsForQuoteLine, projectPublicQuoteSnapshot } from "./catalogFactsProvenance.js";
|
|
8
8
|
const STRICT_D1_COMMERCE_CHANNEL = "D2C";
|
|
9
9
|
export function createDbBackedCommerceQuotePort(deps) {
|
|
10
|
-
const { taxProfile, supportedTaxProfiles, regionCode, currency, now = () => new Date().toISOString(), quoteCatalogReadPort, isStrictD1Quote, pricingResolverPort, commercePriceAuthorityPort, shippingFlatMinor, resolveAutomaticQuotePromotions, applyPromotionCodesV2, hasPromotionCodePort, lineContext, quoteContext, } = deps;
|
|
10
|
+
const { taxProfile, supportedTaxProfiles, regionCode, currency, now = () => new Date().toISOString(), quoteCatalogReadPort, isStrictD1Quote, pricingResolverPort, commercePriceAuthorityPort, shippingFlatMinor, resolveAutomaticQuotePromotions, applyPromotionCodesV2, hasPromotionCodePort, lineContext, quoteContext, resolveQuoteShippingPromotions, } = deps;
|
|
11
11
|
assertSupportedCommerceTaxProfile(taxProfile, supportedTaxProfiles);
|
|
12
12
|
const money = moneyIn(currency);
|
|
13
13
|
async function createServerAuthoritativeQuote(request, options) {
|
|
@@ -129,33 +129,49 @@ export function createDbBackedCommerceQuotePort(deps) {
|
|
|
129
129
|
const referenceProductMinor = sum(pricedLines.map((entry) => entry.referenceGrossMinor));
|
|
130
130
|
const netTotalMinor = sum(lines.map((line) => line.tax.netAmount.amountMinor));
|
|
131
131
|
const taxTotalMinor = sum(lines.map((line) => line.tax.vatAmount.amountMinor));
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
132
|
+
let shippingGrossMinor;
|
|
133
|
+
let promotionResult;
|
|
134
|
+
let hasShipping;
|
|
135
|
+
let shippingTerms;
|
|
136
|
+
let acquisitionTerms;
|
|
137
|
+
if (resolveQuoteShippingPromotions) {
|
|
138
|
+
const resolved = await resolveQuoteShippingPromotions({ request, options, regionCode, currency, referenceProductMinor, subtotalGrossMinor, atTime });
|
|
139
|
+
shippingGrossMinor = resolved.shippingGrossMinor;
|
|
140
|
+
promotionResult = resolved;
|
|
141
|
+
hasShipping = resolved.hasShipping;
|
|
142
|
+
shippingTerms = resolved.shippingTerms;
|
|
143
|
+
acquisitionTerms = resolved.acquisitionTerms;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Shipping is a flat per-order gross from settings; absent settings port ⇒ no
|
|
147
|
+
// shipping (byte-identical to pre-shipping quotes). Resolved before promo eval so
|
|
148
|
+
// the free-shipping lane has a real amount to zero.
|
|
149
|
+
shippingGrossMinor = shippingFlatMinor
|
|
150
|
+
? await shippingFlatMinor()
|
|
151
|
+
: 0;
|
|
152
|
+
const automaticPromotionResult = await resolveAutomaticQuotePromotions({
|
|
153
|
+
request,
|
|
154
|
+
options,
|
|
155
|
+
regionCode,
|
|
156
|
+
referenceProductMinor,
|
|
157
|
+
subtotalGrossMinor,
|
|
158
|
+
shippingGrossMinor,
|
|
159
|
+
atTime,
|
|
160
|
+
});
|
|
161
|
+
promotionResult = await applyPromotionCodesV2({
|
|
162
|
+
preserveLegacyDiscountsWhenNoCandidates: options?.pricingPolicy?.promotionEngineVersion !== PROMOTION_ENGINE_V2,
|
|
163
|
+
clientId: options?.clientId ?? null,
|
|
164
|
+
mode: request.mode,
|
|
165
|
+
promoCodes: request.promoCodes,
|
|
166
|
+
referenceProductMinor: sum(pricedLines.map((entry) => entry.referenceGrossMinor)),
|
|
167
|
+
subtotalGrossMinor,
|
|
168
|
+
shippingGrossMinor,
|
|
169
|
+
discounts: automaticPromotionResult.discounts,
|
|
170
|
+
codeRejections: automaticPromotionResult.codeRejections,
|
|
171
|
+
atTime,
|
|
172
|
+
});
|
|
173
|
+
hasShipping = shippingFlatMinor != null;
|
|
174
|
+
}
|
|
159
175
|
const { discounts, codeRejections, codeRejectionDetails } = promotionResult;
|
|
160
176
|
// Two lanes: order_total/line discounts reduce the subtotal; a shipping discount
|
|
161
177
|
// (free-shipping promo) reduces the shipping gross. They are summed and validated
|
|
@@ -169,7 +185,6 @@ export function createDbBackedCommerceQuotePort(deps) {
|
|
|
169
185
|
const { netMinor: discountNetMinor, vatMinor: discountVatMinor } = splitIncludedVat(orderDiscountMinor, taxProfile.vatRateBps);
|
|
170
186
|
const ship = splitIncludedVat(shippingGrossMinor, taxProfile.vatRateBps);
|
|
171
187
|
const shipDiscount = splitIncludedVat(shippingDiscountMinor, taxProfile.vatRateBps);
|
|
172
|
-
const hasShipping = shippingFlatMinor != null;
|
|
173
188
|
const shippingFields = hasShipping
|
|
174
189
|
? {
|
|
175
190
|
shippingGross: money(shippingGrossMinor),
|
|
@@ -188,6 +203,8 @@ export function createDbBackedCommerceQuotePort(deps) {
|
|
|
188
203
|
pricingComponents: quoteComponents,
|
|
189
204
|
context: {
|
|
190
205
|
...context(),
|
|
206
|
+
...(resolveQuoteShippingPromotions && acquisitionTerms ? { acquisitionTerms } : {}),
|
|
207
|
+
...(resolveQuoteShippingPromotions && shippingTerms ? { shippingTerms } : {}),
|
|
191
208
|
...(options?.pricingPolicy ? { pricingPolicy: options.pricingPolicy } : {}),
|
|
192
209
|
},
|
|
193
210
|
subtotalGross: money(subtotalGrossMinor),
|
package/dist/pricing/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from "./contracts/index.js";
|
|
2
2
|
export { CommercePriceAuthorityError } from "./ports.js";
|
|
3
3
|
export type { CommercePriceAuthorityPort } from "./ports.js";
|
|
4
|
-
export { createCommercePriceAuthorityPort, selectUniqueEffectivePriceList } from "./commercePriceAuthority.js";
|
|
4
|
+
export { createCommercePriceAuthorityPort, createConcurrentCommercePriceAuthorityPort, selectUniqueEffectivePriceList } from "./commercePriceAuthority.js";
|
|
5
5
|
export type { CommercePriceAuthorityReadPort, TimedAuthorityValue } from "./commercePriceAuthority.js";
|
|
6
6
|
export { subscriptionPricePolicySemanticsSchema, subscriptionPricePolicyCanonicalInputSchema, subscriptionPricePolicyRevisionSchema, SubscriptionPricePolicyDigestError, canonicalSubscriptionPricePolicyTuple, digestSubscriptionPricePolicy, assertSubscriptionPricePolicyDigest, deriveSubscriptionUnitPrice } from "./subscriptionPricePolicy.js";
|
|
7
7
|
export { offerPolicyVersionSchema, promotionEngineVersionSchema, pricingPolicyRequestSchema, pricingPolicySnapshotSchema } from "./offerPolicySchemas.js";
|
|
@@ -49,3 +49,4 @@ export * from "./acquisitionOfferPricingSchemas.js";
|
|
|
49
49
|
export * from "./acquisitionOfferPricing.js";
|
|
50
50
|
export * from "./acquisitionOfferPricingHandler.js";
|
|
51
51
|
export * from "./acquisitionReadiness.js";
|
|
52
|
+
export type { QuoteShippingPromotionInput, QuoteShippingPromotionResult } from "./quoteShippingPromotions.js";
|
package/dist/pricing/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from "./contracts/index.js";
|
|
2
2
|
export { CommercePriceAuthorityError } from "./ports.js";
|
|
3
|
-
export { createCommercePriceAuthorityPort, selectUniqueEffectivePriceList } from "./commercePriceAuthority.js";
|
|
3
|
+
export { createCommercePriceAuthorityPort, createConcurrentCommercePriceAuthorityPort, selectUniqueEffectivePriceList } from "./commercePriceAuthority.js";
|
|
4
4
|
export { subscriptionPricePolicySemanticsSchema, subscriptionPricePolicyCanonicalInputSchema, subscriptionPricePolicyRevisionSchema, SubscriptionPricePolicyDigestError, canonicalSubscriptionPricePolicyTuple, digestSubscriptionPricePolicy, assertSubscriptionPricePolicyDigest, deriveSubscriptionUnitPrice } from "./subscriptionPricePolicy.js";
|
|
5
5
|
export { offerPolicyVersionSchema, promotionEngineVersionSchema, pricingPolicyRequestSchema, pricingPolicySnapshotSchema } from "./offerPolicySchemas.js";
|
|
6
6
|
export { createPricingPolicyToken, verifyPricingPolicyToken, resolvePricingPolicy } from "./pricingPolicyToken.js";
|
|
@@ -4,8 +4,8 @@ export declare const offerPolicyVersionSchema: z.ZodEnum<{
|
|
|
4
4
|
"commerce.offer-policy.v2": "commerce.offer-policy.v2";
|
|
5
5
|
}>;
|
|
6
6
|
export declare const promotionEngineVersionSchema: z.ZodEnum<{
|
|
7
|
-
"promotion-engine.v2": "promotion-engine.v2";
|
|
8
7
|
"promotion-engine.v1": "promotion-engine.v1";
|
|
8
|
+
"promotion-engine.v2": "promotion-engine.v2";
|
|
9
9
|
}>;
|
|
10
10
|
/** Client capability plus an opaque server-signed assignment. Never a version selector. */
|
|
11
11
|
export declare const pricingPolicyRequestSchema: z.ZodObject<{
|
|
@@ -18,8 +18,8 @@ export declare const pricingPolicySnapshotSchema: z.ZodObject<{
|
|
|
18
18
|
"commerce.offer-policy.v2": "commerce.offer-policy.v2";
|
|
19
19
|
}>;
|
|
20
20
|
promotionEngineVersion: z.ZodEnum<{
|
|
21
|
-
"promotion-engine.v2": "promotion-engine.v2";
|
|
22
21
|
"promotion-engine.v1": "promotion-engine.v1";
|
|
22
|
+
"promotion-engine.v2": "promotion-engine.v2";
|
|
23
23
|
}>;
|
|
24
24
|
pricingPolicyToken: z.ZodOptional<z.ZodString>;
|
|
25
25
|
}, z.core.$strict>;
|
|
@@ -99,8 +99,8 @@ export declare function createCommerceOfferPricingSchemas<ScopeV1 extends string
|
|
|
99
99
|
"commerce.offer-policy.v2": "commerce.offer-policy.v2";
|
|
100
100
|
}>;
|
|
101
101
|
promotionEngineVersion: z.ZodEnum<{
|
|
102
|
-
"promotion-engine.v2": "promotion-engine.v2";
|
|
103
102
|
"promotion-engine.v1": "promotion-engine.v1";
|
|
103
|
+
"promotion-engine.v2": "promotion-engine.v2";
|
|
104
104
|
}>;
|
|
105
105
|
pricingPolicyToken: z.ZodOptional<z.ZodString>;
|
|
106
106
|
}, z.core.$strict>;
|
|
@@ -241,8 +241,8 @@ export declare function createCommerceOfferPricingSchemas<ScopeV1 extends string
|
|
|
241
241
|
"commerce.offer-policy.v2": "commerce.offer-policy.v2";
|
|
242
242
|
}>;
|
|
243
243
|
promotionEngineVersion: z.ZodEnum<{
|
|
244
|
-
"promotion-engine.v2": "promotion-engine.v2";
|
|
245
244
|
"promotion-engine.v1": "promotion-engine.v1";
|
|
245
|
+
"promotion-engine.v2": "promotion-engine.v2";
|
|
246
246
|
}>;
|
|
247
247
|
pricingPolicyToken: z.ZodOptional<z.ZodString>;
|
|
248
248
|
}, z.core.$strict>;
|
|
@@ -29,11 +29,11 @@ export declare function resolvePricingPolicy(input: {
|
|
|
29
29
|
promotionEngineVersion: PromotionEngineVersion;
|
|
30
30
|
} | {
|
|
31
31
|
offerPolicyVersion: "commerce.offer-policy.v1" | "commerce.offer-policy.v2";
|
|
32
|
-
promotionEngineVersion: "promotion-engine.
|
|
32
|
+
promotionEngineVersion: "promotion-engine.v1" | "promotion-engine.v2";
|
|
33
33
|
pricingPolicyToken?: undefined;
|
|
34
34
|
} | {
|
|
35
35
|
offerPolicyVersion: "commerce.offer-policy.v1" | "commerce.offer-policy.v2";
|
|
36
|
-
promotionEngineVersion: "promotion-engine.
|
|
36
|
+
promotionEngineVersion: "promotion-engine.v1" | "promotion-engine.v2";
|
|
37
37
|
pricingPolicyToken: string;
|
|
38
38
|
};
|
|
39
39
|
export {};
|