@openlup/platform 0.27.0 → 0.28.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 (115) hide show
  1. package/CHANGELOG.md +167 -109
  2. package/README.md +111 -8
  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 +1 -0
  78. package/dist/composition/index.js +1 -0
  79. package/dist/fulfillment/handlerSchemas.d.ts +40 -40
  80. package/dist/identity/customerContracts.d.ts +2 -2
  81. package/dist/invoicing/snapshot.d.ts +1 -1
  82. package/dist/pricing/acquisitionQuoteBinding.d.ts +1 -1
  83. package/dist/pricing/catalogFactsSchemas.d.ts +1 -1
  84. package/dist/pricing/quotePromotionEvidence.d.ts +1 -1
  85. package/dist/pricing/quoteSchemas.d.ts +56 -56
  86. package/dist/promotions/adminPromotionCodesContracts.d.ts +30 -30
  87. package/dist/promotions/adminPromotionsContracts.d.ts +14 -14
  88. package/package.json +12 -2
  89. package/release-gates.json +30 -3
  90. package/sql/20261026120000_account_support_operator_authority.sql +693 -0
  91. package/src/account-support/accountReadFacade.ts +15 -0
  92. package/src/account-support/accountReadModels.ts +371 -0
  93. package/src/account-support/bindings.ts +28 -0
  94. package/src/account-support/contracts/account.ts +306 -0
  95. package/src/account-support/contracts/capabilities.ts +30 -0
  96. package/src/account-support/contracts/commands.ts +243 -0
  97. package/src/account-support/contracts/customer360.ts +150 -0
  98. package/src/account-support/contracts/index.ts +141 -0
  99. package/src/account-support/contracts/invoice.ts +44 -0
  100. package/src/account-support/contracts/operator.ts +123 -0
  101. package/src/account-support/contribution.ts +35 -0
  102. package/src/account-support/customer360.ts +268 -0
  103. package/src/account-support/customerAccountReadDiagnostics.ts +83 -0
  104. package/src/account-support/index.ts +72 -0
  105. package/src/account-support/invoiceAccess.ts +63 -0
  106. package/src/account-support/migrations.ts +7 -0
  107. package/src/account-support/operatorAuthority.ts +393 -0
  108. package/src/account-support/operatorRecovery.ts +136 -0
  109. package/src/account-support/orderHistory.ts +309 -0
  110. package/src/account-support/paymentMethodComposition.ts +8 -0
  111. package/src/account-support/postgresOperatorAuthority.ts +92 -0
  112. package/src/account-support/selfService.ts +83 -0
  113. package/src/account-support/subscriptionCommandCodec.ts +48 -0
  114. package/src/composition/accountSupportContribution.ts +28 -0
  115. package/src/composition/index.ts +1 -0
