@openlup/platform 0.27.0 → 0.29.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 +386 -127
- package/README.md +251 -9
- package/dist/account-support/accountReadFacade.d.ts +8 -0
- package/dist/account-support/accountReadFacade.js +10 -0
- package/dist/account-support/accountReadModels.d.ts +69 -0
- package/dist/account-support/accountReadModels.js +272 -0
- package/dist/account-support/bindings.d.ts +23 -0
- package/dist/account-support/bindings.js +16 -0
- package/dist/account-support/contracts/account.d.ts +281 -0
- package/dist/account-support/contracts/account.js +1 -0
- package/dist/account-support/contracts/capabilities.d.ts +44 -0
- package/dist/account-support/contracts/capabilities.js +1 -0
- package/dist/account-support/contracts/commands.d.ts +307 -0
- package/dist/account-support/contracts/commands.js +1 -0
- package/dist/account-support/contracts/customer360.d.ts +154 -0
- package/dist/account-support/contracts/customer360.js +1 -0
- package/dist/account-support/contracts/index.d.ts +10 -0
- package/dist/account-support/contracts/index.js +1 -0
- package/dist/account-support/contracts/invoice.d.ts +79 -0
- package/dist/account-support/contracts/invoice.js +1 -0
- package/dist/account-support/contracts/operator.d.ts +289 -0
- package/dist/account-support/contracts/operator.js +1 -0
- package/dist/account-support/contribution.d.ts +24 -0
- package/dist/account-support/contribution.js +22 -0
- package/dist/account-support/customer360.d.ts +106 -0
- package/dist/account-support/customer360.js +163 -0
- package/dist/account-support/customerAccountReadDiagnostics.d.ts +11 -0
- package/dist/account-support/customerAccountReadDiagnostics.js +71 -0
- package/dist/account-support/index.d.ts +26 -0
- package/dist/account-support/index.js +15 -0
- package/dist/account-support/invoiceAccess.d.ts +57 -0
- package/dist/account-support/invoiceAccess.js +40 -0
- package/dist/account-support/migrations.d.ts +14 -0
- package/dist/account-support/migrations.js +7 -0
- package/dist/account-support/operatorAuthority.d.ts +67 -0
- package/dist/account-support/operatorAuthority.js +379 -0
- package/dist/account-support/operatorRecovery.d.ts +66 -0
- package/dist/account-support/operatorRecovery.js +129 -0
- package/dist/account-support/orderHistory.d.ts +50 -0
- package/dist/account-support/orderHistory.js +188 -0
- package/dist/account-support/paymentMethodComposition.d.ts +5 -0
- package/dist/account-support/paymentMethodComposition.js +4 -0
- package/dist/account-support/postgresOperatorAuthority.d.ts +16 -0
- package/dist/account-support/postgresOperatorAuthority.js +93 -0
- package/dist/account-support/selfService.d.ts +6 -0
- package/dist/account-support/selfService.js +81 -0
- package/dist/account-support/subscriptionCommandCodec.d.ts +10 -0
- package/dist/account-support/subscriptionCommandCodec.js +38 -0
- package/dist/catalog/draftSchemas.d.ts +1 -1
- package/dist/catalog/foundationSchemas.d.ts +3 -3
- package/dist/catalog/resolvedSkuProjection.d.ts +2 -2
- package/dist/catalog/sellableSchemas.d.ts +2 -2
- package/dist/commerce/checkoutCommandSchemas.d.ts +2 -2
- package/dist/commerce/checkoutResumeSchemas.d.ts +6 -6
- package/dist/commerce/checkoutSchemas.d.ts +12 -12
- package/dist/commerce/commerceReturnsPort.d.ts +2 -2
- package/dist/commerce/configuratorIntentSchemas.d.ts +2 -2
- package/dist/commerce/contractPrimitives.d.ts +8 -8
- package/dist/commerce/customerDefaultsSnapshotSchemas.d.ts +4 -4
- package/dist/commerce/model/omsReadModelHelpers.d.ts +4 -4
- package/dist/commerce/offerAvailabilitySchemas.d.ts +1 -1
- package/dist/commerce/oms/omsContractBase.d.ts +15 -15
- package/dist/commerce/oms/omsContractReadAuxSchemas.d.ts +11 -11
- package/dist/commerce/oms/omsContractReadSchemas.d.ts +121 -121
- package/dist/commerce/oms/omsContractRequests.d.ts +13 -13
- package/dist/commerce/oms/omsControlPlane.d.ts +22 -22
- package/dist/commerce/oms/omsFulfillmentContractSchemas.d.ts +12 -12
- package/dist/commerce/oms/omsFulfillmentDebugSchemas.d.ts +12 -12
- package/dist/commerce/orderReviewContracts.d.ts +33 -33
- package/dist/commerce/outboxEventSchemas.d.ts +4 -4
- package/dist/commerce/schemas.d.ts +21 -21
- package/dist/commerce/server/orderReviewHandlers.d.ts +10 -10
- package/dist/communications/consentPurposeRegistry.d.ts +1 -1
- package/dist/communications/newsletterIntegrationContracts.d.ts +8 -8
- package/dist/composition/accountSupportContribution.d.ts +18 -0
- package/dist/composition/accountSupportContribution.js +21 -0
- package/dist/composition/index.d.ts +2 -0
- package/dist/composition/index.js +2 -0
- package/dist/composition/marketingContribution.d.ts +21 -0
- package/dist/composition/marketingContribution.js +29 -0
- package/dist/fulfillment/handlerSchemas.d.ts +40 -40
- package/dist/identity/customerContracts.d.ts +2 -2
- package/dist/invoicing/snapshot.d.ts +1 -1
- package/dist/marketing/acquisitionCaseSchemas.d.ts +103 -0
- package/dist/marketing/acquisitionCaseSchemas.js +53 -0
- package/dist/marketing/acquisitionEvidenceSchemas.d.ts +105 -0
- package/dist/marketing/acquisitionEvidenceSchemas.js +55 -0
- package/dist/marketing/analytics/dataLayer.d.ts +18 -0
- package/dist/marketing/analytics/dataLayer.js +111 -0
- package/dist/marketing/analytics/noop.d.ts +9 -0
- package/dist/marketing/analytics/noop.js +8 -0
- package/dist/marketing/analytics/purchaseFacts.d.ts +4 -0
- package/dist/marketing/analytics/purchaseFacts.js +69 -0
- package/dist/marketing/analytics/routePolicy.d.ts +10 -0
- package/dist/marketing/analytics/routePolicy.js +59 -0
- package/dist/marketing/consentEvidence.d.ts +8 -0
- package/dist/marketing/consentEvidence.js +32 -0
- package/dist/marketing/contactLegal.d.ts +14 -0
- package/dist/marketing/contactLegal.js +19 -0
- package/dist/marketing/contracts/acquisition.d.ts +88 -0
- package/dist/marketing/contracts/acquisition.js +1 -0
- package/dist/marketing/contracts/acquisitionPorts.d.ts +49 -0
- package/dist/marketing/contracts/acquisitionPorts.js +1 -0
- package/dist/marketing/contracts/analytics.d.ts +144 -0
- package/dist/marketing/contracts/analytics.js +1 -0
- package/dist/marketing/contracts/contactLegal.d.ts +53 -0
- package/dist/marketing/contracts/contactLegal.js +1 -0
- package/dist/marketing/contracts/index.d.ts +6 -0
- package/dist/marketing/contracts/index.js +1 -0
- package/dist/marketing/contracts/prelaunch.d.ts +39 -0
- package/dist/marketing/contracts/prelaunch.js +1 -0
- package/dist/marketing/index.d.ts +16 -0
- package/dist/marketing/index.js +14 -0
- package/dist/marketing/prelaunchAcquisitionSchemas.d.ts +116 -0
- package/dist/marketing/prelaunchAcquisitionSchemas.js +51 -0
- package/dist/marketing/seo/jsonLd.d.ts +157 -0
- package/dist/marketing/seo/jsonLd.js +127 -0
- package/dist/marketing/seo/metadata.d.ts +41 -0
- package/dist/marketing/seo/metadata.js +36 -0
- package/dist/marketing/seo/serializeJsonLd.d.ts +1 -0
- package/dist/marketing/seo/serializeJsonLd.js +6 -0
- package/dist/marketing/seo/urls.d.ts +14 -0
- package/dist/marketing/seo/urls.js +23 -0
- package/dist/marketing/server/acquisitionCaseDirect.d.ts +4 -0
- package/dist/marketing/server/acquisitionCaseDirect.js +88 -0
- package/dist/marketing/server/acquisitionCaseHandlers.d.ts +28 -0
- package/dist/marketing/server/acquisitionCaseHandlers.js +40 -0
- package/dist/marketing/server/acquisitionCasePorts.d.ts +42 -0
- package/dist/marketing/server/acquisitionCasePorts.js +1 -0
- package/dist/marketing/server/acquisitionEvidenceDirect.d.ts +5 -0
- package/dist/marketing/server/acquisitionEvidenceDirect.js +100 -0
- package/dist/marketing/server/analyticsContext.d.ts +3 -0
- package/dist/marketing/server/analyticsContext.js +65 -0
- package/dist/marketing/server/analyticsContextPorts.d.ts +31 -0
- package/dist/marketing/server/analyticsContextPorts.js +1 -0
- package/dist/marketing/server/bindings.d.ts +14 -0
- package/dist/marketing/server/bindings.js +18 -0
- package/dist/marketing/server/directPorts.d.ts +26 -0
- package/dist/marketing/server/directPorts.js +1 -0
- package/dist/marketing/server/index.d.ts +14 -0
- package/dist/marketing/server/index.js +11 -0
- package/dist/marketing/server/postgres/acquisitionCaseRoutines.d.ts +10 -0
- package/dist/marketing/server/postgres/acquisitionCaseRoutines.js +204 -0
- package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.d.ts +10 -0
- package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.js +107 -0
- package/dist/marketing/server/postgres/index.d.ts +3 -0
- package/dist/marketing/server/postgres/index.js +3 -0
- package/dist/marketing/server/prelaunchAcquisitionDirect.d.ts +4 -0
- package/dist/marketing/server/prelaunchAcquisitionDirect.js +122 -0
- package/dist/marketing/server/privacyContributor.d.ts +66 -0
- package/dist/marketing/server/privacyContributor.js +87 -0
- package/dist/marketing/server/research/adminSurveyResponsesHandler.d.ts +9 -0
- package/dist/marketing/server/research/adminSurveyResponsesHandler.js +53 -0
- package/dist/marketing/server/research/managedSurveyResponseSubmit.d.ts +39 -0
- package/dist/marketing/server/research/managedSurveyResponseSubmit.js +102 -0
- package/dist/marketing/server/research/surveyResponseSubmitHandler.d.ts +8 -0
- package/dist/marketing/server/research/surveyResponseSubmitHandler.js +41 -0
- package/dist/marketing/validation.d.ts +2 -0
- package/dist/marketing/validation.js +3 -0
- package/dist/pricing/acquisitionQuoteBinding.d.ts +1 -1
- package/dist/pricing/catalogFactsSchemas.d.ts +1 -1
- package/dist/pricing/quotePromotionEvidence.d.ts +1 -1
- package/dist/pricing/quoteSchemas.d.ts +56 -56
- package/dist/promotions/adminPromotionCodesContracts.d.ts +30 -30
- package/dist/promotions/adminPromotionsContracts.d.ts +14 -14
- package/package.json +32 -2
- package/release-gates.json +99 -4
- package/sql/20261026120000_account_support_operator_authority.sql +693 -0
- package/src/account-support/accountReadFacade.ts +15 -0
- package/src/account-support/accountReadModels.ts +371 -0
- package/src/account-support/bindings.ts +28 -0
- package/src/account-support/contracts/account.ts +306 -0
- package/src/account-support/contracts/capabilities.ts +30 -0
- package/src/account-support/contracts/commands.ts +243 -0
- package/src/account-support/contracts/customer360.ts +150 -0
- package/src/account-support/contracts/index.ts +141 -0
- package/src/account-support/contracts/invoice.ts +44 -0
- package/src/account-support/contracts/operator.ts +123 -0
- package/src/account-support/contribution.ts +35 -0
- package/src/account-support/customer360.ts +268 -0
- package/src/account-support/customerAccountReadDiagnostics.ts +83 -0
- package/src/account-support/index.ts +72 -0
- package/src/account-support/invoiceAccess.ts +63 -0
- package/src/account-support/migrations.ts +7 -0
- package/src/account-support/operatorAuthority.ts +393 -0
- package/src/account-support/operatorRecovery.ts +136 -0
- package/src/account-support/orderHistory.ts +309 -0
- package/src/account-support/paymentMethodComposition.ts +8 -0
- package/src/account-support/postgresOperatorAuthority.ts +92 -0
- package/src/account-support/selfService.ts +83 -0
- package/src/account-support/subscriptionCommandCodec.ts +48 -0
- package/src/composition/accountSupportContribution.ts +28 -0
- package/src/composition/index.ts +2 -0
- package/src/composition/marketingContribution.ts +37 -0
- package/src/marketing/acquisitionCaseSchemas.ts +75 -0
- package/src/marketing/acquisitionEvidenceSchemas.ts +72 -0
- package/src/marketing/analytics/dataLayer.ts +100 -0
- package/src/marketing/analytics/noop.ts +14 -0
- package/src/marketing/analytics/purchaseFacts.ts +63 -0
- package/src/marketing/analytics/routePolicy.ts +55 -0
- package/src/marketing/consentEvidence.ts +36 -0
- package/src/marketing/contactLegal.ts +22 -0
- package/src/marketing/contracts/acquisition.ts +50 -0
- package/src/marketing/contracts/acquisitionPorts.ts +48 -0
- package/src/marketing/contracts/analytics.ts +88 -0
- package/src/marketing/contracts/contactLegal.ts +41 -0
- package/src/marketing/contracts/index.ts +17 -0
- package/src/marketing/contracts/prelaunch.ts +15 -0
- package/src/marketing/index.ts +16 -0
- package/src/marketing/prelaunchAcquisitionSchemas.ts +64 -0
- package/src/marketing/seo/jsonLd.ts +153 -0
- package/src/marketing/seo/metadata.ts +62 -0
- package/src/marketing/seo/serializeJsonLd.ts +6 -0
- package/src/marketing/seo/urls.ts +34 -0
- package/src/marketing/server/acquisitionCaseDirect.ts +67 -0
- package/src/marketing/server/acquisitionCaseHandlers.ts +57 -0
- package/src/marketing/server/acquisitionCasePorts.ts +42 -0
- package/src/marketing/server/acquisitionEvidenceDirect.ts +76 -0
- package/src/marketing/server/analyticsContext.ts +55 -0
- package/src/marketing/server/analyticsContextPorts.ts +28 -0
- package/src/marketing/server/bindings.ts +27 -0
- package/src/marketing/server/directPorts.ts +27 -0
- package/src/marketing/server/index.ts +14 -0
- package/src/marketing/server/postgres/acquisitionCaseRoutines.ts +218 -0
- package/src/marketing/server/postgres/acquisitionEvidenceRoutines.ts +103 -0
- package/src/marketing/server/postgres/index.ts +3 -0
- package/src/marketing/server/prelaunchAcquisitionDirect.ts +121 -0
- package/src/marketing/server/privacyContributor.ts +123 -0
- package/src/marketing/server/research/adminSurveyResponsesHandler.ts +78 -0
- package/src/marketing/server/research/managedSurveyResponseSubmit.ts +158 -0
- package/src/marketing/server/research/surveyResponseSubmitHandler.ts +57 -0
- package/src/marketing/validation.ts +3 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { currentParcelPerOrder, rowsForParcel } from "@openlup/core/fulfillment";
|
|
2
|
+
import {
|
|
3
|
+
deriveOrderMoney,
|
|
4
|
+
customerSafeEvidenceToken,
|
|
5
|
+
customerStepForTerminalOrder,
|
|
6
|
+
customerStepFromTimedSignals,
|
|
7
|
+
resolveProviderExceptionRecovery
|
|
8
|
+
} from "../commerce/index.js";
|
|
9
|
+
import type {
|
|
10
|
+
CanonicalOrderMoney,
|
|
11
|
+
CanonicalOrderMoneyLine,
|
|
12
|
+
OrderMoneyHeaderRow,
|
|
13
|
+
OrderMoneyItemRow,
|
|
14
|
+
ReleasedProviderExceptionHold,
|
|
15
|
+
ResolvedProviderExceptionRecovery
|
|
16
|
+
} from "../commerce/contracts/index.js";
|
|
17
|
+
import { resolveDeliverySelectionEvidence, summarizeDeliverySelection } from "../shipping/index.js";
|
|
18
|
+
import type {
|
|
19
|
+
AccountOrderSummary,
|
|
20
|
+
AccountOrderDetailResponse,
|
|
21
|
+
AccountOrderList,
|
|
22
|
+
AccountOrderLine,
|
|
23
|
+
AccountFulfillmentSummary,
|
|
24
|
+
AccountInvoiceSummary,
|
|
25
|
+
AccountInvoiceDocument
|
|
26
|
+
} from "./contracts/account.js";
|
|
27
|
+
|
|
28
|
+
export const CUSTOMER_ORDERS_CONTRACT_VERSION = "customer.orders.v2" as const;
|
|
29
|
+
export type CustomerOrderHistoryRow = Record<string, unknown>;
|
|
30
|
+
type Row = CustomerOrderHistoryRow;
|
|
31
|
+
type Tracking = Pick<AccountOrderSummary, "trackingReferences"
|
|
32
|
+
| "trackingNumbers"
|
|
33
|
+
| "trackingNumber"
|
|
34
|
+
| "trackingUrl"
|
|
35
|
+
| "carrierKind"
|
|
36
|
+
| "service"
|
|
37
|
+
| "trackingTimeline">;
|
|
38
|
+
|
|
39
|
+
export interface CustomerOrderHistoryReadStore {
|
|
40
|
+
readCustomerOrders(clientId: string, hiddenStatuses: readonly string[], limit: number): Promise<Row[]>;
|
|
41
|
+
readCustomerOrder(clientId: string, orderId: string): Promise<Row | null>;
|
|
42
|
+
readOrderLines(orderId: string): Promise<Row[]>;
|
|
43
|
+
readPaymentStatuses(orderIds: string[]): Promise<Map<string, string>>;
|
|
44
|
+
readFulfillments(clientId: string, orderIds: string[]): Promise<Row[]>;
|
|
45
|
+
readTracking(orderIds: string[]): Promise<Map<string, Row[]>>;
|
|
46
|
+
readFulfillmentOperations(orderIds: string[]): Promise<Map<string, Row[]>>;
|
|
47
|
+
readStatusEvidence(orderIds: string[]): Promise<Map<string, Row[]>>;
|
|
48
|
+
readReleasedProviderExceptionHolds(orderIds: string[]): Promise<Map<string, ReleasedProviderExceptionHold[]>>;
|
|
49
|
+
readReleasedProviderExceptionEvidence(holds: Map<string, ReleasedProviderExceptionHold[]>): Promise<Map<string, Row[]>>;
|
|
50
|
+
readActiveHoldOrderIds(orderIds: string[]): Promise<Set<string>>;
|
|
51
|
+
readInvoiceSummaries(
|
|
52
|
+
orderIds: string[]
|
|
53
|
+
): Promise<
|
|
54
|
+
Map<string, { invoice: AccountInvoiceSummary | null; invoiceDocuments: AccountInvoiceDocument[] }>
|
|
55
|
+
>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Catalogue/captured-label policy is injected in its entirety, including private extension keys. */
|
|
59
|
+
export interface CustomerOrderLinePresentationPort {
|
|
60
|
+
display(row: Row): Pick<AccountOrderLine, "title" | "variantName" | "productSlug" | "displayLabel" | "accentColor">;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CustomerOrderHistoryProjectionPort {
|
|
64
|
+
automaticHoldSource: string;
|
|
65
|
+
automaticReleaseSource: string;
|
|
66
|
+
orderReference(orderId: string): string;
|
|
67
|
+
fulfillmentEvidence(input: {
|
|
68
|
+
trackingRefs: Row[];
|
|
69
|
+
statusEvidenceRows: Row[];
|
|
70
|
+
operationRows: Row[];
|
|
71
|
+
recovery?: ResolvedProviderExceptionRecovery
|
|
72
|
+
}): Tracking;
|
|
73
|
+
presentation: CustomerOrderLinePresentationPort;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function readCustomerOrderSummaries(
|
|
77
|
+
store: CustomerOrderHistoryReadStore,
|
|
78
|
+
clientId: string,
|
|
79
|
+
limit: number,
|
|
80
|
+
projection: CustomerOrderHistoryProjectionPort
|
|
81
|
+
): Promise<AccountOrderList> {
|
|
82
|
+
const rows = await store.readCustomerOrders(clientId, ["cancelled", "expired"], limit);
|
|
83
|
+
const maps = await companions(store, clientId, rows.map(row => text(row.id)), projection);
|
|
84
|
+
return {
|
|
85
|
+
contractVersion: CUSTOMER_ORDERS_CONTRACT_VERSION,
|
|
86
|
+
orders: rows.map(row => summary(row, maps, projection))
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function readCustomerOrderDetail(
|
|
91
|
+
store: CustomerOrderHistoryReadStore,
|
|
92
|
+
clientId: string,
|
|
93
|
+
orderId: string,
|
|
94
|
+
projection: CustomerOrderHistoryProjectionPort
|
|
95
|
+
): Promise<AccountOrderDetailResponse | null> {
|
|
96
|
+
const row = await store.readCustomerOrder(clientId, orderId);
|
|
97
|
+
if (!row) return null;
|
|
98
|
+
const [lines, maps] = await Promise.all([store.readOrderLines(orderId), companions(store, clientId, [orderId], projection)]);
|
|
99
|
+
const money = deriveOrderMoney(toOrderMoneyHeader(row), lines.map(toOrderMoneyItem));
|
|
100
|
+
return {
|
|
101
|
+
contractVersion: CUSTOMER_ORDERS_CONTRACT_VERSION,
|
|
102
|
+
order: {
|
|
103
|
+
...summary(row, maps, projection, money),
|
|
104
|
+
subtotal: customerOrderMoney(money.subtotal, money.currency),
|
|
105
|
+
discount: customerOrderMoney(money.productDiscount, money.currency),
|
|
106
|
+
shipping: customerOrderMoney(money.shippingEffective, money.currency),
|
|
107
|
+
tax: customerOrderMoney(money.tax, money.currency),
|
|
108
|
+
lines: mapCustomerOrderLines(lines, money.lines, money.currency, projection.presentation),
|
|
109
|
+
fulfillment: maps.fulfillment.get(orderId) ?? null
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function companions(
|
|
115
|
+
store: CustomerOrderHistoryReadStore,
|
|
116
|
+
clientId: string,
|
|
117
|
+
ids: string[],
|
|
118
|
+
projection: CustomerOrderHistoryProjectionPort
|
|
119
|
+
) {
|
|
120
|
+
const [payments, parcels, tracking, operations, evidence, holds, active, invoices] = await Promise.all([
|
|
121
|
+
store.readPaymentStatuses(ids),
|
|
122
|
+
store.readFulfillments(clientId, ids),
|
|
123
|
+
store.readTracking(ids),
|
|
124
|
+
store.readFulfillmentOperations(ids),
|
|
125
|
+
store.readStatusEvidence(ids),
|
|
126
|
+
store.readReleasedProviderExceptionHolds(ids),
|
|
127
|
+
store.readActiveHoldOrderIds(ids),
|
|
128
|
+
store.readInvoiceSummaries(ids),
|
|
129
|
+
]);
|
|
130
|
+
const released = await store.readReleasedProviderExceptionEvidence(holds);
|
|
131
|
+
for (const [id, rows] of released) {
|
|
132
|
+
const known = new Set((evidence.get(id) ?? []).map(row => text(row.id)));
|
|
133
|
+
evidence.set(id, [...(evidence.get(id) ?? []), ...rows.filter(row => !known.has(text(row.id)))]);
|
|
134
|
+
}
|
|
135
|
+
const recovery = new Map<string, ResolvedProviderExceptionRecovery>();
|
|
136
|
+
for (const parcel of parcels) {
|
|
137
|
+
const id = text(parcel.order_id);
|
|
138
|
+
if (!id) continue;
|
|
139
|
+
recovery.set(
|
|
140
|
+
id,
|
|
141
|
+
resolveProviderExceptionRecovery({
|
|
142
|
+
fulfillmentOrderId: nullableText(parcel.id),
|
|
143
|
+
releasedHolds: holds.get(id) ?? [],
|
|
144
|
+
automaticHoldSource: projection.automaticHoldSource,
|
|
145
|
+
automaticReleaseSource: projection.automaticReleaseSource,
|
|
146
|
+
statusEvidence: (evidence.get(id) ?? []).map(row => ({
|
|
147
|
+
id: nullableText(row.id),
|
|
148
|
+
fulfillmentOrderId: nullableText(row.fulfillment_order_id),
|
|
149
|
+
providerStatus: nullableText(row.provider_status),
|
|
150
|
+
localStatus: nullableText(row.local_status),
|
|
151
|
+
token: customerSafeEvidenceToken(nullableText(row.local_status), nullableText(row.provider_status)),
|
|
152
|
+
occurredAt: nullableText(row.occurred_at)
|
|
153
|
+
}))
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const readTracking = (id: string, parcelId: string | null) => projection.fulfillmentEvidence({
|
|
158
|
+
trackingRefs: rowsForParcel(tracking.get(id) ?? [], parcelId),
|
|
159
|
+
statusEvidenceRows: evidence.get(id) ?? [],
|
|
160
|
+
operationRows: operations.get(id) ?? [],
|
|
161
|
+
recovery: recovery.get(id)
|
|
162
|
+
});
|
|
163
|
+
const fulfillment = new Map<string, AccountFulfillmentSummary>();
|
|
164
|
+
for (const [id, parcel] of currentParcelPerOrder(parcels)) {
|
|
165
|
+
const details = readTracking(id, text(parcel.id)), op = operations.get(id)?.[0];
|
|
166
|
+
fulfillment.set(
|
|
167
|
+
id,
|
|
168
|
+
{
|
|
169
|
+
fulfillmentOrderId: text(parcel.id) || null,
|
|
170
|
+
status: nullableText(parcel.status),
|
|
171
|
+
providerKind: nullableText(parcel.provider_kind) ?? details.trackingReferences[0]?.providerKind ?? null,
|
|
172
|
+
trackingNumber: details.trackingNumber,
|
|
173
|
+
trackingNumbers: details.trackingNumbers,
|
|
174
|
+
trackingReferences: details.trackingReferences,
|
|
175
|
+
lastEventType: nullableText(op?.operation_type) ?? details.trackingTimeline[0]?.eventType ?? null,
|
|
176
|
+
updatedAt: nullableText(parcel.updated_at) ?? nullableText(op?.occurred_at),
|
|
177
|
+
trackingTimeline: details.trackingTimeline
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return { payments, evidence, recovery, active, invoices, fulfillment, readTracking };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function summary(
|
|
185
|
+
row: Row,
|
|
186
|
+
maps: Awaited<ReturnType<typeof companions>>,
|
|
187
|
+
projection: CustomerOrderHistoryProjectionPort,
|
|
188
|
+
money: CanonicalOrderMoney = deriveOrderMoney(toOrderMoneyHeader(row))
|
|
189
|
+
): AccountOrderSummary {
|
|
190
|
+
const id = text(row.id),
|
|
191
|
+
parcel = maps.fulfillment.get(id),
|
|
192
|
+
tracking = maps.readTracking(id, parcel?.fulfillmentOrderId ?? null),
|
|
193
|
+
history = maps.invoices.get(id) ?? null,
|
|
194
|
+
invoice = history?.invoice ?? null;
|
|
195
|
+
const metadata = record(row.metadata);
|
|
196
|
+
const requested = nullableText(record(metadata.invoiceBuyerSnapshot).taxId) !== null
|
|
197
|
+
|| nullableText(record(record(metadata.runtimeFinalize).invoiceBuyerSnapshot).taxId) !== null;
|
|
198
|
+
const customerFulfillmentStep = maps.active.has(id)
|
|
199
|
+
? "exception"
|
|
200
|
+
: customerStepForTerminalOrder(text(row.status))
|
|
201
|
+
?? customerStepFromTimedSignals(
|
|
202
|
+
[text(row.status), parcel?.status ?? null],
|
|
203
|
+
(maps.evidence.get(id) ?? []).map(value => ({
|
|
204
|
+
id: nullableText(value.id),
|
|
205
|
+
token: customerSafeEvidenceToken(nullableText(value.local_status), nullableText(value.provider_status)),
|
|
206
|
+
occurredAt: nullableText(value.occurred_at)
|
|
207
|
+
})),
|
|
208
|
+
maps.recovery.get(id)
|
|
209
|
+
);
|
|
210
|
+
return {
|
|
211
|
+
orderId: id,
|
|
212
|
+
orderRef: `order_${id}`,
|
|
213
|
+
orderNumber: nullableText(row.order_number) ?? projection.orderReference(id),
|
|
214
|
+
subscriptionId: nullableText(row.subscription_id),
|
|
215
|
+
status: text(row.status),
|
|
216
|
+
paymentStatus: maps.payments.get(id) ?? null,
|
|
217
|
+
fulfillmentStatus: parcel?.status ?? null,
|
|
218
|
+
customerFulfillmentStep,
|
|
219
|
+
total: customerOrderMoney(money.total, money.currency),
|
|
220
|
+
...tracking,
|
|
221
|
+
deliverySelection: summarizeDeliverySelection(resolveDeliverySelectionEvidence({ orderMetadata: row.metadata }).selection),
|
|
222
|
+
invoice,
|
|
223
|
+
invoiceDocuments: history?.invoiceDocuments ?? [],
|
|
224
|
+
invoiceRequestStatus: invoice?.downloadAvailable ? "available" : history || requested ? "preparing" : "not_requested",
|
|
225
|
+
createdAt: text(row.created_at),
|
|
226
|
+
updatedAt: text(row.updated_at)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function mapCustomerOrderLines(
|
|
231
|
+
rows: readonly Row[],
|
|
232
|
+
moneyLines: readonly CanonicalOrderMoneyLine[],
|
|
233
|
+
currency: string,
|
|
234
|
+
presentation: CustomerOrderLinePresentationPort
|
|
235
|
+
): AccountOrderLine[] {
|
|
236
|
+
return rows.map((row, index) => {
|
|
237
|
+
const money = moneyLines[index],
|
|
238
|
+
total = money?.catalogTotal ?? Number(row.total_cents ?? 0),
|
|
239
|
+
list = lineListTotalMinor(record(row.product_snapshot)),
|
|
240
|
+
saving = list !== null && list > total;
|
|
241
|
+
return {
|
|
242
|
+
lineId: text(row.id),
|
|
243
|
+
skuId: nullableText(row.sku_id),
|
|
244
|
+
...presentation.display(row),
|
|
245
|
+
quantity: Number(row.quantity),
|
|
246
|
+
unitPrice: customerOrderMoney(money?.catalogUnit ?? row.unit_price_cents, currency),
|
|
247
|
+
total: customerOrderMoney(total, currency),
|
|
248
|
+
listTotal: saving ? customerOrderMoney(list, currency) : null,
|
|
249
|
+
discount: saving ? customerOrderMoney(list - total, currency) : null
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function lineListTotalMinor(snapshot: Row): number | null {
|
|
255
|
+
const components = record(snapshot.quoteLine).pricingComponents;
|
|
256
|
+
if (!Array.isArray(components)) return null;
|
|
257
|
+
let result = 0, found = false;
|
|
258
|
+
for (const value of components) {
|
|
259
|
+
const component = record(value);
|
|
260
|
+
if (component.componentType === "base_unit") {
|
|
261
|
+
result += Number(component.amountMinor ?? 0);
|
|
262
|
+
found = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return found ? result : null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function customerOrderMoney(value: unknown, currency: string) {
|
|
269
|
+
return { amountMinor: Number(value ?? 0), currency };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function toOrderMoneyHeader(row: Row): OrderMoneyHeaderRow {
|
|
273
|
+
return {
|
|
274
|
+
id: text(row.id),
|
|
275
|
+
currency: text(row.currency),
|
|
276
|
+
subtotal_cents: Number(row.subtotal_cents),
|
|
277
|
+
discount_cents: Number(row.discount_cents),
|
|
278
|
+
shipping_cents: Number(row.shipping_cents),
|
|
279
|
+
shipping_discount_cents: Number(row.shipping_discount_cents ?? 0),
|
|
280
|
+
tax_cents: Number(row.tax_cents),
|
|
281
|
+
total_cents: Number(row.total_cents)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function toOrderMoneyItem(row: Row): OrderMoneyItemRow {
|
|
286
|
+
const number = (value: unknown) => typeof value === "number" ? value : null;
|
|
287
|
+
return {
|
|
288
|
+
id: text(row.id),
|
|
289
|
+
quantity: Number(row.quantity),
|
|
290
|
+
unit_price_cents: Number(row.unit_price_cents),
|
|
291
|
+
total_cents: Number(row.total_cents),
|
|
292
|
+
discount_allocated_cents: number(row.discount_allocated_cents),
|
|
293
|
+
effective_total_cents: number(row.effective_total_cents),
|
|
294
|
+
effective_net_cents: number(row.effective_net_cents),
|
|
295
|
+
vat_rate_bps: Number(row.vat_rate_bps)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function text(value: unknown): string {
|
|
300
|
+
return typeof value === "string" ? value : "";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function nullableText(value: unknown): string | null {
|
|
304
|
+
return typeof value === "string" && value ? value : null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function record(value: unknown): Row {
|
|
308
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Row : {};
|
|
309
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MethodSetupInput, MethodSetupPort, MethodSetupResult } from "./contracts/index.js";
|
|
2
|
+
|
|
3
|
+
/** M07 collection only; ready, replayed and unsupported remain the owner's result. */
|
|
4
|
+
export function createPaymentMethodComposition<Handoff>(port: MethodSetupPort<Handoff>): {
|
|
5
|
+
execute(userId: string, request: MethodSetupInput): Promise<MethodSetupResult<Handoff>>;
|
|
6
|
+
} {
|
|
7
|
+
return { execute(_userId, request) { return port.setupMethod(request); } };
|
|
8
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { NativeOperatorAuthorityPort } from "./contracts/index.js";
|
|
2
|
+
import { nativeSubscriptionCommand, leadAbsorption, recoveryResult, operatorRecoveryMaterial,
|
|
3
|
+
type OperatorAuthorityOptions, CustomerSupportCommandRequiresHumanError, CustomerSupportOperatorInactiveError,
|
|
4
|
+
CustomerSupportCommandInvalidError, CustomerSupportCommandIdempotencyConflictError, OperatorSubscriptionAuthorityUnavailableError } from "./operatorAuthority.js";
|
|
5
|
+
|
|
6
|
+
/** Narrow structural subset of a query executor; the host owns connection and transaction. */
|
|
7
|
+
export interface OperatorQueryExecutor {
|
|
8
|
+
query(sql: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }>;
|
|
9
|
+
}
|
|
10
|
+
export interface OperatorTransactionExecutor {
|
|
11
|
+
run<T>(work: (executor: OperatorQueryExecutor) => Promise<T>): Promise<T>;
|
|
12
|
+
}
|
|
13
|
+
export const OPERATOR_AUTHORITY_UNAVAILABLE = {
|
|
14
|
+
outcome: "refused", refusalCode: "operator_subscription_authority_unavailable",
|
|
15
|
+
} as const;
|
|
16
|
+
/** Native SQLSTATE mapping preserves the existing direct adapter's opaque fallback. */
|
|
17
|
+
function nativeCommandFailure(error: unknown, fallback: string): Error {
|
|
18
|
+
const raw = (error ?? {}) as { code?: unknown; message?: unknown };
|
|
19
|
+
const code = typeof raw.code === "string" ? raw.code : "";
|
|
20
|
+
const message = typeof raw.message === "string" ? raw.message : "";
|
|
21
|
+
if (message.includes("customer_support_command_requires_human")) return new CustomerSupportCommandRequiresHumanError();
|
|
22
|
+
if (code === "42501" || message.includes("communications_operator_inactive")) return new CustomerSupportOperatorInactiveError();
|
|
23
|
+
if (code === "22023" || /customer_support_(subscription_command|absorb_lead)_invalid/.test(message)) return new CustomerSupportCommandInvalidError();
|
|
24
|
+
if (code === "23505" || message.includes("customer_support_idempotency_conflict")) return new CustomerSupportCommandIdempotencyConflictError();
|
|
25
|
+
if (code === "42883" || /does not exist/i.test(message)) return new OperatorSubscriptionAuthorityUnavailableError();
|
|
26
|
+
return new Error(fallback);
|
|
27
|
+
}
|
|
28
|
+
export function createPostgresOperatorAuthority(lane: OperatorTransactionExecutor, options: OperatorAuthorityOptions): NativeOperatorAuthorityPort {
|
|
29
|
+
const operatorId = options.operatorId.trim();
|
|
30
|
+
if (!operatorId) throw new Error("customer_support_operator_required");
|
|
31
|
+
const check = (input: { operatorId: string }) => {
|
|
32
|
+
if (input.operatorId !== operatorId) throw new Error("customer_support_operator_scope_mismatch");
|
|
33
|
+
};
|
|
34
|
+
const call = async (sql: string, values: unknown[], fallback: string): Promise<unknown> => {
|
|
35
|
+
try {
|
|
36
|
+
return await lane.run(async executor => {
|
|
37
|
+
const { rows } = await executor.query(sql, values);
|
|
38
|
+
if (rows.length !== 1 || !("result" in rows[0]!)) throw new Error("customer_support_routine_response_invalid");
|
|
39
|
+
return rows[0]!.result;
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw nativeCommandFailure(error, fallback);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
async applySubscriptionAction(input) {
|
|
47
|
+
check(input);
|
|
48
|
+
if (input.subscriptionAction === "change_shipping_address") return OPERATOR_AUTHORITY_UNAVAILABLE;
|
|
49
|
+
const result = await call("SELECT public.customer_support_apply_subscription_action_v1($1,$2,$3,$4,$5,$6,$7) AS result", [
|
|
50
|
+
operatorId, input.subscriptionId, input.subscriptionAction, input.payload ?? {}, input.expectedVersion,
|
|
51
|
+
input.idempotencyKey, new Date(options.now()).toISOString(),
|
|
52
|
+
], "customer_support_subscription_command_failed");
|
|
53
|
+
return nativeSubscriptionCommand(result);
|
|
54
|
+
},
|
|
55
|
+
async correctSubjectEmail(input) { check(input); return OPERATOR_AUTHORITY_UNAVAILABLE; },
|
|
56
|
+
async correctSubjectPhone(input) { check(input); return OPERATOR_AUTHORITY_UNAVAILABLE; },
|
|
57
|
+
async linkSubjectAccount(input) { check(input); return OPERATOR_AUTHORITY_UNAVAILABLE; },
|
|
58
|
+
async absorbLead(input) {
|
|
59
|
+
check(input);
|
|
60
|
+
const result = await call("SELECT public.customer_support_absorb_lead_v1($1,$2,$3,$4,$5,$6) AS result", [
|
|
61
|
+
operatorId, input.customerId, input.leadId, input.expectedLeadEmail, input.idempotencyKey, new Date(options.now()).toISOString(),
|
|
62
|
+
], "customer_support_lead_absorption_failed");
|
|
63
|
+
return leadAbsorption(result);
|
|
64
|
+
},
|
|
65
|
+
async issueRecovery(input) {
|
|
66
|
+
check(input);
|
|
67
|
+
const material = await operatorRecoveryMaterial(input, options);
|
|
68
|
+
// Preserve recovery's special conflict mapping and opaque non-conflict failure.
|
|
69
|
+
let result: unknown;
|
|
70
|
+
try {
|
|
71
|
+
result = await lane.run(async executor => {
|
|
72
|
+
const { rows } = await executor.query("SELECT public.customer_support_issue_recovery($1,$2,$3,$4,$5,$6,$7,$8) AS result", [
|
|
73
|
+
operatorId, input.subjectId, input.caseId, input.idempotencyKey, material.payloadFingerprint,
|
|
74
|
+
material.tokenHash, material.expiresAt, material.recoveryPath,
|
|
75
|
+
]);
|
|
76
|
+
if (rows.length !== 1 || !("result" in rows[0]!)) throw new Error("customer_support_routine_response_invalid");
|
|
77
|
+
return rows[0]!.result;
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const raw = (error ?? {}) as { code?: unknown; message?: unknown };
|
|
81
|
+
if (typeof raw.message === "string" && raw.message.includes("customer_support_command_requires_human")) {
|
|
82
|
+
throw new CustomerSupportCommandRequiresHumanError();
|
|
83
|
+
}
|
|
84
|
+
if (raw.code === "23505" || (typeof raw.message === "string" && raw.message.includes("idempotency_conflict"))) {
|
|
85
|
+
return { outcome: "conflict", conflictCode: "idempotency_conflict" };
|
|
86
|
+
}
|
|
87
|
+
throw new Error("customer_support_recovery_failed");
|
|
88
|
+
}
|
|
89
|
+
return recoveryResult(result);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { AccountSupportLaneUnavailable, SelfServiceCommandFacade, SelfServiceCommandFacadeInput } from "./contracts/index.js";
|
|
2
|
+
|
|
3
|
+
const HOST_BINDING_REQUIRED: AccountSupportLaneUnavailable<"host_binding_required"> = Object.freeze({ supported: false, refusalCode: "host_binding_required" });
|
|
4
|
+
|
|
5
|
+
/** Neutral per-request graph selection. The host supplies all owner implementations. */
|
|
6
|
+
export function createSelfServiceCommandFacade<Request extends { subscriptionId: string; action: string }, PreviewRequest, Preview, Receipt,
|
|
7
|
+
ProfileRequest, ProfileResult, MethodRequest, MethodResult, CommerceRequest, CommerceResult, RecoveryRequest, RecoveryResult, ControlSnapshot = unknown>(
|
|
8
|
+
input: SelfServiceCommandFacadeInput<Request, PreviewRequest, Preview, Receipt,
|
|
9
|
+
ProfileRequest, ProfileResult, MethodRequest, MethodResult, CommerceRequest, CommerceResult, RecoveryRequest, RecoveryResult, ControlSnapshot>,
|
|
10
|
+
): SelfServiceCommandFacade<Request, PreviewRequest, Preview, Receipt,
|
|
11
|
+
ProfileRequest, ProfileResult, MethodRequest, MethodResult, CommerceRequest, CommerceResult, RecoveryRequest, RecoveryResult, ControlSnapshot> {
|
|
12
|
+
const context = { ...input.context }, native = input.nativeLegacySubscription;
|
|
13
|
+
function requireMethods(lane: object | undefined, methods: string[]) {
|
|
14
|
+
if (lane !== undefined && (!lane || methods.some(method => typeof (lane as Record<string, unknown>)[method] !== "function"))) {
|
|
15
|
+
throw new Error("customer_self_service_lane_configuration_required");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
requireMethods(input.managedSubscription, ["previewAction", "applyAction"]);
|
|
19
|
+
requireMethods(native, ["ownershipRefusal"]);
|
|
20
|
+
if (native) {
|
|
21
|
+
if (!native.explicitPause || !native.control) throw new Error("customer_self_service_lane_configuration_required");
|
|
22
|
+
requireMethods(native.explicitPause, ["applyAction"]);
|
|
23
|
+
requireMethods(native.control, ["applyAction", "previewAction", "hasSubscription", "getSnapshot"]);
|
|
24
|
+
}
|
|
25
|
+
requireMethods(input.nativeRawSubscription, ["preview", "apply"]);
|
|
26
|
+
requireMethods(input.nativeSubscription, ["preview", "apply"]);
|
|
27
|
+
requireMethods(input.profile, ["updateProfile"]);
|
|
28
|
+
requireMethods(input.paymentMethod, ["execute"]);
|
|
29
|
+
requireMethods(input.commerce, ["execute"]);
|
|
30
|
+
requireMethods(input.recovery, ["startPaymentRecovery"]);
|
|
31
|
+
if ((!input.paymentMethod && input.capabilities.paymentMethodSetup.supported)
|
|
32
|
+
|| (!input.commerce && (input.capabilities.commerceRecovery.supported || input.capabilities.configuredPurchase.supported))
|
|
33
|
+
|| (!(input.managedSubscription || native || input.nativeRawSubscription || input.nativeSubscription)
|
|
34
|
+
&& (input.capabilities.subscriptions.genericBundleCommands.supported || input.capabilities.subscriptions.commandBoundReceipts.supported))) {
|
|
35
|
+
throw new Error("customer_self_service_lane_configuration_required");
|
|
36
|
+
}
|
|
37
|
+
if (input.capabilities.graph !== context.graph
|
|
38
|
+
|| (input.managedSubscription && context.graph !== "managed")
|
|
39
|
+
|| ((native || input.nativeRawSubscription || input.nativeSubscription) && context.graph !== "native")) {
|
|
40
|
+
throw new Error("customer_graph_mismatch");
|
|
41
|
+
}
|
|
42
|
+
function verify(principalId: string, clientId: string) {
|
|
43
|
+
if (principalId !== context.principalId || clientId !== context.clientId) throw new Error("customer_principal_mismatch");
|
|
44
|
+
}
|
|
45
|
+
const managed = input.managedSubscription, raw = input.nativeRawSubscription, typed = input.nativeSubscription;
|
|
46
|
+
const profile = input.profile, method = input.paymentMethod, commerce = input.commerce, recovery = input.recovery;
|
|
47
|
+
const methodUnavailable = input.capabilities.paymentMethodSetup.supported ? undefined : input.capabilities.paymentMethodSetup;
|
|
48
|
+
const commerceUnavailable = input.capabilities.commerceRecovery.supported ? undefined : input.capabilities.commerceRecovery;
|
|
49
|
+
return {
|
|
50
|
+
capabilities: input.capabilities,
|
|
51
|
+
managedSubscription: {
|
|
52
|
+
async previewAction(userId, request) { verify(userId, context.clientId); return managed ? managed.previewAction(userId, request) : HOST_BINDING_REQUIRED; },
|
|
53
|
+
async applyAction(userId, request) { verify(userId, context.clientId); return managed ? managed.applyAction(userId, request) : HOST_BINDING_REQUIRED; },
|
|
54
|
+
},
|
|
55
|
+
nativeLegacySubscription: {
|
|
56
|
+
async previewAction(userId, request) { verify(userId, context.clientId); return native ? native.control.previewAction(userId, request) : HOST_BINDING_REQUIRED; },
|
|
57
|
+
async applyAction(userId, request) {
|
|
58
|
+
verify(userId, context.clientId);
|
|
59
|
+
if (!native) return HOST_BINDING_REQUIRED;
|
|
60
|
+
if (request.action === "pause") return native.explicitPause.applyAction(userId, request);
|
|
61
|
+
if (request.action !== "update_bundle") return native.control.applyAction(userId, request);
|
|
62
|
+
const canonical = { ...request, subscriptionId: request.subscriptionId.toLowerCase() };
|
|
63
|
+
if (!await native.control.hasSubscription(userId, canonical.subscriptionId)) throw native.ownershipRefusal();
|
|
64
|
+
return native.control.applyAction(userId, canonical);
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
subscriptionControl: {
|
|
68
|
+
async getSnapshot(userId) { verify(userId, context.clientId); return native ? native.control.getSnapshot(userId) : HOST_BINDING_REQUIRED; },
|
|
69
|
+
},
|
|
70
|
+
nativeRawSubscription: {
|
|
71
|
+
async preview(command) { verify(command.actorId, command.clientId); return raw ? raw.preview(command) : HOST_BINDING_REQUIRED; },
|
|
72
|
+
async apply(command) { verify(command.actorId, command.clientId); return raw ? raw.apply(command) : HOST_BINDING_REQUIRED; },
|
|
73
|
+
},
|
|
74
|
+
nativeSubscription: {
|
|
75
|
+
async preview(command) { verify(command.actor.actorId, command.actor.clientId); return typed ? typed.preview(command) : HOST_BINDING_REQUIRED; },
|
|
76
|
+
async apply(command) { verify(command.actor.actorId, command.actor.clientId); return typed ? typed.apply(command) : HOST_BINDING_REQUIRED; },
|
|
77
|
+
},
|
|
78
|
+
profile: { async updateProfile(userId, request) { verify(userId, context.clientId); return profile ? profile.updateProfile(userId, request) : HOST_BINDING_REQUIRED; } },
|
|
79
|
+
paymentMethod: { async execute(userId, request) { verify(userId, context.clientId); return method ? method.execute(userId, request) : methodUnavailable!; } },
|
|
80
|
+
commerce: { async execute(userId, request) { verify(userId, context.clientId); return commerce ? commerce.execute(userId, request) : commerceUnavailable!; } },
|
|
81
|
+
recovery: { async startPaymentRecovery(userId, request) { verify(userId, context.clientId); return recovery ? recovery.startPaymentRecovery(userId, request) : HOST_BINDING_REQUIRED; } },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { CustomerContext, LegacySubscriptionRequest, NativePreviewProjection, NativeSubscriptionCommand, NativeSubscriptionPreview, RawSubscriptionCommands, SubscriptionDriver } from "./contracts/index.js";
|
|
2
|
+
|
|
3
|
+
/** Encode only verified identity and the host's resolved request; no verb aliases. */
|
|
4
|
+
export function encodeNativeSubscriptionCommand<Action extends string, Payload extends Record<string, unknown>>(
|
|
5
|
+
context: CustomerContext, request: LegacySubscriptionRequest<Action, Payload>, now: string,
|
|
6
|
+
): NativeSubscriptionCommand {
|
|
7
|
+
return { actorId: context.principalId, clientId: context.clientId, idempotencyKey: request.idempotencyKey,
|
|
8
|
+
subscriptionId: request.subscriptionId, action: request.action, payload: request.payload, now };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function decodeNativeSubscriptionPreview<Preview>(result: Record<string, unknown>, project: (record: NativeSubscriptionPreview) => Preview): NativePreviewProjection<Preview> {
|
|
12
|
+
if (result.status !== "previewed") return { kind: "owner", result };
|
|
13
|
+
if (result.canApply !== true
|
|
14
|
+
|| !Number.isSafeInteger(result.templateVersion) || Number(result.templateVersion) < 1
|
|
15
|
+
|| typeof result.quoteHash !== "string" || !/^[a-f0-9]{64}$/.test(result.quoteHash)
|
|
16
|
+
|| typeof result.quoteExpiresAt !== "string" || !Number.isFinite(Date.parse(result.quoteExpiresAt))
|
|
17
|
+
|| !Number.isSafeInteger(result.currentTotalMinor) || Number(result.currentTotalMinor) < 0
|
|
18
|
+
|| !Number.isSafeInteger(result.newTotalMinor) || Number(result.newTotalMinor) < 0
|
|
19
|
+
|| !Array.isArray(result.lines) || result.lines.length === 0 || !result.lines.every(line =>
|
|
20
|
+
line !== null && typeof line === "object" && typeof line.variantId === "string"
|
|
21
|
+
&& Number.isSafeInteger(line.quantity) && line.quantity > 0 && typeof line.isAddon === "boolean"
|
|
22
|
+
&& Number.isSafeInteger(line.sortOrder) && line.sortOrder >= 0
|
|
23
|
+
&& Number.isSafeInteger(line.unitPriceMinor) && line.unitPriceMinor >= 0 && typeof line.currency === "string")
|
|
24
|
+
|| result.priceAgreementPolicy === null || typeof result.priceAgreementPolicy !== "object"
|
|
25
|
+
|| !("kind" in result.priceAgreementPolicy) || result.priceAgreementPolicy.kind !== "lock_until_edit"
|
|
26
|
+
|| !("proration" in result.priceAgreementPolicy) || result.priceAgreementPolicy.proration !== "none"
|
|
27
|
+
|| !("cycleEffect" in result.priceAgreementPolicy) || result.priceAgreementPolicy.cycleEffect !== "future_unlocked_cycles_only") {
|
|
28
|
+
throw new Error("subscription_command_preview_invalid");
|
|
29
|
+
}
|
|
30
|
+
return { kind: "projected", preview: project(result as NativeSubscriptionPreview) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Preview and apply are separate transactions. Apply never creates a quote. */
|
|
34
|
+
export function createNativeSubscriptionDriver<Action extends string, Payload extends Record<string, unknown>, Preview>(input: {
|
|
35
|
+
owner: RawSubscriptionCommands;
|
|
36
|
+
now(): string;
|
|
37
|
+
previewProjection(record: NativeSubscriptionPreview): Preview;
|
|
38
|
+
}): SubscriptionDriver<LegacySubscriptionRequest<Action, Payload>, NativePreviewProjection<Preview>, Record<string, unknown>> {
|
|
39
|
+
return {
|
|
40
|
+
async preview(context, request) {
|
|
41
|
+
const command = encodeNativeSubscriptionCommand(context, request, input.now());
|
|
42
|
+
const payload = { ...command.payload };
|
|
43
|
+
delete payload.acceptedQuoteHash;
|
|
44
|
+
return decodeNativeSubscriptionPreview(await input.owner.preview({ ...command, payload }), input.previewProjection);
|
|
45
|
+
},
|
|
46
|
+
apply(context, request) { return input.owner.apply(encodeNativeSubscriptionCommand(context, request, input.now())); },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Contribution, RequiredSchema } from "@openlup/core/readiness";
|
|
2
|
+
|
|
3
|
+
/** Descriptors only: owner messages, workers and schedules stay with their owners. */
|
|
4
|
+
export function createAccountSupportContribution(input: {
|
|
5
|
+
version: string;
|
|
6
|
+
requiredSchema: RequiredSchema;
|
|
7
|
+
accountSupport: Readonly<Record<string, ((...args: never[]) => unknown) | null>>;
|
|
8
|
+
dependsOn: NonNullable<Contribution["manifest"]["module"]>["dependsOn"];
|
|
9
|
+
routes?: Contribution["routes"];
|
|
10
|
+
}) {
|
|
11
|
+
const routes = [...input.routes ?? []];
|
|
12
|
+
if (new Set(routes.map(route => route.id)).size !== routes.length) throw new Error("account_support_duplicate_route");
|
|
13
|
+
const contribution: Contribution = {
|
|
14
|
+
manifest: {
|
|
15
|
+
name: "@openlup/platform", version: input.version, kind: "rail",
|
|
16
|
+
module: { id: "account-support", kind: "capability", provides: ["./account-support", "./account-support/contracts"], dependsOn: input.dependsOn },
|
|
17
|
+
emits: [], handles: [], schedules: [], env: [], requiredPorts: Object.keys(input.accountSupport),
|
|
18
|
+
routes: routes.map(route => route.id), requiredSchema: input.requiredSchema,
|
|
19
|
+
}, handlers: [], schedules: [], routes,
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
contributions: [contribution],
|
|
23
|
+
ports: Object.fromEntries(Object.entries(input.accountSupport).filter(([, operation]) => operation !== null)
|
|
24
|
+
.map(([key, operation]) => [`@openlup/platform/account-support:${key}`, operation])),
|
|
25
|
+
routes: Object.fromEntries(routes.map(route => [`@openlup/platform/account-support:${route.id}`, route.handle])),
|
|
26
|
+
triggers: {},
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/composition/index.ts
CHANGED
|
@@ -22,3 +22,5 @@ export { createShippingContribution } from "./shippingContribution.js";
|
|
|
22
22
|
export { createFulfillmentContribution } from "./fulfillmentContribution.js";
|
|
23
23
|
export { createSubscriptionContribution } from "./subscriptionContribution.js";
|
|
24
24
|
export { createCommerceContribution } from "./commerceContribution.js";
|
|
25
|
+
export { createAccountSupportContribution } from "./accountSupportContribution.js";
|
|
26
|
+
export { createMarketingContribution } from "./marketingContribution.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Contribution, RequiredSchema } from "@openlup/core/readiness";
|
|
2
|
+
|
|
3
|
+
/** Describe existing selected marketing callables; never install another executor. */
|
|
4
|
+
export function createMarketingContribution(input: {
|
|
5
|
+
version: string;
|
|
6
|
+
requiredSchema: RequiredSchema;
|
|
7
|
+
marketing: Readonly<Record<string, ((...args: never[]) => unknown) | null>>;
|
|
8
|
+
handlers: Contribution["handlers"];
|
|
9
|
+
schedules: Contribution["schedules"];
|
|
10
|
+
routes: Contribution["routes"];
|
|
11
|
+
}) {
|
|
12
|
+
for (const [kind, names] of [
|
|
13
|
+
["handler", input.handlers.map(handler => handler.eventType)],
|
|
14
|
+
["schedule", input.schedules.map(schedule => schedule.id)],
|
|
15
|
+
["route", input.routes.map(route => route.id)],
|
|
16
|
+
] as const) {
|
|
17
|
+
if (new Set(names).size !== names.length) throw new Error(`marketing_duplicate_${kind}`);
|
|
18
|
+
}
|
|
19
|
+
const contribution: Contribution = {
|
|
20
|
+
manifest: {
|
|
21
|
+
name: "@openlup/platform", version: input.version, kind: "rail",
|
|
22
|
+
module: { id: "marketing", kind: "capability", provides: ["./marketing", "./marketing/contracts", "./marketing/server"],
|
|
23
|
+
// The server entry validates obligations with identity's runtime contract schema.
|
|
24
|
+
dependsOn: [{ module: "identity", entries: ["./identity/contracts"] }] },
|
|
25
|
+
emits: [], handles: input.handlers.map(handler => handler.eventType),
|
|
26
|
+
schedules: input.schedules.map(({ id, cadenceSeconds }) => ({ id, cadenceSeconds })), env: [],
|
|
27
|
+
requiredPorts: Object.keys(input.marketing), routes: input.routes.map(route => route.id), requiredSchema: input.requiredSchema,
|
|
28
|
+
},
|
|
29
|
+
handlers: input.handlers.map(({ eventType, timeoutMs }) => Object.freeze({ eventType, timeoutMs })), schedules: input.schedules, routes: input.routes,
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
contributions: [contribution],
|
|
33
|
+
ports: Object.fromEntries(Object.entries(input.marketing).filter(([, fn]) => typeof fn === "function").map(([name, fn]) => [`@openlup/platform/marketing:${name}`, fn])),
|
|
34
|
+
routes: Object.fromEntries(input.routes.map(route => [`@openlup/platform/marketing:${route.id}`, route.handle])),
|
|
35
|
+
triggers: Object.fromEntries(input.schedules.map(schedule => [`@openlup/platform/marketing:${schedule.id}`, schedule.run])),
|
|
36
|
+
};
|
|
37
|
+
}
|