@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.
- package/CHANGELOG.md +167 -109
- package/README.md +111 -8
- 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 +1 -0
- package/dist/composition/index.js +1 -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/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 +12 -2
- package/release-gates.json +30 -3
- 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 +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;
|