@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.
Files changed (241) hide show
  1. package/CHANGELOG.md +440 -158
  2. package/README.md +141 -589
  3. package/dist/fulfillment/adapterConformance.js +1 -1
  4. package/dist/fulfillment/bindings.js +1 -1
  5. package/dist/fulfillment/contracts/conformance.d.ts +3 -1
  6. package/dist/fulfillment/contracts/dispatch.d.ts +45 -3
  7. package/dist/fulfillment/contracts/index.d.ts +3 -3
  8. package/dist/fulfillment/contracts/observation.d.ts +21 -1
  9. package/dist/fulfillment/contracts/readback.d.ts +21 -0
  10. package/dist/fulfillment/contracts/store.d.ts +3 -0
  11. package/dist/fulfillment/contracts/sync.d.ts +58 -2
  12. package/dist/fulfillment/dispatchProjection.d.ts +20 -1
  13. package/dist/fulfillment/dispatchProjection.js +97 -6
  14. package/dist/fulfillment/index.d.ts +3 -2
  15. package/dist/fulfillment/index.js +2 -2
  16. package/dist/fulfillment/migrations.d.ts +5 -0
  17. package/dist/fulfillment/migrations.js +1 -0
  18. package/dist/fulfillment/observationProjection.d.ts +24 -3
  19. package/dist/fulfillment/observationProjection.js +65 -12
  20. package/dist/fulfillment/omnipack/client.d.ts +3 -1
  21. package/dist/fulfillment/omnipack/client.js +31 -13
  22. package/dist/fulfillment/omnipack/index.d.ts +4 -1
  23. package/dist/fulfillment/omnipack/index.js +107 -45
  24. package/dist/fulfillment/omnipack/mappers.d.ts +7 -1
  25. package/dist/fulfillment/omnipack/mappers.js +24 -2
  26. package/dist/fulfillment/omnipack/observation.d.ts +11 -4
  27. package/dist/fulfillment/omnipack/observation.js +44 -29
  28. package/dist/fulfillment/omnipack/reconciliation.d.ts +15 -0
  29. package/dist/fulfillment/omnipack/reconciliation.js +55 -0
  30. package/dist/fulfillment/postgresDispatchStore.js +1 -1
  31. package/dist/fulfillment/postgresShipmentSpine.d.ts +8 -1
  32. package/dist/fulfillment/postgresShipmentSpine.js +17 -3
  33. package/dist/fulfillment/postgresStockSyncPort.js +6 -3
  34. package/dist/fulfillment/providerDispatchAcceptance.d.ts +1 -0
  35. package/dist/fulfillment/providerDispatchAcceptance.js +8 -2
  36. package/dist/fulfillment/providerDispatchWorker.d.ts +51 -0
  37. package/dist/fulfillment/providerDispatchWorker.js +65 -20
  38. package/dist/fulfillment/providerReconciliationWorker.d.ts +27 -19
  39. package/dist/fulfillment/providerReconciliationWorker.js +75 -19
  40. package/dist/fulfillment/providerStockSyncWorker.d.ts +5 -1
  41. package/dist/fulfillment/providerStockSyncWorker.js +229 -120
  42. package/dist/fulfillment/providerTrackingReferences.d.ts +3 -2
  43. package/dist/fulfillment/verifiedIngress.d.ts +38 -11
  44. package/dist/fulfillment/verifiedIngress.js +36 -8
  45. package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
  46. package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
  47. package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
  48. package/dist/invoicing/accountingLineAllocation.js +69 -0
  49. package/dist/invoicing/adapterConformance.d.ts +42 -0
  50. package/dist/invoicing/adapterConformance.js +296 -0
  51. package/dist/invoicing/artifacts.d.ts +7 -0
  52. package/dist/invoicing/artifacts.js +20 -0
  53. package/dist/invoicing/bindings.d.ts +46 -0
  54. package/dist/invoicing/bindings.js +69 -0
  55. package/dist/invoicing/capabilities.d.ts +11 -0
  56. package/dist/invoicing/capabilities.js +10 -0
  57. package/dist/invoicing/channelPolicy.d.ts +28 -0
  58. package/dist/invoicing/channelPolicy.js +27 -0
  59. package/dist/invoicing/continuity.d.ts +12 -0
  60. package/dist/invoicing/continuity.js +18 -0
  61. package/dist/invoicing/contracts/capabilities.d.ts +40 -0
  62. package/dist/invoicing/contracts/capabilities.js +1 -0
  63. package/dist/invoicing/contracts/companyLookup.d.ts +49 -0
  64. package/dist/invoicing/contracts/companyLookup.js +1 -0
  65. package/dist/invoicing/contracts/delivery.d.ts +74 -0
  66. package/dist/invoicing/contracts/delivery.js +1 -0
  67. package/dist/invoicing/contracts/facts.d.ts +110 -0
  68. package/dist/invoicing/contracts/facts.js +1 -0
  69. package/dist/invoicing/contracts/host.d.ts +245 -0
  70. package/dist/invoicing/contracts/host.js +1 -0
  71. package/dist/invoicing/contracts/index.d.ts +16 -0
  72. package/dist/invoicing/contracts/index.js +1 -0
  73. package/dist/invoicing/contracts/provider.d.ts +126 -0
  74. package/dist/invoicing/contracts/provider.js +1 -0
  75. package/dist/invoicing/contracts/settlement.d.ts +66 -0
  76. package/dist/invoicing/contracts/settlement.js +1 -0
  77. package/dist/invoicing/contracts/store.d.ts +277 -0
  78. package/dist/invoicing/contracts/store.js +1 -0
  79. package/dist/invoicing/contribution.d.ts +21 -0
  80. package/dist/invoicing/contribution.js +19 -0
  81. package/dist/invoicing/correction.d.ts +42 -0
  82. package/dist/invoicing/correction.js +44 -0
  83. package/dist/invoicing/delivery.d.ts +85 -0
  84. package/dist/invoicing/delivery.js +79 -0
  85. package/dist/invoicing/documentHistory.d.ts +50 -0
  86. package/dist/invoicing/documentHistory.js +213 -0
  87. package/dist/invoicing/documentSync.d.ts +24 -0
  88. package/dist/invoicing/documentSync.js +26 -0
  89. package/dist/invoicing/dutyCollector.d.ts +15 -0
  90. package/dist/invoicing/dutyCollector.js +35 -0
  91. package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
  92. package/dist/invoicing/fulfillmentTrigger.js +90 -0
  93. package/dist/invoicing/handoff.d.ts +37 -0
  94. package/dist/invoicing/handoff.js +62 -0
  95. package/dist/invoicing/index.d.ts +39 -0
  96. package/dist/invoicing/index.js +37 -0
  97. package/dist/invoicing/issue.d.ts +39 -0
  98. package/dist/invoicing/issue.js +55 -0
  99. package/dist/invoicing/migrations.d.ts +48 -0
  100. package/dist/invoicing/migrations.js +25 -0
  101. package/dist/invoicing/nativeUnsupported.d.ts +9 -0
  102. package/dist/invoicing/nativeUnsupported.js +22 -0
  103. package/dist/invoicing/paymentReadback.d.ts +20 -0
  104. package/dist/invoicing/paymentReadback.js +47 -0
  105. package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
  106. package/dist/invoicing/postgresInvoiceStore.js +344 -0
  107. package/dist/invoicing/readModel.d.ts +74 -0
  108. package/dist/invoicing/readModel.js +90 -0
  109. package/dist/invoicing/readback.d.ts +4 -0
  110. package/dist/invoicing/readback.js +30 -0
  111. package/dist/invoicing/reversal.d.ts +37 -0
  112. package/dist/invoicing/reversal.js +28 -0
  113. package/dist/invoicing/sellerRevision.d.ts +19 -0
  114. package/dist/invoicing/sellerRevision.js +14 -0
  115. package/dist/invoicing/settlementSync.d.ts +18 -0
  116. package/dist/invoicing/settlementSync.js +32 -0
  117. package/dist/invoicing/snapshot.d.ts +52 -0
  118. package/dist/invoicing/snapshot.js +141 -0
  119. package/dist/subscriptions/automaticRenewal.d.ts +44 -0
  120. package/dist/subscriptions/automaticRenewal.js +81 -0
  121. package/dist/subscriptions/automaticRenewalExecution.d.ts +21 -0
  122. package/dist/subscriptions/automaticRenewalExecution.js +44 -0
  123. package/dist/subscriptions/contracts/commands.d.ts +115 -0
  124. package/dist/subscriptions/contracts/commands.js +1 -0
  125. package/dist/subscriptions/contracts/dunning.d.ts +133 -0
  126. package/dist/subscriptions/contracts/dunning.js +1 -0
  127. package/dist/subscriptions/contracts/index.d.ts +6 -0
  128. package/dist/subscriptions/contracts/index.js +1 -0
  129. package/dist/subscriptions/contracts/messages.d.ts +43 -0
  130. package/dist/subscriptions/contracts/messages.js +1 -0
  131. package/dist/subscriptions/contracts/renewal.d.ts +533 -0
  132. package/dist/subscriptions/contracts/renewal.js +1 -0
  133. package/dist/subscriptions/contracts/stockWait.d.ts +73 -0
  134. package/dist/subscriptions/contracts/stockWait.js +1 -0
  135. package/dist/subscriptions/index.d.ts +39 -0
  136. package/dist/subscriptions/index.js +17 -0
  137. package/dist/subscriptions/paymentOutcomes.d.ts +8 -0
  138. package/dist/subscriptions/paymentOutcomes.js +27 -0
  139. package/dist/subscriptions/postgresRenewalStore.d.ts +20 -0
  140. package/dist/subscriptions/postgresRenewalStore.js +61 -0
  141. package/dist/subscriptions/postgresRetryStore.d.ts +78 -0
  142. package/dist/subscriptions/postgresRetryStore.js +142 -0
  143. package/dist/subscriptions/renewalCycleFailureMechanism.d.ts +34 -0
  144. package/dist/subscriptions/renewalCycleFailureMechanism.js +79 -0
  145. package/dist/subscriptions/renewalCycleMechanism.d.ts +46 -0
  146. package/dist/subscriptions/renewalCycleMechanism.js +157 -0
  147. package/dist/subscriptions/renewalCycleRefusalMechanism.d.ts +41 -0
  148. package/dist/subscriptions/renewalCycleRefusalMechanism.js +160 -0
  149. package/dist/subscriptions/renewalIdentity.d.ts +3 -0
  150. package/dist/subscriptions/renewalIdentity.js +4 -0
  151. package/package.json +23 -3
  152. package/release-gates.json +74 -7
  153. package/sql/20260809210000_accounting_document_rail.sql +889 -0
  154. package/sql/20260811150000_accounting_payout_rail.sql +333 -0
  155. package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
  156. package/sql/20261022160000_subscription_retry_lifecycle.sql +328 -0
  157. package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -0
  158. package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
  159. package/sql/20261022191000_invoicing_document_effects.sql +249 -0
  160. package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
  161. package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
  162. package/src/fulfillment/adapterConformance.ts +1 -1
  163. package/src/fulfillment/bindings.ts +1 -1
  164. package/src/fulfillment/contracts/conformance.ts +3 -1
  165. package/src/fulfillment/contracts/dispatch.ts +24 -5
  166. package/src/fulfillment/contracts/index.ts +3 -3
  167. package/src/fulfillment/contracts/observation.ts +11 -3
  168. package/src/fulfillment/contracts/readback.ts +13 -0
  169. package/src/fulfillment/contracts/store.ts +1 -0
  170. package/src/fulfillment/contracts/sync.ts +13 -2
  171. package/src/fulfillment/dispatchProjection.ts +94 -7
  172. package/src/fulfillment/index.ts +3 -2
  173. package/src/fulfillment/migrations.ts +1 -0
  174. package/src/fulfillment/observationProjection.ts +64 -13
  175. package/src/fulfillment/omnipack/client.ts +32 -13
  176. package/src/fulfillment/omnipack/index.ts +99 -39
  177. package/src/fulfillment/omnipack/mappers.ts +23 -2
  178. package/src/fulfillment/omnipack/observation.ts +45 -27
  179. package/src/fulfillment/omnipack/reconciliation.ts +56 -0
  180. package/src/fulfillment/postgresDispatchStore.ts +1 -1
  181. package/src/fulfillment/postgresShipmentSpine.ts +21 -4
  182. package/src/fulfillment/postgresStockSyncPort.ts +6 -3
  183. package/src/fulfillment/providerDispatchAcceptance.ts +9 -3
  184. package/src/fulfillment/providerDispatchWorker.ts +61 -20
  185. package/src/fulfillment/providerReconciliationWorker.ts +81 -25
  186. package/src/fulfillment/providerStockSyncWorker.ts +222 -126
  187. package/src/fulfillment/providerTrackingReferences.ts +3 -2
  188. package/src/fulfillment/verifiedIngress.ts +44 -11
  189. package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
  190. package/src/invoicing/accountingLineAllocation.ts +69 -0
  191. package/src/invoicing/adapterConformance.ts +249 -0
  192. package/src/invoicing/artifacts.ts +19 -0
  193. package/src/invoicing/bindings.ts +80 -0
  194. package/src/invoicing/capabilities.ts +9 -0
  195. package/src/invoicing/channelPolicy.ts +50 -0
  196. package/src/invoicing/continuity.ts +15 -0
  197. package/src/invoicing/contracts/capabilities.ts +15 -0
  198. package/src/invoicing/contracts/companyLookup.ts +13 -0
  199. package/src/invoicing/contracts/delivery.ts +24 -0
  200. package/src/invoicing/contracts/facts.ts +71 -0
  201. package/src/invoicing/contracts/host.ts +84 -0
  202. package/src/invoicing/contracts/index.ts +16 -0
  203. package/src/invoicing/contracts/provider.ts +78 -0
  204. package/src/invoicing/contracts/settlement.ts +39 -0
  205. package/src/invoicing/contracts/store.ts +102 -0
  206. package/src/invoicing/contribution.ts +28 -0
  207. package/src/invoicing/correction.ts +56 -0
  208. package/src/invoicing/delivery.ts +122 -0
  209. package/src/invoicing/documentHistory.ts +301 -0
  210. package/src/invoicing/documentSync.ts +51 -0
  211. package/src/invoicing/dutyCollector.ts +31 -0
  212. package/src/invoicing/fulfillmentTrigger.ts +144 -0
  213. package/src/invoicing/handoff.ts +71 -0
  214. package/src/invoicing/index.ts +39 -0
  215. package/src/invoicing/issue.ts +64 -0
  216. package/src/invoicing/migrations.ts +25 -0
  217. package/src/invoicing/nativeUnsupported.ts +24 -0
  218. package/src/invoicing/paymentReadback.ts +70 -0
  219. package/src/invoicing/postgresInvoiceStore.ts +334 -0
  220. package/src/invoicing/readModel.ts +80 -0
  221. package/src/invoicing/readback.ts +26 -0
  222. package/src/invoicing/reversal.ts +34 -0
  223. package/src/invoicing/sellerRevision.ts +20 -0
  224. package/src/invoicing/settlementSync.ts +37 -0
  225. package/src/invoicing/snapshot.ts +192 -0
  226. package/src/subscriptions/automaticRenewal.ts +87 -0
  227. package/src/subscriptions/automaticRenewalExecution.ts +39 -0
  228. package/src/subscriptions/contracts/commands.ts +46 -0
  229. package/src/subscriptions/contracts/dunning.ts +48 -0
  230. package/src/subscriptions/contracts/index.ts +6 -0
  231. package/src/subscriptions/contracts/messages.ts +18 -0
  232. package/src/subscriptions/contracts/renewal.ts +325 -0
  233. package/src/subscriptions/contracts/stockWait.ts +34 -0
  234. package/src/subscriptions/index.ts +24 -0
  235. package/src/subscriptions/paymentOutcomes.ts +23 -0
  236. package/src/subscriptions/postgresRenewalStore.ts +76 -0
  237. package/src/subscriptions/postgresRetryStore.ts +174 -0
  238. package/src/subscriptions/renewalCycleFailureMechanism.ts +119 -0
  239. package/src/subscriptions/renewalCycleMechanism.ts +225 -0
  240. package/src/subscriptions/renewalCycleRefusalMechanism.ts +215 -0
  241. package/src/subscriptions/renewalIdentity.ts +5 -0
