@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,296 @@
|
|
|
1
|
+
class ContractFailure extends Error {
|
|
2
|
+
}
|
|
3
|
+
function requireFact(value, code) {
|
|
4
|
+
if (!value)
|
|
5
|
+
throw new ContractFailure(`INVOICE_CONFORMANCE:${code}`);
|
|
6
|
+
}
|
|
7
|
+
function same(a, b) {
|
|
8
|
+
if (a === b)
|
|
9
|
+
return true;
|
|
10
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object")
|
|
11
|
+
return false;
|
|
12
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
13
|
+
return false;
|
|
14
|
+
const left = Object.keys(a), right = Object.keys(b);
|
|
15
|
+
return left.length === right.length && left.every(key => Object.hasOwn(b, key) && same(a[key], b[key]));
|
|
16
|
+
}
|
|
17
|
+
function keys(value, allowed) {
|
|
18
|
+
requireFact(value !== null && typeof value === "object", "proof_required");
|
|
19
|
+
requireFact(Object.keys(value).every(key => allowed.includes(key)), "unexpected_field");
|
|
20
|
+
}
|
|
21
|
+
function proof(value, extra = []) {
|
|
22
|
+
requireFact(value !== null && typeof value === "object", "proof_required");
|
|
23
|
+
keys(value, ["evidenceRef", "fingerprint", "observedAt", ...extra]);
|
|
24
|
+
requireFact([value.evidenceRef, value.fingerprint, value.observedAt].every(v => typeof v === "string" && v.trim().length > 0), "proof_required");
|
|
25
|
+
}
|
|
26
|
+
function identity(value, expected) {
|
|
27
|
+
keys(value, "documentRef" in expected ? ["providerRef", "documentRef", "providerDocumentRef", "orderRef", "invoiceRef", "parentProviderDocumentRef"] : ["providerRef", "requestKey", "providerCreateIdentity", "factRevision", "orderRef", "invoiceRef", "operation", "correctionRef", "parentProviderDocumentRef"]);
|
|
28
|
+
requireFact(same(value, expected), "identity_scope");
|
|
29
|
+
}
|
|
30
|
+
function safe(result, subject) {
|
|
31
|
+
const serialized = JSON.stringify(result);
|
|
32
|
+
requireFact(subject.sensitiveValues.every(value => value.length > 0 && !serialized.includes(value) && !serialized.includes(encodeURIComponent(value))), "sensitive_result");
|
|
33
|
+
}
|
|
34
|
+
function documentResult(result, expected, operation) {
|
|
35
|
+
requireFact(operation === "void" ? result.kind === "unsupported" : operation === "read" ? ["found", "absent", "unknown", "unsupported"].includes(result.kind) : ["issued", "refused", "unknown", "unsupported"].includes(result.kind), "document_result_kind");
|
|
36
|
+
if (result.kind === "unsupported") {
|
|
37
|
+
keys(result, ["kind", "code", "retryable", "effectMayHaveSucceeded"]);
|
|
38
|
+
requireFact(result.retryable === false && result.effectMayHaveSucceeded === false && ["unsupported", "host_binding_required"].includes(result.code), "unsupported_before_write");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
identity(result.identity, expected);
|
|
42
|
+
if (result.kind === "issued" || result.kind === "found") {
|
|
43
|
+
keys(result, result.kind === "found" ? ["kind", "identity", "providerDocumentRef", "providerDocumentNumber", "proof"] : ["kind", "identity", "providerDocumentRef", "providerDocumentNumber", "proof", "replayed"]);
|
|
44
|
+
proof(result.proof);
|
|
45
|
+
if (result.kind === "issued")
|
|
46
|
+
requireFact(typeof result.replayed === "boolean", "issued_replay");
|
|
47
|
+
requireFact(result.providerDocumentRef.trim().length > 0 && (result.providerDocumentNumber === null || typeof result.providerDocumentNumber === "string"), "issued_identity");
|
|
48
|
+
}
|
|
49
|
+
else if (result.kind === "absent") {
|
|
50
|
+
keys(result, ["kind", "identity", "evidence"]);
|
|
51
|
+
proof(result.evidence, ["exact", "complete", "scope"]);
|
|
52
|
+
requireFact(result.evidence.exact === true && result.evidence.complete === true, "exact_complete_absence");
|
|
53
|
+
identity(result.evidence.scope, expected);
|
|
54
|
+
}
|
|
55
|
+
else if (result.kind === "unknown") {
|
|
56
|
+
keys(result, ["kind", "identity", "code", "effectMayHaveSucceeded"]);
|
|
57
|
+
requireFact(result.code === "unknown" && (operation === "write" ? "effectMayHaveSucceeded" in result && result.effectMayHaveSucceeded === true : !("effectMayHaveSucceeded" in result)), "unknown_not_absent");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
keys(result, ["kind", "identity", "code", "retryable", "effectMayHaveSucceeded"]);
|
|
61
|
+
requireFact(result.kind === "refused" && result.effectMayHaveSucceeded === false && typeof result.retryable === "boolean" && ["blocked", "retryable", "missing_facts", "identity_mismatch", "parent_mismatch", "money_mismatch"].includes(result.code), "refusal_before_write");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function hostResult(operation, input, value) {
|
|
65
|
+
const result = value;
|
|
66
|
+
if (result.kind === "unsupported") {
|
|
67
|
+
keys(result, ["kind", "code", "retryable", "effectMayHaveSucceeded"]);
|
|
68
|
+
requireFact(result.retryable === false && result.effectMayHaveSucceeded === false && (result.code === "unsupported" || result.code === "host_binding_required"), "host_unsupported");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (operation === "artifact") {
|
|
72
|
+
const artifact = value, request = input;
|
|
73
|
+
identity(artifact.identity, request.identity);
|
|
74
|
+
if (artifact.kind === "found") {
|
|
75
|
+
keys(artifact, ["kind", "identity", "artifact"]);
|
|
76
|
+
keys(artifact.artifact, ["kind", "ref", "owner", "mediaType", "proof"]);
|
|
77
|
+
requireFact(artifact.artifact.kind === request.kind && artifact.artifact.ref.length > 0 && artifact.artifact.mediaType.length > 0 && ["provider", "host_storage"].includes(artifact.artifact.owner), "artifact_reference");
|
|
78
|
+
proof(artifact.artifact.proof);
|
|
79
|
+
}
|
|
80
|
+
else if (artifact.kind === "absent") {
|
|
81
|
+
keys(artifact, ["kind", "identity", "evidence"]);
|
|
82
|
+
proof(artifact.evidence, ["exact", "complete", "scope"]);
|
|
83
|
+
keys(artifact.evidence.scope, ["identity", "kind", "artifactRef"]);
|
|
84
|
+
requireFact(artifact.evidence.exact === true && artifact.evidence.complete === true && artifact.evidence.scope.kind === request.kind && artifact.evidence.scope.artifactRef === null, "artifact_absence");
|
|
85
|
+
identity(artifact.evidence.scope.identity, request.identity);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
keys(artifact, ["kind", "identity", "code"]);
|
|
89
|
+
requireFact(artifact.kind === "unknown" && artifact.code === "unknown", "artifact_unknown");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (operation === "clearance") {
|
|
93
|
+
const observation = value;
|
|
94
|
+
if (observation.kind === "observed") {
|
|
95
|
+
keys(observation, ["kind", "observation"]);
|
|
96
|
+
const observed = observation.observation;
|
|
97
|
+
keys(observed, ["state", "reference", "proof"]);
|
|
98
|
+
proof(observed.proof);
|
|
99
|
+
requireFact(["not_submitted", "pending", "rejected", "accepted", "not_required", "unsupported"].includes(observed.state), "clearance_state");
|
|
100
|
+
requireFact(observed.state === "accepted" ? typeof observed.reference === "string" && observed.reference.trim().length > 0 : observed.reference === null || typeof observed.reference === "string", "clearance_reference");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
keys(observation, ["kind", "code"]);
|
|
104
|
+
requireFact(observation.kind === "unknown" && observation.code === "unknown", "clearance_unknown");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (operation === "settlement") {
|
|
108
|
+
const item = input;
|
|
109
|
+
requireFact([item.grossMinor, item.feeMinor, item.netMinor].every(Number.isSafeInteger) && item.grossMinor - item.feeMinor === item.netMinor, "settlement_money");
|
|
110
|
+
keys(result, ["kind", "itemRef", "settlementItemRef", "replayed", "code"]);
|
|
111
|
+
requireFact(result.itemRef === item.itemRef, "settlement_item_identity");
|
|
112
|
+
requireFact(["recorded", "conflict", "unknown"].includes(String(result.kind)), "settlement_result");
|
|
113
|
+
if (result.kind === "recorded")
|
|
114
|
+
requireFact(typeof result.replayed === "boolean" && typeof result.settlementItemRef === "string" && result.settlementItemRef.length > 0, "settlement_receipt");
|
|
115
|
+
if (result.kind === "conflict")
|
|
116
|
+
requireFact(result.code === "identity_mismatch" || result.code === "money_mismatch", "settlement_conflict");
|
|
117
|
+
if (result.kind === "unknown")
|
|
118
|
+
requireFact(result.code === "unknown", "settlement_unknown");
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const company = value;
|
|
122
|
+
keys(result, ["kind", "identifier", "legalName", "registrationIds", "address", "taxRegistrations", "sourceRef", "proof", "code"]);
|
|
123
|
+
const request = input;
|
|
124
|
+
requireFact(company.sourceRef === request.sourceRef, "company_source");
|
|
125
|
+
if (company.kind !== "unknown")
|
|
126
|
+
requireFact(same(company.identifier, request.identifier), "company_identity");
|
|
127
|
+
if (company.kind === "found" || company.kind === "partial") {
|
|
128
|
+
proof(company.proof);
|
|
129
|
+
for (const registration of company.taxRegistrations) {
|
|
130
|
+
keys(registration, ["scheme", "status"]);
|
|
131
|
+
requireFact(["active", "exempt", "not_registered", "unknown"].includes(registration.status), "company_tax_status");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (company.kind === "not_found")
|
|
135
|
+
proof(company.proof);
|
|
136
|
+
else
|
|
137
|
+
requireFact(company.kind === "unknown" && company.code === "unknown", "company_unknown");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** @consumer Offline provider bridge tests and installed cold consumer. Runner-
|
|
141
|
+
* agnostic contract evidence only; adapters own fixtures, transport and I/O counts.
|
|
142
|
+
* Throws closed failure codes. It neither certifies live fiscal readiness nor logs
|
|
143
|
+
* provider results, secrets, buyer facts or raw adapter errors.
|
|
144
|
+
*/
|
|
145
|
+
export async function runInvoiceAdapterConformance(subjects) {
|
|
146
|
+
const evidence = [];
|
|
147
|
+
try {
|
|
148
|
+
requireFact(subjects.length > 0, "subject_required");
|
|
149
|
+
for (const subject of subjects) {
|
|
150
|
+
const { capability: caps, provider, fixtures } = subject;
|
|
151
|
+
const cases = [];
|
|
152
|
+
requireFact(caps.defaultOff === true, "default_off");
|
|
153
|
+
requireFact(caps.version === "invoicing.capabilities.v1" && caps.numberingOwner === "provider"
|
|
154
|
+
&& [caps.providerRef, caps.fiscalScope.scopeRef, caps.fiscalScope.version, ...Object.values(caps.policies)].every(value => value.trim().length > 0), "capability_scope");
|
|
155
|
+
for (const operation of ["issue", "correction"]) {
|
|
156
|
+
const rows = fixtures[operation];
|
|
157
|
+
requireFact(rows.length > 0, "write_fixture_required");
|
|
158
|
+
if (caps.operations[operation].supported)
|
|
159
|
+
requireFact(rows.some(row => row.expected.kind === "issued"), "issued_fixture_required");
|
|
160
|
+
if (caps.operations[operation].supported && caps.operations.recovery.supported)
|
|
161
|
+
requireFact(rows.some(row => row.replay), "recovery_fixture_required");
|
|
162
|
+
const unknownRecovery = rows.filter(row => row.input.recoveryLookupRequired
|
|
163
|
+
&& row.expected.kind === (caps.operations.recovery.supported ? "unknown" : "unsupported"));
|
|
164
|
+
if (caps.operations[operation].supported) {
|
|
165
|
+
requireFact(unknownRecovery.length > 0, "recovery_unknown_fixture_required");
|
|
166
|
+
if (caps.operations.recovery.supported && caps.operations.readback.supported)
|
|
167
|
+
requireFact(unknownRecovery.every(row => fixtures.readback.some(read => read.expected.kind === "unknown" && same(read.input, row.input.identity))), "recovery_unknown_readback_required");
|
|
168
|
+
}
|
|
169
|
+
for (const row of rows) {
|
|
170
|
+
requireFact(row.input.identity.providerRef === caps.providerRef, "selected_provider");
|
|
171
|
+
const before = subject.readWriteCount();
|
|
172
|
+
const invoke = operation === "issue" ? () => provider.issue(row.input) : () => provider.correct(row.input);
|
|
173
|
+
const result = await invoke();
|
|
174
|
+
documentResult(result, row.input.identity, "write");
|
|
175
|
+
safe(result, subject);
|
|
176
|
+
if (unknownRecovery.includes(row))
|
|
177
|
+
requireFact(result.kind === row.expected.kind && subject.readWriteCount() === before, "recovery_unknown_no_write");
|
|
178
|
+
if (row.input.recoveryLookupRequired === true) {
|
|
179
|
+
if (!caps.operations.recovery.supported)
|
|
180
|
+
requireFact(result.kind !== "issued" && subject.readWriteCount() === before, "recovery_create_without_absence");
|
|
181
|
+
if (subject.readWriteCount() > before || (result.kind === "issued" && result.replayed === false)) {
|
|
182
|
+
requireFact(caps.operations.recovery.supported && caps.operations.readback.supported && caps.operations.exactAbsence.supported
|
|
183
|
+
&& fixtures.readback.some(read => same(read.input, row.input.identity) && read.expected.kind === "absent")
|
|
184
|
+
&& !fixtures.readback.some(read => same(read.input, row.input.identity) && (read.expected.kind === "unknown" || read.expected.kind === "unsupported")), "recovery_create_without_absence");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
requireFact(same(result, row.expected), "write_fixture");
|
|
188
|
+
if (!caps.operations[operation].supported)
|
|
189
|
+
requireFact(result.kind === "unsupported", "declared_unsupported");
|
|
190
|
+
if (operation === "correction" && caps.operations.correction.supported) {
|
|
191
|
+
const request = row.input;
|
|
192
|
+
requireFact(request.identity.correctionRef.length > 0 && request.identity.parentProviderDocumentRef.length > 0
|
|
193
|
+
&& request.before.totalGrossMinor > 0 && Object.values(request.after).every(value => value === 0), "correction_before_after");
|
|
194
|
+
const parent = fixtures.correction.find(fixture => fixture.expected.kind === "issued" && fixture.input.identity.correctionRef === request.identity.correctionRef);
|
|
195
|
+
if (parent && parent.input.identity.parentProviderDocumentRef !== request.identity.parentProviderDocumentRef)
|
|
196
|
+
requireFact((result.kind === "refused" || result.kind === "unknown") && subject.readWriteCount() === before, "parent_mismatch_no_write");
|
|
197
|
+
}
|
|
198
|
+
if (result.kind === "unsupported" || result.kind === "refused")
|
|
199
|
+
requireFact(subject.readWriteCount() === before, "refused_write");
|
|
200
|
+
if (row.replay) {
|
|
201
|
+
requireFact(caps.operations.recovery.supported && result.kind === "issued", "replay_requires_issued");
|
|
202
|
+
const count = subject.readWriteCount();
|
|
203
|
+
const recovered = operation === "issue" ? await provider.issue({ ...row.input, recoveryLookupRequired: true }) : await provider.correct({ ...row.input, recoveryLookupRequired: true });
|
|
204
|
+
documentResult(recovered, row.input.identity, "write");
|
|
205
|
+
safe(recovered, subject);
|
|
206
|
+
requireFact(recovered.kind === "issued" && recovered.replayed === true && recovered.providerDocumentRef === result.providerDocumentRef && recovered.providerDocumentNumber === result.providerDocumentNumber, "recovery_identity");
|
|
207
|
+
requireFact(subject.readWriteCount() === count, "recovery_no_second_write");
|
|
208
|
+
}
|
|
209
|
+
cases.push(operation + ":" + result.kind + (row.replay ? ":recovery" : ""));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (caps.operations.correction.supported) {
|
|
213
|
+
const issued = fixtures.correction.find(row => row.expected.kind === "issued");
|
|
214
|
+
requireFact(issued && fixtures.correction.some(row => row.input.recoveryLookupRequired
|
|
215
|
+
&& row.input.identity.correctionRef === issued.input.identity.correctionRef
|
|
216
|
+
&& row.input.identity.providerCreateIdentity === issued.input.identity.providerCreateIdentity
|
|
217
|
+
&& row.input.identity.parentProviderDocumentRef !== issued.input.identity.parentProviderDocumentRef
|
|
218
|
+
&& (row.expected.kind === "refused" || row.expected.kind === "unknown")), "correction_parent_fixture_required");
|
|
219
|
+
}
|
|
220
|
+
requireFact(fixtures.readback.length > 0 && fixtures.void.length > 0, "read_void_fixture_required");
|
|
221
|
+
if (caps.operations.readback.supported)
|
|
222
|
+
requireFact(fixtures.readback.some(row => row.expected.kind === "unknown") && fixtures.readback.some(row => row.expected.kind === "found"), "found_unknown_fixtures_required");
|
|
223
|
+
if (caps.operations.exactAbsence.supported)
|
|
224
|
+
requireFact(fixtures.readback.some(row => row.expected.kind === "absent"), "absence_fixture_required");
|
|
225
|
+
for (const row of fixtures.readback) {
|
|
226
|
+
const before = subject.readWriteCount(), result = await provider.read(row.input);
|
|
227
|
+
documentResult(result, row.input, "read");
|
|
228
|
+
safe(result, subject);
|
|
229
|
+
requireFact(same(result, row.expected), "read_fixture");
|
|
230
|
+
requireFact(subject.readWriteCount() === before, "read_no_write");
|
|
231
|
+
if (!caps.operations.readback.supported)
|
|
232
|
+
requireFact(result.kind === "unsupported", "declared_unsupported");
|
|
233
|
+
if (result.kind === "absent")
|
|
234
|
+
requireFact(caps.operations.exactAbsence.supported, "absence_capability");
|
|
235
|
+
cases.push("read:" + result.kind);
|
|
236
|
+
}
|
|
237
|
+
requireFact(caps.operations.remoteVoid.supported === false && caps.operations.remoteVoid.reason === "not_implemented", "remote_void_cut");
|
|
238
|
+
for (const row of fixtures.void) {
|
|
239
|
+
const before = subject.readWriteCount(), result = await provider.void(row.input);
|
|
240
|
+
documentResult(result, row.input, "void");
|
|
241
|
+
safe(result, subject);
|
|
242
|
+
requireFact(same(result, row.expected), "void_fixture");
|
|
243
|
+
requireFact(subject.readWriteCount() === before, "void_refused_write");
|
|
244
|
+
cases.push("void:" + result.kind);
|
|
245
|
+
}
|
|
246
|
+
const settlementReceipts = new Map();
|
|
247
|
+
const settlementRows = fixtures.settlement ?? [];
|
|
248
|
+
const distinctPayoutRows = settlementRows.filter(row => row.expected.kind === "recorded" && settlementRows.some(other => other.expected.kind === "recorded"
|
|
249
|
+
&& row.input.itemRef !== other.input.itemRef && row.input.providerRef === other.input.providerRef
|
|
250
|
+
&& row.input.batchRef === other.input.batchRef && row.input.providerPaymentRef === other.input.providerPaymentRef));
|
|
251
|
+
if (caps.operations.settlement.supported)
|
|
252
|
+
requireFact(distinctPayoutRows.length >= 2, "distinct_payout_fixtures_required");
|
|
253
|
+
// Optional host-owned capabilities require their own fixtures when declared.
|
|
254
|
+
for (const operation of ["artifact", "clearance", "settlement", "companyLookup"]) {
|
|
255
|
+
const rows = fixtures[operation] ?? [];
|
|
256
|
+
if (caps.operations[operation].supported)
|
|
257
|
+
requireFact(subject.host && rows.some(row => ["found", "partial", "recorded", "observed"].includes(row.expected.kind) && !("observation" in row.expected && row.expected.observation.state === "unsupported")), "host_fixture_required");
|
|
258
|
+
for (const row of rows) {
|
|
259
|
+
requireFact(subject.host, "host_required");
|
|
260
|
+
const before = subject.readWriteCount();
|
|
261
|
+
const result = operation === "artifact" ? await subject.host.artifacts.read(row.input)
|
|
262
|
+
: operation === "clearance" ? await subject.host.clearance.observe(row.input)
|
|
263
|
+
: operation === "settlement" ? await subject.host.settlement.record(row.input)
|
|
264
|
+
: await subject.host.registry.lookup(row.input);
|
|
265
|
+
hostResult(operation, row.input, result);
|
|
266
|
+
if (operation === "settlement" && distinctPayoutRows.some(fixture => fixture === row))
|
|
267
|
+
requireFact(result.kind === "recorded", "distinct_payout_items");
|
|
268
|
+
if (operation === "settlement" && "kind" in result && result.kind === "recorded") {
|
|
269
|
+
const input = row.input;
|
|
270
|
+
const item = JSON.stringify([input.providerRef, input.batchRef, input.itemRef]);
|
|
271
|
+
requireFact(!settlementReceipts.has(result.settlementItemRef) || settlementReceipts.get(result.settlementItemRef) === item, "distinct_payout_items");
|
|
272
|
+
settlementReceipts.set(result.settlementItemRef, item);
|
|
273
|
+
}
|
|
274
|
+
safe(result, subject);
|
|
275
|
+
requireFact(same(result, row.expected), "host_fixture");
|
|
276
|
+
if (result.kind === "unsupported")
|
|
277
|
+
requireFact(subject.readWriteCount() === before, "host_unsupported_write");
|
|
278
|
+
if (operation !== "settlement")
|
|
279
|
+
requireFact(subject.readWriteCount() === before, "host_read_no_write");
|
|
280
|
+
if (!caps.operations[operation].supported)
|
|
281
|
+
requireFact((result.kind === "unsupported" || ("observation" in result && result.observation.state === "unsupported")) && subject.readWriteCount() === before, "host_unsupported_before_write");
|
|
282
|
+
cases.push(operation);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const row = { subjectId: subject.subjectId, providerRef: caps.providerRef, cases };
|
|
286
|
+
safe(row, subject);
|
|
287
|
+
evidence.push(row);
|
|
288
|
+
}
|
|
289
|
+
return { subjects: evidence };
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (error instanceof ContractFailure)
|
|
293
|
+
throw error;
|
|
294
|
+
throw new ContractFailure("INVOICE_CONFORMANCE:adapter_threw");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Validate transient bytes; this is neither storage proof nor delivery proof. */
|
|
2
|
+
export declare function assertInvoicePdf(pdf: {
|
|
3
|
+
content: Uint8Array;
|
|
4
|
+
contentType: string;
|
|
5
|
+
}): void;
|
|
6
|
+
/** The host supplies its existing fallback caption. No locale/content default. */
|
|
7
|
+
export declare function invoicePdfFilename(reference: string, fallback?: string): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const MAX_INVOICE_PDF_BYTES = 25 * 1024 * 1024;
|
|
2
|
+
/** Validate transient bytes; this is neither storage proof nor delivery proof. */
|
|
3
|
+
export function assertInvoicePdf(pdf) {
|
|
4
|
+
const contentType = pdf.contentType.split(";", 1)[0]?.trim().toLowerCase();
|
|
5
|
+
if (contentType !== "application/pdf")
|
|
6
|
+
throw new Error("invoice_pdf_content_type_invalid");
|
|
7
|
+
if (!pdf.content || pdf.content.byteLength === 0)
|
|
8
|
+
throw new Error("invoice_pdf_empty");
|
|
9
|
+
if (pdf.content.byteLength > MAX_INVOICE_PDF_BYTES)
|
|
10
|
+
throw new Error("invoice_pdf_too_large");
|
|
11
|
+
if (String.fromCharCode(...pdf.content.subarray(0, 5)) !== "%PDF-") {
|
|
12
|
+
throw new Error("invoice_pdf_signature_invalid");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** The host supplies its existing fallback caption. No locale/content default. */
|
|
16
|
+
export function invoicePdfFilename(reference, fallback = "invoice") {
|
|
17
|
+
const safe = (value) => value.normalize("NFKD")
|
|
18
|
+
.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
|
|
19
|
+
return `${safe(reference) || safe(fallback) || "invoice"}.pdf`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { InvoiceServicesOptions } from "./contracts/host.js";
|
|
2
|
+
import type { InvoiceUnsupported } from "./contracts/provider.js";
|
|
3
|
+
import { runNativeInvoiceIssueJob } from "./postgresInvoiceStore.js";
|
|
4
|
+
import { syncAcceptedInvoiceDocuments } from "./documentSync.js";
|
|
5
|
+
import { syncInvoicePayoutSettlements } from "./settlementSync.js";
|
|
6
|
+
import { createInvoiceReadModel } from "./readModel.js";
|
|
7
|
+
type Operation = "issue" | "correction" | "delivery" | "documentSync" | "settlementSync" | "collectDuties" | "history";
|
|
8
|
+
type Ports = Pick<InvoiceServicesOptions, "host"> & Required<{
|
|
9
|
+
provider: NonNullable<InvoiceServicesOptions["provider"]>;
|
|
10
|
+
store: NonNullable<InvoiceServicesOptions["store"]>;
|
|
11
|
+
capability: NonNullable<InvoiceServicesOptions["capability"]>;
|
|
12
|
+
}>;
|
|
13
|
+
type Refusal = InvoiceUnsupported | {
|
|
14
|
+
readonly kind: "refused";
|
|
15
|
+
readonly code: "disabled" | "selection_required";
|
|
16
|
+
readonly retryable: false;
|
|
17
|
+
readonly effectMayHaveSucceeded: false;
|
|
18
|
+
};
|
|
19
|
+
/** Host factories bind existing workers/readers, never another executor or cron. */
|
|
20
|
+
export interface InvoiceServices<Result = unknown> {
|
|
21
|
+
readiness(): Promise<{
|
|
22
|
+
readonly kind: "ready";
|
|
23
|
+
} | Refusal>;
|
|
24
|
+
run(operation: Operation): Promise<Result | Refusal>;
|
|
25
|
+
}
|
|
26
|
+
/** Explicit selection and host readiness precede factory construction and claim.
|
|
27
|
+
* Native jobs use the installed module helpers; transport-specific host facades
|
|
28
|
+
* may supply factories. Auth, invocation scope and leases stay host-owned.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createInvoiceServices<Result = unknown>(options: InvoiceServicesOptions & {
|
|
31
|
+
readonly factories?: Partial<Record<Operation, (ports: Ports) => () => Promise<Result>>>;
|
|
32
|
+
readonly jobs?: {
|
|
33
|
+
readonly issue?: Omit<Parameters<typeof runNativeInvoiceIssueJob>[0], "store" | "provider" | "money" | "providerRef">;
|
|
34
|
+
readonly delivery?: {
|
|
35
|
+
readonly claimOptions: Parameters<NonNullable<InvoiceServicesOptions["store"]>["claimDeliveries"]>[0];
|
|
36
|
+
filename(claim: Exclude<Awaited<ReturnType<NonNullable<InvoiceServicesOptions["store"]>["claimDeliveries"]>>, InvoiceUnsupported>[number]): string;
|
|
37
|
+
};
|
|
38
|
+
readonly documentSync?: Parameters<typeof syncAcceptedInvoiceDocuments>[0];
|
|
39
|
+
readonly settlementSync?: Omit<Parameters<typeof syncInvoicePayoutSettlements>[0], "settlement">;
|
|
40
|
+
readonly history?: Parameters<ReturnType<typeof createInvoiceReadModel>["history"]>[0];
|
|
41
|
+
readonly collectDuties?: {
|
|
42
|
+
readonly limit?: number;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}): InvoiceServices<Result>;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createIndependentInvoiceDutyBinding } from "./continuity.js";
|
|
2
|
+
import { runNativeInvoiceIssueJob } from "./postgresInvoiceStore.js";
|
|
3
|
+
import { runInvoiceDeliveryJob } from "./delivery.js";
|
|
4
|
+
import { syncAcceptedInvoiceDocuments } from "./documentSync.js";
|
|
5
|
+
import { syncInvoicePayoutSettlements } from "./settlementSync.js";
|
|
6
|
+
import { createInvoiceReadModel } from "./readModel.js";
|
|
7
|
+
import { readInvoiceCapabilities } from "./capabilities.js";
|
|
8
|
+
/** Explicit selection and host readiness precede factory construction and claim.
|
|
9
|
+
* Native jobs use the installed module helpers; transport-specific host facades
|
|
10
|
+
* may supply factories. Auth, invocation scope and leases stay host-owned.
|
|
11
|
+
*/
|
|
12
|
+
export function createInvoiceServices(options) {
|
|
13
|
+
const { enabled, selection, capability, provider, store, host, readiness: observe } = options;
|
|
14
|
+
const factories = { ...options.factories };
|
|
15
|
+
const unsupported = { kind: "unsupported", code: "host_binding_required", retryable: false, effectMayHaveSucceeded: false };
|
|
16
|
+
const readiness = async () => {
|
|
17
|
+
const selected = readInvoiceCapabilities({ enabled, selection, capability });
|
|
18
|
+
if (selected.kind !== "ready")
|
|
19
|
+
return selected;
|
|
20
|
+
if (!provider || !store)
|
|
21
|
+
return unsupported;
|
|
22
|
+
return observe();
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
readiness,
|
|
26
|
+
async run(operation) {
|
|
27
|
+
const state = await readiness();
|
|
28
|
+
if (state.kind !== "ready")
|
|
29
|
+
return state;
|
|
30
|
+
const factory = factories[operation];
|
|
31
|
+
const capabilityKey = operation === "documentSync" ? "readback"
|
|
32
|
+
: operation === "settlementSync" ? "settlement" : operation;
|
|
33
|
+
if ((capabilityKey === "issue" || capabilityKey === "correction" || capabilityKey === "readback" || capabilityKey === "settlement")
|
|
34
|
+
&& !capability.operations[capabilityKey].supported)
|
|
35
|
+
return unsupported;
|
|
36
|
+
if (factory)
|
|
37
|
+
return factory({ provider: provider, store: store, capability: capability, host })();
|
|
38
|
+
const jobs = options.jobs;
|
|
39
|
+
// Inputs are explicit host selections. No operation acquires a host run lease.
|
|
40
|
+
if (operation === "collectDuties" && jobs?.collectDuties)
|
|
41
|
+
return createIndependentInvoiceDutyBinding({ ...jobs.collectDuties, store: store }).collect();
|
|
42
|
+
// The native claim batch includes issue and correction/readback duties.
|
|
43
|
+
if (operation === "issue" && jobs?.issue && (!capability.operations.correction.supported || !capability.operations.readback.supported))
|
|
44
|
+
return unsupported;
|
|
45
|
+
if (operation === "issue" && jobs?.issue)
|
|
46
|
+
return runNativeInvoiceIssueJob({ ...jobs.issue, store: store, provider: provider, money: host.money, providerRef: selection.providerRef });
|
|
47
|
+
if (operation === "delivery" && jobs?.delivery)
|
|
48
|
+
return runInvoiceDeliveryJob({
|
|
49
|
+
enabled: true, providerEnabled: true, deliveryEnabled: true, store: store, ...jobs.delivery,
|
|
50
|
+
download: async (claim) => {
|
|
51
|
+
const artifact = await host.artifacts.read({ identity: claim.identity, kind: "pdf" });
|
|
52
|
+
if (artifact.kind !== "found")
|
|
53
|
+
throw new Error("invoicing_artifact_unavailable");
|
|
54
|
+
const result = await host.artifacts.download({ identity: claim.identity, artifactRef: artifact.artifact.ref });
|
|
55
|
+
if (result.kind !== "downloaded")
|
|
56
|
+
throw new Error("invoicing_artifact_unavailable");
|
|
57
|
+
return result;
|
|
58
|
+
}, send: input => host.delivery.send(input),
|
|
59
|
+
});
|
|
60
|
+
if (operation === "documentSync" && jobs?.documentSync)
|
|
61
|
+
return syncAcceptedInvoiceDocuments(jobs.documentSync);
|
|
62
|
+
if (operation === "settlementSync" && jobs?.settlementSync)
|
|
63
|
+
return syncInvoicePayoutSettlements({ ...jobs.settlementSync, settlement: host.settlement });
|
|
64
|
+
if (operation === "history" && jobs?.history)
|
|
65
|
+
return createInvoiceReadModel({ accountHistory: host.accountHistory }).history(jobs.history);
|
|
66
|
+
return unsupported;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { InvoiceCapabilities, InvoiceCapabilitySelection } from "./contracts/capabilities.js";
|
|
2
|
+
/** @consumer Host readiness / installed cold consumer. Default off and explicit
|
|
3
|
+
* provider selection precede capability use; construction never selects a provider.
|
|
4
|
+
*/
|
|
5
|
+
export declare function readInvoiceCapabilities(input: {
|
|
6
|
+
readonly enabled?: boolean;
|
|
7
|
+
readonly selection: {
|
|
8
|
+
readonly providerRef: string;
|
|
9
|
+
} | null;
|
|
10
|
+
readonly capability: InvoiceCapabilities | null;
|
|
11
|
+
}): InvoiceCapabilitySelection;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** @consumer Host readiness / installed cold consumer. Default off and explicit
|
|
2
|
+
* provider selection precede capability use; construction never selects a provider.
|
|
3
|
+
*/
|
|
4
|
+
export function readInvoiceCapabilities(input) {
|
|
5
|
+
if (input.enabled !== true)
|
|
6
|
+
return { kind: "refused", code: "disabled", retryable: false, effectMayHaveSucceeded: false };
|
|
7
|
+
if (!input.selection || !input.capability || input.selection.providerRef !== input.capability.providerRef)
|
|
8
|
+
return { kind: "refused", code: "selection_required", retryable: false, effectMayHaveSucceeded: false };
|
|
9
|
+
return { kind: "ready", capability: input.capability };
|
|
10
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type ChannelInvoiceAction = "issue" | "channel_issues" | "suppress";
|
|
2
|
+
export type InvoicePolicyLookup = {
|
|
3
|
+
by: "order";
|
|
4
|
+
orderUuid: string;
|
|
5
|
+
} | {
|
|
6
|
+
by: "fulfillmentOrder";
|
|
7
|
+
fulfillmentOrderId: string;
|
|
8
|
+
};
|
|
9
|
+
/** Reads the resolved sales-source invoice action for either producer key.
|
|
10
|
+
* Read failures propagate to the caller.
|
|
11
|
+
*/
|
|
12
|
+
export interface OrderInvoicePolicyReader {
|
|
13
|
+
readOrderInvoicePolicy(lookup: InvoicePolicyLookup): Promise<ChannelInvoiceAction>;
|
|
14
|
+
}
|
|
15
|
+
export interface ChannelInvoiceDecision {
|
|
16
|
+
issue: boolean;
|
|
17
|
+
action: ChannelInvoiceAction;
|
|
18
|
+
detail: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether this deployment should request the sales document for an order.
|
|
22
|
+
*
|
|
23
|
+
* An ABSENT reader answers `issue`. Composition roots that predate this wave —
|
|
24
|
+
* and every test that builds a producer without one — keep today's behaviour
|
|
25
|
+
* exactly, which is what makes the storefront path provably unchanged.
|
|
26
|
+
*/
|
|
27
|
+
export declare function decideChannelInvoice(reader: OrderInvoicePolicyReader | undefined, lookup: InvoicePolicyLookup): Promise<ChannelInvoiceDecision>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const ISSUE = { issue: true, action: "issue", detail: {} };
|
|
2
|
+
function refusal(action) {
|
|
3
|
+
return {
|
|
4
|
+
issue: false,
|
|
5
|
+
action,
|
|
6
|
+
detail: action === "channel_issues"
|
|
7
|
+
// The far side mints the document. Stamped with the same key an order
|
|
8
|
+
// metadata write would have used, so a later wave that gains a metadata
|
|
9
|
+
// path moves the value rather than renaming it.
|
|
10
|
+
? { invoicePolicy: action, invoiceIssuedBy: "channel", accountingInvoiceSkipped: action }
|
|
11
|
+
// The operator declared that no sales document is produced at all.
|
|
12
|
+
: { invoicePolicy: action, accountingInvoiceSkipped: action },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Whether this deployment should request the sales document for an order.
|
|
17
|
+
*
|
|
18
|
+
* An ABSENT reader answers `issue`. Composition roots that predate this wave —
|
|
19
|
+
* and every test that builds a producer without one — keep today's behaviour
|
|
20
|
+
* exactly, which is what makes the storefront path provably unchanged.
|
|
21
|
+
*/
|
|
22
|
+
export async function decideChannelInvoice(reader, lookup) {
|
|
23
|
+
if (!reader)
|
|
24
|
+
return ISSUE;
|
|
25
|
+
const action = await reader.readOrderInvoicePolicy(lookup);
|
|
26
|
+
return action === "issue" ? ISSUE : refusal(action);
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { collectInvoiceDuties } from "./dutyCollector.js";
|
|
2
|
+
/** Explicit independent host invocation, never an inline fulfillment/payment hook.
|
|
3
|
+
* Durable source receipts and fact leases own replay, backoff and fault ceilings.
|
|
4
|
+
* A collector outage remains visible and can be retried by the host later.
|
|
5
|
+
* @consumer Reference native accounting runtime and installed cold consumer.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createIndependentInvoiceDutyBinding(input: Parameters<typeof collectInvoiceDuties>[0]): {
|
|
8
|
+
collect(): Promise<Awaited<ReturnType<typeof collectInvoiceDuties>> | {
|
|
9
|
+
kind: "unavailable";
|
|
10
|
+
code: "retryable";
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { collectInvoiceDuties } from "./dutyCollector.js";
|
|
2
|
+
/** Explicit independent host invocation, never an inline fulfillment/payment hook.
|
|
3
|
+
* Durable source receipts and fact leases own replay, backoff and fault ceilings.
|
|
4
|
+
* A collector outage remains visible and can be retried by the host later.
|
|
5
|
+
* @consumer Reference native accounting runtime and installed cold consumer.
|
|
6
|
+
*/
|
|
7
|
+
export function createIndependentInvoiceDutyBinding(input) {
|
|
8
|
+
return {
|
|
9
|
+
async collect() {
|
|
10
|
+
try {
|
|
11
|
+
return await collectInvoiceDuties(input);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return { kind: "unavailable", code: "retryable" };
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { InvoiceDiagnosticCode, InvoiceUnsupported } from "./provider.js";
|
|
2
|
+
export interface InvoiceCapabilities {
|
|
3
|
+
readonly version: "invoicing.capabilities.v1";
|
|
4
|
+
readonly providerRef: string;
|
|
5
|
+
readonly fiscalScope: {
|
|
6
|
+
readonly scopeRef: string;
|
|
7
|
+
readonly version: string;
|
|
8
|
+
};
|
|
9
|
+
readonly policies: {
|
|
10
|
+
readonly identifierRef: string;
|
|
11
|
+
readonly taxRef: string;
|
|
12
|
+
readonly roundingRef: string;
|
|
13
|
+
};
|
|
14
|
+
readonly documentKinds: readonly string[];
|
|
15
|
+
readonly correctionKinds: readonly "full_reversal"[];
|
|
16
|
+
readonly numberingOwner: "provider";
|
|
17
|
+
readonly sellerTiming: "execution" | "first_claim";
|
|
18
|
+
readonly operations: Readonly<Record<"issue" | "correction" | "recovery" | "readback" | "exactAbsence" | "artifact" | "clearance" | "settlement" | "companyLookup", {
|
|
19
|
+
readonly supported: true;
|
|
20
|
+
} | {
|
|
21
|
+
readonly supported: false;
|
|
22
|
+
readonly reason: "host_required" | "not_implemented" | "not_exposed";
|
|
23
|
+
}>> & {
|
|
24
|
+
readonly remoteVoid: {
|
|
25
|
+
readonly supported: false;
|
|
26
|
+
readonly reason: "not_implemented";
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
readonly defaultOff: true;
|
|
30
|
+
}
|
|
31
|
+
/** Interpretation only: no environment discovery, activation, writes or provider calls. */
|
|
32
|
+
export type InvoiceCapabilitySelection = InvoiceUnsupported | {
|
|
33
|
+
readonly kind: "ready";
|
|
34
|
+
readonly capability: InvoiceCapabilities;
|
|
35
|
+
} | {
|
|
36
|
+
readonly kind: "refused";
|
|
37
|
+
readonly code: Extract<InvoiceDiagnosticCode, "disabled" | "selection_required">;
|
|
38
|
+
readonly retryable: false;
|
|
39
|
+
readonly effectMayHaveSucceeded: false;
|
|
40
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|