@openlup/platform 0.22.0 → 0.24.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 +440 -158
- package/README.md +141 -589
- package/dist/fulfillment/adapterConformance.js +1 -1
- package/dist/fulfillment/bindings.js +1 -1
- package/dist/fulfillment/contracts/conformance.d.ts +3 -1
- package/dist/fulfillment/contracts/dispatch.d.ts +45 -3
- package/dist/fulfillment/contracts/index.d.ts +3 -3
- package/dist/fulfillment/contracts/observation.d.ts +21 -1
- package/dist/fulfillment/contracts/readback.d.ts +21 -0
- package/dist/fulfillment/contracts/store.d.ts +3 -0
- package/dist/fulfillment/contracts/sync.d.ts +58 -2
- package/dist/fulfillment/dispatchProjection.d.ts +20 -1
- package/dist/fulfillment/dispatchProjection.js +97 -6
- package/dist/fulfillment/index.d.ts +3 -2
- package/dist/fulfillment/index.js +2 -2
- package/dist/fulfillment/migrations.d.ts +5 -0
- package/dist/fulfillment/migrations.js +1 -0
- package/dist/fulfillment/observationProjection.d.ts +24 -3
- package/dist/fulfillment/observationProjection.js +65 -12
- package/dist/fulfillment/omnipack/client.d.ts +3 -1
- package/dist/fulfillment/omnipack/client.js +31 -13
- package/dist/fulfillment/omnipack/index.d.ts +4 -1
- package/dist/fulfillment/omnipack/index.js +107 -45
- package/dist/fulfillment/omnipack/mappers.d.ts +7 -1
- package/dist/fulfillment/omnipack/mappers.js +24 -2
- package/dist/fulfillment/omnipack/observation.d.ts +11 -4
- package/dist/fulfillment/omnipack/observation.js +44 -29
- package/dist/fulfillment/omnipack/reconciliation.d.ts +15 -0
- package/dist/fulfillment/omnipack/reconciliation.js +55 -0
- package/dist/fulfillment/postgresDispatchStore.js +1 -1
- package/dist/fulfillment/postgresShipmentSpine.d.ts +8 -1
- package/dist/fulfillment/postgresShipmentSpine.js +17 -3
- package/dist/fulfillment/postgresStockSyncPort.js +6 -3
- package/dist/fulfillment/providerDispatchAcceptance.d.ts +1 -0
- package/dist/fulfillment/providerDispatchAcceptance.js +8 -2
- package/dist/fulfillment/providerDispatchWorker.d.ts +51 -0
- package/dist/fulfillment/providerDispatchWorker.js +65 -20
- package/dist/fulfillment/providerReconciliationWorker.d.ts +27 -19
- package/dist/fulfillment/providerReconciliationWorker.js +75 -19
- package/dist/fulfillment/providerStockSyncWorker.d.ts +5 -1
- package/dist/fulfillment/providerStockSyncWorker.js +229 -120
- package/dist/fulfillment/providerTrackingReferences.d.ts +3 -2
- package/dist/fulfillment/verifiedIngress.d.ts +38 -11
- package/dist/fulfillment/verifiedIngress.js +36 -8
- package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
- package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
- package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
- package/dist/invoicing/accountingLineAllocation.js +69 -0
- package/dist/invoicing/adapterConformance.d.ts +42 -0
- package/dist/invoicing/adapterConformance.js +296 -0
- package/dist/invoicing/artifacts.d.ts +7 -0
- package/dist/invoicing/artifacts.js +20 -0
- package/dist/invoicing/bindings.d.ts +46 -0
- package/dist/invoicing/bindings.js +69 -0
- package/dist/invoicing/capabilities.d.ts +11 -0
- package/dist/invoicing/capabilities.js +10 -0
- package/dist/invoicing/channelPolicy.d.ts +28 -0
- package/dist/invoicing/channelPolicy.js +27 -0
- package/dist/invoicing/continuity.d.ts +12 -0
- package/dist/invoicing/continuity.js +18 -0
- package/dist/invoicing/contracts/capabilities.d.ts +40 -0
- package/dist/invoicing/contracts/capabilities.js +1 -0
- package/dist/invoicing/contracts/companyLookup.d.ts +49 -0
- package/dist/invoicing/contracts/companyLookup.js +1 -0
- package/dist/invoicing/contracts/delivery.d.ts +74 -0
- package/dist/invoicing/contracts/delivery.js +1 -0
- package/dist/invoicing/contracts/facts.d.ts +110 -0
- package/dist/invoicing/contracts/facts.js +1 -0
- package/dist/invoicing/contracts/host.d.ts +245 -0
- package/dist/invoicing/contracts/host.js +1 -0
- package/dist/invoicing/contracts/index.d.ts +16 -0
- package/dist/invoicing/contracts/index.js +1 -0
- package/dist/invoicing/contracts/provider.d.ts +126 -0
- package/dist/invoicing/contracts/provider.js +1 -0
- package/dist/invoicing/contracts/settlement.d.ts +66 -0
- package/dist/invoicing/contracts/settlement.js +1 -0
- package/dist/invoicing/contracts/store.d.ts +277 -0
- package/dist/invoicing/contracts/store.js +1 -0
- package/dist/invoicing/contribution.d.ts +21 -0
- package/dist/invoicing/contribution.js +19 -0
- package/dist/invoicing/correction.d.ts +42 -0
- package/dist/invoicing/correction.js +44 -0
- package/dist/invoicing/delivery.d.ts +85 -0
- package/dist/invoicing/delivery.js +79 -0
- package/dist/invoicing/documentHistory.d.ts +50 -0
- package/dist/invoicing/documentHistory.js +213 -0
- package/dist/invoicing/documentSync.d.ts +24 -0
- package/dist/invoicing/documentSync.js +26 -0
- package/dist/invoicing/dutyCollector.d.ts +15 -0
- package/dist/invoicing/dutyCollector.js +35 -0
- package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
- package/dist/invoicing/fulfillmentTrigger.js +90 -0
- package/dist/invoicing/handoff.d.ts +37 -0
- package/dist/invoicing/handoff.js +62 -0
- package/dist/invoicing/index.d.ts +39 -0
- package/dist/invoicing/index.js +37 -0
- package/dist/invoicing/issue.d.ts +39 -0
- package/dist/invoicing/issue.js +55 -0
- package/dist/invoicing/migrations.d.ts +48 -0
- package/dist/invoicing/migrations.js +25 -0
- package/dist/invoicing/nativeUnsupported.d.ts +9 -0
- package/dist/invoicing/nativeUnsupported.js +22 -0
- package/dist/invoicing/paymentReadback.d.ts +20 -0
- package/dist/invoicing/paymentReadback.js +47 -0
- package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
- package/dist/invoicing/postgresInvoiceStore.js +344 -0
- package/dist/invoicing/readModel.d.ts +74 -0
- package/dist/invoicing/readModel.js +90 -0
- package/dist/invoicing/readback.d.ts +4 -0
- package/dist/invoicing/readback.js +30 -0
- package/dist/invoicing/reversal.d.ts +37 -0
- package/dist/invoicing/reversal.js +28 -0
- package/dist/invoicing/sellerRevision.d.ts +19 -0
- package/dist/invoicing/sellerRevision.js +14 -0
- package/dist/invoicing/settlementSync.d.ts +18 -0
- package/dist/invoicing/settlementSync.js +32 -0
- package/dist/invoicing/snapshot.d.ts +52 -0
- package/dist/invoicing/snapshot.js +141 -0
- package/dist/subscriptions/automaticRenewal.d.ts +44 -0
- package/dist/subscriptions/automaticRenewal.js +81 -0
- package/dist/subscriptions/automaticRenewalExecution.d.ts +21 -0
- package/dist/subscriptions/automaticRenewalExecution.js +44 -0
- package/dist/subscriptions/contracts/commands.d.ts +115 -0
- package/dist/subscriptions/contracts/commands.js +1 -0
- package/dist/subscriptions/contracts/dunning.d.ts +133 -0
- package/dist/subscriptions/contracts/dunning.js +1 -0
- package/dist/subscriptions/contracts/index.d.ts +6 -0
- package/dist/subscriptions/contracts/index.js +1 -0
- package/dist/subscriptions/contracts/messages.d.ts +43 -0
- package/dist/subscriptions/contracts/messages.js +1 -0
- package/dist/subscriptions/contracts/renewal.d.ts +533 -0
- package/dist/subscriptions/contracts/renewal.js +1 -0
- package/dist/subscriptions/contracts/stockWait.d.ts +73 -0
- package/dist/subscriptions/contracts/stockWait.js +1 -0
- package/dist/subscriptions/index.d.ts +39 -0
- package/dist/subscriptions/index.js +17 -0
- package/dist/subscriptions/paymentOutcomes.d.ts +8 -0
- package/dist/subscriptions/paymentOutcomes.js +27 -0
- package/dist/subscriptions/postgresRenewalStore.d.ts +20 -0
- package/dist/subscriptions/postgresRenewalStore.js +61 -0
- package/dist/subscriptions/postgresRetryStore.d.ts +78 -0
- package/dist/subscriptions/postgresRetryStore.js +142 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.d.ts +34 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.js +79 -0
- package/dist/subscriptions/renewalCycleMechanism.d.ts +46 -0
- package/dist/subscriptions/renewalCycleMechanism.js +157 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.d.ts +41 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.js +160 -0
- package/dist/subscriptions/renewalIdentity.d.ts +3 -0
- package/dist/subscriptions/renewalIdentity.js +4 -0
- package/package.json +23 -3
- package/release-gates.json +74 -7
- package/sql/20260809210000_accounting_document_rail.sql +889 -0
- package/sql/20260811150000_accounting_payout_rail.sql +333 -0
- package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
- package/sql/20261022160000_subscription_retry_lifecycle.sql +328 -0
- package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -0
- package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
- package/sql/20261022191000_invoicing_document_effects.sql +249 -0
- package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
- package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
- package/src/fulfillment/adapterConformance.ts +1 -1
- package/src/fulfillment/bindings.ts +1 -1
- package/src/fulfillment/contracts/conformance.ts +3 -1
- package/src/fulfillment/contracts/dispatch.ts +24 -5
- package/src/fulfillment/contracts/index.ts +3 -3
- package/src/fulfillment/contracts/observation.ts +11 -3
- package/src/fulfillment/contracts/readback.ts +13 -0
- package/src/fulfillment/contracts/store.ts +1 -0
- package/src/fulfillment/contracts/sync.ts +13 -2
- package/src/fulfillment/dispatchProjection.ts +94 -7
- package/src/fulfillment/index.ts +3 -2
- package/src/fulfillment/migrations.ts +1 -0
- package/src/fulfillment/observationProjection.ts +64 -13
- package/src/fulfillment/omnipack/client.ts +32 -13
- package/src/fulfillment/omnipack/index.ts +99 -39
- package/src/fulfillment/omnipack/mappers.ts +23 -2
- package/src/fulfillment/omnipack/observation.ts +45 -27
- package/src/fulfillment/omnipack/reconciliation.ts +56 -0
- package/src/fulfillment/postgresDispatchStore.ts +1 -1
- package/src/fulfillment/postgresShipmentSpine.ts +21 -4
- package/src/fulfillment/postgresStockSyncPort.ts +6 -3
- package/src/fulfillment/providerDispatchAcceptance.ts +9 -3
- package/src/fulfillment/providerDispatchWorker.ts +61 -20
- package/src/fulfillment/providerReconciliationWorker.ts +81 -25
- package/src/fulfillment/providerStockSyncWorker.ts +222 -126
- package/src/fulfillment/providerTrackingReferences.ts +3 -2
- package/src/fulfillment/verifiedIngress.ts +44 -11
- package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
- package/src/invoicing/accountingLineAllocation.ts +69 -0
- package/src/invoicing/adapterConformance.ts +249 -0
- package/src/invoicing/artifacts.ts +19 -0
- package/src/invoicing/bindings.ts +80 -0
- package/src/invoicing/capabilities.ts +9 -0
- package/src/invoicing/channelPolicy.ts +50 -0
- package/src/invoicing/continuity.ts +15 -0
- package/src/invoicing/contracts/capabilities.ts +15 -0
- package/src/invoicing/contracts/companyLookup.ts +13 -0
- package/src/invoicing/contracts/delivery.ts +24 -0
- package/src/invoicing/contracts/facts.ts +71 -0
- package/src/invoicing/contracts/host.ts +84 -0
- package/src/invoicing/contracts/index.ts +16 -0
- package/src/invoicing/contracts/provider.ts +78 -0
- package/src/invoicing/contracts/settlement.ts +39 -0
- package/src/invoicing/contracts/store.ts +102 -0
- package/src/invoicing/contribution.ts +28 -0
- package/src/invoicing/correction.ts +56 -0
- package/src/invoicing/delivery.ts +122 -0
- package/src/invoicing/documentHistory.ts +301 -0
- package/src/invoicing/documentSync.ts +51 -0
- package/src/invoicing/dutyCollector.ts +31 -0
- package/src/invoicing/fulfillmentTrigger.ts +144 -0
- package/src/invoicing/handoff.ts +71 -0
- package/src/invoicing/index.ts +39 -0
- package/src/invoicing/issue.ts +64 -0
- package/src/invoicing/migrations.ts +25 -0
- package/src/invoicing/nativeUnsupported.ts +24 -0
- package/src/invoicing/paymentReadback.ts +70 -0
- package/src/invoicing/postgresInvoiceStore.ts +334 -0
- package/src/invoicing/readModel.ts +80 -0
- package/src/invoicing/readback.ts +26 -0
- package/src/invoicing/reversal.ts +34 -0
- package/src/invoicing/sellerRevision.ts +20 -0
- package/src/invoicing/settlementSync.ts +37 -0
- package/src/invoicing/snapshot.ts +192 -0
- package/src/subscriptions/automaticRenewal.ts +87 -0
- package/src/subscriptions/automaticRenewalExecution.ts +39 -0
- package/src/subscriptions/contracts/commands.ts +46 -0
- package/src/subscriptions/contracts/dunning.ts +48 -0
- package/src/subscriptions/contracts/index.ts +6 -0
- package/src/subscriptions/contracts/messages.ts +18 -0
- package/src/subscriptions/contracts/renewal.ts +325 -0
- package/src/subscriptions/contracts/stockWait.ts +34 -0
- package/src/subscriptions/index.ts +24 -0
- package/src/subscriptions/paymentOutcomes.ts +23 -0
- package/src/subscriptions/postgresRenewalStore.ts +76 -0
- package/src/subscriptions/postgresRetryStore.ts +174 -0
- package/src/subscriptions/renewalCycleFailureMechanism.ts +119 -0
- package/src/subscriptions/renewalCycleMechanism.ts +225 -0
- package/src/subscriptions/renewalCycleRefusalMechanism.ts +215 -0
- package/src/subscriptions/renewalIdentity.ts +5 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
type InvoiceDocumentRole = "original" | "correction" | "replacement";
|
|
2
|
+
type InvoiceDocumentArtifact = "invoice" | "correction";
|
|
3
|
+
type InvoiceDocumentEmailState = "not_requested" | "pending" | "provider_accepted" | "failed";
|
|
4
|
+
type InvoiceDocumentHistoryRow = Record<string, unknown>;
|
|
5
|
+
interface InvoiceDocumentHistoryEntry {
|
|
6
|
+
documentKey: string;
|
|
7
|
+
invoiceId: string;
|
|
8
|
+
parentDocumentRef: string | null;
|
|
9
|
+
orderId: string;
|
|
10
|
+
invoiceRef: string;
|
|
11
|
+
role: InvoiceDocumentRole;
|
|
12
|
+
artifact: InvoiceDocumentArtifact;
|
|
13
|
+
status: string;
|
|
14
|
+
blockedReason: string | null;
|
|
15
|
+
documentKind: string | null;
|
|
16
|
+
providerKind: string | null;
|
|
17
|
+
providerDocumentRef: string | null;
|
|
18
|
+
providerDocumentNumber: string | null;
|
|
19
|
+
clearanceReference: string | null;
|
|
20
|
+
clearanceState: string;
|
|
21
|
+
totalGrossMinor: number | null;
|
|
22
|
+
/**
|
|
23
|
+
* The currency the document was **issued in**, copied from
|
|
24
|
+
* `accounting_invoices.currency` (`NOT NULL CHECK (char_length = 3)`).
|
|
25
|
+
*
|
|
26
|
+
* It travels with the amount rather than being resolved by whoever renders it. An
|
|
27
|
+
* issued invoice is the most historical money the platform holds: it is a document
|
|
28
|
+
* of record whose denomination was fixed the day it was written, and a reader that
|
|
29
|
+
* asked the current settlement profile instead would re-label the customer's own
|
|
30
|
+
* accounting the first time a deployment changed what it sells in.
|
|
31
|
+
*/
|
|
32
|
+
currency: string;
|
|
33
|
+
issuedAt: string | null;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
isCurrent: boolean;
|
|
36
|
+
downloadAvailable: boolean;
|
|
37
|
+
emailState: InvoiceDocumentEmailState;
|
|
38
|
+
}
|
|
39
|
+
interface InvoiceDocumentHistory {
|
|
40
|
+
current: InvoiceDocumentHistoryEntry | null;
|
|
41
|
+
documents: InvoiceDocumentHistoryEntry[];
|
|
42
|
+
}
|
|
43
|
+
interface ResolveInvoiceDocumentHistoryInput {
|
|
44
|
+
invoices: InvoiceDocumentHistoryRow[];
|
|
45
|
+
operations?: InvoiceDocumentHistoryRow[];
|
|
46
|
+
deliveryOutboxes?: InvoiceDocumentHistoryRow[];
|
|
47
|
+
providerArtifactEligible?: (providerKind: string) => boolean;
|
|
48
|
+
}
|
|
49
|
+
export declare function resolveInvoiceDocumentHistory({ invoices, operations, deliveryOutboxes, providerArtifactEligible, }: ResolveInvoiceDocumentHistoryInput): InvoiceDocumentHistory;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
const CURRENT_STATUSES = new Set(["issued", "accepted"]);
|
|
2
|
+
const HISTORICAL_INVOICE_STATUSES = new Set(["issued", "accepted", "corrected"]);
|
|
3
|
+
export function resolveInvoiceDocumentHistory({ invoices, operations = [], deliveryOutboxes = [], providerArtifactEligible = () => true, }) {
|
|
4
|
+
const orderedInvoices = [...invoices].sort(compareRowsChronologically);
|
|
5
|
+
const correctionOperations = operations.filter((row) => text(row.operation) === "correction_issued");
|
|
6
|
+
const activeInvoice = [...orderedInvoices]
|
|
7
|
+
.filter((row) => currentInvoiceEligible(row, providerArtifactEligible))
|
|
8
|
+
.sort(compareRowsNewestFirst)[0] ?? null;
|
|
9
|
+
const activeInvoiceId = activeInvoice ? text(activeInvoice.id) : null;
|
|
10
|
+
const documents = [];
|
|
11
|
+
for (const invoice of orderedInvoices) {
|
|
12
|
+
const invoiceId = text(invoice.id);
|
|
13
|
+
const replacement = nullableText(invoice.correction_of_invoice_id) !== null;
|
|
14
|
+
const providerDocumentRef = nullableText(invoice.provider_document_ref);
|
|
15
|
+
documents.push({
|
|
16
|
+
documentKey: `invoice:${invoiceId}`,
|
|
17
|
+
invoiceId,
|
|
18
|
+
parentDocumentRef: replacement ? `invoice:${nullableText(invoice.correction_of_invoice_id)}` : null,
|
|
19
|
+
orderId: text(invoice.order_id),
|
|
20
|
+
invoiceRef: text(invoice.invoice_ref),
|
|
21
|
+
role: replacement ? "replacement" : "original",
|
|
22
|
+
artifact: "invoice",
|
|
23
|
+
status: text(invoice.status),
|
|
24
|
+
blockedReason: nullableText(invoice.blocked_reason),
|
|
25
|
+
documentKind: nullableText(invoice.document_kind),
|
|
26
|
+
providerKind: nullableText(invoice.provider_kind),
|
|
27
|
+
providerDocumentRef,
|
|
28
|
+
providerDocumentNumber: nullableText(invoice.provider_document_number),
|
|
29
|
+
clearanceReference: nullableText(invoice.clearance_reference),
|
|
30
|
+
clearanceState: text(invoice.clearance_state),
|
|
31
|
+
totalGrossMinor: nonnegativeNumber(invoice.total_gross_minor),
|
|
32
|
+
currency: text(invoice.currency),
|
|
33
|
+
issuedAt: nullableText(invoice.created_at),
|
|
34
|
+
createdAt: text(invoice.created_at),
|
|
35
|
+
isCurrent: invoiceId === activeInvoiceId,
|
|
36
|
+
downloadAvailable: historicalInvoiceDownloadEligible(invoice, providerArtifactEligible),
|
|
37
|
+
emailState: resolveEmailState({
|
|
38
|
+
invoice,
|
|
39
|
+
providerDocumentRef,
|
|
40
|
+
operations,
|
|
41
|
+
deliveryOutboxes,
|
|
42
|
+
preferInvoiceFallback: !correctionProviderDocumentRef(invoice),
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
const correctionProviderId = correctionProviderDocumentRef(invoice);
|
|
46
|
+
if (!correctionProviderId)
|
|
47
|
+
continue;
|
|
48
|
+
const correctionOperation = correctionOperations
|
|
49
|
+
.filter((row) => text(row.invoice_id) === invoiceId)
|
|
50
|
+
.sort(compareRowsNewestFirst)[0] ?? null;
|
|
51
|
+
const correctionNumber = correctionProviderDocumentNumber(invoice);
|
|
52
|
+
const correctionCreatedAt = nullableText(correctionOperation?.created_at)
|
|
53
|
+
?? laterTimestamp(text(invoice.updated_at), text(invoice.created_at));
|
|
54
|
+
const providerKind = nullableText(invoice.provider_kind);
|
|
55
|
+
const persistedCorrectionStatus = normalize(invoice.correction_status);
|
|
56
|
+
const correctionStatus = ["issued", "accepted"].includes(persistedCorrectionStatus)
|
|
57
|
+
? persistedCorrectionStatus
|
|
58
|
+
: "not_ready";
|
|
59
|
+
documents.push({
|
|
60
|
+
documentKey: `correction:${invoiceId}`,
|
|
61
|
+
invoiceId,
|
|
62
|
+
parentDocumentRef: `invoice:${invoiceId}`,
|
|
63
|
+
orderId: text(invoice.order_id),
|
|
64
|
+
invoiceRef: correctionNumber ?? `correction-${text(invoice.invoice_ref)}`.slice(0, 160),
|
|
65
|
+
role: "correction",
|
|
66
|
+
artifact: "correction",
|
|
67
|
+
status: correctionStatus,
|
|
68
|
+
blockedReason: nullableText(invoice.blocked_reason),
|
|
69
|
+
documentKind: "correction",
|
|
70
|
+
providerKind,
|
|
71
|
+
providerDocumentRef: correctionProviderId,
|
|
72
|
+
providerDocumentNumber: correctionNumber,
|
|
73
|
+
clearanceReference: null,
|
|
74
|
+
clearanceState: "not_required",
|
|
75
|
+
totalGrossMinor: null,
|
|
76
|
+
// A correction has no total of its own on this row, but it is a document of the
|
|
77
|
+
// same invoice and therefore of the same currency. Stating it keeps the field
|
|
78
|
+
// total, so a consumer never has to ask "which document has a currency?".
|
|
79
|
+
currency: text(invoice.currency),
|
|
80
|
+
issuedAt: correctionCreatedAt || null,
|
|
81
|
+
createdAt: correctionCreatedAt,
|
|
82
|
+
isCurrent: false,
|
|
83
|
+
downloadAvailable: Boolean(providerKind
|
|
84
|
+
&& providerArtifactEligible(providerKind)
|
|
85
|
+
&& nullableText(invoice.blocked_reason) === null
|
|
86
|
+
&& ["issued", "accepted"].includes(correctionStatus)),
|
|
87
|
+
emailState: resolveEmailState({
|
|
88
|
+
invoice,
|
|
89
|
+
providerDocumentRef: correctionProviderId,
|
|
90
|
+
operations,
|
|
91
|
+
deliveryOutboxes,
|
|
92
|
+
preferInvoiceFallback: true,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
documents.sort(compareDocumentsChronologically);
|
|
97
|
+
return {
|
|
98
|
+
current: documents.find((document) => document.isCurrent) ?? null,
|
|
99
|
+
documents,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function currentInvoiceEligible(row, providerArtifactEligible) {
|
|
103
|
+
const providerKind = nullableText(row.provider_kind);
|
|
104
|
+
return Boolean(providerKind
|
|
105
|
+
&& providerArtifactEligible(providerKind)
|
|
106
|
+
&& nullableText(row.provider_document_ref)
|
|
107
|
+
&& nullableText(row.blocked_reason) === null
|
|
108
|
+
&& CURRENT_STATUSES.has(normalize(row.status)));
|
|
109
|
+
}
|
|
110
|
+
function historicalInvoiceDownloadEligible(row, providerArtifactEligible) {
|
|
111
|
+
const providerKind = nullableText(row.provider_kind);
|
|
112
|
+
return Boolean(providerKind
|
|
113
|
+
&& providerArtifactEligible(providerKind)
|
|
114
|
+
&& nullableText(row.provider_document_ref)
|
|
115
|
+
&& nullableText(row.blocked_reason) === null
|
|
116
|
+
&& HISTORICAL_INVOICE_STATUSES.has(normalize(row.status)));
|
|
117
|
+
}
|
|
118
|
+
function resolveEmailState({ invoice, providerDocumentRef, operations, deliveryOutboxes, preferInvoiceFallback, }) {
|
|
119
|
+
if (!providerDocumentRef)
|
|
120
|
+
return "not_requested";
|
|
121
|
+
const invoiceId = text(invoice.id);
|
|
122
|
+
const accepted = operations.some((row) => {
|
|
123
|
+
if (text(row.invoice_id) !== invoiceId || text(row.operation) !== "provider_email_sent")
|
|
124
|
+
return false;
|
|
125
|
+
const payloadProviderId = nullableText(record(row.payload).providerDocumentRef);
|
|
126
|
+
return (payloadProviderId ?? nullableText(row.provider_ref)) === providerDocumentRef;
|
|
127
|
+
});
|
|
128
|
+
if (accepted)
|
|
129
|
+
return "provider_accepted";
|
|
130
|
+
const outbox = deliveryOutboxes
|
|
131
|
+
.filter((row) => text(row.invoice_id) === invoiceId)
|
|
132
|
+
.filter((row) => {
|
|
133
|
+
const targetProviderId = nullableText(record(row.metadata).providerDocumentRef);
|
|
134
|
+
return targetProviderId ? targetProviderId === providerDocumentRef : preferInvoiceFallback;
|
|
135
|
+
})
|
|
136
|
+
.sort(compareRowsNewestFirst)[0];
|
|
137
|
+
const outboxStatus = normalize(outbox?.status);
|
|
138
|
+
if (["failed", "uncertain", "cancelled"].includes(outboxStatus))
|
|
139
|
+
return "failed";
|
|
140
|
+
if (["pending", "processing", "queued"].includes(outboxStatus))
|
|
141
|
+
return "pending";
|
|
142
|
+
if (outboxStatus === "succeeded")
|
|
143
|
+
return "provider_accepted";
|
|
144
|
+
if (preferInvoiceFallback) {
|
|
145
|
+
const emailStatus = normalize(invoice.email_status);
|
|
146
|
+
if (emailStatus === "failed")
|
|
147
|
+
return "failed";
|
|
148
|
+
if (emailStatus === "pending")
|
|
149
|
+
return "pending";
|
|
150
|
+
if (emailStatus === "sent")
|
|
151
|
+
return "provider_accepted";
|
|
152
|
+
}
|
|
153
|
+
return "not_requested";
|
|
154
|
+
}
|
|
155
|
+
function correctionProviderDocumentRef(row) {
|
|
156
|
+
return nullableText(record(row.metadata).correctionProviderDocumentRef);
|
|
157
|
+
}
|
|
158
|
+
function correctionProviderDocumentNumber(row) {
|
|
159
|
+
return nullableText(record(row.metadata).correctionProviderDocumentNumber);
|
|
160
|
+
}
|
|
161
|
+
function compareRowsChronologically(a, b) {
|
|
162
|
+
return compareTimestamps(a.created_at, b.created_at) || text(a.id).localeCompare(text(b.id));
|
|
163
|
+
}
|
|
164
|
+
function compareRowsNewestFirst(a, b) {
|
|
165
|
+
return compareTimestamps(b.created_at ?? b.updated_at, a.created_at ?? a.updated_at) || text(b.id).localeCompare(text(a.id));
|
|
166
|
+
}
|
|
167
|
+
function compareDocumentsChronologically(a, b) {
|
|
168
|
+
return compareTimestamps(a.createdAt, b.createdAt)
|
|
169
|
+
|| documentRolePriority(a.role) - documentRolePriority(b.role)
|
|
170
|
+
|| a.documentKey.localeCompare(b.documentKey);
|
|
171
|
+
}
|
|
172
|
+
function documentRolePriority(role) {
|
|
173
|
+
if (role === "original")
|
|
174
|
+
return 0;
|
|
175
|
+
if (role === "correction")
|
|
176
|
+
return 1;
|
|
177
|
+
return 2;
|
|
178
|
+
}
|
|
179
|
+
function laterTimestamp(a, b) {
|
|
180
|
+
const aTime = Date.parse(a);
|
|
181
|
+
const bTime = Date.parse(b);
|
|
182
|
+
if (Number.isFinite(aTime) && Number.isFinite(bTime))
|
|
183
|
+
return aTime >= bTime ? a : b;
|
|
184
|
+
return a || b;
|
|
185
|
+
}
|
|
186
|
+
function compareTimestamps(a, b) {
|
|
187
|
+
const aTime = Date.parse(text(a));
|
|
188
|
+
const bTime = Date.parse(text(b));
|
|
189
|
+
if (Number.isFinite(aTime) && Number.isFinite(bTime) && aTime !== bTime)
|
|
190
|
+
return aTime - bTime;
|
|
191
|
+
if (Number.isFinite(aTime) && !Number.isFinite(bTime))
|
|
192
|
+
return -1;
|
|
193
|
+
if (!Number.isFinite(aTime) && Number.isFinite(bTime))
|
|
194
|
+
return 1;
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
function nonnegativeNumber(value) {
|
|
198
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
|
|
199
|
+
}
|
|
200
|
+
function record(value) {
|
|
201
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
202
|
+
? value
|
|
203
|
+
: {};
|
|
204
|
+
}
|
|
205
|
+
function normalize(value) {
|
|
206
|
+
return text(value).trim().toLowerCase();
|
|
207
|
+
}
|
|
208
|
+
function text(value) {
|
|
209
|
+
return typeof value === "string" ? value : "";
|
|
210
|
+
}
|
|
211
|
+
function nullableText(value) {
|
|
212
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
213
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Direct donor extraction: host supplies attachment kinds, durable provider
|
|
2
|
+
* references, status/event keys, hashing and recording. Downloads confer no blob
|
|
3
|
+
* storage proof. The optional observation mode retains polling without adding
|
|
4
|
+
* another public runner/export or provider-specific status vocabulary. */
|
|
5
|
+
export declare function syncAcceptedInvoiceDocuments<Target, Status, Document>(input: {
|
|
6
|
+
target: Target;
|
|
7
|
+
status: Status;
|
|
8
|
+
downloads: readonly ((target: Target) => Promise<Document>)[];
|
|
9
|
+
record(target: Target, status: Status, documents: readonly Document[]): Promise<unknown>;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
export declare function syncAcceptedInvoiceDocuments<Target, Status, Document>(input: {
|
|
12
|
+
targets(): Promise<readonly Target[]>;
|
|
13
|
+
observe(target: Target): Promise<Status>;
|
|
14
|
+
recordStatus(target: Target, status: Status): Promise<unknown>;
|
|
15
|
+
accepted(status: Status): boolean;
|
|
16
|
+
downloads: readonly ((target: Target) => Promise<Document>)[];
|
|
17
|
+
record(target: Target, status: Status, documents: readonly Document[]): Promise<unknown>;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
ok: boolean;
|
|
20
|
+
checked: number;
|
|
21
|
+
updated: number;
|
|
22
|
+
skipped: false;
|
|
23
|
+
failures: number;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export async function syncAcceptedInvoiceDocuments(input) {
|
|
2
|
+
const sync = async (target, status) => {
|
|
3
|
+
const documents = await Promise.all(input.downloads.map(download => download(target)));
|
|
4
|
+
await input.record(target, status, documents);
|
|
5
|
+
};
|
|
6
|
+
if (!input.targets) {
|
|
7
|
+
await sync(input.target, input.status);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const targets = await input.targets();
|
|
11
|
+
let updated = 0;
|
|
12
|
+
let failures = 0;
|
|
13
|
+
for (const target of targets) {
|
|
14
|
+
try {
|
|
15
|
+
const status = await input.observe(target);
|
|
16
|
+
await input.recordStatus(target, status);
|
|
17
|
+
if (input.accepted(status))
|
|
18
|
+
await sync(target, status);
|
|
19
|
+
updated += 1;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
failures += 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { ok: failures === 0, checked: targets.length, updated, skipped: false, failures };
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { InvoiceStorePort, InvoiceUnsupported } from "./contracts/index.js";
|
|
2
|
+
/** Collect retained sources, then prepare each leased duty in separate transactions.
|
|
3
|
+
* The host commits every store call; no money/parcel callback or provider I/O is
|
|
4
|
+
* accepted here. Missing facts stay owed until the host installs their revision.
|
|
5
|
+
* @consumer Independent invoice duty binding.
|
|
6
|
+
*/
|
|
7
|
+
export declare function collectInvoiceDuties(input: {
|
|
8
|
+
store: Pick<InvoiceStorePort, "sweep" | "prepareFacts" | "finishFacts">;
|
|
9
|
+
limit?: number;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
kind: "collected";
|
|
12
|
+
collected: number;
|
|
13
|
+
prepared: number;
|
|
14
|
+
failures: number;
|
|
15
|
+
} | InvoiceUnsupported>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Collect retained sources, then prepare each leased duty in separate transactions.
|
|
2
|
+
* The host commits every store call; no money/parcel callback or provider I/O is
|
|
3
|
+
* accepted here. Missing facts stay owed until the host installs their revision.
|
|
4
|
+
* @consumer Independent invoice duty binding.
|
|
5
|
+
*/
|
|
6
|
+
export async function collectInvoiceDuties(input) {
|
|
7
|
+
const batch = await input.store.sweep(input.limit ?? 10);
|
|
8
|
+
if ("kind" in batch)
|
|
9
|
+
return batch;
|
|
10
|
+
let prepared = 0, failures = 0;
|
|
11
|
+
for (const claim of batch.claims) {
|
|
12
|
+
try {
|
|
13
|
+
const result = await input.store.prepareFacts(claim);
|
|
14
|
+
if ("kind" in result) {
|
|
15
|
+
failures++;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (result.status === "stale")
|
|
19
|
+
continue;
|
|
20
|
+
const finished = await input.store.finishFacts({ ...claim, result: { ...result, status: result.status } });
|
|
21
|
+
if ("kind" in finished)
|
|
22
|
+
failures++;
|
|
23
|
+
else if (finished.status === "ready" || finished.status === "issued" || finished.status === "voided")
|
|
24
|
+
prepared++;
|
|
25
|
+
else if (finished.status !== "stale")
|
|
26
|
+
failures++;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// A failed prepare/finalize leaves the persisted lease reclaimable. Never
|
|
30
|
+
// manufacture a successful finish or prevent another claim's progress.
|
|
31
|
+
failures++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { kind: "collected", collected: batch.collected, prepared, failures };
|
|
35
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
interface AccountingInvoiceIssuePort {
|
|
2
|
+
requestInvoiceIssueFromPaidOrder(input: {
|
|
3
|
+
idempotencyKey: string;
|
|
4
|
+
orderId: string;
|
|
5
|
+
providerKind: string;
|
|
6
|
+
}): Promise<unknown>;
|
|
7
|
+
requestInvoiceIssueFromFulfillmentHandoff(input: {
|
|
8
|
+
idempotencyKey: string;
|
|
9
|
+
fulfillmentOrderId: string;
|
|
10
|
+
providerKind: string;
|
|
11
|
+
}): Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
import { type OrderInvoicePolicyReader } from "./channelPolicy.js";
|
|
14
|
+
export type AutoDispatchFulfillmentResult = {
|
|
15
|
+
kind: "completed" | "skipped";
|
|
16
|
+
detail?: Record<string, unknown>;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "retryable" | "snooze" | "fatal";
|
|
19
|
+
reason: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "manual_review";
|
|
22
|
+
reason: string;
|
|
23
|
+
detail?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
export interface AutoDispatchFulfillmentPort {
|
|
26
|
+
ensureFulfilledFromPaidOrder(input: {
|
|
27
|
+
orderUuid: string;
|
|
28
|
+
outboxEventId: string;
|
|
29
|
+
signal: AbortSignal;
|
|
30
|
+
}): Promise<AutoDispatchFulfillmentResult>;
|
|
31
|
+
}
|
|
32
|
+
type FulfillmentHandoffRequest = {
|
|
33
|
+
idempotencyKey: string;
|
|
34
|
+
fulfillmentOrderId: string;
|
|
35
|
+
actorUserId: string;
|
|
36
|
+
};
|
|
37
|
+
type FulfillmentHandoffPort<TResult = unknown> = {
|
|
38
|
+
handOffCommerceFulfillmentOrder: (request: FulfillmentHandoffRequest) => Promise<TResult>;
|
|
39
|
+
};
|
|
40
|
+
export interface AccountingHandoffTriggerOptions {
|
|
41
|
+
accountingPort: AccountingInvoiceIssuePort;
|
|
42
|
+
providerKind: string;
|
|
43
|
+
keys: {
|
|
44
|
+
shadow(key: string): string;
|
|
45
|
+
paid(orderId: string): string;
|
|
46
|
+
handoff(orderId: string): string;
|
|
47
|
+
};
|
|
48
|
+
failOnAccountingError?: boolean;
|
|
49
|
+
issueTrigger?: "handoff" | "paid";
|
|
50
|
+
invoicePolicyReader?: OrderInvoicePolicyReader;
|
|
51
|
+
}
|
|
52
|
+
export declare function withAccountingInvoiceShadowTrigger<TResult, TPort extends FulfillmentHandoffPort<TResult>>(fulfillmentPort: TPort, options: Omit<AccountingHandoffTriggerOptions, "accountingPort"> & {
|
|
53
|
+
accountingPort: Pick<AccountingInvoiceIssuePort, "requestInvoiceIssueFromFulfillmentHandoff"> & Partial<AccountingInvoiceIssuePort>;
|
|
54
|
+
}): TPort;
|
|
55
|
+
export declare function withAutoDispatchAccountingInvoiceTrigger(fulfillmentPort: AutoDispatchFulfillmentPort, options: AccountingHandoffTriggerOptions): AutoDispatchFulfillmentPort;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { decideChannelInvoice, } from "./channelPolicy.js";
|
|
2
|
+
export function withAccountingInvoiceShadowTrigger(fulfillmentPort, options) {
|
|
3
|
+
return {
|
|
4
|
+
...fulfillmentPort,
|
|
5
|
+
async handOffCommerceFulfillmentOrder(request) {
|
|
6
|
+
const result = await fulfillmentPort.handOffCommerceFulfillmentOrder(request);
|
|
7
|
+
try {
|
|
8
|
+
// The operator hand-off reaches this wrapper holding only the
|
|
9
|
+
// fulfillment order id, so the policy is read by that key.
|
|
10
|
+
const decision = await decideChannelInvoice(options.invoicePolicyReader, {
|
|
11
|
+
by: "fulfillmentOrder",
|
|
12
|
+
fulfillmentOrderId: request.fulfillmentOrderId,
|
|
13
|
+
});
|
|
14
|
+
if (decision.issue) {
|
|
15
|
+
await options.accountingPort.requestInvoiceIssueFromFulfillmentHandoff({
|
|
16
|
+
idempotencyKey: options.keys.shadow(request.idempotencyKey),
|
|
17
|
+
fulfillmentOrderId: request.fulfillmentOrderId,
|
|
18
|
+
providerKind: options.providerKind,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (options.failOnAccountingError)
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Auto-dispatch sibling of withAccountingInvoiceShadowTrigger. The outbox
|
|
31
|
+
// commerce.order.paid -> fulfillment path drives handoff through the
|
|
32
|
+
// OrderPaidFulfillmentPort (ensureFulfilledFromPaidOrder), which issues the
|
|
33
|
+
// mark-handed-over RPC directly and therefore never reaches the admin
|
|
34
|
+
// hand-off wrapper above. Without this, an auto-dispatched paid order reaches
|
|
35
|
+
// handed_over but no accounting invoice is ever requested. Same semantics:
|
|
36
|
+
// idempotent (the issue RPC dedups on the order-derived invoice_ref, so any
|
|
37
|
+
// key is duplicate-safe) and fail-soft (accounting errors never fail the
|
|
38
|
+
// fulfillment dispatch — the outbox event still settles as processed).
|
|
39
|
+
export function withAutoDispatchAccountingInvoiceTrigger(fulfillmentPort, options) {
|
|
40
|
+
return {
|
|
41
|
+
async ensureFulfilledFromPaidOrder(input) {
|
|
42
|
+
// Both branches hold the order uuid, so both ask by the order key — the
|
|
43
|
+
// strongest key available, and the one that needs no join.
|
|
44
|
+
const lookup = { by: "order", orderUuid: input.orderUuid };
|
|
45
|
+
if (options.issueTrigger === "paid") {
|
|
46
|
+
try {
|
|
47
|
+
const decision = await decideChannelInvoice(options.invoicePolicyReader, lookup);
|
|
48
|
+
if (decision.issue) {
|
|
49
|
+
await options.accountingPort.requestInvoiceIssueFromPaidOrder({
|
|
50
|
+
idempotencyKey: options.keys.paid(input.orderUuid),
|
|
51
|
+
orderId: input.orderUuid,
|
|
52
|
+
providerKind: options.providerKind,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (options.failOnAccountingError)
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const result = await fulfillmentPort.ensureFulfilledFromPaidOrder(input);
|
|
62
|
+
if (options.issueTrigger === "paid")
|
|
63
|
+
return result;
|
|
64
|
+
const fulfillmentOrderId = readHandedOverFulfillmentOrderId(result);
|
|
65
|
+
if (fulfillmentOrderId) {
|
|
66
|
+
try {
|
|
67
|
+
const decision = await decideChannelInvoice(options.invoicePolicyReader, lookup);
|
|
68
|
+
if (decision.issue) {
|
|
69
|
+
await options.accountingPort.requestInvoiceIssueFromFulfillmentHandoff({
|
|
70
|
+
idempotencyKey: options.keys.handoff(input.orderUuid),
|
|
71
|
+
fulfillmentOrderId,
|
|
72
|
+
providerKind: options.providerKind,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (options.failOnAccountingError)
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function readHandedOverFulfillmentOrderId(result) {
|
|
86
|
+
if (result.kind !== "completed" && result.kind !== "skipped")
|
|
87
|
+
return null;
|
|
88
|
+
const id = result.detail?.fulfillmentOrderId;
|
|
89
|
+
return typeof id === "string" ? id : null;
|
|
90
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ChannelInvoiceDecision } from "./channelPolicy.js";
|
|
2
|
+
/** Host retains event schema, key scopes, RPC error class and compatibility detail. */
|
|
3
|
+
export declare function createFulfillmentHandoffInvoiceHandler<Row extends {
|
|
4
|
+
payload: unknown;
|
|
5
|
+
}, Payload>(input: {
|
|
6
|
+
descriptor: {
|
|
7
|
+
eventType: string;
|
|
8
|
+
timeoutMs: number;
|
|
9
|
+
};
|
|
10
|
+
parsePayload(value: unknown): {
|
|
11
|
+
success: true;
|
|
12
|
+
data: Payload;
|
|
13
|
+
} | {
|
|
14
|
+
success: false;
|
|
15
|
+
};
|
|
16
|
+
decide(payload: Payload): Promise<ChannelInvoiceDecision>;
|
|
17
|
+
request(row: Row, payload: Payload): Promise<Record<string, unknown>>;
|
|
18
|
+
errorDetails(error: unknown): {
|
|
19
|
+
message: string;
|
|
20
|
+
causeCode?: string;
|
|
21
|
+
causeMessage?: string;
|
|
22
|
+
};
|
|
23
|
+
}): {
|
|
24
|
+
handle(row: Row, signal: AbortSignal): Promise<{
|
|
25
|
+
kind: "processed";
|
|
26
|
+
detail: Record<string, unknown>;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "discard";
|
|
29
|
+
reason: string;
|
|
30
|
+
benign?: boolean;
|
|
31
|
+
} | {
|
|
32
|
+
kind: "retry";
|
|
33
|
+
reason: string;
|
|
34
|
+
}>;
|
|
35
|
+
eventType: string;
|
|
36
|
+
timeoutMs: number;
|
|
37
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SQLSTATEs the issue-request RPCs raise for permanently non-retryable
|
|
2
|
+
// conditions. Retrying these can never succeed, so the event must be discarded
|
|
3
|
+
// (terminal) rather than retry-looped into a `failed` backlog that the staging
|
|
4
|
+
// outbox-preview gate treats as blocking (see outboxOrderPaidFulfillmentHandler,
|
|
5
|
+
// which likewise maps fatal -> discard).
|
|
6
|
+
// - 22023 (invalid_parameter_value): every validation RAISE in
|
|
7
|
+
// accounting_invoice_issue_request_from_handoff / _from_paid_order — invalid
|
|
8
|
+
// input, missing fulfillment order (deleted), missing order/client/address,
|
|
9
|
+
// payment-not-succeeded, package-not-shipped. The handed_over event commits
|
|
10
|
+
// atomically with handed_over_at, so a missing row means it was deleted, not
|
|
11
|
+
// a visibility race.
|
|
12
|
+
// - 23514 (check_violation): the #1811 provider-immutability guard
|
|
13
|
+
// (accounting_invoice_provider_mismatch). The provider is immutable, so a
|
|
14
|
+
// replay against a different provider can never be persisted.
|
|
15
|
+
const PERMANENT_ISSUE_SQLSTATES = new Set(["22023", "23514"]);
|
|
16
|
+
// The subset of permanent (22023) RAISE identifiers that mean the aggregate or
|
|
17
|
+
// its graph was legitimately deleted before this async event dispatched — an
|
|
18
|
+
// orphan from smoke/order cleanup or a real order deletion. Discarding these is
|
|
19
|
+
// the expected, correct outcome, so they are classified `benign` (logged at warn,
|
|
20
|
+
// not error). Every OTHER permanent condition — payment-not-succeeded,
|
|
21
|
+
// package-not-shipped, invalid-input, and the 23514 provider-immutability guard —
|
|
22
|
+
// is still discarded but kept at error level because it signals a genuine anomaly.
|
|
23
|
+
const BENIGN_ISSUE_RAISE_IDENTIFIERS = new Set([
|
|
24
|
+
"accounting_invoice_handoff_not_found",
|
|
25
|
+
"accounting_invoice_order_not_found",
|
|
26
|
+
"accounting_invoice_client_not_found",
|
|
27
|
+
"accounting_invoice_address_not_found",
|
|
28
|
+
]);
|
|
29
|
+
/** Host retains event schema, key scopes, RPC error class and compatibility detail. */
|
|
30
|
+
export function createFulfillmentHandoffInvoiceHandler(input) {
|
|
31
|
+
return {
|
|
32
|
+
...input.descriptor,
|
|
33
|
+
async handle(row, signal) {
|
|
34
|
+
const parsed = input.parsePayload(row.payload);
|
|
35
|
+
if (!parsed.success)
|
|
36
|
+
return { kind: "discard", reason: "fulfillment_handoff_payload_invalid" };
|
|
37
|
+
if (signal.aborted)
|
|
38
|
+
return { kind: "retry", reason: "fulfillment_handoff_handler_aborted" };
|
|
39
|
+
try {
|
|
40
|
+
const decision = await input.decide(parsed.data);
|
|
41
|
+
if (!decision.issue)
|
|
42
|
+
return { kind: "processed", detail: decision.detail };
|
|
43
|
+
return { kind: "processed", detail: await input.request(row, parsed.data) };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const { message, causeCode, causeMessage } = input.errorDetails(error);
|
|
47
|
+
// Surface the RPC's own RAISE identifier so a discard reads explicitly
|
|
48
|
+
// (e.g. "…: accounting_invoice_handoff_not_found (22023)") instead of an
|
|
49
|
+
// opaque SQLSTATE — this is what lets an orphan discard be told apart
|
|
50
|
+
// from a real payment-not-succeeded failure in the logs.
|
|
51
|
+
const detail = causeMessage && causeMessage !== message ? `${message}: ${causeMessage}` : message;
|
|
52
|
+
const reason = (causeCode ? `${detail} (${causeCode})` : detail).slice(0, 300);
|
|
53
|
+
if (causeCode !== undefined && PERMANENT_ISSUE_SQLSTATES.has(causeCode)) {
|
|
54
|
+
const benign = causeMessage !== undefined
|
|
55
|
+
&& BENIGN_ISSUE_RAISE_IDENTIFIERS.has(causeMessage);
|
|
56
|
+
return benign ? { kind: "discard", reason, benign: true } : { kind: "discard", reason };
|
|
57
|
+
}
|
|
58
|
+
return { kind: "retry", reason };
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export { readInvoiceCapabilities } from "./capabilities.js";
|
|
2
|
+
export { createNativeUnsupportedInvoicePorts } from "./nativeUnsupported.js";
|
|
3
|
+
export { runInvoiceAdapterConformance } from "./adapterConformance.js";
|
|
4
|
+
export { runInvoiceIssueJob } from "./issue.js";
|
|
5
|
+
/** @consumer server/domains/accounting/accountingProviderSnapshot.ts */
|
|
6
|
+
export { mapInvoiceClaimToProviderSnapshot } from "./snapshot.js";
|
|
7
|
+
/** @consumer server/domains/accounting/accountingCanonicalProviderLines.ts */
|
|
8
|
+
export { mapCanonicalIssueLines } from "./accountingCanonicalProviderLines.js";
|
|
9
|
+
/** @consumer server/domains/accounting/accountingLineAllocation.ts */
|
|
10
|
+
export { allocateChargedGrossTotals } from "./accountingLineAllocation.js";
|
|
11
|
+
/** @consumer server/domains/accounting/accountingLineAllocation.ts */
|
|
12
|
+
export { allocateDiscountedGrossTotals } from "./accountingLineAllocation.js";
|
|
13
|
+
/** @consumer server/domains/accounting/accountingPaymentReadback.ts */
|
|
14
|
+
export { readInvoicePaymentEvidence } from "./paymentReadback.js";
|
|
15
|
+
/** @consumer server/domains/accounting/fulfillmentHandoffInvoiceHandler.ts */
|
|
16
|
+
export { createFulfillmentHandoffInvoiceHandler } from "./handoff.js";
|
|
17
|
+
/** @consumer server/domains/accounting/fulfillmentInvoiceTrigger.ts */
|
|
18
|
+
export { withAccountingInvoiceShadowTrigger } from "./fulfillmentTrigger.js";
|
|
19
|
+
/** @consumer server/domains/accounting/fulfillmentInvoiceTrigger.ts */
|
|
20
|
+
export { withAutoDispatchAccountingInvoiceTrigger } from "./fulfillmentTrigger.js";
|
|
21
|
+
export { runInvoiceCorrectionJob } from "./correction.js";
|
|
22
|
+
export { createInvoiceReversalHandlers } from "./reversal.js";
|
|
23
|
+
/** @consumer Reference native accounting adapter/runtime and installed-consumer conformance. */
|
|
24
|
+
export { createPostgresInvoiceStore, runNativeInvoiceIssueJob } from "./postgresInvoiceStore.js";
|
|
25
|
+
/** @consumer Native migration conformance and installed-consumer asset verification. */
|
|
26
|
+
export { INVOICING_MIGRATIONS } from "./migrations.js";
|
|
27
|
+
export { collectInvoiceDuties } from "./dutyCollector.js";
|
|
28
|
+
export { createIndependentInvoiceDutyBinding } from "./continuity.js";
|
|
29
|
+
export { runInvoiceDeliveryJob } from "./delivery.js";
|
|
30
|
+
export { assertInvoicePdf, invoicePdfFilename } from "./artifacts.js";
|
|
31
|
+
export { syncAcceptedInvoiceDocuments } from "./documentSync.js";
|
|
32
|
+
export { syncInvoicePayoutSettlements } from "./settlementSync.js";
|
|
33
|
+
export { resolveInvoiceDocumentHistory } from "./documentHistory.js";
|
|
34
|
+
export { createInvoiceReadModel } from "./readModel.js";
|
|
35
|
+
export { createInvoiceServices } from "./bindings.js";
|
|
36
|
+
export type { InvoiceServices } from "./bindings.js";
|
|
37
|
+
export { createInvoiceContribution } from "./contribution.js";
|
|
38
|
+
export { claimInvoiceSellerRevision } from "./sellerRevision.js";
|
|
39
|
+
export type { InvoiceSellerRevision, InvoiceClaimedFacts } from "./contracts/facts.js";
|