@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.
Files changed (232) hide show
  1. package/CHANGELOG.md +386 -127
  2. package/README.md +251 -9
  3. package/dist/account-support/accountReadFacade.d.ts +8 -0
  4. package/dist/account-support/accountReadFacade.js +10 -0
  5. package/dist/account-support/accountReadModels.d.ts +69 -0
  6. package/dist/account-support/accountReadModels.js +272 -0
  7. package/dist/account-support/bindings.d.ts +23 -0
  8. package/dist/account-support/bindings.js +16 -0
  9. package/dist/account-support/contracts/account.d.ts +281 -0
  10. package/dist/account-support/contracts/account.js +1 -0
  11. package/dist/account-support/contracts/capabilities.d.ts +44 -0
  12. package/dist/account-support/contracts/capabilities.js +1 -0
  13. package/dist/account-support/contracts/commands.d.ts +307 -0
  14. package/dist/account-support/contracts/commands.js +1 -0
  15. package/dist/account-support/contracts/customer360.d.ts +154 -0
  16. package/dist/account-support/contracts/customer360.js +1 -0
  17. package/dist/account-support/contracts/index.d.ts +10 -0
  18. package/dist/account-support/contracts/index.js +1 -0
  19. package/dist/account-support/contracts/invoice.d.ts +79 -0
  20. package/dist/account-support/contracts/invoice.js +1 -0
  21. package/dist/account-support/contracts/operator.d.ts +289 -0
  22. package/dist/account-support/contracts/operator.js +1 -0
  23. package/dist/account-support/contribution.d.ts +24 -0
  24. package/dist/account-support/contribution.js +22 -0
  25. package/dist/account-support/customer360.d.ts +106 -0
  26. package/dist/account-support/customer360.js +163 -0
  27. package/dist/account-support/customerAccountReadDiagnostics.d.ts +11 -0
  28. package/dist/account-support/customerAccountReadDiagnostics.js +71 -0
  29. package/dist/account-support/index.d.ts +26 -0
  30. package/dist/account-support/index.js +15 -0
  31. package/dist/account-support/invoiceAccess.d.ts +57 -0
  32. package/dist/account-support/invoiceAccess.js +40 -0
  33. package/dist/account-support/migrations.d.ts +14 -0
  34. package/dist/account-support/migrations.js +7 -0
  35. package/dist/account-support/operatorAuthority.d.ts +67 -0
  36. package/dist/account-support/operatorAuthority.js +379 -0
  37. package/dist/account-support/operatorRecovery.d.ts +66 -0
  38. package/dist/account-support/operatorRecovery.js +129 -0
  39. package/dist/account-support/orderHistory.d.ts +50 -0
  40. package/dist/account-support/orderHistory.js +188 -0
  41. package/dist/account-support/paymentMethodComposition.d.ts +5 -0
  42. package/dist/account-support/paymentMethodComposition.js +4 -0
  43. package/dist/account-support/postgresOperatorAuthority.d.ts +16 -0
  44. package/dist/account-support/postgresOperatorAuthority.js +93 -0
  45. package/dist/account-support/selfService.d.ts +6 -0
  46. package/dist/account-support/selfService.js +81 -0
  47. package/dist/account-support/subscriptionCommandCodec.d.ts +10 -0
  48. package/dist/account-support/subscriptionCommandCodec.js +38 -0
  49. package/dist/catalog/draftSchemas.d.ts +1 -1
  50. package/dist/catalog/foundationSchemas.d.ts +3 -3
  51. package/dist/catalog/resolvedSkuProjection.d.ts +2 -2
  52. package/dist/catalog/sellableSchemas.d.ts +2 -2
  53. package/dist/commerce/checkoutCommandSchemas.d.ts +2 -2
  54. package/dist/commerce/checkoutResumeSchemas.d.ts +6 -6
  55. package/dist/commerce/checkoutSchemas.d.ts +12 -12
  56. package/dist/commerce/commerceReturnsPort.d.ts +2 -2
  57. package/dist/commerce/configuratorIntentSchemas.d.ts +2 -2
  58. package/dist/commerce/contractPrimitives.d.ts +8 -8
  59. package/dist/commerce/customerDefaultsSnapshotSchemas.d.ts +4 -4
  60. package/dist/commerce/model/omsReadModelHelpers.d.ts +4 -4
  61. package/dist/commerce/offerAvailabilitySchemas.d.ts +1 -1
  62. package/dist/commerce/oms/omsContractBase.d.ts +15 -15
  63. package/dist/commerce/oms/omsContractReadAuxSchemas.d.ts +11 -11
  64. package/dist/commerce/oms/omsContractReadSchemas.d.ts +121 -121
  65. package/dist/commerce/oms/omsContractRequests.d.ts +13 -13
  66. package/dist/commerce/oms/omsControlPlane.d.ts +22 -22
  67. package/dist/commerce/oms/omsFulfillmentContractSchemas.d.ts +12 -12
  68. package/dist/commerce/oms/omsFulfillmentDebugSchemas.d.ts +12 -12
  69. package/dist/commerce/orderReviewContracts.d.ts +33 -33
  70. package/dist/commerce/outboxEventSchemas.d.ts +4 -4
  71. package/dist/commerce/schemas.d.ts +21 -21
  72. package/dist/commerce/server/orderReviewHandlers.d.ts +10 -10
  73. package/dist/communications/consentPurposeRegistry.d.ts +1 -1
  74. package/dist/communications/newsletterIntegrationContracts.d.ts +8 -8
  75. package/dist/composition/accountSupportContribution.d.ts +18 -0
  76. package/dist/composition/accountSupportContribution.js +21 -0
  77. package/dist/composition/index.d.ts +2 -0
  78. package/dist/composition/index.js +2 -0
  79. package/dist/composition/marketingContribution.d.ts +21 -0
  80. package/dist/composition/marketingContribution.js +29 -0
  81. package/dist/fulfillment/handlerSchemas.d.ts +40 -40
  82. package/dist/identity/customerContracts.d.ts +2 -2
  83. package/dist/invoicing/snapshot.d.ts +1 -1
  84. package/dist/marketing/acquisitionCaseSchemas.d.ts +103 -0
  85. package/dist/marketing/acquisitionCaseSchemas.js +53 -0
  86. package/dist/marketing/acquisitionEvidenceSchemas.d.ts +105 -0
  87. package/dist/marketing/acquisitionEvidenceSchemas.js +55 -0
  88. package/dist/marketing/analytics/dataLayer.d.ts +18 -0
  89. package/dist/marketing/analytics/dataLayer.js +111 -0
  90. package/dist/marketing/analytics/noop.d.ts +9 -0
  91. package/dist/marketing/analytics/noop.js +8 -0
  92. package/dist/marketing/analytics/purchaseFacts.d.ts +4 -0
  93. package/dist/marketing/analytics/purchaseFacts.js +69 -0
  94. package/dist/marketing/analytics/routePolicy.d.ts +10 -0
  95. package/dist/marketing/analytics/routePolicy.js +59 -0
  96. package/dist/marketing/consentEvidence.d.ts +8 -0
  97. package/dist/marketing/consentEvidence.js +32 -0
  98. package/dist/marketing/contactLegal.d.ts +14 -0
  99. package/dist/marketing/contactLegal.js +19 -0
  100. package/dist/marketing/contracts/acquisition.d.ts +88 -0
  101. package/dist/marketing/contracts/acquisition.js +1 -0
  102. package/dist/marketing/contracts/acquisitionPorts.d.ts +49 -0
  103. package/dist/marketing/contracts/acquisitionPorts.js +1 -0
  104. package/dist/marketing/contracts/analytics.d.ts +144 -0
  105. package/dist/marketing/contracts/analytics.js +1 -0
  106. package/dist/marketing/contracts/contactLegal.d.ts +53 -0
  107. package/dist/marketing/contracts/contactLegal.js +1 -0
  108. package/dist/marketing/contracts/index.d.ts +6 -0
  109. package/dist/marketing/contracts/index.js +1 -0
  110. package/dist/marketing/contracts/prelaunch.d.ts +39 -0
  111. package/dist/marketing/contracts/prelaunch.js +1 -0
  112. package/dist/marketing/index.d.ts +16 -0
  113. package/dist/marketing/index.js +14 -0
  114. package/dist/marketing/prelaunchAcquisitionSchemas.d.ts +116 -0
  115. package/dist/marketing/prelaunchAcquisitionSchemas.js +51 -0
  116. package/dist/marketing/seo/jsonLd.d.ts +157 -0
  117. package/dist/marketing/seo/jsonLd.js +127 -0
  118. package/dist/marketing/seo/metadata.d.ts +41 -0
  119. package/dist/marketing/seo/metadata.js +36 -0
  120. package/dist/marketing/seo/serializeJsonLd.d.ts +1 -0
  121. package/dist/marketing/seo/serializeJsonLd.js +6 -0
  122. package/dist/marketing/seo/urls.d.ts +14 -0
  123. package/dist/marketing/seo/urls.js +23 -0
  124. package/dist/marketing/server/acquisitionCaseDirect.d.ts +4 -0
  125. package/dist/marketing/server/acquisitionCaseDirect.js +88 -0
  126. package/dist/marketing/server/acquisitionCaseHandlers.d.ts +28 -0
  127. package/dist/marketing/server/acquisitionCaseHandlers.js +40 -0
  128. package/dist/marketing/server/acquisitionCasePorts.d.ts +42 -0
  129. package/dist/marketing/server/acquisitionCasePorts.js +1 -0
  130. package/dist/marketing/server/acquisitionEvidenceDirect.d.ts +5 -0
  131. package/dist/marketing/server/acquisitionEvidenceDirect.js +100 -0
  132. package/dist/marketing/server/analyticsContext.d.ts +3 -0
  133. package/dist/marketing/server/analyticsContext.js +65 -0
  134. package/dist/marketing/server/analyticsContextPorts.d.ts +31 -0
  135. package/dist/marketing/server/analyticsContextPorts.js +1 -0
  136. package/dist/marketing/server/bindings.d.ts +14 -0
  137. package/dist/marketing/server/bindings.js +18 -0
  138. package/dist/marketing/server/directPorts.d.ts +26 -0
  139. package/dist/marketing/server/directPorts.js +1 -0
  140. package/dist/marketing/server/index.d.ts +14 -0
  141. package/dist/marketing/server/index.js +11 -0
  142. package/dist/marketing/server/postgres/acquisitionCaseRoutines.d.ts +10 -0
  143. package/dist/marketing/server/postgres/acquisitionCaseRoutines.js +204 -0
  144. package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.d.ts +10 -0
  145. package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.js +107 -0
  146. package/dist/marketing/server/postgres/index.d.ts +3 -0
  147. package/dist/marketing/server/postgres/index.js +3 -0
  148. package/dist/marketing/server/prelaunchAcquisitionDirect.d.ts +4 -0
  149. package/dist/marketing/server/prelaunchAcquisitionDirect.js +122 -0
  150. package/dist/marketing/server/privacyContributor.d.ts +66 -0
  151. package/dist/marketing/server/privacyContributor.js +87 -0
  152. package/dist/marketing/server/research/adminSurveyResponsesHandler.d.ts +9 -0
  153. package/dist/marketing/server/research/adminSurveyResponsesHandler.js +53 -0
  154. package/dist/marketing/server/research/managedSurveyResponseSubmit.d.ts +39 -0
  155. package/dist/marketing/server/research/managedSurveyResponseSubmit.js +102 -0
  156. package/dist/marketing/server/research/surveyResponseSubmitHandler.d.ts +8 -0
  157. package/dist/marketing/server/research/surveyResponseSubmitHandler.js +41 -0
  158. package/dist/marketing/validation.d.ts +2 -0
  159. package/dist/marketing/validation.js +3 -0
  160. package/dist/pricing/acquisitionQuoteBinding.d.ts +1 -1
  161. package/dist/pricing/catalogFactsSchemas.d.ts +1 -1
  162. package/dist/pricing/quotePromotionEvidence.d.ts +1 -1
  163. package/dist/pricing/quoteSchemas.d.ts +56 -56
  164. package/dist/promotions/adminPromotionCodesContracts.d.ts +30 -30
  165. package/dist/promotions/adminPromotionsContracts.d.ts +14 -14
  166. package/package.json +32 -2
  167. package/release-gates.json +99 -4
  168. package/sql/20261026120000_account_support_operator_authority.sql +693 -0
  169. package/src/account-support/accountReadFacade.ts +15 -0
  170. package/src/account-support/accountReadModels.ts +371 -0
  171. package/src/account-support/bindings.ts +28 -0
  172. package/src/account-support/contracts/account.ts +306 -0
  173. package/src/account-support/contracts/capabilities.ts +30 -0
  174. package/src/account-support/contracts/commands.ts +243 -0
  175. package/src/account-support/contracts/customer360.ts +150 -0
  176. package/src/account-support/contracts/index.ts +141 -0
  177. package/src/account-support/contracts/invoice.ts +44 -0
  178. package/src/account-support/contracts/operator.ts +123 -0
  179. package/src/account-support/contribution.ts +35 -0
  180. package/src/account-support/customer360.ts +268 -0
  181. package/src/account-support/customerAccountReadDiagnostics.ts +83 -0
  182. package/src/account-support/index.ts +72 -0
  183. package/src/account-support/invoiceAccess.ts +63 -0
  184. package/src/account-support/migrations.ts +7 -0
  185. package/src/account-support/operatorAuthority.ts +393 -0
  186. package/src/account-support/operatorRecovery.ts +136 -0
  187. package/src/account-support/orderHistory.ts +309 -0
  188. package/src/account-support/paymentMethodComposition.ts +8 -0
  189. package/src/account-support/postgresOperatorAuthority.ts +92 -0
  190. package/src/account-support/selfService.ts +83 -0
  191. package/src/account-support/subscriptionCommandCodec.ts +48 -0
  192. package/src/composition/accountSupportContribution.ts +28 -0
  193. package/src/composition/index.ts +2 -0
  194. package/src/composition/marketingContribution.ts +37 -0
  195. package/src/marketing/acquisitionCaseSchemas.ts +75 -0
  196. package/src/marketing/acquisitionEvidenceSchemas.ts +72 -0
  197. package/src/marketing/analytics/dataLayer.ts +100 -0
  198. package/src/marketing/analytics/noop.ts +14 -0
  199. package/src/marketing/analytics/purchaseFacts.ts +63 -0
  200. package/src/marketing/analytics/routePolicy.ts +55 -0
  201. package/src/marketing/consentEvidence.ts +36 -0
  202. package/src/marketing/contactLegal.ts +22 -0
  203. package/src/marketing/contracts/acquisition.ts +50 -0
  204. package/src/marketing/contracts/acquisitionPorts.ts +48 -0
  205. package/src/marketing/contracts/analytics.ts +88 -0
  206. package/src/marketing/contracts/contactLegal.ts +41 -0
  207. package/src/marketing/contracts/index.ts +17 -0
  208. package/src/marketing/contracts/prelaunch.ts +15 -0
  209. package/src/marketing/index.ts +16 -0
  210. package/src/marketing/prelaunchAcquisitionSchemas.ts +64 -0
  211. package/src/marketing/seo/jsonLd.ts +153 -0
  212. package/src/marketing/seo/metadata.ts +62 -0
  213. package/src/marketing/seo/serializeJsonLd.ts +6 -0
  214. package/src/marketing/seo/urls.ts +34 -0
  215. package/src/marketing/server/acquisitionCaseDirect.ts +67 -0
  216. package/src/marketing/server/acquisitionCaseHandlers.ts +57 -0
  217. package/src/marketing/server/acquisitionCasePorts.ts +42 -0
  218. package/src/marketing/server/acquisitionEvidenceDirect.ts +76 -0
  219. package/src/marketing/server/analyticsContext.ts +55 -0
  220. package/src/marketing/server/analyticsContextPorts.ts +28 -0
  221. package/src/marketing/server/bindings.ts +27 -0
  222. package/src/marketing/server/directPorts.ts +27 -0
  223. package/src/marketing/server/index.ts +14 -0
  224. package/src/marketing/server/postgres/acquisitionCaseRoutines.ts +218 -0
  225. package/src/marketing/server/postgres/acquisitionEvidenceRoutines.ts +103 -0
  226. package/src/marketing/server/postgres/index.ts +3 -0
  227. package/src/marketing/server/prelaunchAcquisitionDirect.ts +121 -0
  228. package/src/marketing/server/privacyContributor.ts +123 -0
  229. package/src/marketing/server/research/adminSurveyResponsesHandler.ts +78 -0
  230. package/src/marketing/server/research/managedSurveyResponseSubmit.ts +158 -0
  231. package/src/marketing/server/research/surveyResponseSubmitHandler.ts +57 -0
  232. 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
+ }
@@ -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
+ }