@@ -1,4 +1,4 @@
1
- import type { FulfillmentObservation, FulfillmentObservationPort, FulfillmentRefusal } from "./contracts/index.js";
1
+ import type { FulfillmentObservation, FulfillmentObservationCodecs, FulfillmentObservationPort, FulfillmentRefusal } from "./contracts/index.js";
2
2
  import { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
3
3
  type EffectInput = Parameters<typeof applyFulfillmentObservationEffects<FulfillmentObservation["fact"]["canonicalStatus"], "in_transit" | "delivered">>[0];
4
4
  type DispatchRef = {
@@ -7,20 +7,54 @@ type DispatchRef = {
7
7
  dispatchRef: string;
8
8
  externalRef: string | null;
9
9
  };
10
+ export interface FulfillmentIngressCounts {
11
+ invalidTimestamps: number;
12
+ rejectedCorrelationRefs: number;
13
+ rejectedTrackingRefs: number;
14
+ rejectedTrackingUrls: number;
15
+ }
16
+ export type FulfillmentIngressResult = {
17
+ counts: FulfillmentIngressCounts;
18
+ } & ({
19
+ kind: "refused";
20
+ receiptId: null;
21
+ refusal: FulfillmentRefusal;
22
+ diagnostics?: {
23
+ invalidTimestamp?: true;
24
+ rejectedCorrelationRefs?: number;
25
+ };
26
+ } | {
27
+ kind: "processed" | "ignored" | "superseded";
28
+ receiptId: string;
29
+ observation: FulfillmentObservation;
30
+ replayed: boolean;
31
+ trackingRefs: number;
32
+ trackingReadBacks: number;
33
+ supersededCause?: string;
34
+ });
35
+ /** Safe response classes only; the host owns HTTP and localized message text. */
36
+ export declare function fulfillmentIngressHttpResult(result: FulfillmentIngressResult): {
37
+ status: 200 | 400 | 403 | 503;
38
+ messageClass: "accepted" | "bad_request" | "forbidden" | "upstream_unavailable";
39
+ };
10
40
  /** Adapter verification owns auth-before-parse; the host owns durable replay. */
11
41
  export declare function verifyAndNormalizeFulfillmentObservation(input: {
12
42
  enabled: boolean;
43
+ approveTrackingUrl?(url: string): boolean;
44
+ observationCodecs?: FulfillmentObservationCodecs;
13
45
  provider: FulfillmentObservationPort;
14
46
  verification: Parameters<FulfillmentObservationPort["verifyAndNormalize"]>[0];
15
47
  localPersistence: {
16
48
  /** G17: atomic insert by eventKey; retries keep the first stored firstReceivedAt. */
17
49
  recordObservation(observation: FulfillmentObservation): Promise<{
18
50
  replayed: boolean;
51
+ receiptId?: string;
19
52
  }>;
20
53
  /** Idempotent; only called after completion or a classified superseding conflict. */
21
54
  markProcessed(eventKey: string, outcome: {
22
55
  status: "processed" | "ignored";
23
56
  refusal?: FulfillmentRefusal;
57
+ supersededCause?: string;
24
58
  }): Promise<void>;
25
59
  };
26
60
  lifecycle: {
@@ -36,17 +70,10 @@ export declare function verifyAndNormalizeFulfillmentObservation(input: {
36
70
  tracking(eventKey: string, number: string): string;
37
71
  };
38
72
  issueAccountingInvoice?(ref: string): Promise<void>;
73
+ /** Return a machine identity, never exception prose. */
74
+ supersededCause?(error: unknown): string | null;
39
75
  classifyFailure(error: unknown): "stock_contention" | "state_conflict" | "other";
40
76
  annotateStockContention?(eventKey: string, fulfillmentRef: string): Promise<void>;
41
77
  };
42
- }): Promise<{
43
- kind: "refused";
44
- refusal: FulfillmentRefusal;
45
- } | {
46
- kind: "processed" | "ignored" | "superseded";
47
- observation: FulfillmentObservation;
48
- replayed: boolean;
49
- trackingRefs: number;
50
- trackingReadBacks: number;
51
- }>;
78
+ }): Promise<FulfillmentIngressResult>;
52
79
  export {};
@@ -1,19 +1,44 @@
1
+ import { safeDiagnosticCode } from "./dispatchProjection.js";
1
2
  import { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
2
3
  import { observationStatusPolicy, projectObservation } from "./observationProjection.js";
3
4
  import { uniqueTrackingReferences } from "./providerTrackingReferences.js";
5
+ /** Safe response classes only; the host owns HTTP and localized message text. */
6
+ export function fulfillmentIngressHttpResult(result) {
7
+ if (result.kind !== "refused")
8
+ return { status: 200, messageClass: "accepted" };
9
+ if (result.refusal.reason === "untrusted")
10
+ return { status: 403, messageClass: "forbidden" };
11
+ if (result.refusal.reason === "disabled" || result.refusal.reason === "not_ready" || result.refusal.retryable)
12
+ return { status: 503, messageClass: "upstream_unavailable" };
13
+ return { status: 400, messageClass: "bad_request" };
14
+ }
4
15
  /** Adapter verification owns auth-before-parse; the host owns durable replay. */
5
16
  export async function verifyAndNormalizeFulfillmentObservation(input) {
17
+ const counts = { invalidTimestamps: 0, rejectedCorrelationRefs: 0, rejectedTrackingRefs: 0, rejectedTrackingUrls: 0 };
6
18
  if (!input.enabled)
7
- return { kind: "refused", refusal: { reason: "disabled", phase: "verify", retryable: false } };
19
+ return { kind: "refused", receiptId: null, counts, refusal: { reason: "disabled", phase: "verify", retryable: false } };
8
20
  const verified = await input.provider.verifyAndNormalize(input.verification);
9
- if (verified.kind === "refused")
10
- return verified;
11
- const observation = projectObservation(verified.observation);
21
+ if (verified.kind === "refused") {
22
+ if (verified.diagnostics?.invalidTimestamp)
23
+ counts.invalidTimestamps++;
24
+ const rejected = verified.diagnostics?.rejectedCorrelationRefs;
25
+ if (typeof rejected === "number" && Number.isSafeInteger(rejected) && rejected > 0)
26
+ counts.rejectedCorrelationRefs = rejected;
27
+ return { kind: "refused", receiptId: null, refusal: verified.refusal, counts,
28
+ ...(counts.invalidTimestamps || counts.rejectedCorrelationRefs ? { diagnostics: {
29
+ ...(counts.invalidTimestamps ? { invalidTimestamp: true } : {}),
30
+ ...(counts.rejectedCorrelationRefs ? { rejectedCorrelationRefs: counts.rejectedCorrelationRefs } : {}),
31
+ } } : {}) };
32
+ }
33
+ const observation = projectObservation(verified.observation, input.approveTrackingUrl, input.observationCodecs);
34
+ counts.rejectedTrackingRefs = observation.diagnostics?.admission?.rejectedTrackingRefs ?? 0;
35
+ counts.rejectedTrackingUrls = observation.diagnostics?.admission?.rejectedTrackingUrls ?? 0;
12
36
  const inbound = await input.localPersistence.recordObservation(observation);
37
+ const receiptId = inbound.receiptId ?? observation.eventKey;
13
38
  const ref = await input.lifecycle.findDispatchRef(observation);
14
39
  if (!ref) {
15
40
  await input.localPersistence.markProcessed(observation.eventKey, { status: "ignored", refusal: { reason: "local_missing", phase: "apply", retryable: false } });
16
- return { kind: "ignored", observation, replayed: inbound.replayed, trackingRefs: 0, trackingReadBacks: 0 };
41
+ return { kind: "ignored", receiptId, counts, observation, replayed: inbound.replayed, trackingRefs: 0, trackingReadBacks: 0 };
17
42
  }
18
43
  const externalRef = observation.correlation.externalRef?.trim() || ref.externalRef?.trim();
19
44
  if (!externalRef)
@@ -57,10 +82,13 @@ export async function verifyAndNormalizeFulfillmentObservation(input) {
57
82
  catch (error) {
58
83
  if (input.lifecycle.classifyFailure(error) !== "state_conflict")
59
84
  throw error;
60
- await input.localPersistence.markProcessed(observation.eventKey, { status: "processed" });
61
- return { kind: "superseded", observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
85
+ const cause = input.lifecycle.supersededCause ? input.lifecycle.supersededCause(error)
86
+ : error && typeof error === "object" && "reason" in error ? error.reason : null;
87
+ const supersededCause = safeDiagnosticCode(cause) ?? "fulfillment_state_conflict";
88
+ await input.localPersistence.markProcessed(observation.eventKey, { status: "processed", supersededCause });
89
+ return { kind: "superseded", receiptId, counts, supersededCause, observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
62
90
  }
63
91
  }
64
92
  await input.localPersistence.markProcessed(observation.eventKey, { status: "processed" });
65
- return { kind: "processed", observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
93
+ return { kind: "processed", receiptId, counts, observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
66
94
  }
@@ -0,0 +1,5 @@
1
+ import type { InvoiceLine } from "./contracts/facts.js";
2
+ export declare function mapCanonicalIssueLines(linesSnapshot: unknown[], chargedTotalGrossMinor: number, chargedTotalNetMinor: number, defaultLabel: string): Array<Pick<InvoiceLine, "quantity" | "unitGrossMinor" | "unitNetMinor" | "totalGrossMinor" | "totalNetMinor"> & {
3
+ name: string;
4
+ vatRate: string;
5
+ }>;
@@ -0,0 +1,104 @@
1
+ import { allocateDiscountedGrossTotals } from "./accountingLineAllocation.js";
2
+ export function mapCanonicalIssueLines(linesSnapshot, chargedTotalGrossMinor, chargedTotalNetMinor, defaultLabel) {
3
+ const lines = linesSnapshot.map((value) => {
4
+ const row = isRecord(value) ? value : {};
5
+ const quantity = requiredPositiveInteger(row.quantity, "quantity");
6
+ const totalGrossMinor = requiredNonnegativeInteger(row.totalGrossMinor, "total_gross");
7
+ const totalNetMinor = requiredNonnegativeInteger(row.totalNetMinor, "total_net");
8
+ const vatRateBps = requiredNonnegativeInteger(row.vatRateBps, "vat_rate_bps");
9
+ if (vatRateBps > 10_000 || netFromGross(totalGrossMinor, vatRateBps) !== totalNetMinor) {
10
+ throw new Error("accounting_invoice_canonical_position_vat_mismatch");
11
+ }
12
+ const vatRate = canonicalVatRate(vatRateBps);
13
+ const suppliedVatRate = textValue(row.vatRate);
14
+ if (suppliedVatRate !== null && normalizeVatRate(suppliedVatRate) !== vatRate) {
15
+ throw new Error("accounting_invoice_canonical_position_vat_label_mismatch");
16
+ }
17
+ return {
18
+ positionKind: textValue(row.positionKind),
19
+ name: textValue(row.name) ?? defaultLabel,
20
+ quantity,
21
+ unitGrossMinor: requiredNonnegativeInteger(row.unitGrossMinor, "unit_gross"),
22
+ unitNetMinor: requiredNonnegativeInteger(row.unitNetMinor, "unit_net"),
23
+ totalGrossMinor,
24
+ totalNetMinor,
25
+ vatRate,
26
+ catalogTotalGrossMinor: numberValue(row.catalogTotalGrossMinor),
27
+ discountAllocatedMinor: numberValue(row.discountAllocatedMinor),
28
+ shippingGrossMinor: numberValue(row.shippingGrossMinor),
29
+ shippingDiscountMinor: numberValue(row.shippingDiscountMinor),
30
+ };
31
+ });
32
+ const items = lines.filter((line) => line.positionKind === "item");
33
+ const delivery = lines.filter((line) => line.positionKind === "delivery");
34
+ if (items.length === 0 || delivery.length > 1) {
35
+ throw new Error("accounting_invoice_canonical_position_cardinality_invalid");
36
+ }
37
+ const actualDelivery = delivery[0]?.totalGrossMinor ?? 0;
38
+ if (delivery.length === 1) {
39
+ const shippingGross = delivery[0].shippingGrossMinor;
40
+ const shippingDiscount = delivery[0].shippingDiscountMinor;
41
+ if (!Number.isSafeInteger(shippingGross) || !Number.isSafeInteger(shippingDiscount)
42
+ || shippingGross < 0 || shippingDiscount < 0 || shippingDiscount > shippingGross
43
+ || shippingGross - shippingDiscount !== actualDelivery) {
44
+ throw new Error("accounting_invoice_canonical_delivery_mismatch");
45
+ }
46
+ }
47
+ const catalogTotals = items.map((line) => {
48
+ if (!Number.isInteger(line.catalogTotalGrossMinor) || (line.catalogTotalGrossMinor ?? -1) < 0) {
49
+ throw new Error("accounting_invoice_canonical_catalog_total_invalid");
50
+ }
51
+ if (!Number.isInteger(line.discountAllocatedMinor) || (line.discountAllocatedMinor ?? -1) < 0) {
52
+ throw new Error("accounting_invoice_canonical_discount_allocation_invalid");
53
+ }
54
+ if (line.totalGrossMinor + line.discountAllocatedMinor !== line.catalogTotalGrossMinor) {
55
+ throw new Error("accounting_invoice_canonical_line_allocation_mismatch");
56
+ }
57
+ return line.catalogTotalGrossMinor;
58
+ });
59
+ const independentlyAllocated = allocateDiscountedGrossTotals(catalogTotals, chargedTotalGrossMinor - actualDelivery);
60
+ if (items.some((line, index) => line.totalGrossMinor !== independentlyAllocated[index])) {
61
+ throw new Error("accounting_invoice_canonical_allocator_delta_nonzero");
62
+ }
63
+ const grossSum = lines.reduce((sum, line) => sum + line.totalGrossMinor, 0);
64
+ const netSum = lines.reduce((sum, line) => sum + line.totalNetMinor, 0);
65
+ if (grossSum !== chargedTotalGrossMinor || netSum !== chargedTotalNetMinor) {
66
+ throw new Error(`accounting_invoice_canonical_positions_header_mismatch gross=${grossSum}/${chargedTotalGrossMinor} net=${netSum}/${chargedTotalNetMinor}`);
67
+ }
68
+ return lines.map(({ positionKind: _kind, catalogTotalGrossMinor: _catalog, discountAllocatedMinor: _discount, shippingGrossMinor: _shippingGross, shippingDiscountMinor: _shippingDiscount, ...line }) => line);
69
+ }
70
+ function requiredNonnegativeInteger(value, field) {
71
+ const number = numberValue(value);
72
+ if (number === null || !Number.isSafeInteger(number) || number < 0) {
73
+ throw new Error(`accounting_invoice_canonical_${field}_invalid`);
74
+ }
75
+ return number;
76
+ }
77
+ function requiredPositiveInteger(value, field) {
78
+ const number = requiredNonnegativeInteger(value, field);
79
+ if (number === 0)
80
+ throw new Error(`accounting_invoice_canonical_${field}_invalid`);
81
+ return number;
82
+ }
83
+ function netFromGross(gross, vatRateBps) {
84
+ return Math.round(gross * 10_000 / (10_000 + vatRateBps));
85
+ }
86
+ function canonicalVatRate(vatRateBps) {
87
+ return normalizeVatRate(String(vatRateBps / 100));
88
+ }
89
+ function normalizeVatRate(value) {
90
+ const parsed = Number(value.replace(",", "."));
91
+ if (!Number.isFinite(parsed) || parsed < 0) {
92
+ throw new Error("accounting_invoice_canonical_position_vat_label_invalid");
93
+ }
94
+ return String(parsed);
95
+ }
96
+ function textValue(value) {
97
+ return typeof value === "string" && value.trim() ? value.trim() : null;
98
+ }
99
+ function numberValue(value) {
100
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
101
+ }
102
+ function isRecord(value) {
103
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
104
+ }
@@ -0,0 +1,2 @@
1
+ export declare function allocateChargedGrossTotals(lineTotals: number[], chargedTotal: number): number[];
2
+ export declare function allocateDiscountedGrossTotals(lineTotals: number[], chargedTotal: number): number[];
@@ -0,0 +1,69 @@
1
+ // Independent document-boundary allocation. The database writer performs its
2
+ // own SQL allocation; this copy deliberately verifies persisted results rather
3
+ // than sharing an implementation with the order writer.
4
+ export function allocateChargedGrossTotals(lineTotals, chargedTotal) {
5
+ if (!Number.isInteger(chargedTotal) || chargedTotal < 0) {
6
+ throw new Error(`accounting_invoice_charged_total_invalid charged=${chargedTotal}`);
7
+ }
8
+ if (lineTotals.some((total) => !Number.isInteger(total) || total < 0)) {
9
+ throw new Error("accounting_invoice_line_totals_invalid");
10
+ }
11
+ const linesTotal = lineTotals.reduce((sum, total) => sum + total, 0);
12
+ if (linesTotal === chargedTotal)
13
+ return lineTotals;
14
+ if (linesTotal < chargedTotal) {
15
+ throw new Error(`accounting_invoice_lines_below_charged_total lines=${linesTotal} charged=${chargedTotal}`);
16
+ }
17
+ const scaled = lineTotals.map((total) => (total * chargedTotal) / linesTotal);
18
+ const allocated = scaled.map((value) => Math.floor(value));
19
+ let remainder = chargedTotal - allocated.reduce((sum, total) => sum + total, 0);
20
+ const byFraction = scaled
21
+ .map((value, index) => ({ index, fraction: value - allocated[index] }))
22
+ .sort((a, b) => b.fraction - a.fraction || a.index - b.index);
23
+ for (const { index } of byFraction) {
24
+ if (remainder <= 0)
25
+ break;
26
+ allocated[index] += 1;
27
+ remainder -= 1;
28
+ }
29
+ return allocated;
30
+ }
31
+ // Mirrors the order writer's discount allocation rule for verification only:
32
+ // allocate the discount (not the charged total) by largest remainder, then
33
+ // subtract it from each catalog line. Allocating the charged total directly
34
+ // reverses the deterministic tie-break for equal lines.
35
+ export function allocateDiscountedGrossTotals(lineTotals, chargedTotal) {
36
+ const linesTotal = lineTotals.reduce((sum, total) => sum + total, 0);
37
+ const discount = linesTotal - chargedTotal;
38
+ if (!Number.isInteger(chargedTotal) || chargedTotal < 0 || discount < 0) {
39
+ throw new Error(`accounting_invoice_charged_total_invalid charged=${chargedTotal}`);
40
+ }
41
+ if (lineTotals.some((total) => !Number.isInteger(total) || total < 0)) {
42
+ throw new Error("accounting_invoice_line_totals_invalid");
43
+ }
44
+ if (discount === 0)
45
+ return lineTotals;
46
+ if (linesTotal === 0) {
47
+ throw new Error("accounting_invoice_discount_allocation_zero_spread");
48
+ }
49
+ const sumExact = BigInt(linesTotal);
50
+ const discountExact = BigInt(discount);
51
+ const scaledDiscounts = lineTotals.map((total) => {
52
+ const product = BigInt(total) * discountExact;
53
+ return { base: product / sumExact, remainder: product % sumExact };
54
+ });
55
+ const allocatedDiscounts = scaledDiscounts.map(({ base }) => Number(base));
56
+ let remainder = discount - allocatedDiscounts.reduce((sum, total) => sum + total, 0);
57
+ const byFraction = scaledDiscounts
58
+ .map((value, index) => ({ index, fraction: value.remainder }))
59
+ .sort((a, b) => a.fraction === b.fraction
60
+ ? a.index - b.index
61
+ : a.fraction > b.fraction ? -1 : 1);
62
+ for (const { index } of byFraction) {
63
+ if (remainder <= 0)
64
+ break;
65
+ allocatedDiscounts[index] += 1;
66
+ remainder -= 1;
67
+ }
68
+ return lineTotals.map((total, index) => total - allocatedDiscounts[index]);
69
+ }
@@ -0,0 +1,42 @@
1
+ import type { InvoiceCapabilities, InvoiceIdentity, InvoiceWriteResult, InvoiceReadbackResult, InvoiceArtifactResult, InvoiceHostPorts, InvoiceServicesOptions } from "./contracts/index.js";
2
+ type Provider = NonNullable<InvoiceServicesOptions["provider"]>;
3
+ type Fixture<Input, Result> = {
4
+ readonly input: Input;
5
+ readonly expected: Result;
6
+ };
7
+ interface Subject {
8
+ readonly subjectId: string;
9
+ readonly capability: InvoiceCapabilities;
10
+ readonly provider: Provider;
11
+ readonly host?: InvoiceHostPorts;
12
+ /** Adapter-owned offline fixtures; count writes across every injected transport. */
13
+ readonly readWriteCount: () => number;
14
+ readonly sensitiveValues: readonly string[];
15
+ readonly fixtures: {
16
+ readonly issue: readonly (Fixture<Parameters<Provider["issue"]>[0], InvoiceWriteResult> & {
17
+ readonly replay?: boolean;
18
+ })[];
19
+ readonly correction: readonly (Fixture<Parameters<Provider["correct"]>[0], InvoiceWriteResult> & {
20
+ readonly replay?: boolean;
21
+ })[];
22
+ readonly readback: readonly Fixture<InvoiceIdentity, InvoiceReadbackResult>[];
23
+ readonly void: readonly Fixture<Parameters<Provider["void"]>[0], Awaited<ReturnType<Provider["void"]>>>[];
24
+ readonly artifact?: readonly Fixture<Parameters<InvoiceHostPorts["artifacts"]["read"]>[0], InvoiceArtifactResult>[];
25
+ readonly clearance?: readonly Fixture<Parameters<InvoiceHostPorts["clearance"]["observe"]>[0], Awaited<ReturnType<InvoiceHostPorts["clearance"]["observe"]>>>[];
26
+ readonly settlement?: readonly Fixture<Parameters<InvoiceHostPorts["settlement"]["record"]>[0], Awaited<ReturnType<InvoiceHostPorts["settlement"]["record"]>>>[];
27
+ readonly companyLookup?: readonly Fixture<Parameters<InvoiceHostPorts["registry"]["lookup"]>[0], Awaited<ReturnType<InvoiceHostPorts["registry"]["lookup"]>>>[];
28
+ };
29
+ }
30
+ /** @consumer Offline provider bridge tests and installed cold consumer. Runner-
31
+ * agnostic contract evidence only; adapters own fixtures, transport and I/O counts.
32
+ * Throws closed failure codes. It neither certifies live fiscal readiness nor logs
33
+ * provider results, secrets, buyer facts or raw adapter errors.
34
+ */
35
+ export declare function runInvoiceAdapterConformance(subjects: readonly Subject[]): Promise<{
36
+ readonly subjects: readonly {
37
+ readonly subjectId: string;
38
+ readonly providerRef: string;
39
+ readonly cases: readonly string[];
40
+ }[];
41
+ }>;
42
+ export {};