@fantastic.dev/better-stripe 0.12.0 → 0.14.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 +24 -0
- package/README.md +93 -8
- package/dist/client/connect/disputes.d.ts +20 -0
- package/dist/client/connect/disputes.d.ts.map +1 -1
- package/dist/client/connect/disputes.js +55 -0
- package/dist/client/connect/disputes.js.map +1 -1
- package/dist/client/costRecovery-invoices.d.ts +44 -0
- package/dist/client/costRecovery-invoices.d.ts.map +1 -0
- package/dist/client/costRecovery-invoices.js +109 -0
- package/dist/client/costRecovery-invoices.js.map +1 -0
- package/dist/client/costRecovery.d.ts +59 -2
- package/dist/client/costRecovery.d.ts.map +1 -1
- package/dist/client/costRecovery.js +73 -2
- package/dist/client/costRecovery.js.map +1 -1
- package/dist/client/errors.d.ts +1 -1
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js.map +1 -1
- package/dist/client/index.d.ts +5 -14
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -8
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +137 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/costRecovery/claims.d.ts +12 -0
- package/dist/component/costRecovery/claims.d.ts.map +1 -1
- package/dist/component/costRecovery/claims.js +15 -8
- package/dist/component/costRecovery/claims.js.map +1 -1
- package/dist/component/costRecovery/completion.d.ts +6 -0
- package/dist/component/costRecovery/completion.d.ts.map +1 -1
- package/dist/component/costRecovery/helpers.d.ts +6 -0
- package/dist/component/costRecovery/helpers.d.ts.map +1 -1
- package/dist/component/costRecovery/imports.d.ts +53 -0
- package/dist/component/costRecovery/imports.d.ts.map +1 -0
- package/dist/component/costRecovery/imports.js +67 -0
- package/dist/component/costRecovery/imports.js.map +1 -0
- package/dist/component/costRecovery/mutations.d.ts +119 -0
- package/dist/component/costRecovery/mutations.d.ts.map +1 -1
- package/dist/component/costRecovery/mutations.js +64 -1
- package/dist/component/costRecovery/mutations.js.map +1 -1
- package/dist/component/costRecovery/queries.d.ts +18 -0
- package/dist/component/costRecovery/queries.d.ts.map +1 -1
- package/dist/component/costRecovery/reservations.d.ts +12 -0
- package/dist/component/costRecovery/reservations.d.ts.map +1 -1
- package/dist/component/costRecovery/schema.d.ts +116 -4
- package/dist/component/costRecovery/schema.d.ts.map +1 -1
- package/dist/component/costRecovery/schema.js +19 -0
- package/dist/component/costRecovery/schema.js.map +1 -1
- package/dist/component/schema.d.ts +36 -2
- package/dist/react/workflows/Collections.d.ts.map +1 -1
- package/dist/react/workflows/Collections.js +2 -1
- package/dist/react/workflows/Collections.js.map +1 -1
- package/dist/react/workflows/EvidenceUpload.d.ts +18 -0
- package/dist/react/workflows/EvidenceUpload.d.ts.map +1 -0
- package/dist/react/workflows/EvidenceUpload.js +35 -0
- package/dist/react/workflows/EvidenceUpload.js.map +1 -0
- package/dist/react/workflows/Onboarding.d.ts +3 -3
- package/dist/react/workflows/Onboarding.d.ts.map +1 -1
- package/dist/react/workflows/Onboarding.js +8 -6
- package/dist/react/workflows/Onboarding.js.map +1 -1
- package/dist/react/workflows/Refunds.d.ts +14 -3
- package/dist/react/workflows/Refunds.d.ts.map +1 -1
- package/dist/react/workflows/Refunds.js +58 -16
- package/dist/react/workflows/Refunds.js.map +1 -1
- package/dist/react/workflows/index.d.ts +5 -1
- package/dist/react/workflows/index.d.ts.map +1 -1
- package/dist/react/workflows/index.js +3 -1
- package/dist/react/workflows/index.js.map +1 -1
- package/dist/react/workflows/refund-preview.d.ts +16 -0
- package/dist/react/workflows/refund-preview.d.ts.map +1 -0
- package/dist/react/workflows/refund-preview.js +38 -0
- package/dist/react/workflows/refund-preview.js.map +1 -0
- package/dist/react/workflows/useWorkflowAction.d.ts +1 -1
- package/dist/react/workflows/useWorkflowAction.d.ts.map +1 -1
- package/dist/react/workflows/useWorkflowAction.js +6 -2
- package/dist/react/workflows/useWorkflowAction.js.map +1 -1
- package/docs/react-workflows.md +121 -4
- package/package.json +1 -1
- package/src/client/connect/dispute-upload.test.ts +208 -0
- package/src/client/connect/disputes.ts +103 -0
- package/src/client/costRecovery-import.test.ts +174 -0
- package/src/client/costRecovery-invoices.test.ts +470 -0
- package/src/client/costRecovery-invoices.transport.test.ts +107 -0
- package/src/client/costRecovery-invoices.ts +176 -0
- package/src/client/costRecovery.ts +123 -2
- package/src/client/errors.ts +1 -0
- package/src/client/index.ts +15 -14
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +164 -0
- package/src/component/costRecovery/claims.ts +18 -8
- package/src/component/costRecovery/imports.ts +74 -0
- package/src/component/costRecovery/mutations.ts +80 -0
- package/src/component/costRecovery/schema.ts +21 -0
- package/src/react/workflows/Collections.test.tsx +36 -0
- package/src/react/workflows/Collections.tsx +3 -1
- package/src/react/workflows/EvidenceUpload.test.tsx +56 -0
- package/src/react/workflows/EvidenceUpload.tsx +71 -0
- package/src/react/workflows/Onboarding.test.tsx +73 -0
- package/src/react/workflows/Onboarding.tsx +8 -5
- package/src/react/workflows/Refunds.currency.test.tsx +290 -0
- package/src/react/workflows/Refunds.tsx +115 -25
- package/src/react/workflows/index.ts +11 -1
- package/src/react/workflows/refund-preview.test.ts +65 -0
- package/src/react/workflows/refund-preview.ts +56 -0
- package/src/react/workflows/useWorkflowAction.ts +6 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { getProcessingCostContext } from "./settlement/processingCosts.js";
|
|
2
|
+
import type Stripe from "stripe";
|
|
3
|
+
import type { RecoveryGroup, RecoveryScope } from "./costRecovery.js";
|
|
4
|
+
export type RecoveryInvoiceOptions = RecoveryScope & { groupKey: string };
|
|
5
|
+
const marker = "bsRecoveryGroup";
|
|
6
|
+
const id = (value: string | { id: string } | null) =>
|
|
7
|
+
typeof value === "string" ? value : value?.id;
|
|
8
|
+
|
|
9
|
+
type InvoiceReceipt = {
|
|
10
|
+
group: RecoveryGroup;
|
|
11
|
+
invoiceItemId: string;
|
|
12
|
+
invoiceStatus: Stripe.Invoice["status"];
|
|
13
|
+
};
|
|
14
|
+
type BindInvoiceItem = (invoiceItemId: string) => Promise<RecoveryGroup>;
|
|
15
|
+
|
|
16
|
+
/** Attachment always produces an item; reconciliation can release an unbound void invoice. */
|
|
17
|
+
export function attachRecoveryInvoice(
|
|
18
|
+
stripe: Stripe,
|
|
19
|
+
group: RecoveryGroup | null,
|
|
20
|
+
opts: RecoveryInvoiceOptions,
|
|
21
|
+
bind: BindInvoiceItem,
|
|
22
|
+
create?: true,
|
|
23
|
+
): Promise<InvoiceReceipt>;
|
|
24
|
+
export function attachRecoveryInvoice(
|
|
25
|
+
stripe: Stripe,
|
|
26
|
+
group: RecoveryGroup | null,
|
|
27
|
+
opts: RecoveryInvoiceOptions,
|
|
28
|
+
bind: BindInvoiceItem,
|
|
29
|
+
create: false,
|
|
30
|
+
): Promise<
|
|
31
|
+
| InvoiceReceipt
|
|
32
|
+
| (Omit<InvoiceReceipt, "invoiceItemId"> & { invoiceItemId: null })
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
/** One immutable item per sealed group. Host serializes provider operations per group. */
|
|
36
|
+
export async function attachRecoveryInvoice(
|
|
37
|
+
stripe: Stripe,
|
|
38
|
+
group: RecoveryGroup | null,
|
|
39
|
+
opts: RecoveryInvoiceOptions,
|
|
40
|
+
bind: (invoiceItemId: string) => Promise<RecoveryGroup>,
|
|
41
|
+
create = true,
|
|
42
|
+
) {
|
|
43
|
+
if (
|
|
44
|
+
!group ||
|
|
45
|
+
group.groupKey !== opts.groupKey ||
|
|
46
|
+
group.sourceKind !== "invoice" ||
|
|
47
|
+
group.accountId !== opts.accountId ||
|
|
48
|
+
group.scopeKey !== opts.scopeKey ||
|
|
49
|
+
group.currency !== opts.currency
|
|
50
|
+
)
|
|
51
|
+
throw new Error(
|
|
52
|
+
"Recovery invoice scope does not match the authorized group.",
|
|
53
|
+
);
|
|
54
|
+
if (
|
|
55
|
+
group.status === "open" ||
|
|
56
|
+
(create && ["released", "releasing"].includes(group.status))
|
|
57
|
+
)
|
|
58
|
+
throw new Error("Invoice recovery group must be sealed and active.");
|
|
59
|
+
const invoice = await stripe.invoices.retrieve(group.sourceId);
|
|
60
|
+
if (
|
|
61
|
+
invoice.id !== group.sourceId ||
|
|
62
|
+
invoice.customer_account !== group.accountId
|
|
63
|
+
)
|
|
64
|
+
throw new Error("Recovery invoice ownership does not match.");
|
|
65
|
+
if (invoice.currency !== group.currency)
|
|
66
|
+
throw new Error("Recovery invoice currency does not match.");
|
|
67
|
+
let found: Stripe.InvoiceItem | undefined;
|
|
68
|
+
for await (const item of stripe.invoiceItems.list({
|
|
69
|
+
invoice: invoice.id,
|
|
70
|
+
limit: 100,
|
|
71
|
+
})) {
|
|
72
|
+
if (item.metadata?.[marker] !== group._id) continue;
|
|
73
|
+
if (found)
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Duplicate recovery invoice items require reconciliation.",
|
|
76
|
+
);
|
|
77
|
+
if (
|
|
78
|
+
id(item.invoice) !== invoice.id ||
|
|
79
|
+
item.customer_account !== group.accountId ||
|
|
80
|
+
item.currency !== group.currency ||
|
|
81
|
+
item.amount !== group.amount ||
|
|
82
|
+
item.discountable ||
|
|
83
|
+
(group.invoiceItemId && group.invoiceItemId !== item.id)
|
|
84
|
+
)
|
|
85
|
+
throw new Error(
|
|
86
|
+
"Recovery invoice item economics or identity do not match.",
|
|
87
|
+
);
|
|
88
|
+
found = item;
|
|
89
|
+
}
|
|
90
|
+
if (!found) {
|
|
91
|
+
if (group.invoiceItemId)
|
|
92
|
+
throw new Error(
|
|
93
|
+
"Bound recovery invoice item is missing; reconcile before retrying.",
|
|
94
|
+
);
|
|
95
|
+
if (
|
|
96
|
+
!create &&
|
|
97
|
+
invoice.status === "void" &&
|
|
98
|
+
["sealed", "releasing", "released"].includes(group.status)
|
|
99
|
+
)
|
|
100
|
+
return { group, invoiceItemId: null, invoiceStatus: invoice.status };
|
|
101
|
+
if (!create)
|
|
102
|
+
throw new Error(
|
|
103
|
+
"Recovery invoice item is missing; no collection can be confirmed.",
|
|
104
|
+
);
|
|
105
|
+
if (group.status !== "sealed")
|
|
106
|
+
throw new Error(
|
|
107
|
+
"New recovery items require a sealed, uncompleted group.",
|
|
108
|
+
);
|
|
109
|
+
if (invoice.status !== "draft")
|
|
110
|
+
throw new Error("New recovery items require a draft invoice.");
|
|
111
|
+
found = await stripe.invoiceItems.create(
|
|
112
|
+
{
|
|
113
|
+
invoice: invoice.id,
|
|
114
|
+
customer_account: group.accountId,
|
|
115
|
+
currency: group.currency,
|
|
116
|
+
amount: group.amount,
|
|
117
|
+
discountable: false,
|
|
118
|
+
description: "Refund and dispute cost adjustment",
|
|
119
|
+
metadata: { [marker]: group._id },
|
|
120
|
+
},
|
|
121
|
+
{ idempotencyKey: `better-stripe:recovery:${group._id}` },
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const bound = group.invoiceItemId === found.id ? group : await bind(found.id);
|
|
125
|
+
return {
|
|
126
|
+
group: bound,
|
|
127
|
+
invoiceItemId: found.id,
|
|
128
|
+
invoiceStatus: invoice.status,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Refuse ambiguous cash/credit or multi-payment allocations instead of inventing economics. */
|
|
133
|
+
export async function readInvoiceFunding(stripe: Stripe, group: RecoveryGroup) {
|
|
134
|
+
const invoice = await stripe.invoices.retrieve(group.sourceId);
|
|
135
|
+
if (
|
|
136
|
+
invoice.customer_account !== group.accountId ||
|
|
137
|
+
invoice.currency !== group.currency ||
|
|
138
|
+
invoice.status !== "paid"
|
|
139
|
+
)
|
|
140
|
+
throw new Error(
|
|
141
|
+
"Invoice funding ownership, currency or paid state does not match.",
|
|
142
|
+
);
|
|
143
|
+
const payments: Stripe.InvoicePayment[] = [];
|
|
144
|
+
for await (const payment of stripe.invoicePayments.list({
|
|
145
|
+
invoice: group.sourceId,
|
|
146
|
+
limit: 100,
|
|
147
|
+
})) {
|
|
148
|
+
if (
|
|
149
|
+
id(payment.invoice) !== group.sourceId ||
|
|
150
|
+
payment.currency !== group.currency
|
|
151
|
+
)
|
|
152
|
+
throw new Error("Invoice payment identity or currency does not match.");
|
|
153
|
+
if (payment.status === "paid") payments.push(payment);
|
|
154
|
+
}
|
|
155
|
+
const payment = payments[0];
|
|
156
|
+
if (
|
|
157
|
+
payments.length !== 1 ||
|
|
158
|
+
payment?.payment.type !== "payment_intent" ||
|
|
159
|
+
!payment.payment.payment_intent ||
|
|
160
|
+
payment.amount_paid !== invoice.amount_paid
|
|
161
|
+
)
|
|
162
|
+
return {
|
|
163
|
+
status: "not_ready" as const,
|
|
164
|
+
reason: "invoice_funding_allocation_required" as const,
|
|
165
|
+
};
|
|
166
|
+
const cost = await getProcessingCostContext(stripe, {
|
|
167
|
+
paymentIntentId: id(payment.payment.payment_intent)!,
|
|
168
|
+
});
|
|
169
|
+
if (cost.status === "not_ready") return cost;
|
|
170
|
+
if (cost.currency !== group.currency || cost.gross !== invoice.amount_paid)
|
|
171
|
+
return {
|
|
172
|
+
status: "not_ready" as const,
|
|
173
|
+
reason: "invoice_funding_allocation_required" as const,
|
|
174
|
+
};
|
|
175
|
+
return cost;
|
|
176
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type Stripe from "stripe";
|
|
2
|
+
import {
|
|
3
|
+
readInvoiceFunding,
|
|
4
|
+
attachRecoveryInvoice,
|
|
5
|
+
} from "./costRecovery-invoices.js";
|
|
1
6
|
import type { Component, RunCtx } from "./helpers.js";
|
|
2
7
|
import { runMutationOrThrow } from "./helpers.js";
|
|
3
8
|
import { componentRef } from "./webhooks/helpers.js";
|
|
@@ -21,6 +26,12 @@ export type RecoveryClaim = RecoveryObservation & {
|
|
|
21
26
|
reservedAmount: number;
|
|
22
27
|
outstanding: number;
|
|
23
28
|
sign: "positive" | "negative" | "zero";
|
|
29
|
+
openingCollection?: {
|
|
30
|
+
operationKey: string;
|
|
31
|
+
amountDue: number;
|
|
32
|
+
revision: number;
|
|
33
|
+
collectedAmount: number;
|
|
34
|
+
};
|
|
24
35
|
};
|
|
25
36
|
export type RecoveryBalance = RecoveryScope & {
|
|
26
37
|
amountDue: number;
|
|
@@ -52,6 +63,12 @@ export type RecoveryGroup = RecoveryGroupOptions & {
|
|
|
52
63
|
_creationTime: number;
|
|
53
64
|
amount: number;
|
|
54
65
|
pendingOperations: number;
|
|
66
|
+
invoiceItemId?: string;
|
|
67
|
+
invoiceFunding?: {
|
|
68
|
+
sourceChargeId: string;
|
|
69
|
+
sourceGross: number;
|
|
70
|
+
sourceProcessingFee: number;
|
|
71
|
+
};
|
|
55
72
|
status:
|
|
56
73
|
"open" | "sealed" | "collecting" | "releasing" | "collected" | "released";
|
|
57
74
|
};
|
|
@@ -65,9 +82,12 @@ export type RecoveryReservation = {
|
|
|
65
82
|
allocations: { claimId: string; claimKey: string; amount: number }[];
|
|
66
83
|
status: "reserved" | "collected" | "released" | "empty";
|
|
67
84
|
};
|
|
68
|
-
/**
|
|
85
|
+
/** Host authorizes policy and scope. Provider methods verify invoice outcomes. */
|
|
69
86
|
export class CostRecoveryClient {
|
|
70
|
-
constructor(
|
|
87
|
+
constructor(
|
|
88
|
+
private readonly component: Component,
|
|
89
|
+
private readonly provider?: () => Stripe,
|
|
90
|
+
) {}
|
|
71
91
|
private async mutate<T>(
|
|
72
92
|
ctx: RunCtx,
|
|
73
93
|
path: string,
|
|
@@ -89,6 +109,107 @@ export class CostRecoveryClient {
|
|
|
89
109
|
args,
|
|
90
110
|
)) as T;
|
|
91
111
|
}
|
|
112
|
+
/** Host authorizes the account, scope and invoice group; call from an action. */
|
|
113
|
+
async attachInvoice(ctx: RunCtx, opts: RecoveryScope & { groupKey: string }) {
|
|
114
|
+
if (!this.provider)
|
|
115
|
+
throw new Error("Invoice recovery requires a Stripe client.");
|
|
116
|
+
const group = await this.getGroup(ctx, { groupKey: opts.groupKey });
|
|
117
|
+
return attachRecoveryInvoice(
|
|
118
|
+
this.provider(),
|
|
119
|
+
group,
|
|
120
|
+
opts,
|
|
121
|
+
(invoiceItemId) =>
|
|
122
|
+
this.mutate<RecoveryGroup>(ctx, "mutations/bindInvoiceItem", {
|
|
123
|
+
groupKey: opts.groupKey,
|
|
124
|
+
invoiceItemId,
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
/** Re-read Stripe; keep unpaid items reserved. Repeat until isDone after terminal outcomes. */
|
|
129
|
+
async reconcileInvoice(
|
|
130
|
+
ctx: RunCtx,
|
|
131
|
+
opts: RecoveryScope & { groupKey: string; releaseUncollectible?: boolean },
|
|
132
|
+
) {
|
|
133
|
+
if (!this.provider)
|
|
134
|
+
throw new Error("Invoice recovery requires a Stripe client.");
|
|
135
|
+
const group = await this.getGroup(ctx, { groupKey: opts.groupKey });
|
|
136
|
+
const receipt = await attachRecoveryInvoice(
|
|
137
|
+
this.provider(),
|
|
138
|
+
group,
|
|
139
|
+
opts,
|
|
140
|
+
(invoiceItemId) =>
|
|
141
|
+
this.mutate<RecoveryGroup>(ctx, "mutations/bindInvoiceItem", {
|
|
142
|
+
groupKey: opts.groupKey,
|
|
143
|
+
invoiceItemId,
|
|
144
|
+
}),
|
|
145
|
+
false,
|
|
146
|
+
);
|
|
147
|
+
const outcome =
|
|
148
|
+
receipt.invoiceStatus === "paid"
|
|
149
|
+
? "collect"
|
|
150
|
+
: receipt.invoiceStatus === "void" ||
|
|
151
|
+
(receipt.invoiceStatus === "uncollectible" &&
|
|
152
|
+
opts.releaseUncollectible === true)
|
|
153
|
+
? "release"
|
|
154
|
+
: null;
|
|
155
|
+
if (!outcome) return { ...receipt, isDone: false };
|
|
156
|
+
return {
|
|
157
|
+
...receipt,
|
|
158
|
+
...(await this.finish(ctx, { groupKey: opts.groupKey, outcome })),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** Reconcile a returned funding charge without changing the original cost claims. */
|
|
162
|
+
async rollbackInvoice(
|
|
163
|
+
ctx: RunCtx,
|
|
164
|
+
opts: RecoveryScope & { groupKey: string; revision: number },
|
|
165
|
+
) {
|
|
166
|
+
if (!this.provider)
|
|
167
|
+
throw new Error("Invoice recovery requires a Stripe client.");
|
|
168
|
+
const stripe = this.provider();
|
|
169
|
+
const group = await this.getGroup(ctx, { groupKey: opts.groupKey });
|
|
170
|
+
const receipt = await attachRecoveryInvoice(
|
|
171
|
+
stripe,
|
|
172
|
+
group,
|
|
173
|
+
opts,
|
|
174
|
+
(invoiceItemId) =>
|
|
175
|
+
this.mutate<RecoveryGroup>(ctx, "mutations/bindInvoiceItem", {
|
|
176
|
+
groupKey: opts.groupKey,
|
|
177
|
+
invoiceItemId,
|
|
178
|
+
}),
|
|
179
|
+
false,
|
|
180
|
+
);
|
|
181
|
+
if (
|
|
182
|
+
receipt.group.status !== "collected" ||
|
|
183
|
+
receipt.invoiceStatus !== "paid"
|
|
184
|
+
)
|
|
185
|
+
throw new Error("Invoice rollback requires verified collection.");
|
|
186
|
+
const cost = await readInvoiceFunding(stripe, receipt.group);
|
|
187
|
+
if (cost.status === "not_ready") return cost;
|
|
188
|
+
await this.mutate<RecoveryGroup>(ctx, "mutations/bindInvoiceFunding", {
|
|
189
|
+
groupKey: opts.groupKey,
|
|
190
|
+
sourceChargeId: cost.chargeId,
|
|
191
|
+
sourceGross: cost.gross,
|
|
192
|
+
sourceProcessingFee: cost.processingFee,
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
status: "ready" as const,
|
|
196
|
+
claim: await this.rollback(ctx, {
|
|
197
|
+
groupKey: opts.groupKey,
|
|
198
|
+
returnedPrincipal: cost.principalReturned,
|
|
199
|
+
revision: opts.revision,
|
|
200
|
+
}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Import a host-verified opening collection. Never overwrites a live claim. */
|
|
204
|
+
importClaim(
|
|
205
|
+
ctx: RunCtx,
|
|
206
|
+
opts: RecoveryObservation & {
|
|
207
|
+
operationKey: string;
|
|
208
|
+
collectedAmount: number;
|
|
209
|
+
},
|
|
210
|
+
) {
|
|
211
|
+
return this.mutate<RecoveryClaim>(ctx, "mutations/importClaim", opts);
|
|
212
|
+
}
|
|
92
213
|
recordClaim(ctx: RunCtx, opts: RecoveryObservation) {
|
|
93
214
|
return this.mutate<RecoveryClaim>(ctx, "mutations/record", opts);
|
|
94
215
|
}
|
package/src/client/errors.ts
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -94,6 +94,8 @@ export { computeFee, computeSplit } from "./core/fees.js";
|
|
|
94
94
|
export type { FeeBreakdown, SplitResult, SplitTransfer } from "./core/fees.js";
|
|
95
95
|
export { groupSubscriptionsByStore } from "./billing/subscriptions.js";
|
|
96
96
|
export {
|
|
97
|
+
type DisputeEvidenceUploadOptions,
|
|
98
|
+
type DisputeEvidenceFileField,
|
|
97
99
|
buildDisputeEvidence,
|
|
98
100
|
disputeEvidenceCountdown,
|
|
99
101
|
} from "./connect/disputes.js";
|
|
@@ -1265,7 +1267,7 @@ export class BetterStripe {
|
|
|
1265
1267
|
return providerReads.getCouponFromStripe(this.stripe(), opts);
|
|
1266
1268
|
}
|
|
1267
1269
|
get costRecovery() {
|
|
1268
|
-
return new CostRecoveryClient(this.component);
|
|
1270
|
+
return new CostRecoveryClient(this.component, () => this.stripe());
|
|
1269
1271
|
}
|
|
1270
1272
|
|
|
1271
1273
|
async getProcessingCostContext(
|
|
@@ -1403,13 +1405,6 @@ export class BetterStripe {
|
|
|
1403
1405
|
return disputesImpl.listDisputes(this.component, ctx, opts);
|
|
1404
1406
|
}
|
|
1405
1407
|
|
|
1406
|
-
/**
|
|
1407
|
-
* Submit or stage dispute evidence. Stripe's `submit` defaults to TRUE, so
|
|
1408
|
-
* this method stages by default: EVERY call sends an explicit
|
|
1409
|
-
* `submit: false` — evidence-bearing or metadata-only — unless you pass
|
|
1410
|
-
* `submit: true` to submit to the bank (one-shot, outcome-affecting;
|
|
1411
|
-
* BTS-61, BTS-72). See {@link disputesImpl.updateDispute}.
|
|
1412
|
-
*/
|
|
1413
1408
|
async updateDispute(
|
|
1414
1409
|
ctx: RunCtx,
|
|
1415
1410
|
opts: {
|
|
@@ -1418,23 +1413,29 @@ export class BetterStripe {
|
|
|
1418
1413
|
metadata?: Record<string, string>;
|
|
1419
1414
|
submit?: boolean;
|
|
1420
1415
|
stripeAccountId?: string;
|
|
1421
|
-
/**
|
|
1422
|
-
* Who is acting on the dispute (BTS-107). A platform admin may act on any
|
|
1423
|
-
* dispute; a seller may act only on a dispute for a sale routed to their
|
|
1424
|
-
* own account. Omit for platform-initiated (unrestricted) actions.
|
|
1425
|
-
*/
|
|
1426
1416
|
actor?: RefundActor;
|
|
1427
1417
|
},
|
|
1428
1418
|
): Promise<{ success: true }> {
|
|
1429
1419
|
return disputesImpl.updateDispute(this.stripe(), this.component, ctx, opts);
|
|
1430
1420
|
}
|
|
1431
1421
|
|
|
1422
|
+
async uploadDisputeEvidence(
|
|
1423
|
+
ctx: RunCtx,
|
|
1424
|
+
opts: disputesImpl.DisputeEvidenceUploadOptions,
|
|
1425
|
+
) {
|
|
1426
|
+
return disputesImpl.uploadDisputeEvidence(
|
|
1427
|
+
this.stripe(),
|
|
1428
|
+
this.component,
|
|
1429
|
+
ctx,
|
|
1430
|
+
opts,
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1432
1434
|
async closeDispute(
|
|
1433
1435
|
ctx: RunCtx,
|
|
1434
1436
|
opts: {
|
|
1435
1437
|
stripeDisputeId: string;
|
|
1436
1438
|
stripeAccountId?: string;
|
|
1437
|
-
/** See {@link updateDispute}'s `actor` (BTS-107). Omit for unrestricted. */
|
|
1438
1439
|
actor?: RefundActor;
|
|
1439
1440
|
},
|
|
1440
1441
|
): Promise<{ success: true }> {
|
|
@@ -20,6 +20,7 @@ import type * as core_validators from "../core/validators.js";
|
|
|
20
20
|
import type * as costRecovery_claims from "../costRecovery/claims.js";
|
|
21
21
|
import type * as costRecovery_completion from "../costRecovery/completion.js";
|
|
22
22
|
import type * as costRecovery_helpers from "../costRecovery/helpers.js";
|
|
23
|
+
import type * as costRecovery_imports from "../costRecovery/imports.js";
|
|
23
24
|
import type * as costRecovery_mutations from "../costRecovery/mutations.js";
|
|
24
25
|
import type * as costRecovery_queries from "../costRecovery/queries.js";
|
|
25
26
|
import type * as costRecovery_reservations from "../costRecovery/reservations.js";
|
|
@@ -57,6 +58,7 @@ const fullApi: ApiFromModules<{
|
|
|
57
58
|
"costRecovery/claims": typeof costRecovery_claims;
|
|
58
59
|
"costRecovery/completion": typeof costRecovery_completion;
|
|
59
60
|
"costRecovery/helpers": typeof costRecovery_helpers;
|
|
61
|
+
"costRecovery/imports": typeof costRecovery_imports;
|
|
60
62
|
"costRecovery/mutations": typeof costRecovery_mutations;
|
|
61
63
|
"costRecovery/queries": typeof costRecovery_queries;
|
|
62
64
|
"costRecovery/reservations": typeof costRecovery_reservations;
|
|
@@ -1594,6 +1594,81 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1594
1594
|
};
|
|
1595
1595
|
costRecovery: {
|
|
1596
1596
|
mutations: {
|
|
1597
|
+
bindInvoiceFunding: FunctionReference<
|
|
1598
|
+
"mutation",
|
|
1599
|
+
"internal",
|
|
1600
|
+
{
|
|
1601
|
+
groupKey: string;
|
|
1602
|
+
sourceChargeId: string;
|
|
1603
|
+
sourceGross: number;
|
|
1604
|
+
sourceProcessingFee: number;
|
|
1605
|
+
},
|
|
1606
|
+
{
|
|
1607
|
+
_creationTime: number;
|
|
1608
|
+
_id: string;
|
|
1609
|
+
accountId: string;
|
|
1610
|
+
amount: number;
|
|
1611
|
+
currency: string;
|
|
1612
|
+
groupKey: string;
|
|
1613
|
+
invoiceFunding?: {
|
|
1614
|
+
sourceChargeId: string;
|
|
1615
|
+
sourceGross: number;
|
|
1616
|
+
sourceProcessingFee: number;
|
|
1617
|
+
};
|
|
1618
|
+
invoiceItemId?: string;
|
|
1619
|
+
maxCredit: number;
|
|
1620
|
+
maxDebit: number;
|
|
1621
|
+
pendingOperations: number;
|
|
1622
|
+
scopeKey: string;
|
|
1623
|
+
sourceGross?: number;
|
|
1624
|
+
sourceId: string;
|
|
1625
|
+
sourceKind: "settlement" | "invoice";
|
|
1626
|
+
sourceProcessingFee?: number;
|
|
1627
|
+
status:
|
|
1628
|
+
| "open"
|
|
1629
|
+
| "sealed"
|
|
1630
|
+
| "collecting"
|
|
1631
|
+
| "releasing"
|
|
1632
|
+
| "collected"
|
|
1633
|
+
| "released";
|
|
1634
|
+
},
|
|
1635
|
+
Name
|
|
1636
|
+
>;
|
|
1637
|
+
bindInvoiceItem: FunctionReference<
|
|
1638
|
+
"mutation",
|
|
1639
|
+
"internal",
|
|
1640
|
+
{ groupKey: string; invoiceItemId: string },
|
|
1641
|
+
{
|
|
1642
|
+
_creationTime: number;
|
|
1643
|
+
_id: string;
|
|
1644
|
+
accountId: string;
|
|
1645
|
+
amount: number;
|
|
1646
|
+
currency: string;
|
|
1647
|
+
groupKey: string;
|
|
1648
|
+
invoiceFunding?: {
|
|
1649
|
+
sourceChargeId: string;
|
|
1650
|
+
sourceGross: number;
|
|
1651
|
+
sourceProcessingFee: number;
|
|
1652
|
+
};
|
|
1653
|
+
invoiceItemId?: string;
|
|
1654
|
+
maxCredit: number;
|
|
1655
|
+
maxDebit: number;
|
|
1656
|
+
pendingOperations: number;
|
|
1657
|
+
scopeKey: string;
|
|
1658
|
+
sourceGross?: number;
|
|
1659
|
+
sourceId: string;
|
|
1660
|
+
sourceKind: "settlement" | "invoice";
|
|
1661
|
+
sourceProcessingFee?: number;
|
|
1662
|
+
status:
|
|
1663
|
+
| "open"
|
|
1664
|
+
| "sealed"
|
|
1665
|
+
| "collecting"
|
|
1666
|
+
| "releasing"
|
|
1667
|
+
| "collected"
|
|
1668
|
+
| "released";
|
|
1669
|
+
},
|
|
1670
|
+
Name
|
|
1671
|
+
>;
|
|
1597
1672
|
finish: FunctionReference<
|
|
1598
1673
|
"mutation",
|
|
1599
1674
|
"internal",
|
|
@@ -1606,6 +1681,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1606
1681
|
amount: number;
|
|
1607
1682
|
currency: string;
|
|
1608
1683
|
groupKey: string;
|
|
1684
|
+
invoiceFunding?: {
|
|
1685
|
+
sourceChargeId: string;
|
|
1686
|
+
sourceGross: number;
|
|
1687
|
+
sourceProcessingFee: number;
|
|
1688
|
+
};
|
|
1689
|
+
invoiceItemId?: string;
|
|
1609
1690
|
maxCredit: number;
|
|
1610
1691
|
maxDebit: number;
|
|
1611
1692
|
pendingOperations: number;
|
|
@@ -1626,6 +1707,47 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1626
1707
|
},
|
|
1627
1708
|
Name
|
|
1628
1709
|
>;
|
|
1710
|
+
importClaim: FunctionReference<
|
|
1711
|
+
"mutation",
|
|
1712
|
+
"internal",
|
|
1713
|
+
{
|
|
1714
|
+
accountId: string;
|
|
1715
|
+
amountDue: number;
|
|
1716
|
+
claimKey: string;
|
|
1717
|
+
collectedAmount: number;
|
|
1718
|
+
currency: string;
|
|
1719
|
+
operationKey: string;
|
|
1720
|
+
revision: number;
|
|
1721
|
+
scopeKey: string;
|
|
1722
|
+
sourceChargeId: string;
|
|
1723
|
+
sourceGross: number;
|
|
1724
|
+
sourceProcessingFee: number;
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
_creationTime: number;
|
|
1728
|
+
_id: string;
|
|
1729
|
+
accountId: string;
|
|
1730
|
+
amountDue: number;
|
|
1731
|
+
claimKey: string;
|
|
1732
|
+
collectedAmount: number;
|
|
1733
|
+
currency: string;
|
|
1734
|
+
openingCollection?: {
|
|
1735
|
+
amountDue: number;
|
|
1736
|
+
collectedAmount: number;
|
|
1737
|
+
operationKey: string;
|
|
1738
|
+
revision: number;
|
|
1739
|
+
};
|
|
1740
|
+
outstanding: number;
|
|
1741
|
+
reservedAmount: number;
|
|
1742
|
+
revision: number;
|
|
1743
|
+
scopeKey: string;
|
|
1744
|
+
sign: "positive" | "negative" | "zero";
|
|
1745
|
+
sourceChargeId: string;
|
|
1746
|
+
sourceGross: number;
|
|
1747
|
+
sourceProcessingFee: number;
|
|
1748
|
+
},
|
|
1749
|
+
Name
|
|
1750
|
+
>;
|
|
1629
1751
|
record: FunctionReference<
|
|
1630
1752
|
"mutation",
|
|
1631
1753
|
"internal",
|
|
@@ -1648,6 +1770,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1648
1770
|
claimKey: string;
|
|
1649
1771
|
collectedAmount: number;
|
|
1650
1772
|
currency: string;
|
|
1773
|
+
openingCollection?: {
|
|
1774
|
+
amountDue: number;
|
|
1775
|
+
collectedAmount: number;
|
|
1776
|
+
operationKey: string;
|
|
1777
|
+
revision: number;
|
|
1778
|
+
};
|
|
1651
1779
|
outstanding: number;
|
|
1652
1780
|
reservedAmount: number;
|
|
1653
1781
|
revision: number;
|
|
@@ -1684,6 +1812,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1684
1812
|
amount: number;
|
|
1685
1813
|
currency: string;
|
|
1686
1814
|
groupKey: string;
|
|
1815
|
+
invoiceFunding?: {
|
|
1816
|
+
sourceChargeId: string;
|
|
1817
|
+
sourceGross: number;
|
|
1818
|
+
sourceProcessingFee: number;
|
|
1819
|
+
};
|
|
1820
|
+
invoiceItemId?: string;
|
|
1687
1821
|
maxCredit: number;
|
|
1688
1822
|
maxDebit: number;
|
|
1689
1823
|
pendingOperations: number;
|
|
@@ -1729,6 +1863,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1729
1863
|
claimKey: string;
|
|
1730
1864
|
collectedAmount: number;
|
|
1731
1865
|
currency: string;
|
|
1866
|
+
openingCollection?: {
|
|
1867
|
+
amountDue: number;
|
|
1868
|
+
collectedAmount: number;
|
|
1869
|
+
operationKey: string;
|
|
1870
|
+
revision: number;
|
|
1871
|
+
};
|
|
1732
1872
|
outstanding: number;
|
|
1733
1873
|
reservedAmount: number;
|
|
1734
1874
|
revision: number;
|
|
@@ -1751,6 +1891,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1751
1891
|
amount: number;
|
|
1752
1892
|
currency: string;
|
|
1753
1893
|
groupKey: string;
|
|
1894
|
+
invoiceFunding?: {
|
|
1895
|
+
sourceChargeId: string;
|
|
1896
|
+
sourceGross: number;
|
|
1897
|
+
sourceProcessingFee: number;
|
|
1898
|
+
};
|
|
1899
|
+
invoiceItemId?: string;
|
|
1754
1900
|
maxCredit: number;
|
|
1755
1901
|
maxDebit: number;
|
|
1756
1902
|
pendingOperations: number;
|
|
@@ -1799,6 +1945,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1799
1945
|
claimKey: string;
|
|
1800
1946
|
collectedAmount: number;
|
|
1801
1947
|
currency: string;
|
|
1948
|
+
openingCollection?: {
|
|
1949
|
+
amountDue: number;
|
|
1950
|
+
collectedAmount: number;
|
|
1951
|
+
operationKey: string;
|
|
1952
|
+
revision: number;
|
|
1953
|
+
};
|
|
1802
1954
|
outstanding: number;
|
|
1803
1955
|
reservedAmount: number;
|
|
1804
1956
|
revision: number;
|
|
@@ -1821,6 +1973,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1821
1973
|
amount: number;
|
|
1822
1974
|
currency: string;
|
|
1823
1975
|
groupKey: string;
|
|
1976
|
+
invoiceFunding?: {
|
|
1977
|
+
sourceChargeId: string;
|
|
1978
|
+
sourceGross: number;
|
|
1979
|
+
sourceProcessingFee: number;
|
|
1980
|
+
};
|
|
1981
|
+
invoiceItemId?: string;
|
|
1824
1982
|
maxCredit: number;
|
|
1825
1983
|
maxDebit: number;
|
|
1826
1984
|
pendingOperations: number;
|
|
@@ -1866,6 +2024,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1866
2024
|
claimKey: string;
|
|
1867
2025
|
collectedAmount: number;
|
|
1868
2026
|
currency: string;
|
|
2027
|
+
openingCollection?: {
|
|
2028
|
+
amountDue: number;
|
|
2029
|
+
collectedAmount: number;
|
|
2030
|
+
operationKey: string;
|
|
2031
|
+
revision: number;
|
|
2032
|
+
};
|
|
1869
2033
|
outstanding: number;
|
|
1870
2034
|
reservedAmount: number;
|
|
1871
2035
|
revision: number;
|
|
@@ -83,16 +83,28 @@ export async function rollbackClaim(
|
|
|
83
83
|
const group = await getGroup(ctx, args.groupKey);
|
|
84
84
|
if (
|
|
85
85
|
group.status !== "collected" ||
|
|
86
|
-
group.sourceKind
|
|
87
|
-
|
|
86
|
+
(group.sourceKind === "settlement"
|
|
87
|
+
? !group.sourceGross
|
|
88
|
+
: !group.invoiceFunding)
|
|
88
89
|
)
|
|
89
90
|
throw new Error(
|
|
90
|
-
"Rollback requires a collected
|
|
91
|
+
"Rollback requires a collected group with original funding economics.",
|
|
91
92
|
);
|
|
92
|
-
|
|
93
|
+
const funding =
|
|
94
|
+
group.sourceKind === "settlement"
|
|
95
|
+
? {
|
|
96
|
+
sourceChargeId: group.sourceId,
|
|
97
|
+
sourceGross: group.sourceGross!,
|
|
98
|
+
sourceProcessingFee: group.sourceProcessingFee!,
|
|
99
|
+
}
|
|
100
|
+
: group.invoiceFunding!;
|
|
101
|
+
if (
|
|
102
|
+
args.returnedPrincipal < 0 ||
|
|
103
|
+
args.returnedPrincipal > funding.sourceGross
|
|
104
|
+
)
|
|
93
105
|
throw new Error("Returned principal exceeds the original funding sale.");
|
|
94
106
|
const abs = BigInt(Math.abs(group.amount)),
|
|
95
|
-
gross = BigInt(
|
|
107
|
+
gross = BigInt(funding.sourceGross),
|
|
96
108
|
principal = BigInt(args.returnedPrincipal);
|
|
97
109
|
const rounded = Number((abs * principal + gross / 2n) / gross);
|
|
98
110
|
return recordClaim(ctx, {
|
|
@@ -100,9 +112,7 @@ export async function rollbackClaim(
|
|
|
100
112
|
scopeKey: group.scopeKey,
|
|
101
113
|
currency: group.currency,
|
|
102
114
|
claimKey: `rollback:${group.groupKey}`,
|
|
103
|
-
|
|
104
|
-
sourceGross: group.sourceGross,
|
|
105
|
-
sourceProcessingFee: group.sourceProcessingFee!,
|
|
115
|
+
...funding,
|
|
106
116
|
amountDue: group.amount < 0 ? -rounded : rounded,
|
|
107
117
|
revision: args.revision,
|
|
108
118
|
});
|