@fantastic.dev/better-stripe 0.18.0 → 0.19.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 +6 -0
- package/README.md +4 -1
- package/dist/client/connect/transferReceipt.d.ts +48 -0
- package/dist/client/connect/transferReceipt.d.ts.map +1 -0
- package/dist/client/connect/transferReceipt.js +296 -0
- package/dist/client/connect/transferReceipt.js.map +1 -0
- package/dist/client/payoutPolicy.d.ts +3 -0
- package/dist/client/payoutPolicy.d.ts.map +1 -1
- package/dist/client/payoutPolicy.js +4 -0
- package/dist/client/payoutPolicy.js.map +1 -1
- package/docs/settlement.md +50 -0
- package/package.json +1 -1
- package/src/client/connect/transferReceipt.pagination.test.ts +209 -0
- package/src/client/connect/transferReceipt.test.ts +520 -0
- package/src/client/connect/transferReceipt.ts +422 -0
- package/src/client/payoutPolicy.ts +13 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type Stripe from "stripe";
|
|
2
|
+
import { expect, it, vi } from "vitest";
|
|
3
|
+
import type { Component, RunCtx } from "../helpers.js";
|
|
4
|
+
import { getTransferFundingReceipt } from "./transferReceipt.js";
|
|
5
|
+
const symbol = Symbol.for("toReferencePath");
|
|
6
|
+
const component = {
|
|
7
|
+
connect: {
|
|
8
|
+
queries: {
|
|
9
|
+
getTransferByStripeId: {
|
|
10
|
+
[symbol]: "betterStripe/connect/queries/getTransferByStripeId",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
} as unknown as Component;
|
|
15
|
+
const ledger = {
|
|
16
|
+
stripeTransferId: "tr_1",
|
|
17
|
+
destinationAccountId: "acct_owner",
|
|
18
|
+
amount: 1000,
|
|
19
|
+
currency: "usd",
|
|
20
|
+
status: "paid",
|
|
21
|
+
};
|
|
22
|
+
const tx = (id: string, amount: number, source = "py_1") => ({
|
|
23
|
+
id,
|
|
24
|
+
source,
|
|
25
|
+
amount,
|
|
26
|
+
net: amount,
|
|
27
|
+
fee: 0,
|
|
28
|
+
currency: "usd",
|
|
29
|
+
created: 1,
|
|
30
|
+
available_on: 2,
|
|
31
|
+
status: "available",
|
|
32
|
+
type: amount > 0 ? "payment" : "adjustment",
|
|
33
|
+
exchange_rate: null,
|
|
34
|
+
});
|
|
35
|
+
function setup() {
|
|
36
|
+
const ctx = {
|
|
37
|
+
runQuery: vi.fn().mockResolvedValue(ledger),
|
|
38
|
+
} as unknown as RunCtx;
|
|
39
|
+
const provider = {
|
|
40
|
+
transfers: {
|
|
41
|
+
retrieve: vi.fn().mockResolvedValue({
|
|
42
|
+
id: "tr_1",
|
|
43
|
+
destination: "acct_owner",
|
|
44
|
+
destination_payment: "py_1",
|
|
45
|
+
source_transaction: null,
|
|
46
|
+
amount: 1000,
|
|
47
|
+
amount_reversed: 0,
|
|
48
|
+
currency: "usd",
|
|
49
|
+
}),
|
|
50
|
+
listReversals: vi.fn().mockResolvedValue({ data: [], has_more: false }),
|
|
51
|
+
},
|
|
52
|
+
charges: {
|
|
53
|
+
retrieve: vi.fn().mockResolvedValue({
|
|
54
|
+
id: "py_1",
|
|
55
|
+
source_transfer: "tr_1",
|
|
56
|
+
amount: 1000,
|
|
57
|
+
currency: "usd",
|
|
58
|
+
paid: true,
|
|
59
|
+
status: "succeeded",
|
|
60
|
+
balance_transaction: "txn_credit",
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
refunds: { retrieve: vi.fn() },
|
|
64
|
+
balanceTransactions: {
|
|
65
|
+
list: vi
|
|
66
|
+
.fn()
|
|
67
|
+
.mockResolvedValue({ data: [tx("txn_credit", 1000)], has_more: false }),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
ctx,
|
|
72
|
+
provider,
|
|
73
|
+
run: (extra: object = {}) =>
|
|
74
|
+
getTransferFundingReceipt(provider as unknown as Stripe, component, ctx, {
|
|
75
|
+
accountId: "acct_owner",
|
|
76
|
+
stripeTransferId: "tr_1",
|
|
77
|
+
...extra,
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
it("reads all 205 source receipts instead of truncating at the first provider page", async () => {
|
|
82
|
+
const { provider, run } = setup();
|
|
83
|
+
const rows = [
|
|
84
|
+
tx("txn_credit", 1000),
|
|
85
|
+
...Array.from({ length: 204 }, (_, i) => tx(`txn_fee_${i}`, -1)),
|
|
86
|
+
];
|
|
87
|
+
provider.balanceTransactions.list.mockImplementation(
|
|
88
|
+
async (args: { starting_after?: string }) => {
|
|
89
|
+
const start = args.starting_after
|
|
90
|
+
? rows.findIndex((t) => t.id === args.starting_after) + 1
|
|
91
|
+
: 0;
|
|
92
|
+
return {
|
|
93
|
+
data: rows.slice(start, start + 100),
|
|
94
|
+
has_more: start + 100 < rows.length,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
const result = await run();
|
|
99
|
+
expect(result).toMatchObject({
|
|
100
|
+
status: "ready",
|
|
101
|
+
totalNet: 796,
|
|
102
|
+
matureNet: 796,
|
|
103
|
+
pendingCredits: 0,
|
|
104
|
+
});
|
|
105
|
+
if (result.status === "ready") expect(result.transactions).toHaveLength(205);
|
|
106
|
+
expect(provider.balanceTransactions.list).toHaveBeenCalledTimes(3);
|
|
107
|
+
const limited = await run({ maxRequests: 6 });
|
|
108
|
+
expect(limited).toMatchObject({
|
|
109
|
+
status: "not_ready",
|
|
110
|
+
reason: "history_incomplete",
|
|
111
|
+
});
|
|
112
|
+
expect(limited).not.toHaveProperty("totalNet");
|
|
113
|
+
});
|
|
114
|
+
it("does not publish a source snapshot if its authoritative ledger changed during the read", async () => {
|
|
115
|
+
const { ctx, run } = setup();
|
|
116
|
+
(ctx.runQuery as ReturnType<typeof vi.fn>)
|
|
117
|
+
.mockReset()
|
|
118
|
+
.mockResolvedValueOnce(ledger)
|
|
119
|
+
.mockResolvedValueOnce({ ...ledger, reversedAmount: 1 });
|
|
120
|
+
expect(await run()).toMatchObject({
|
|
121
|
+
status: "not_ready",
|
|
122
|
+
reason: "ledger_changed",
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
it("propagates genuine provider failures and detects aggregate overflow", async () => {
|
|
126
|
+
const { provider, run } = setup();
|
|
127
|
+
provider.charges.retrieve.mockRejectedValueOnce(
|
|
128
|
+
new Error("provider unavailable"),
|
|
129
|
+
);
|
|
130
|
+
await expect(run()).rejects.toThrow("provider unavailable");
|
|
131
|
+
provider.balanceTransactions.list.mockResolvedValue({
|
|
132
|
+
data: [
|
|
133
|
+
tx("txn_credit", 1000),
|
|
134
|
+
{ ...tx("txn_extra", Number.MAX_SAFE_INTEGER), type: "adjustment" },
|
|
135
|
+
],
|
|
136
|
+
has_more: false,
|
|
137
|
+
});
|
|
138
|
+
await expect(run()).rejects.toThrow("safe integers");
|
|
139
|
+
});
|
|
140
|
+
it("never uses a pending fee credit to qualify a fully returned mature transfer", async () => {
|
|
141
|
+
const { ctx, provider, run } = setup();
|
|
142
|
+
(ctx.runQuery as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
143
|
+
...ledger,
|
|
144
|
+
reversedAmount: 1000,
|
|
145
|
+
});
|
|
146
|
+
provider.transfers.retrieve.mockResolvedValue({
|
|
147
|
+
id: "tr_1",
|
|
148
|
+
destination: "acct_owner",
|
|
149
|
+
destination_payment: "py_1",
|
|
150
|
+
source_transaction: null,
|
|
151
|
+
amount: 1000,
|
|
152
|
+
amount_reversed: 1000,
|
|
153
|
+
currency: "usd",
|
|
154
|
+
});
|
|
155
|
+
provider.transfers.listReversals.mockResolvedValue({
|
|
156
|
+
data: [
|
|
157
|
+
{
|
|
158
|
+
id: "trr_1",
|
|
159
|
+
transfer: "tr_1",
|
|
160
|
+
amount: 1000,
|
|
161
|
+
currency: "usd",
|
|
162
|
+
destination_payment_refund: "pyr_1",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
has_more: false,
|
|
166
|
+
} as never);
|
|
167
|
+
provider.refunds.retrieve.mockResolvedValue({
|
|
168
|
+
id: "pyr_1",
|
|
169
|
+
charge: "py_1",
|
|
170
|
+
amount: 1000,
|
|
171
|
+
currency: "usd",
|
|
172
|
+
status: "succeeded",
|
|
173
|
+
balance_transaction: "txn_debit",
|
|
174
|
+
});
|
|
175
|
+
provider.balanceTransactions.list.mockImplementation(
|
|
176
|
+
async (args: { source: string }) => ({
|
|
177
|
+
data:
|
|
178
|
+
args.source === "py_1"
|
|
179
|
+
? [
|
|
180
|
+
tx("txn_credit", 1000),
|
|
181
|
+
{ ...tx("txn_fee_credit", 100), status: "pending" },
|
|
182
|
+
]
|
|
183
|
+
: [{ ...tx("txn_debit", -1000, "pyr_1"), status: "pending" }],
|
|
184
|
+
has_more: false,
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
expect(await run()).toMatchObject({
|
|
188
|
+
status: "ready",
|
|
189
|
+
totalNet: 100,
|
|
190
|
+
matureNet: 0,
|
|
191
|
+
pendingCredits: 100,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
it("preserves an already available debit while its original credit is pending", async () => {
|
|
195
|
+
const { provider, run } = setup();
|
|
196
|
+
provider.balanceTransactions.list.mockResolvedValue({
|
|
197
|
+
data: [
|
|
198
|
+
{ ...tx("txn_credit", 1000), status: "pending" },
|
|
199
|
+
tx("txn_fee", -200),
|
|
200
|
+
],
|
|
201
|
+
has_more: false,
|
|
202
|
+
});
|
|
203
|
+
expect(await run()).toMatchObject({
|
|
204
|
+
status: "ready",
|
|
205
|
+
totalNet: 800,
|
|
206
|
+
matureNet: -200,
|
|
207
|
+
pendingCredits: 1000,
|
|
208
|
+
});
|
|
209
|
+
});
|