@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,18 @@
|
|
|
1
|
+
import type { InvoiceSettlementPort, InvoiceSettlementItem } from "./contracts/settlement.js";
|
|
2
|
+
/** Preserve each immutable balance item, nullable links and original host key.
|
|
3
|
+
* Transport, resolution and richer durable item storage remain host-owned.
|
|
4
|
+
* Legacy batch/payment receipts may collapse items: later reuse is returned as
|
|
5
|
+
* identity_mismatch without another write; processing continues. Only an item-capable
|
|
6
|
+
* recorder can produce distinct recorded receipts. This records bank evidence only and has no payment-success writer.
|
|
7
|
+
*/
|
|
8
|
+
export declare function syncInvoicePayoutSettlements<Item>(input: {
|
|
9
|
+
list(limit: number): Promise<readonly Item[]>;
|
|
10
|
+
project(item: Item): InvoiceSettlementItem | Promise<InvoiceSettlementItem>;
|
|
11
|
+
settlement: Pick<InvoiceSettlementPort, "record">;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
checked: number;
|
|
15
|
+
recorded: number;
|
|
16
|
+
replayed: number;
|
|
17
|
+
items: Awaited<ReturnType<InvoiceSettlementPort["record"]>>[];
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Preserve each immutable balance item, nullable links and original host key.
|
|
2
|
+
* Transport, resolution and richer durable item storage remain host-owned.
|
|
3
|
+
* Legacy batch/payment receipts may collapse items: later reuse is returned as
|
|
4
|
+
* identity_mismatch without another write; processing continues. Only an item-capable
|
|
5
|
+
* recorder can produce distinct recorded receipts. This records bank evidence only and has no payment-success writer.
|
|
6
|
+
*/
|
|
7
|
+
export async function syncInvoicePayoutSettlements(input) {
|
|
8
|
+
const items = await input.list(input.limit ?? 10);
|
|
9
|
+
const result = { checked: items.length, recorded: 0, replayed: 0, items: [] };
|
|
10
|
+
const receipts = new Map();
|
|
11
|
+
for (const item of items) {
|
|
12
|
+
const evidence = await input.project(item);
|
|
13
|
+
const write = await input.settlement.record(evidence);
|
|
14
|
+
if (write.kind !== "recorded" || write.itemRef !== evidence.itemRef) {
|
|
15
|
+
// Unknown/conflict/unsupported are never counted as accepted evidence.
|
|
16
|
+
throw new Error(write.kind === "conflict" ? write.code : write.kind === "unknown" ? "unknown" : "unsupported");
|
|
17
|
+
}
|
|
18
|
+
const scope = JSON.stringify([evidence.providerRef, evidence.batchRef, evidence.itemRef, evidence.providerPaymentRef]);
|
|
19
|
+
const previous = receipts.get(write.settlementItemRef);
|
|
20
|
+
if (previous !== undefined && previous !== scope) {
|
|
21
|
+
result.items.push({ kind: "conflict", itemRef: evidence.itemRef, code: "identity_mismatch" });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
receipts.set(write.settlementItemRef, scope);
|
|
25
|
+
result.items.push(write);
|
|
26
|
+
if (write.replayed)
|
|
27
|
+
result.replayed += 1;
|
|
28
|
+
else
|
|
29
|
+
result.recorded += 1;
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
interface Options {
|
|
2
|
+
defaultLineLabel: string;
|
|
3
|
+
deliveryLabel: string;
|
|
4
|
+
defaultTaxRate: string;
|
|
5
|
+
}
|
|
6
|
+
/** Compatibility projection of a claimed host snapshot, not a second store contract.
|
|
7
|
+
* Seller/date/kind/payment selection remains with the host; no environment is read.
|
|
8
|
+
*/
|
|
9
|
+
export declare function mapInvoiceClaimToProviderSnapshot<Seller, Kind, Payment>(input: {
|
|
10
|
+
orderRef: string;
|
|
11
|
+
issueDate: string;
|
|
12
|
+
sellDate: string;
|
|
13
|
+
currency: string;
|
|
14
|
+
seller: Seller;
|
|
15
|
+
buyerSnapshot: Record<string, unknown>;
|
|
16
|
+
documentKind: Kind;
|
|
17
|
+
governmentClearanceRequired: boolean;
|
|
18
|
+
payment: Payment;
|
|
19
|
+
linesSnapshot: unknown[];
|
|
20
|
+
totalGrossMinor: number;
|
|
21
|
+
totalNetMinor: number;
|
|
22
|
+
orderMoney: unknown;
|
|
23
|
+
}, options: Options & {
|
|
24
|
+
defaultBuyerLabel: string;
|
|
25
|
+
}): {
|
|
26
|
+
orderRef: string;
|
|
27
|
+
issueDate: string;
|
|
28
|
+
sellDate: string;
|
|
29
|
+
currency: string;
|
|
30
|
+
seller: Seller;
|
|
31
|
+
buyer: {
|
|
32
|
+
name: string;
|
|
33
|
+
email: string | null;
|
|
34
|
+
taxId: string | null;
|
|
35
|
+
companyName: string | null;
|
|
36
|
+
address: {
|
|
37
|
+
readonly line1: string | null;
|
|
38
|
+
readonly postalCode: string | null;
|
|
39
|
+
readonly city: string | null;
|
|
40
|
+
readonly country: string | null;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
documentKind: Kind;
|
|
44
|
+
governmentClearanceRequired: boolean;
|
|
45
|
+
payment: Payment;
|
|
46
|
+
lines: (Pick<import("./contracts/facts.js").InvoiceLine, "quantity" | "unitGrossMinor" | "unitNetMinor" | "totalGrossMinor" | "totalNetMinor"> & {
|
|
47
|
+
name: string;
|
|
48
|
+
vatRate: string;
|
|
49
|
+
})[];
|
|
50
|
+
chargedTotalGrossMinor: number;
|
|
51
|
+
};
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { mapCanonicalIssueLines } from "./accountingCanonicalProviderLines.js";
|
|
2
|
+
import { allocateChargedGrossTotals } from "./accountingLineAllocation.js";
|
|
3
|
+
/** Compatibility projection of a claimed host snapshot, not a second store contract.
|
|
4
|
+
* Seller/date/kind/payment selection remains with the host; no environment is read.
|
|
5
|
+
*/
|
|
6
|
+
export function mapInvoiceClaimToProviderSnapshot(input, options) {
|
|
7
|
+
return {
|
|
8
|
+
orderRef: input.orderRef,
|
|
9
|
+
issueDate: input.issueDate,
|
|
10
|
+
sellDate: input.sellDate,
|
|
11
|
+
currency: input.currency,
|
|
12
|
+
seller: input.seller,
|
|
13
|
+
buyer: {
|
|
14
|
+
name: textValue(input.buyerSnapshot.name) ?? options.defaultBuyerLabel,
|
|
15
|
+
email: textValue(input.buyerSnapshot.email),
|
|
16
|
+
taxId: textValue(input.buyerSnapshot.taxId),
|
|
17
|
+
companyName: textValue(input.buyerSnapshot.companyName),
|
|
18
|
+
address: mapBuyerAddress(input.buyerSnapshot.address),
|
|
19
|
+
},
|
|
20
|
+
documentKind: input.documentKind,
|
|
21
|
+
governmentClearanceRequired: input.governmentClearanceRequired,
|
|
22
|
+
payment: input.payment,
|
|
23
|
+
lines: mapIssueLinesToChargedTotal(input.linesSnapshot, input.totalGrossMinor, input.totalNetMinor, input.orderMoney, options),
|
|
24
|
+
chargedTotalGrossMinor: input.totalGrossMinor,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Canonical issue snapshots already carry effective item and explicit delivery
|
|
28
|
+
// positions and are verified as a zero-delta passthrough. Historical snapshots
|
|
29
|
+
// without positionKind retain the independent #1693 reconciliation below.
|
|
30
|
+
function mapIssueLinesToChargedTotal(linesSnapshot, chargedTotalGrossMinor, chargedTotalNetMinor, orderMoney, options) {
|
|
31
|
+
const canonicalKinds = linesSnapshot.map((line) => textValue(isRecord(line) ? line.positionKind : null));
|
|
32
|
+
if (canonicalKinds.some(Boolean)) {
|
|
33
|
+
if (!canonicalKinds.every((kind) => kind === "item" || kind === "delivery")) {
|
|
34
|
+
throw new Error("accounting_invoice_canonical_positions_mixed_or_invalid");
|
|
35
|
+
}
|
|
36
|
+
return mapCanonicalIssueLines(linesSnapshot, chargedTotalGrossMinor, chargedTotalNetMinor, options.defaultLineLabel);
|
|
37
|
+
}
|
|
38
|
+
const parsed = linesSnapshot.map((line) => parseLineSnapshot(line, options));
|
|
39
|
+
const deliveryGrossMinor = readDeliveryGrossMinor(orderMoney, chargedTotalGrossMinor);
|
|
40
|
+
const chargedTotals = allocateChargedGrossTotals(parsed.map((line) => line.totalGrossMinor), chargedTotalGrossMinor - deliveryGrossMinor);
|
|
41
|
+
const positions = parsed.map((line, index) => {
|
|
42
|
+
const totalGrossMinor = chargedTotals[index];
|
|
43
|
+
const totalNetMinor = estimateNetMinor(totalGrossMinor, line.vatRate);
|
|
44
|
+
return {
|
|
45
|
+
name: line.name,
|
|
46
|
+
quantity: line.quantity,
|
|
47
|
+
unitGrossMinor: Math.round(totalGrossMinor / line.quantity),
|
|
48
|
+
unitNetMinor: Math.round(totalNetMinor / line.quantity),
|
|
49
|
+
totalGrossMinor,
|
|
50
|
+
totalNetMinor,
|
|
51
|
+
vatRate: line.vatRate,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
if (deliveryGrossMinor > 0) {
|
|
55
|
+
positions.push(buildDeliveryPosition(deliveryGrossMinor, parsed, options));
|
|
56
|
+
}
|
|
57
|
+
return positions;
|
|
58
|
+
}
|
|
59
|
+
// Legacy-only compatibility: pre-canonical claims had no explicit delivery
|
|
60
|
+
// position, so their effective delivery is the closed header residual. Current
|
|
61
|
+
// canonical claims never enter this path and use persisted shipping columns.
|
|
62
|
+
// Every inconsistent legacy shape degrades to 0 and then fails closed if the
|
|
63
|
+
// catalog lines cannot reconcile to the charged header.
|
|
64
|
+
function readDeliveryGrossMinor(orderMoney, chargedTotalGrossMinor) {
|
|
65
|
+
if (!isRecord(orderMoney))
|
|
66
|
+
return 0;
|
|
67
|
+
const subtotal = numberValue(orderMoney.subtotalCents);
|
|
68
|
+
const discount = numberValue(orderMoney.discountCents);
|
|
69
|
+
const total = numberValue(orderMoney.totalCents);
|
|
70
|
+
if (subtotal === null || discount === null || total === null)
|
|
71
|
+
return 0;
|
|
72
|
+
if (![subtotal, discount, total].every((value) => Number.isInteger(value) && value >= 0))
|
|
73
|
+
return 0;
|
|
74
|
+
if (discount > subtotal)
|
|
75
|
+
return 0;
|
|
76
|
+
if (total !== chargedTotalGrossMinor)
|
|
77
|
+
return 0;
|
|
78
|
+
const deliveryGrossMinor = chargedTotalGrossMinor - (subtotal - discount);
|
|
79
|
+
return deliveryGrossMinor > 0 ? deliveryGrossMinor : 0;
|
|
80
|
+
}
|
|
81
|
+
// Delivery is an ancillary supply and follows the goods' VAT rate; with mixed
|
|
82
|
+
// item rates (or no item line to source a rate from) the correct rate is
|
|
83
|
+
// ambiguous, so the claim fails closed into the outbox last_error.
|
|
84
|
+
function buildDeliveryPosition(deliveryGrossMinor, itemLines, options) {
|
|
85
|
+
const rates = [...new Set(itemLines.map((line) => line.vatRate.trim()))];
|
|
86
|
+
if (rates.length === 0) {
|
|
87
|
+
throw new Error(`accounting_invoice_delivery_vat_rate_unavailable delivery=${deliveryGrossMinor}`);
|
|
88
|
+
}
|
|
89
|
+
if (rates.length > 1) {
|
|
90
|
+
throw new Error(`accounting_invoice_delivery_vat_rates_mixed rates=${rates.join(",")}`);
|
|
91
|
+
}
|
|
92
|
+
const vatRate = rates[0];
|
|
93
|
+
const totalNetMinor = estimateNetMinor(deliveryGrossMinor, vatRate);
|
|
94
|
+
return {
|
|
95
|
+
name: options.deliveryLabel,
|
|
96
|
+
quantity: 1,
|
|
97
|
+
unitGrossMinor: deliveryGrossMinor,
|
|
98
|
+
unitNetMinor: totalNetMinor,
|
|
99
|
+
totalGrossMinor: deliveryGrossMinor,
|
|
100
|
+
totalNetMinor,
|
|
101
|
+
vatRate,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function mapBuyerAddress(value) {
|
|
105
|
+
const address = isRecord(value) ? value : {};
|
|
106
|
+
return {
|
|
107
|
+
line1: textValue(address.line1),
|
|
108
|
+
postalCode: textValue(address.postalCode),
|
|
109
|
+
city: textValue(address.city),
|
|
110
|
+
country: textValue(address.country),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function parseLineSnapshot(value, options) {
|
|
114
|
+
const row = isRecord(value) ? value : {};
|
|
115
|
+
const quantity = numberValue(row.quantity) || 1;
|
|
116
|
+
const vatRate = textValue(row.vatRate) ?? options.defaultTaxRate;
|
|
117
|
+
// Legacy (pre-#1693) lines_snapshot rows carry only the per-unit
|
|
118
|
+
// unitGrossMinor, so the line total is unit price times quantity.
|
|
119
|
+
const unitGrossMinor = numberValue(row.unitGrossMinor);
|
|
120
|
+
const totalGrossMinor = numberValue(row.totalGrossMinor) ??
|
|
121
|
+
(unitGrossMinor === null ? 0 : unitGrossMinor * quantity);
|
|
122
|
+
return {
|
|
123
|
+
name: textValue(row.name) ?? options.defaultLineLabel,
|
|
124
|
+
quantity,
|
|
125
|
+
totalGrossMinor,
|
|
126
|
+
vatRate,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function estimateNetMinor(grossMinor, vatRate) {
|
|
130
|
+
const rate = Number(vatRate.replace(",", "."));
|
|
131
|
+
return Number.isFinite(rate) ? Math.round(grossMinor / (1 + rate / 100)) : grossMinor;
|
|
132
|
+
}
|
|
133
|
+
function textValue(value) {
|
|
134
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
135
|
+
}
|
|
136
|
+
function numberValue(value) {
|
|
137
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
138
|
+
}
|
|
139
|
+
function isRecord(value) {
|
|
140
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
141
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { RenewalDue, RenewalExecutionRequest, RenewalFrozenSnapshot, RenewalPaymentRail, RenewalPreparedSettlement, RenewalReadback, RenewalReservationResult, RenewalSettlementPort, RenewalStorePort } from "./contracts/renewal.js";
|
|
2
|
+
import type { RenewalFailureHandoffPort } from "./contracts/dunning.js";
|
|
3
|
+
export type AutomaticRenewalRunStatus = "succeeded" | "refused" | "deferred" | "awaiting_provider" | "error" | "lease_active" | "terminal_replay";
|
|
4
|
+
export interface AutomaticRenewalRunResult {
|
|
5
|
+
subscriptionId: string;
|
|
6
|
+
identityKey: string;
|
|
7
|
+
operationId: string;
|
|
8
|
+
status: AutomaticRenewalRunStatus;
|
|
9
|
+
replayed: boolean;
|
|
10
|
+
reason: string | null;
|
|
11
|
+
caseRef?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AutomaticRenewalPolicy {
|
|
14
|
+
executionInput(due: RenewalDue, operation: RenewalReadback, prepared: Pick<RenewalPreparedSettlement, "orderId" | "settlementIntentId">): RenewalExecutionRequest;
|
|
15
|
+
validateInput(input: RenewalExecutionRequest): void;
|
|
16
|
+
classifyFailure(reason: string): string;
|
|
17
|
+
identityKey(due: RenewalDue): string;
|
|
18
|
+
}
|
|
19
|
+
export interface AutomaticRenewalRunDeps {
|
|
20
|
+
store: RenewalStorePort;
|
|
21
|
+
inventory: {
|
|
22
|
+
expireReservations(input: {
|
|
23
|
+
now: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
}): Promise<number>;
|
|
26
|
+
reserve(input: {
|
|
27
|
+
operationId: string;
|
|
28
|
+
claimToken: string;
|
|
29
|
+
now: string;
|
|
30
|
+
}): Promise<RenewalReservationResult>;
|
|
31
|
+
};
|
|
32
|
+
settlement: RenewalSettlementPort;
|
|
33
|
+
paymentRails: {
|
|
34
|
+
resolve(channelKey: string): RenewalPaymentRail | null;
|
|
35
|
+
};
|
|
36
|
+
failureHandoff: RenewalFailureHandoffPort;
|
|
37
|
+
policy: AutomaticRenewalPolicy;
|
|
38
|
+
workerId: string;
|
|
39
|
+
now?: () => string;
|
|
40
|
+
}
|
|
41
|
+
export declare function runAutomaticSubscriptionRenewals(deps: AutomaticRenewalRunDeps, limit?: number): Promise<AutomaticRenewalRunResult[]>;
|
|
42
|
+
export declare function runOneAutomaticSubscriptionRenewal(deps: AutomaticRenewalRunDeps, due: RenewalDue, occurredAt?: string): Promise<AutomaticRenewalRunResult>;
|
|
43
|
+
/** No implicit +1: ambiguous history must be resolved by the host before opening a repair case. */
|
|
44
|
+
export declare function correlatedLogicalCycle(snapshot: RenewalFrozenSnapshot): number;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { automaticRenewalResult, handoffRefusal, recoverPreparedExternalAttempt, refuseRenewalBeforePayment, runRenewalBatchRows } from "./automaticRenewalExecution.js";
|
|
2
|
+
import { readAndApplyProviderOutcome } from "./paymentOutcomes.js";
|
|
3
|
+
export async function runAutomaticSubscriptionRenewals(deps, limit = 50) {
|
|
4
|
+
const now = (deps.now ?? defaultNow)();
|
|
5
|
+
await deps.inventory.expireReservations({ now, limit: Math.max(limit, 100) });
|
|
6
|
+
const due = await deps.store.listDue(limit, now);
|
|
7
|
+
return runRenewalBatchRows(due, row => runOneAutomaticSubscriptionRenewal(deps, row, now), deps.policy);
|
|
8
|
+
}
|
|
9
|
+
export async function runOneAutomaticSubscriptionRenewal(deps, due, occurredAt = (deps.now ?? defaultNow)()) {
|
|
10
|
+
const schedule = { subscriptionId: due.subscriptionId, scheduledAt: due.scheduledAt, scheduledInstant: due.scheduledInstant };
|
|
11
|
+
const snapshot = await deps.store.buildCycleSnapshots(schedule);
|
|
12
|
+
const claim = await deps.store.claimDue({ snapshot, workerId: deps.workerId, now: occurredAt });
|
|
13
|
+
if (claim.state === "succeeded" || claim.state === "refused")
|
|
14
|
+
return automaticRenewalResult(claim, "terminal_replay", claim.terminalReason);
|
|
15
|
+
if (!claim.acquired)
|
|
16
|
+
return automaticRenewalResult(claim, "lease_active", claim.reason);
|
|
17
|
+
const rail = deps.paymentRails.resolve(snapshot.settlementChannelKey);
|
|
18
|
+
if (!rail)
|
|
19
|
+
return refuseRenewalBeforePayment({ store: deps.store, operation: claim, reason: "settlement_channel_not_configured", occurredAt });
|
|
20
|
+
if (claim.state === "attempt_prepared")
|
|
21
|
+
return resumePreparedAttempt(deps, claim, rail, occurredAt);
|
|
22
|
+
const mandate = await deps.store.readMandateEvidence(schedule);
|
|
23
|
+
if (!mandate.authorized || mandate.authorizationRef !== snapshot.authorizationRef)
|
|
24
|
+
return refuseRenewalBeforePayment({ store: deps.store, operation: claim, reason: "authorization_evidence_mismatch", occurredAt });
|
|
25
|
+
const delivery = await deps.store.admitDeliveryAlignment({ ...schedule, occurredAt });
|
|
26
|
+
if (!delivery.allowed) {
|
|
27
|
+
if (delivery.deferred)
|
|
28
|
+
return { ...automaticRenewalResult(claim, "deferred", delivery.reason), ...(delivery.caseRef ? { caseRef: delivery.caseRef } : {}) };
|
|
29
|
+
return refuseRenewalBeforePayment({ store: deps.store, operation: claim, reason: delivery.reason, occurredAt });
|
|
30
|
+
}
|
|
31
|
+
const reservation = await deps.inventory.reserve({ operationId: claim.operationId, claimToken: claim.claimToken, now: occurredAt });
|
|
32
|
+
if (!reservation.reserved)
|
|
33
|
+
return automaticRenewalResult(claim, "refused", reservation.reason ?? "insufficient_atp");
|
|
34
|
+
const prepared = await deps.settlement.prepareRenewal({ operationId: claim.operationId, claimToken: claim.claimToken, now: occurredAt });
|
|
35
|
+
if (prepared.replayed) {
|
|
36
|
+
const readback = await deps.store.readRenewal(claim.identityKey);
|
|
37
|
+
if (!readback)
|
|
38
|
+
throw new Error("subscription_renewal_prepared_readback_missing");
|
|
39
|
+
return resumePreparedAttempt(deps, readback, rail, occurredAt);
|
|
40
|
+
}
|
|
41
|
+
const executionInput = deps.policy.executionInput(due, claim, prepared);
|
|
42
|
+
try {
|
|
43
|
+
deps.policy.validateInput(executionInput);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
await deps.settlement.recordTerminalOutcome({ operationId: claim.operationId, eventKey: `${claim.identityKey}:provider-preflight`, operationFingerprint: claim.operationFingerprint, outcome: "refused", reason: "provider_input_invalid", occurredAt });
|
|
47
|
+
return automaticRenewalResult(claim, "refused", "provider_input_invalid");
|
|
48
|
+
}
|
|
49
|
+
let execution;
|
|
50
|
+
try {
|
|
51
|
+
execution = await rail.execute(executionInput);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return automaticRenewalResult(claim, "awaiting_provider", "provider_execution_indeterminate");
|
|
55
|
+
}
|
|
56
|
+
const externalAttemptRef = execution.externalAttemptRef?.trim() || null;
|
|
57
|
+
if (externalAttemptRef)
|
|
58
|
+
await deps.settlement.acknowledgeExternalAttempt({ operationId: claim.operationId, operationFingerprint: claim.operationFingerprint, externalAttemptRef, acknowledgedAt: occurredAt });
|
|
59
|
+
if (execution.failure || execution.status === "requires_action") {
|
|
60
|
+
const reason = execution.failure?.reason ?? (execution.status === "requires_action" ? "off_session_sca_required" : "provider_failed");
|
|
61
|
+
return handoffRefusal(deps, claim, prepared, externalAttemptRef, reason, execution.failure?.failureClass ?? deps.policy.classifyFailure(reason), occurredAt);
|
|
62
|
+
}
|
|
63
|
+
if (!externalAttemptRef)
|
|
64
|
+
return automaticRenewalResult(claim, "awaiting_provider", "provider_attempt_ack_missing");
|
|
65
|
+
return readAndApplyProviderOutcome(deps, claim, prepared, rail, externalAttemptRef, occurredAt);
|
|
66
|
+
}
|
|
67
|
+
async function resumePreparedAttempt(deps, operation, rail, occurredAt) {
|
|
68
|
+
if (!operation.cycleId || !operation.orderId || !operation.settlementIntentId)
|
|
69
|
+
throw new Error("subscription_renewal_prepared_artifacts_missing");
|
|
70
|
+
const externalAttemptRef = await recoverPreparedExternalAttempt({ operation, rail, settlement: deps.settlement, occurredAt });
|
|
71
|
+
if (!externalAttemptRef)
|
|
72
|
+
return automaticRenewalResult(operation, "awaiting_provider", "prepared_attempt_ack_missing");
|
|
73
|
+
return readAndApplyProviderOutcome(deps, operation, { cycleId: operation.cycleId, orderId: operation.orderId, settlementIntentId: operation.settlementIntentId }, rail, externalAttemptRef, occurredAt);
|
|
74
|
+
}
|
|
75
|
+
function defaultNow() { return new Date().toISOString(); }
|
|
76
|
+
/** No implicit +1: ambiguous history must be resolved by the host before opening a repair case. */
|
|
77
|
+
export function correlatedLogicalCycle(snapshot) {
|
|
78
|
+
if (snapshot.cycle.status !== "correlated")
|
|
79
|
+
throw new Error("subscription_renewal_cycle_correlation_required");
|
|
80
|
+
return snapshot.cycle.logicalCycleNumber;
|
|
81
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RenewalDue, RenewalPaymentRail, RenewalReadback, RenewalSettlementPort, RenewalStorePort } from "./contracts/renewal.js";
|
|
2
|
+
import type { AutomaticRenewalPolicy, AutomaticRenewalRunDeps, AutomaticRenewalRunResult, AutomaticRenewalRunStatus } from "./automaticRenewal.js";
|
|
3
|
+
export declare function automaticRenewalResult(operation: RenewalReadback, status: AutomaticRenewalRunStatus, reason: string | null): AutomaticRenewalRunResult;
|
|
4
|
+
export declare function recoverPreparedExternalAttempt(input: {
|
|
5
|
+
operation: RenewalReadback;
|
|
6
|
+
rail: RenewalPaymentRail;
|
|
7
|
+
settlement: RenewalSettlementPort;
|
|
8
|
+
occurredAt: string;
|
|
9
|
+
}): Promise<string | null>;
|
|
10
|
+
export declare function runRenewalBatchRows(due: RenewalDue[], run: (row: RenewalDue) => Promise<AutomaticRenewalRunResult>, policy: Pick<AutomaticRenewalPolicy, "identityKey">): Promise<AutomaticRenewalRunResult[]>;
|
|
11
|
+
export declare function refuseRenewalBeforePayment(input: {
|
|
12
|
+
store: RenewalStorePort;
|
|
13
|
+
operation: RenewalReadback;
|
|
14
|
+
reason: string;
|
|
15
|
+
occurredAt: string;
|
|
16
|
+
}): Promise<AutomaticRenewalRunResult>;
|
|
17
|
+
export declare function handoffRefusal(deps: AutomaticRenewalRunDeps, operation: RenewalReadback, prepared: {
|
|
18
|
+
cycleId: string;
|
|
19
|
+
orderId: string;
|
|
20
|
+
settlementIntentId: string;
|
|
21
|
+
}, externalAttemptRef: string | null, reason: string, failureClass: string, occurredAt: string): Promise<AutomaticRenewalRunResult>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { nextRetryAttemptAt } from "@openlup/core/subscription";
|
|
2
|
+
export function automaticRenewalResult(operation, status, reason) {
|
|
3
|
+
return { subscriptionId: operation.subscriptionId, identityKey: operation.identityKey, operationId: operation.operationId, status, replayed: operation.replayed, reason };
|
|
4
|
+
}
|
|
5
|
+
export async function recoverPreparedExternalAttempt(input) {
|
|
6
|
+
if (input.operation.externalAttemptRef)
|
|
7
|
+
return input.operation.externalAttemptRef;
|
|
8
|
+
if (!input.operation.settlementIntentId)
|
|
9
|
+
return null;
|
|
10
|
+
const externalAttemptRef = await input.rail.findExternalAttempt({ settlementIntentId: input.operation.settlementIntentId });
|
|
11
|
+
if (!externalAttemptRef)
|
|
12
|
+
return null;
|
|
13
|
+
await input.settlement.acknowledgeExternalAttempt({ operationId: input.operation.operationId, operationFingerprint: input.operation.operationFingerprint, externalAttemptRef, acknowledgedAt: input.occurredAt });
|
|
14
|
+
return externalAttemptRef;
|
|
15
|
+
}
|
|
16
|
+
export async function runRenewalBatchRows(due, run, policy) {
|
|
17
|
+
const results = [];
|
|
18
|
+
for (const row of due) {
|
|
19
|
+
try {
|
|
20
|
+
results.push(await run(row));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
results.push({ subscriptionId: row.subscriptionId, identityKey: policy.identityKey(row), operationId: "", status: "error", replayed: false, reason: "renewal_processing_failed" });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
export async function refuseRenewalBeforePayment(input) {
|
|
29
|
+
const refused = await input.store.refuseBeforePayment({ operationId: input.operation.operationId, claimToken: input.operation.claimToken, reason: input.reason, occurredAt: input.occurredAt });
|
|
30
|
+
return automaticRenewalResult(refused, "refused", input.reason);
|
|
31
|
+
}
|
|
32
|
+
const HANDOFF_OPENS_AT_RETRY_ATTEMPT = 1;
|
|
33
|
+
export async function handoffRefusal(deps, operation, prepared, externalAttemptRef, reason, failureClass, occurredAt) {
|
|
34
|
+
await deps.failureHandoff.handoffFailure({
|
|
35
|
+
idempotencyKey: `${operation.identityKey}:failure-handoff`, subscriptionId: operation.subscriptionId,
|
|
36
|
+
clientId: operation.cycleSnapshot.clientId, cycleId: prepared.cycleId, orderId: prepared.orderId,
|
|
37
|
+
paymentIntentId: prepared.settlementIntentId, retryAttempt: HANDOFF_OPENS_AT_RETRY_ATTEMPT,
|
|
38
|
+
failureClass, failureReason: reason,
|
|
39
|
+
nextRetryAt: nextRetryAttemptAt(occurredAt, HANDOFF_OPENS_AT_RETRY_ATTEMPT, undefined, failureClass),
|
|
40
|
+
occurredAt, amountMinor: operation.cycleSnapshot.pricingSnapshot.totalAmountMinor, currency: operation.cycleSnapshot.pricingSnapshot.currency,
|
|
41
|
+
});
|
|
42
|
+
await deps.settlement.recordTerminalOutcome({ operationId: operation.operationId, eventKey: `${operation.identityKey}:provider-refusal`, operationFingerprint: operation.operationFingerprint, outcome: "refused", externalRef: externalAttemptRef, reason, occurredAt });
|
|
43
|
+
return automaticRenewalResult(operation, "refused", reason);
|
|
44
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export interface SubscriptionCommandContext {
|
|
2
|
+
actor: {
|
|
3
|
+
kind: "customer" | "operator";
|
|
4
|
+
actorId: string;
|
|
5
|
+
clientId: string;
|
|
6
|
+
};
|
|
7
|
+
subscriptionId: string;
|
|
8
|
+
idempotencyKey: string;
|
|
9
|
+
now: string;
|
|
10
|
+
}
|
|
11
|
+
export type SubscriptionCommand = SubscriptionCommandContext & ({
|
|
12
|
+
action: "pause";
|
|
13
|
+
pausePreset: string;
|
|
14
|
+
reason: string | null;
|
|
15
|
+
survey: string | null;
|
|
16
|
+
saveOffer: string | null;
|
|
17
|
+
} | {
|
|
18
|
+
action: "resume" | "skip" | "cancel";
|
|
19
|
+
reason: string | null;
|
|
20
|
+
confirmedChargeTiming: boolean;
|
|
21
|
+
} | {
|
|
22
|
+
action: "slide";
|
|
23
|
+
newNextCycleAt: string;
|
|
24
|
+
confirmedChargeTiming: boolean;
|
|
25
|
+
} | {
|
|
26
|
+
action: "edit_future_terms";
|
|
27
|
+
shippingAddressId: string;
|
|
28
|
+
planDays: number;
|
|
29
|
+
cadenceDays: number;
|
|
30
|
+
expectedTemplateVersion: number;
|
|
31
|
+
acceptedQuoteHash: string;
|
|
32
|
+
confirmedChargeTiming: boolean;
|
|
33
|
+
coreLines: readonly SubscriptionBundleLine[];
|
|
34
|
+
addonLines: readonly SubscriptionBundleLine[];
|
|
35
|
+
compositionConstraint: string;
|
|
36
|
+
});
|
|
37
|
+
export interface SubscriptionBundleLine {
|
|
38
|
+
sku: string;
|
|
39
|
+
quantity: number;
|
|
40
|
+
}
|
|
41
|
+
export interface SubscriptionCommandReceipt {
|
|
42
|
+
actorId: string;
|
|
43
|
+
idempotencyKey: string;
|
|
44
|
+
commandFingerprint: string;
|
|
45
|
+
outcome: "applied" | "replayed" | "noop";
|
|
46
|
+
subscriptionId: string;
|
|
47
|
+
originalState: string;
|
|
48
|
+
nextCycleAt: string | null;
|
|
49
|
+
revision: string;
|
|
50
|
+
eventId: string | null;
|
|
51
|
+
}
|
|
52
|
+
export type SubscriptionUnsupportedCode = "multi_cycle_prepaid_unsupported" | "native_generic_bundle_commands_unsupported" | "native_command_bound_receipts_unsupported" | "native_prepared_attempt_store_unsupported";
|
|
53
|
+
export interface SubscriptionUnsupported {
|
|
54
|
+
supported: false;
|
|
55
|
+
refusalCode: SubscriptionUnsupportedCode;
|
|
56
|
+
}
|
|
57
|
+
/** Readiness must be selected explicitly; a port declaration does not install a store. */
|
|
58
|
+
export interface SubscriptionCapabilities {
|
|
59
|
+
graph: "managed" | "native";
|
|
60
|
+
multiCyclePrepaid: {
|
|
61
|
+
supported: false;
|
|
62
|
+
refusalCode: "multi_cycle_prepaid_unsupported";
|
|
63
|
+
};
|
|
64
|
+
genericBundleCommands: {
|
|
65
|
+
supported: true;
|
|
66
|
+
} | {
|
|
67
|
+
supported: false;
|
|
68
|
+
refusalCode: "native_generic_bundle_commands_unsupported";
|
|
69
|
+
};
|
|
70
|
+
commandBoundReceipts: {
|
|
71
|
+
supported: true;
|
|
72
|
+
} | {
|
|
73
|
+
supported: false;
|
|
74
|
+
refusalCode: "native_command_bound_receipts_unsupported";
|
|
75
|
+
};
|
|
76
|
+
preparedAttemptStore: {
|
|
77
|
+
supported: true;
|
|
78
|
+
} | {
|
|
79
|
+
supported: false;
|
|
80
|
+
refusalCode: "native_prepared_attempt_store_unsupported";
|
|
81
|
+
};
|
|
82
|
+
retryRecovery: "unavailable" | "selected";
|
|
83
|
+
stockRecovery: "unavailable" | "selected";
|
|
84
|
+
deliveryAdmission: "required";
|
|
85
|
+
executorCount: 1;
|
|
86
|
+
defaultEnabled: false;
|
|
87
|
+
}
|
|
88
|
+
export interface SubscriptionCommandPort {
|
|
89
|
+
readonly capabilities: SubscriptionCapabilities;
|
|
90
|
+
/** Future term writes, actor/command receipt and events must commit atomically. */
|
|
91
|
+
apply(command: SubscriptionCommand): Promise<SubscriptionCommandReceipt | SubscriptionUnsupported>;
|
|
92
|
+
}
|
|
93
|
+
export interface SubscriptionActivationPort {
|
|
94
|
+
activate(input: {
|
|
95
|
+
idempotencyKey: string;
|
|
96
|
+
orderId: string;
|
|
97
|
+
paymentIntentId: string;
|
|
98
|
+
paidEvidenceRef: string;
|
|
99
|
+
paidAt: string;
|
|
100
|
+
paymentMethodRef: string;
|
|
101
|
+
paymentMethodKind: string;
|
|
102
|
+
checkoutKind: string;
|
|
103
|
+
}): Promise<{
|
|
104
|
+
outcome: "noop" | "blocked";
|
|
105
|
+
reason: string;
|
|
106
|
+
} | {
|
|
107
|
+
outcome: "active";
|
|
108
|
+
subscriptionId: string;
|
|
109
|
+
orderId: string;
|
|
110
|
+
paymentIntentId: string;
|
|
111
|
+
nextCycleAt: string;
|
|
112
|
+
cadenceDays: number;
|
|
113
|
+
replayed: boolean;
|
|
114
|
+
}>;
|
|
115
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|