@@ -0,0 +1,268 @@
1
+ import type {
2
+ Customer360Snapshot,
3
+ Customer360Subject,
4
+ Customer360ContactHealth,
5
+ Customer360ReadPort,
6
+ Customer360Lookup,
7
+ Customer360SummaryReadPort,
8
+ DiagnosticHistoryReadPort
9
+ } from "./contracts/customer360.js";
10
+
11
+ export const SUPPORT_CUSTOMER_360_CONTRACT_VERSION = "support.customer_360.v2" as const;
12
+ type Row = Record<string, unknown>;
13
+ export type CustomerJourneyAuditEvent = {
14
+ eventId: string;
15
+ occurredAt: string;
16
+ action: string;
17
+ outcome: string;
18
+ entity: { kind: "subject" | "order" | "subscription" | "dunning_case"; id: string }
19
+ };
20
+
21
+ export interface ContactHealthDelivery {
22
+ readonly status: string;
23
+ readonly occurredAt: string | null;
24
+ readonly permanentBounce: boolean | null
25
+ }
26
+
27
+ export interface Customer360ProjectionInput<Extension = unknown> {
28
+ subject: {
29
+ subjectId: string;
30
+ displayName: string | null;
31
+ email: string | null;
32
+ phone?: string | null;
33
+ lifecycleStage: Customer360Subject["lifecycleStage"];
34
+ createdAt: string | null;
35
+ lastActivityAt: string | null
36
+ };
37
+ legacy: {
38
+ lookup: { warnings: string[] };
39
+ customer: { authUserLinked: boolean };
40
+ orders: { orderId: string; orderNumber: string | null; status: string | null; omsProjection: Row }[];
41
+ subscriptions: {
42
+ subscriptionId: string;
43
+ status: string | null;
44
+ nextCycleAt: string | null;
45
+ templateVersion?: number | null;
46
+ paymentMethodStatus?: string | null;
47
+ renewalOrders: Row[]
48
+ }[];
49
+ payment: { status: string | null; intents: Row[] };
50
+ timeline: Row[];
51
+ } | null;
52
+ cases: Row[];
53
+ notices: Row[];
54
+ tokens: Row[];
55
+ recoveryAudit: Row[];
56
+ orderTruth: Row[];
57
+ now: number;
58
+ contactHealth?: Customer360ContactHealth | null;
59
+ signedIn?: boolean;
60
+ addresses?: Customer360Snapshot["addresses"] | null;
61
+ subscriptionPointers?: Map<string, { shippingAddressId: string | null; paymentMethodKind: string | null }> | null;
62
+ /** false means failed/unavailable; null means no enrichment selected. */
63
+ profile?: { extension: Extension; partial: boolean } | false | null;
64
+ }
65
+
66
+ export interface Customer360ProjectionPolicy {
67
+ lifecycle(subject: Customer360ProjectionInput["subject"]): Customer360Subject["lifecycle"];
68
+ /** Stable audit identity hashing remains in the server host. */
69
+ timelineAudit(event: Row, subjectId: string): CustomerJourneyAuditEvent[];
70
+ }
71
+
72
+ export function projectCustomer360Snapshot<Extension>(
73
+ input: Customer360ProjectionInput<Extension>,
74
+ policy: Customer360ProjectionPolicy
75
+ ): Customer360Snapshot<Extension> {
76
+ const { subject, legacy, cases, notices, tokens, recoveryAudit, orderTruth, now } = input;
77
+ const orders = legacy?.orders.map(order => ({
78
+ orderId: order.orderId,
79
+ orderNumber: order.orderNumber,
80
+ status: order.status ?? "unknown",
81
+ subscriptionId: string(order.omsProjection.subscriptionId),
82
+ totalMinor: integer(order.omsProjection.totalMinor),
83
+ currency: string(order.omsProjection.currency),
84
+ createdAt: date(order.omsProjection.createdAt)
85
+ })) ?? [];
86
+ const pending = (row: Row) => orderTruth.some(
87
+ order => order.id === row.order_id && order.client_id === row.client_id && order.status === "pending_payment"
88
+ );
89
+ const eligible = (row: Row) => subject.lifecycleStage === "customer" && row.status === "open" && pending(row);
90
+ const audit = boundedCustomerJourneyAudit([
91
+ ...(legacy?.timeline ?? []).flatMap(event => policy.timelineAudit(event, subject.subjectId)),
92
+ ...recoveryAudit.flatMap(recoveryEvent)
93
+ ]);
94
+ const profile = input.profile;
95
+ const warnings = [...new Set([
96
+ ...(legacy?.lookup.warnings ?? []),
97
+ ...(audit.windowed ? ["evidence_window_limited:customer_360_audit_trail"] : []),
98
+ ...(recoveryAudit.length === 80 ? ["evidence_window_limited:customer_recovery_audit"] : []),
99
+ ...(profile === false ? ["evidence_unavailable:customer_profile"] : profile?.partial ? ["evidence_partial:customer_profile"] : [])
100
+ ])];
101
+ return {
102
+ contractVersion: SUPPORT_CUSTOMER_360_CONTRACT_VERSION,
103
+ lookup: { query: subject.subjectId, matchedBy: "subject_id", confidence: "exact", warnings },
104
+ ...(input.contactHealth ? { contactHealth: input.contactHealth } : {}),
105
+ ...(input.addresses ? { addresses: input.addresses } : {}),
106
+ ...(profile ? { extension: profile.extension } : {}),
107
+ subject: {
108
+ subjectId: subject.subjectId,
109
+ displayName: subject.displayName,
110
+ email: subject.email,
111
+ phone: subject.phone ?? null,
112
+ lifecycleStage: subject.lifecycleStage,
113
+ lifecycle: policy.lifecycle(subject),
114
+ firstSeenAt: subject.createdAt,
115
+ lastActivityAt: latest([...(legacy?.timeline ?? []), ...cases, ...recoveryAudit]) ?? subject.lastActivityAt,
116
+ ...(legacy ? { authUserLinked: legacy.customer.authUserLinked } : {}),
117
+ ...(input.signedIn === undefined ? {} : { hasSignedIn: input.signedIn })
118
+ },
119
+ orders,
120
+ subscriptions: legacy?.subscriptions.map(value => ({
121
+ subscriptionId: value.subscriptionId,
122
+ status: value.status ?? "unknown",
123
+ nextCycleAt: value.nextCycleAt,
124
+ latestOrderId: string(value.renewalOrders[0]?.orderId),
125
+ ...(typeof value.templateVersion === "number" ? { templateVersion: value.templateVersion } : {}),
126
+ ...(input.subscriptionPointers
127
+ ? {
128
+ shippingAddressId: input.subscriptionPointers.get(value.subscriptionId)?.shippingAddressId ?? null,
129
+ paymentMethodKind: input.subscriptionPointers.get(value.subscriptionId)?.paymentMethodKind ?? null
130
+ }
131
+ : {}),
132
+ paymentMethodStatus: string(value.paymentMethodStatus)
133
+ })) ?? [],
134
+ payment: orders.map(order => {
135
+ const intent = legacy?.payment.intents.find(value => string(value.orderId) === order.orderId);
136
+ return {
137
+ orderId: order.orderId,
138
+ status: string(intent?.status) ?? legacy?.payment.status ?? "unknown",
139
+ recoverable: cases.some(row => row.order_id === order.orderId && eligible(row)),
140
+ amountMinor: order.totalMinor,
141
+ currency: order.currency,
142
+ lastAttemptAt: date(intent?.updatedAt)
143
+ };
144
+ }),
145
+ dunningCases: cases.map(row => ({
146
+ caseId: string(row.id)!,
147
+ subscriptionId: string(row.subscription_id)!,
148
+ orderId: string(row.order_id),
149
+ status: string(row.status) ?? "unknown",
150
+ retryAttempt: integer(row.retry_attempt) ?? 0,
151
+ nextRetryAt: date(row.next_retry_at),
152
+ notificationStatus: string(latestNotice(notices, string(row.id)!)?.status),
153
+ recoveryAvailable: eligible(row)
154
+ })),
155
+ recovery: cases.map(row => {
156
+ const id = string(row.id)!, related = tokens.filter(value => value.case_id === id);
157
+ return {
158
+ caseId: id,
159
+ status: pending(row) && related.some(value => {
160
+ const expiry = date(value.expires_at);
161
+ return Boolean(expiry && Date.parse(expiry) > now && !value.used_at && !value.revoked_at);
162
+ }) ? "available" : "unavailable",
163
+ actionAvailable: eligible(row),
164
+ lastIssuedAt: related.map(value => date(value.created_at)).filter(value => value !== null).sort().at(-1) ?? null,
165
+ lastDeliveryStatus: string(latestNotice(notices, id, "payment_recovery")?.status)
166
+ };
167
+ }),
168
+ auditTrail: audit.events
169
+ };
170
+ }
171
+
172
+ export function boundedCustomerJourneyAudit(events: CustomerJourneyAuditEvent[]) {
173
+ const instant = (value: string) => Number.isFinite(Date.parse(value)) ? Date.parse(value) : 0;
174
+ const newest = [...events].sort((a, b) => instant(b.occurredAt) - instant(a.occurredAt) || a.eventId.localeCompare(b.eventId));
175
+ return {
176
+ events: newest.slice(0, 80).sort((a, b) => instant(a.occurredAt) - instant(b.occurredAt) || a.eventId.localeCompare(b.eventId)),
177
+ windowed: newest.length > 80
178
+ };
179
+ }
180
+
181
+ export function deriveContactHealth(deliveries: readonly ContactHealthDelivery[]): Customer360ContactHealth {
182
+ const terminal = ["delivered", "bounced", "complained", "failed"] as const;
183
+ const newest = deliveries.filter(
184
+ (value): value is ContactHealthDelivery & { occurredAt: string } =>
185
+ (terminal as readonly string[]).includes(value.status) && typeof value.occurredAt === "string"
186
+ )
187
+ .reduce<(ContactHealthDelivery & { occurredAt: string }) | null>(
188
+ (best, value) => best === null || value.occurredAt > best.occurredAt ? value : best,
189
+ null
190
+ );
191
+ if (!newest) return { lastTerminalStatus: null, lastTerminalAt: null, reachable: true };
192
+ return {
193
+ lastTerminalStatus: newest.status as typeof terminal[number],
194
+ lastTerminalAt: newest.occurredAt,
195
+ reachable: newest.status === "delivered" || (newest.status === "bounced" && newest.permanentBounce === false)
196
+ };
197
+ }
198
+
199
+ /** Authorize before every source read; retain host defaults, ambiguity, null and failure semantics. */
200
+ export function createCustomer360ReadFacade<Search, Snapshot, SummaryRequest, SummaryResult, DiagnosticRequest, DiagnosticResult>(ports: {
201
+ authorize(): Promise<boolean>;
202
+ journey: Customer360ReadPort<Search, Snapshot>;
203
+ summary: Customer360SummaryReadPort<SummaryRequest, SummaryResult>;
204
+ diagnostics: DiagnosticHistoryReadPort<DiagnosticRequest, DiagnosticResult>;
205
+ }) {
206
+ async function read<Result>(query: () => Promise<Result>) {
207
+ if (!await ports.authorize()) return { kind: "denied" as const };
208
+ return query();
209
+ }
210
+ return {
211
+ searchCustomerJourney: (request: Customer360Lookup) => read(() => ports.journey.searchCustomerJourney(request)),
212
+ getCustomerJourney: (request: Customer360Lookup) => read(() => ports.journey.getCustomerJourney(request)),
213
+ getPortableSummary: (request: SummaryRequest) => read(() => ports.summary.getPortableSummary(request)),
214
+ history: (request: DiagnosticRequest) => read(() => ports.diagnostics.history(request))
215
+ };
216
+ }
217
+
218
+ function recoveryEvent(row: Row): CustomerJourneyAuditEvent[] {
219
+ const eventId = string(row.id),
220
+ occurredAt = date(row.occurred_at),
221
+ id = string(row.case_id),
222
+ outcome = string(row.outcome);
223
+ if (!eventId || !occurredAt || !id || !outcome) return [];
224
+ const refusal = string(row.refusal_code);
225
+ return [{
226
+ eventId,
227
+ occurredAt,
228
+ action: "recovery.issue",
229
+ outcome: refusal ? `${outcome}:${refusal}` : outcome,
230
+ entity: { kind: "dunning_case", id }
231
+ }];
232
+ }
233
+
234
+ function latestNotice(rows: Row[], id: string, kind?: string) {
235
+ return rows.filter(
236
+ row => row.case_id === id && row.recipient_kind === "customer" && (!kind || row.notification_kind === kind)
237
+ )
238
+ .sort(
239
+ (a, b) => `${string(b.created_at) ?? ""}:${string(b.id) ?? ""}`.localeCompare(
240
+ `${string(a.created_at) ?? ""}:${string(a.id) ?? ""}`
241
+ )
242
+ )[0];
243
+ }
244
+
245
+ function string(value: unknown): string | null {
246
+ return typeof value === "string" && value.trim() ? value.trim() : null;
247
+ }
248
+
249
+ function integer(value: unknown): number | null {
250
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
251
+ }
252
+
253
+ function date(value: unknown): string | null {
254
+ const text = string(value);
255
+ return text && !Number.isNaN(Date.parse(text)) ? text : null;
256
+ }
257
+
258
+ function latest(rows: Row[]): string | null {
259
+ return rows.map(
260
+ row => date(
261
+ row.updated_at ?? row.updatedAt ?? row.created_at ?? row.createdAt ?? row.occurred_at ?? row.occurredAt
262
+ )
263
+ )
264
+ .filter(value => value !== null)
265
+ .sort((a, b) => Date.parse(a) - Date.parse(b))
266
+ .at(-1)
267
+ ?? null;
268
+ }
@@ -0,0 +1,83 @@
1
+ export type CustomerAccountReadStage = "account_aggregate";
2
+
3
+ type DiagnosticRecord = Record<string, unknown>;
4
+
5
+ export class CustomerAccountReadModelError extends Error {
6
+ readonly stage: CustomerAccountReadStage;
7
+ readonly cause: unknown;
8
+
9
+ constructor(stage: CustomerAccountReadStage, cause: unknown) {
10
+ super(`Customer account read model failed at ${stage}`);
11
+ this.name = "CustomerAccountReadModelError";
12
+ this.stage = stage;
13
+ this.cause = cause;
14
+ }
15
+ }
16
+
17
+ export async function withCustomerAccountReadStage<T>(
18
+ stage: CustomerAccountReadStage,
19
+ operation: () => Promise<T>,
20
+ ): Promise<T> {
21
+ try {
22
+ return await operation();
23
+ } catch (error) {
24
+ if (error instanceof CustomerAccountReadModelError) throw error;
25
+ throw new CustomerAccountReadModelError(stage, error);
26
+ }
27
+ }
28
+
29
+ export function customerAccountReadFailureDetails(error: unknown): DiagnosticRecord {
30
+ if (!(error instanceof CustomerAccountReadModelError)) {
31
+ return { reason: "read_model_failed", stage: "unknown" };
32
+ }
33
+ return {
34
+ reason: "read_model_failed",
35
+ stage: error.stage,
36
+ providerCode: diagnosticText(readRecord(error.cause).code),
37
+ };
38
+ }
39
+
40
+ export function logCustomerAccountReadFailure(error: unknown): void {
41
+ if (error instanceof CustomerAccountReadModelError) {
42
+ console.error("[customer-account] read model failed:", JSON.stringify({
43
+ stage: error.stage,
44
+ cause: safeCause(error.cause),
45
+ }));
46
+ return;
47
+ }
48
+
49
+ console.error("[customer-account] read failed:", JSON.stringify({ cause: safeCause(error) }));
50
+ }
51
+
52
+ function safeCause(error: unknown): DiagnosticRecord {
53
+ if (error instanceof Error) {
54
+ const record = readRecord(error);
55
+ return compact({
56
+ name: error.name,
57
+ message: error.message,
58
+ code: diagnosticText(record.code),
59
+ hint: diagnosticText(record.hint),
60
+ details: diagnosticText(record.details),
61
+ });
62
+ }
63
+ return compact({
64
+ type: typeof error,
65
+ code: diagnosticText(readRecord(error).code),
66
+ message: diagnosticText(readRecord(error).message),
67
+ });
68
+ }
69
+
70
+ function readRecord(value: unknown): DiagnosticRecord {
71
+ return value && typeof value === "object" && !Array.isArray(value) ? value as DiagnosticRecord : {};
72
+ }
73
+
74
+ function diagnosticText(value: unknown): string | undefined {
75
+ if (typeof value !== "string") return undefined;
76
+ const trimmed = value.trim();
77
+ if (!trimmed) return undefined;
78
+ return trimmed.slice(0, 300);
79
+ }
80
+
81
+ function compact(value: DiagnosticRecord): DiagnosticRecord {
82
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
83
+ }
@@ -0,0 +1,72 @@
1
+ export { createAccountReadFacade } from "./accountReadFacade.js";
2
+ export { CustomerAccountReadModelError, withCustomerAccountReadStage, customerAccountReadFailureDetails, logCustomerAccountReadFailure } from "./customerAccountReadDiagnostics.js";
3
+ export type { CustomerAccountReadStage } from "./customerAccountReadDiagnostics.js";
4
+ export type * from "./contracts/index.js";
5
+ export {
6
+ CUSTOMER_ACCOUNT_V2_CONTRACT_VERSION,
7
+ readAccountSnapshot,
8
+ isCustomerVisibleSubscription,
9
+ recurringPrice,
10
+ computeEditState,
11
+ editCutoff,
12
+ buildActionRequired
13
+ } from "./accountReadModels.js";
14
+ export type {
15
+ AccountReadSources,
16
+ AccountReadProjectionPolicy,
17
+ CustomerSubscriptionEligibilityInput,
18
+ CustomerSubscriptionEligibility,
19
+ SubscriptionActionState
20
+ } from "./accountReadModels.js";
21
+ export {
22
+ CUSTOMER_ORDERS_CONTRACT_VERSION,
23
+ readCustomerOrderSummaries,
24
+ readCustomerOrderDetail,
25
+ customerOrderMoney,
26
+ toOrderMoneyHeader,
27
+ toOrderMoneyItem,
28
+ mapCustomerOrderLines,
29
+ lineListTotalMinor
30
+ } from "./orderHistory.js";
31
+ export type {
32
+ CustomerOrderHistoryRow,
33
+ CustomerOrderHistoryReadStore,
34
+ CustomerOrderLinePresentationPort,
35
+ CustomerOrderHistoryProjectionPort
36
+ } from "./orderHistory.js";
37
+ export {
38
+ SUPPORT_CUSTOMER_360_CONTRACT_VERSION,
39
+ projectCustomer360Snapshot,
40
+ deriveContactHealth,
41
+ boundedCustomerJourneyAudit,
42
+ createCustomer360ReadFacade
43
+ } from "./customer360.js";
44
+ export type {
45
+ Customer360ProjectionInput,
46
+ Customer360ProjectionPolicy,
47
+ ContactHealthDelivery,
48
+ CustomerJourneyAuditEvent
49
+ } from "./customer360.js";
50
+ export { createInvoiceAccess, downloadOwnedInvoicePdf, deriveCustomerDocumentDeliveryStatus } from "./invoiceAccess.js";
51
+ export type {
52
+ InvoiceOwnerAccessPort,
53
+ InvoiceArtifactDownloadPort,
54
+ InvoiceAccessOptions,
55
+ CustomerInvoiceDocumentStatusInput
56
+ } from "./invoiceAccess.js";
57
+ export { createSelfServiceCommandFacade } from "./selfService.js";
58
+ export { encodeNativeSubscriptionCommand, decodeNativeSubscriptionPreview, createNativeSubscriptionDriver } from "./subscriptionCommandCodec.js";
59
+ export { createPaymentMethodComposition } from "./paymentMethodComposition.js";
60
+ export { createOperatorRecovery, moveIdentityForEmailCorrection, OperatorIdentityMoveRefusedError, executeOperatorAccountLink, linkThenApply } from "./operatorRecovery.js";
61
+ export type { OperatorRecoverySelection } from "./operatorRecovery.js";
62
+ export { createManagedOperatorAuthority, operatorAuthorityCapabilities, CustomerSupportCommandInvalidError,
63
+ CustomerSupportCommandIdempotencyConflictError, OperatorSubscriptionAuthorityUnavailableError,
64
+ CustomerSupportCommandRequiresHumanError, CustomerSupportOperatorInactiveError, CustomerSupportOperatorNotProvisionedError } from "./operatorAuthority.js";
65
+ export type { OperatorRpcExecutor, OperatorAuthorityOptions } from "./operatorAuthority.js";
66
+ export { createPostgresOperatorAuthority } from "./postgresOperatorAuthority.js";
67
+ export type { OperatorQueryExecutor, OperatorTransactionExecutor } from "./postgresOperatorAuthority.js";
68
+ export { ACCOUNT_SUPPORT_MIGRATIONS } from "./migrations.js";
69
+ export { createAccountSupportBindings } from "./bindings.js";
70
+ export type { AccountSupportCallable, AccountSupportOperations, AccountSupportHostBindingRequired } from "./bindings.js";
71
+ export { createAccountSupportServices } from "./contribution.js";
72
+ export type { AccountSupportBindingRefusal, AccountSupportReadiness, GuardedAccountSupportOperation, GuardedAccountSupportOperations } from "./contribution.js";
@@ -0,0 +1,63 @@
1
+ import { createInvoiceReadModel } from "../invoicing/index.js";
2
+ import type { InvoiceHostPorts } from "../invoicing/contracts/index.js";
3
+ import type { CustomerDocumentDeliveryStatus } from "./contracts/invoice.js";
4
+
5
+ export type InvoiceOwnerAccessPort = InvoiceHostPorts["accountHistory"];
6
+ export type InvoiceArtifactDownloadPort = Pick<InvoiceHostPorts["artifacts"], "download">;
7
+
8
+ export interface InvoiceAccessOptions {
9
+ accountHistory?: InvoiceOwnerAccessPort;
10
+ artifacts?: InvoiceArtifactDownloadPort
11
+ }
12
+
13
+ export function createInvoiceAccess(options: InvoiceAccessOptions = {}) {
14
+ const accountHistory = options.accountHistory;
15
+ const owner = createInvoiceReadModel({
16
+ ...options,
17
+ accountHistory: accountHistory && {
18
+ read: input => accountHistory.read(input),
19
+ async resolveDownload(input) {
20
+ const history = await accountHistory.read({ accountRef: input.accountRef, orderId: input.orderId });
21
+ if (history.kind !== "found") return history;
22
+ if (history.accountRef !== input.accountRef || history.orderId !== input.orderId
23
+ || !history.documents.some(document => document.documentRef === input.documentRef)) return { kind: "denied" };
24
+ return accountHistory.resolveDownload(input);
25
+ }
26
+ }
27
+ });
28
+ return { history: owner.history, download: owner.download };
29
+ }
30
+
31
+ export function downloadOwnedInvoicePdf<Ticket, Pdf extends { content: Uint8Array; contentType: string }>(input: {
32
+ resolve(): Promise<Ticket | null>;
33
+ download(ticket: Ticket): Promise<Pdf>
34
+ }) {
35
+ return createInvoiceReadModel().ownedPdf(input);
36
+ }
37
+
38
+ export interface CustomerInvoiceDocumentStatusInput {
39
+ blockedReason: string | null;
40
+ status: string | null;
41
+ documentKind: string | null;
42
+ ksefStatus: string | null;
43
+ emailStatus: string | null;
44
+ outboxStatus: string | null;
45
+ downloadAvailable: boolean
46
+ }
47
+
48
+ export function deriveCustomerDocumentDeliveryStatus(input: CustomerInvoiceDocumentStatusInput): CustomerDocumentDeliveryStatus {
49
+ const status = normalized(input.status),
50
+ clearance = normalized(input.ksefStatus),
51
+ email = normalized(input.emailStatus),
52
+ outbox = normalized(input.outboxStatus);
53
+ if (input.blockedReason || ["rejected", "voided", "cancelled"].includes(status) || clearance === "rejected") return "requires_correction";
54
+ if (email === "failed" || ["failed", "uncertain", "cancelled"].includes(outbox)) return "delivery_failed";
55
+ if (input.documentKind === "b2b_vat" && !["accepted", "not_required"].includes(clearance)) return "ksef_pending";
56
+ if (["sent", "provider_accepted"].includes(email) || outbox === "succeeded") return "email_provider_accepted";
57
+ if (email === "pending" || ["pending", "processing"].includes(outbox)) return "email_pending";
58
+ return input.downloadAvailable ? "pdf_ready" : "not_ready";
59
+ }
60
+
61
+ function normalized(value: string | null): string {
62
+ return value?.trim().toLowerCase() ?? "";
63
+ }
@@ -0,0 +1,7 @@
1
+ /** Sparse projections only. Install the complete native/managed prefix, never this list alone. */
2
+ export const ACCOUNT_SUPPORT_MIGRATIONS = {
3
+ native: { units: [{ position: 87, catalogPath: "db/platform/migrations/20261026120000_account_support_operator_authority.sql",
4
+ sha256: "ac1e31679179e712d7558e7b1d82e8113f264abf9aaa44c2562ab988acdc7508",
5
+ url: new URL("../../sql/20261026120000_account_support_operator_authority.sql", import.meta.url) }] },
6
+ managed: { units: ["supabase/migrations/20261026120100_account_support_current_authority.sql"] },
7
+ } as const;