@vaam-apps/vpay-sdk 0.2.1
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/LICENSE +21 -0
- package/README.md +732 -0
- package/dist/auth.d.ts +180 -0
- package/dist/auth.js +297 -0
- package/dist/client.d.ts +75 -0
- package/dist/client.js +79 -0
- package/dist/errors.d.ts +91 -0
- package/dist/errors.js +112 -0
- package/dist/form.d.ts +47 -0
- package/dist/form.js +127 -0
- package/dist/http.d.ts +33 -0
- package/dist/http.js +115 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/resources/account-holders.d.ts +36 -0
- package/dist/resources/account-holders.js +46 -0
- package/dist/resources/balance.d.ts +8 -0
- package/dist/resources/balance.js +10 -0
- package/dist/resources/checkout-sessions.d.ts +55 -0
- package/dist/resources/checkout-sessions.js +127 -0
- package/dist/resources/customers.d.ts +97 -0
- package/dist/resources/customers.js +157 -0
- package/dist/resources/events.d.ts +8 -0
- package/dist/resources/events.js +10 -0
- package/dist/resources/invoices.d.ts +193 -0
- package/dist/resources/invoices.js +275 -0
- package/dist/resources/payment-intents.d.ts +12 -0
- package/dist/resources/payment-intents.js +44 -0
- package/dist/resources/refunds.d.ts +92 -0
- package/dist/resources/refunds.js +106 -0
- package/dist/stripe-auth.d.ts +177 -0
- package/dist/stripe-auth.js +192 -0
- package/dist/types.d.ts +1268 -0
- package/dist/types.js +48 -0
- package/dist/validate.d.ts +16 -0
- package/dist/validate.js +20 -0
- package/dist/version.d.ts +12 -0
- package/dist/version.js +12 -0
- package/dist/webhooks.d.ts +22 -0
- package/dist/webhooks.js +94 -0
- package/package.json +63 -0
- package/scripts/mint-assertion.mjs +73 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { assertIntegerAmount } from "../validate.js";
|
|
2
|
+
/**
|
|
3
|
+
* One patch field, in the wire's own three-state encoding.
|
|
4
|
+
*
|
|
5
|
+
* Returns `undefined` for "the request did not mention this", which the
|
|
6
|
+
* caller drops from the body entirely, and the empty string for "clear it".
|
|
7
|
+
* The twin of `customers.ts`'s `patch`, kept per file rather than shared
|
|
8
|
+
* because the two differ in the value type they carry and merging them would
|
|
9
|
+
* cost a generic that says less than the two lines it replaced.
|
|
10
|
+
*/
|
|
11
|
+
function patch(value) {
|
|
12
|
+
if (value === undefined) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return value === null ? "" : value;
|
|
16
|
+
}
|
|
17
|
+
/** `client.invoices` — see the module doc. */
|
|
18
|
+
export class InvoicesResource {
|
|
19
|
+
#http;
|
|
20
|
+
constructor(http) {
|
|
21
|
+
this.#http = http;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `POST /v1/invoices` — creates a **draft**, with no number and zero
|
|
25
|
+
* amounts.
|
|
26
|
+
*
|
|
27
|
+
* Field order is the wire order `sdks/rust/tests/resources.rs` pins byte
|
|
28
|
+
* for byte; an unset field is omitted from the body entirely, because
|
|
29
|
+
* `description=` and no `description` are different requests and only the
|
|
30
|
+
* second means "not applicable".
|
|
31
|
+
*
|
|
32
|
+
* A `400` naming `customer` when it is absent, unknown, or another
|
|
33
|
+
* merchant's — one sentence for all three, so the parameter is not an
|
|
34
|
+
* oracle; and a `400` naming `currency` for one this deployment does not
|
|
35
|
+
* settle.
|
|
36
|
+
*/
|
|
37
|
+
async create(params, options) {
|
|
38
|
+
const body = {
|
|
39
|
+
customer: params.customer,
|
|
40
|
+
currency: params.currency.toLowerCase(),
|
|
41
|
+
};
|
|
42
|
+
if (params.description !== undefined) {
|
|
43
|
+
body["description"] = params.description;
|
|
44
|
+
}
|
|
45
|
+
if (params.due_date !== undefined) {
|
|
46
|
+
body["due_date"] = params.due_date;
|
|
47
|
+
}
|
|
48
|
+
if (params.metadata !== undefined) {
|
|
49
|
+
body["metadata"] = params.metadata;
|
|
50
|
+
}
|
|
51
|
+
return this.#http.request("POST", "/invoices", body, options);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* `GET /v1/invoices/{id}` — the invoice **with its lines**.
|
|
55
|
+
*
|
|
56
|
+
* This is the read a webhook handler falls back to: an `invoice.*` event
|
|
57
|
+
* body carries an empty `lines.data`, and this response always carries the
|
|
58
|
+
* lines.
|
|
59
|
+
*
|
|
60
|
+
* Another merchant's `in_…` is the same `404`, byte for byte, as one that
|
|
61
|
+
* never existed.
|
|
62
|
+
*/
|
|
63
|
+
async retrieve(id) {
|
|
64
|
+
return this.#http.request("GET", `/invoices/${encodeURIComponent(id)}`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* `POST /v1/invoices/{id}` — patches a **draft**.
|
|
68
|
+
*
|
|
69
|
+
* A `POST` and not a `PATCH`, for `customers.update`'s reason, even though
|
|
70
|
+
* the server mounts both verbs on the one handler: `POST` is what a
|
|
71
|
+
* merchant's existing Stripe client sends, so it is the one that has to
|
|
72
|
+
* work.
|
|
73
|
+
*
|
|
74
|
+
* A `409` naming the status once the invoice is no longer a draft — an
|
|
75
|
+
* issued document's terms are what was sent to the payer. Void it and issue
|
|
76
|
+
* a new one.
|
|
77
|
+
*/
|
|
78
|
+
async update(id, params, options) {
|
|
79
|
+
const body = {};
|
|
80
|
+
const description = patch(params.description);
|
|
81
|
+
if (description !== undefined) {
|
|
82
|
+
body["description"] = description;
|
|
83
|
+
}
|
|
84
|
+
const dueDate = patch(params.due_date);
|
|
85
|
+
if (dueDate !== undefined) {
|
|
86
|
+
body["due_date"] = dueDate;
|
|
87
|
+
}
|
|
88
|
+
if (params.metadata !== undefined) {
|
|
89
|
+
body["metadata"] = params.metadata;
|
|
90
|
+
}
|
|
91
|
+
return this.#http.request("POST", `/invoices/${encodeURIComponent(id)}`, body, options);
|
|
92
|
+
}
|
|
93
|
+
/** `GET /v1/invoices` — newest first. */
|
|
94
|
+
async list(params) {
|
|
95
|
+
return this.#http.request("GET", "/invoices", params);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* `DELETE /v1/invoices/{id}` — **draft only**, and its lines go with it.
|
|
99
|
+
*
|
|
100
|
+
* An issued invoice is {@link InvoicesResource.void}ed, never deleted: a
|
|
101
|
+
* voided draft would be a non-draft row with no number, which the database
|
|
102
|
+
* refuses outright, and a document that vanished is a hole an accountant
|
|
103
|
+
* reads as a destroyed one.
|
|
104
|
+
*
|
|
105
|
+
* `del` and not `delete`, for `customers.del`'s reason. Carries an
|
|
106
|
+
* `Idempotency-Key` like every other write, which is what makes a retried
|
|
107
|
+
* delete answer the original `{deleted: true}` rather than a `404`.
|
|
108
|
+
*/
|
|
109
|
+
async del(id, options) {
|
|
110
|
+
return this.#http.request("DELETE", `/invoices/${encodeURIComponent(id)}`, undefined, options);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* `POST /v1/invoices/{id}/finalize` — issues the document. **Draft only.**
|
|
114
|
+
*
|
|
115
|
+
* The money transition: the invoice takes the next number out of this
|
|
116
|
+
* merchant's own sequence under a row lock, its lines freeze, its amounts
|
|
117
|
+
* are computed once, and it moves to `"open"` — all in one transaction,
|
|
118
|
+
* with `invoice.finalized` inside it. Numbers are consecutive and have no
|
|
119
|
+
* holes, unlike Stripe's.
|
|
120
|
+
*
|
|
121
|
+
* A `400` naming `invoice` when it has no lines, or when its total is past
|
|
122
|
+
* `Number.MAX_SAFE_INTEGER` minor units; a `409` when it is not a draft.
|
|
123
|
+
*/
|
|
124
|
+
async finalize(id, options) {
|
|
125
|
+
return this.#transition(id, "finalize", options);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* `POST /v1/invoices/{id}/void` — cancels an issued document. **Open
|
|
129
|
+
* only.**
|
|
130
|
+
*
|
|
131
|
+
* The invoice **keeps its number**. Terminal.
|
|
132
|
+
*
|
|
133
|
+
* A `409` when it is not open, and a `409` while a payment intent is
|
|
134
|
+
* attached and not yet canceled — cancel that intent first.
|
|
135
|
+
*/
|
|
136
|
+
async void(id, options) {
|
|
137
|
+
return this.#transition(id, "void", options);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* `POST /v1/invoices/{id}/mark_uncollectible` — writes an issued document
|
|
141
|
+
* off. **Open only.**
|
|
142
|
+
*
|
|
143
|
+
* Still owed, never expected. Terminal, and it **emits no event**: vpay
|
|
144
|
+
* does not write `invoice.marked_uncollectible`, so nothing arrives on a
|
|
145
|
+
* webhook and a merchant reconciling write-offs reads them with
|
|
146
|
+
* `list({ status: "uncollectible" })`.
|
|
147
|
+
*
|
|
148
|
+
* # `markUncollectible` here, `mark_uncollectible` in `sdks/rust`
|
|
149
|
+
*
|
|
150
|
+
* The only capability the two SDKs spell differently, and the difference is
|
|
151
|
+
* each language's own casing rather than a divergence: `sdks/rust` may not
|
|
152
|
+
* spell `markUncollectible` (`non_snake_case` is a rustc lint, not a
|
|
153
|
+
* preference) and this package should not spell `mark_uncollectible` —
|
|
154
|
+
* Stripe's own Node SDK, which merchants arrive from, says
|
|
155
|
+
* `markUncollectible`, and the other spelling would be the one snake_case
|
|
156
|
+
* method in a camelCase package.
|
|
157
|
+
*
|
|
158
|
+
* [ADR-0015](../../../docs/adr/0015-sdk-parity.md) decision 1 is that
|
|
159
|
+
* parity is **per capability, not per method name**, and its alternatives
|
|
160
|
+
* reject method-name parity in as many words. Until 2026-09-08
|
|
161
|
+
* `verify-sdk-parity` could not express that — it keyed a row on one
|
|
162
|
+
* spelling — and this method was named `mark_uncollectible` to satisfy the
|
|
163
|
+
* gate. It is `markUncollectible` now because the gate learned the
|
|
164
|
+
* distinction instead: `PARITY_COLUMN_SPELLINGS` in `.xtask/src/main.rs`
|
|
165
|
+
* carries the one entry, and an entry that exempts nothing fails the build.
|
|
166
|
+
*/
|
|
167
|
+
async markUncollectible(id, options) {
|
|
168
|
+
return this.#transition(id, "mark_uncollectible", options);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* `POST /v1/invoices/{id}/pay` — mints a payment intent and a hosted
|
|
172
|
+
* checkout for what is left. **Open only.**
|
|
173
|
+
*
|
|
174
|
+
* **This does not charge anybody.** Stripe's `pay` charges a stored payment
|
|
175
|
+
* method; vpay has none, so this answers the invoice with
|
|
176
|
+
* {@link Invoice.payment_intent} and {@link Invoice.hosted_invoice_url}
|
|
177
|
+
* set — a page to send the payer to. The invoice stays `"open"` until the
|
|
178
|
+
* settlement moves it, which is when `invoice.paid` is emitted.
|
|
179
|
+
*
|
|
180
|
+
* One payment at a time: calling this again while the attached intent is
|
|
181
|
+
* live is a `409`, and cancelling that intent is the way back. Two
|
|
182
|
+
* concurrent calls attach exactly one intent.
|
|
183
|
+
*
|
|
184
|
+
* A `500 checkout_not_configured` when this deployment serves no checkout
|
|
185
|
+
* page.
|
|
186
|
+
*/
|
|
187
|
+
async pay(id, params, options) {
|
|
188
|
+
return this.#http.request("POST", `/invoices/${encodeURIComponent(id)}/pay`, { success_url: params.success_url, cancel_url: params.cancel_url }, options);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The three transitions that take no parameters, which are one shape:
|
|
192
|
+
* a `POST` with an **empty body** to the invoice's own sub-path, carrying
|
|
193
|
+
* an `Idempotency-Key` like every other write.
|
|
194
|
+
*
|
|
195
|
+
* Private, so it is not a capability the parity gate has to record — and
|
|
196
|
+
* written once rather than three times, because the empty body is the part
|
|
197
|
+
* that is easy to get wrong in only one of them.
|
|
198
|
+
*/
|
|
199
|
+
async #transition(id, name, options) {
|
|
200
|
+
return this.#http.request("POST", `/invoices/${encodeURIComponent(id)}/${name}`, {}, options);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* `client.invoiceItems` — the four operations on `/v1/invoice_items`: the
|
|
205
|
+
* lines of a **draft** invoice.
|
|
206
|
+
*
|
|
207
|
+
* There is deliberately no `list`: an invoice's lines are read off the
|
|
208
|
+
* invoice ({@link Invoice.lines}, expanded on every render), and a
|
|
209
|
+
* merchant-wide list of lines across invoices answers no question anybody
|
|
210
|
+
* asked. The server mounts no collection `GET` either, so this is not an
|
|
211
|
+
* omission here.
|
|
212
|
+
*
|
|
213
|
+
* Every write requires the parent to still be a `"draft"`;
|
|
214
|
+
* {@link InvoiceItemsResource.retrieve} does not.
|
|
215
|
+
*/
|
|
216
|
+
export class InvoiceItemsResource {
|
|
217
|
+
#http;
|
|
218
|
+
constructor(http) {
|
|
219
|
+
this.#http = http;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* `POST /v1/invoice_items` — adds a line to a draft, and the invoice's
|
|
223
|
+
* `amount_due` moves with it.
|
|
224
|
+
*
|
|
225
|
+
* Throws `TypeError` before any request when `unit_amount` is not a
|
|
226
|
+
* non-negative safe integer; a `400` naming `invoice` when it is not one of
|
|
227
|
+
* your drafts.
|
|
228
|
+
*/
|
|
229
|
+
async create(params, options) {
|
|
230
|
+
assertIntegerAmount(params.unit_amount, "unit_amount");
|
|
231
|
+
const body = {
|
|
232
|
+
invoice: params.invoice,
|
|
233
|
+
description: params.description,
|
|
234
|
+
};
|
|
235
|
+
if (params.quantity !== undefined) {
|
|
236
|
+
body["quantity"] = params.quantity;
|
|
237
|
+
}
|
|
238
|
+
body["unit_amount"] = params.unit_amount;
|
|
239
|
+
return this.#http.request("POST", "/invoice_items", body, options);
|
|
240
|
+
}
|
|
241
|
+
/** `GET /v1/invoice_items/{id}` — readable whatever the parent's status. */
|
|
242
|
+
async retrieve(id) {
|
|
243
|
+
return this.#http.request("GET", `/invoice_items/${encodeURIComponent(id)}`);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* `POST /v1/invoice_items/{id}` — **draft parent only**, with `amount`
|
|
247
|
+
* recomputed.
|
|
248
|
+
*
|
|
249
|
+
* Throws `TypeError` before any request for a `unit_amount` outside the
|
|
250
|
+
* bound; a `409` once the parent is no longer a draft.
|
|
251
|
+
*/
|
|
252
|
+
async update(id, params, options) {
|
|
253
|
+
if (params.unit_amount !== undefined) {
|
|
254
|
+
assertIntegerAmount(params.unit_amount, "unit_amount");
|
|
255
|
+
}
|
|
256
|
+
const body = {};
|
|
257
|
+
if (params.description !== undefined) {
|
|
258
|
+
body["description"] = params.description;
|
|
259
|
+
}
|
|
260
|
+
if (params.quantity !== undefined) {
|
|
261
|
+
body["quantity"] = params.quantity;
|
|
262
|
+
}
|
|
263
|
+
if (params.unit_amount !== undefined) {
|
|
264
|
+
body["unit_amount"] = params.unit_amount;
|
|
265
|
+
}
|
|
266
|
+
return this.#http.request("POST", `/invoice_items/${encodeURIComponent(id)}`, body, options);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* `DELETE /v1/invoice_items/{id}` — **draft parent only**, and the invoice
|
|
270
|
+
* re-totals.
|
|
271
|
+
*/
|
|
272
|
+
async del(id, options) {
|
|
273
|
+
return this.#http.request("DELETE", `/invoice_items/${encodeURIComponent(id)}`, undefined, options);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HttpClient } from "../http.js";
|
|
2
|
+
import type { ConfirmPaymentIntentParams, CreatePaymentIntentParams, List, ListParams, PaymentIntent, RequestOptions } from "../types.js";
|
|
3
|
+
/** `/v1/payment_intents` — docs/flows/merchant-auth.md, "Resources". */
|
|
4
|
+
export declare class PaymentIntentsResource {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(http: HttpClient);
|
|
7
|
+
create(params: CreatePaymentIntentParams, options?: RequestOptions): Promise<PaymentIntent>;
|
|
8
|
+
retrieve(id: string): Promise<PaymentIntent>;
|
|
9
|
+
confirm(id: string, params: ConfirmPaymentIntentParams, options?: RequestOptions): Promise<PaymentIntent>;
|
|
10
|
+
cancel(id: string, options?: RequestOptions): Promise<PaymentIntent>;
|
|
11
|
+
list(params?: ListParams): Promise<List<PaymentIntent>>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { assertIntegerAmount } from "../validate.js";
|
|
2
|
+
/** `/v1/payment_intents` — docs/flows/merchant-auth.md, "Resources". */
|
|
3
|
+
export class PaymentIntentsResource {
|
|
4
|
+
#http;
|
|
5
|
+
constructor(http) {
|
|
6
|
+
this.#http = http;
|
|
7
|
+
}
|
|
8
|
+
async create(params, options) {
|
|
9
|
+
assertIntegerAmount(params.amount);
|
|
10
|
+
const body = {
|
|
11
|
+
amount: params.amount,
|
|
12
|
+
currency: params.currency.toLowerCase(),
|
|
13
|
+
payment_method_types: params.payment_method_types,
|
|
14
|
+
};
|
|
15
|
+
if (params.metadata !== undefined) {
|
|
16
|
+
body["metadata"] = params.metadata;
|
|
17
|
+
}
|
|
18
|
+
if (params.description !== undefined) {
|
|
19
|
+
body["description"] = params.description;
|
|
20
|
+
}
|
|
21
|
+
if (params.customer !== undefined) {
|
|
22
|
+
body["customer"] = params.customer;
|
|
23
|
+
}
|
|
24
|
+
return this.#http.request("POST", "/payment_intents", body, options);
|
|
25
|
+
}
|
|
26
|
+
async retrieve(id) {
|
|
27
|
+
return this.#http.request("GET", `/payment_intents/${encodeURIComponent(id)}`);
|
|
28
|
+
}
|
|
29
|
+
async confirm(id, params, options) {
|
|
30
|
+
const body = {
|
|
31
|
+
payment_method_data: params.payment_method_data,
|
|
32
|
+
};
|
|
33
|
+
if ("return_url" in params) {
|
|
34
|
+
body["return_url"] = params.return_url;
|
|
35
|
+
}
|
|
36
|
+
return this.#http.request("POST", `/payment_intents/${encodeURIComponent(id)}/confirm`, body, options);
|
|
37
|
+
}
|
|
38
|
+
async cancel(id, options) {
|
|
39
|
+
return this.#http.request("POST", `/payment_intents/${encodeURIComponent(id)}/cancel`, {}, options);
|
|
40
|
+
}
|
|
41
|
+
async list(params) {
|
|
42
|
+
return this.#http.request("GET", "/payment_intents", params);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/v1/refunds` — the five merchant operations on the Refund object
|
|
3
|
+
* (RFC-0003 § 2).
|
|
4
|
+
*
|
|
5
|
+
* # No refund any of these creates has ever moved money
|
|
6
|
+
*
|
|
7
|
+
* Read this before any sentence elsewhere that says a refund "works". The
|
|
8
|
+
* routes are real and so is every refusal they make, but nothing on the other
|
|
9
|
+
* side of them pays anybody:
|
|
10
|
+
*
|
|
11
|
+
* - `orange_money`'s refund is a declared `NotImplemented` token — an Orange
|
|
12
|
+
* refund is an outbound transfer this repository has no specification for;
|
|
13
|
+
* - `mtn_momo`'s is MTN's Disbursements `transfer`, WireMock-proven and
|
|
14
|
+
* rail-unproven: **no real MTN Disbursements credential exists in this
|
|
15
|
+
* project** — the only `disbursement_subscription_key` anywhere is the stub
|
|
16
|
+
* the e2e/demo stack points at a WireMock container — and the product has
|
|
17
|
+
* never been called;
|
|
18
|
+
* - **nothing settles a `pending` refund.** There is no refund poll ladder,
|
|
19
|
+
* so a refund created here stays `"pending"` until an operator or the
|
|
20
|
+
* settlement path moves it.
|
|
21
|
+
*
|
|
22
|
+
* `docs/status.md` carries all three. A `200` from
|
|
23
|
+
* {@link RefundsResource.create} means a refund row exists and its amount is
|
|
24
|
+
* reserved against the intent. It does not mean the payee has been paid.
|
|
25
|
+
*
|
|
26
|
+
* # The state machine is the server's `WHERE` clause
|
|
27
|
+
*
|
|
28
|
+
* {@link RefundsResource.cancel} works while the refund is `"pending"` and
|
|
29
|
+
* answers a `409` naming the current status otherwise. This SDK does not try
|
|
30
|
+
* to predict which — reading the refund first to decide whether to call is a
|
|
31
|
+
* race. Call, and read the refusal.
|
|
32
|
+
*/
|
|
33
|
+
import type { HttpClient } from "../http.js";
|
|
34
|
+
import type { CreateRefundParams, List, ListRefundsParams, Refund, RequestOptions, UpdateRefundParams } from "../types.js";
|
|
35
|
+
/** `client.refunds` — see the module doc. */
|
|
36
|
+
export declare class RefundsResource {
|
|
37
|
+
#private;
|
|
38
|
+
constructor(http: HttpClient);
|
|
39
|
+
/**
|
|
40
|
+
* `POST /v1/refunds`.
|
|
41
|
+
*
|
|
42
|
+
* Field order is the wire order `sdks/rust/tests/resources.rs` pins byte
|
|
43
|
+
* for byte; an unset field is omitted from the body entirely.
|
|
44
|
+
*
|
|
45
|
+
* A `400` naming `destination` for a payee this rail will not take — and
|
|
46
|
+
* for **no** payee at all, since both rails vpay carries need one; a `400`
|
|
47
|
+
* naming `amount` for a partial refund on a rail that has no partials; a
|
|
48
|
+
* `409` for an intent with nothing left to refund.
|
|
49
|
+
*
|
|
50
|
+
* See the module doc for what a `200` here does and does not mean.
|
|
51
|
+
*/
|
|
52
|
+
create(params: CreateRefundParams, options?: RequestOptions): Promise<Refund>;
|
|
53
|
+
/**
|
|
54
|
+
* `GET /v1/refunds/{id}`.
|
|
55
|
+
*
|
|
56
|
+
* A refund is asynchronous on this rail — `RefundStatus` has a
|
|
57
|
+
* non-terminal `"pending"` — and webhook delivery is at-least-once and
|
|
58
|
+
* unordered, so this is the authoritative read a reconciliation job falls
|
|
59
|
+
* back to when a `charge.refund.updated` was missed or arrived out of
|
|
60
|
+
* order.
|
|
61
|
+
*/
|
|
62
|
+
retrieve(id: string): Promise<Refund>;
|
|
63
|
+
/**
|
|
64
|
+
* `POST /v1/refunds/{id}` — the **metadata** update, and nothing else.
|
|
65
|
+
*
|
|
66
|
+
* See {@link UpdateRefundParams}: the other four fields of a refund are
|
|
67
|
+
* fixed once it exists, the merge is key-wise, and an empty value deletes a
|
|
68
|
+
* key. A refund whose metadata actually changes emits
|
|
69
|
+
* `charge.refund.updated`; one sent no `metadata` is read back unchanged
|
|
70
|
+
* and emits nothing.
|
|
71
|
+
*
|
|
72
|
+
* A `404` for a refund this merchant has none of, and a `400` naming the
|
|
73
|
+
* parameter for anything but `metadata` — which this params type has no way
|
|
74
|
+
* to send.
|
|
75
|
+
*/
|
|
76
|
+
update(id: string, params: UpdateRefundParams, options?: RequestOptions): Promise<Refund>;
|
|
77
|
+
/** `GET /v1/refunds` — this merchant's refunds, newest first. */
|
|
78
|
+
list(params?: ListRefundsParams): Promise<List<Refund>>;
|
|
79
|
+
/**
|
|
80
|
+
* `POST /v1/refunds/{id}/cancel` — **while it is still pending**. No
|
|
81
|
+
* request fields.
|
|
82
|
+
*
|
|
83
|
+
* The reservation this refund holds against its intent is released and
|
|
84
|
+
* `charge.refund.updated` is written. A `404` for a refund this merchant
|
|
85
|
+
* has none of, and a `409` naming the current status for one that is no
|
|
86
|
+
* longer `"pending"`.
|
|
87
|
+
*
|
|
88
|
+
* Nothing here reaches a rail: a cancel is a vpay-side release, because no
|
|
89
|
+
* rail was ever told to start. See the module doc.
|
|
90
|
+
*/
|
|
91
|
+
cancel(id: string, options?: RequestOptions): Promise<Refund>;
|
|
92
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { assertIntegerAmount } from "../validate.js";
|
|
2
|
+
/**
|
|
3
|
+
* The rail-agnostic envelope, built around the rail-specific interior.
|
|
4
|
+
*
|
|
5
|
+
* The rail's own code is the **outer key** and the interior is what that
|
|
6
|
+
* rail's adapter parses; see {@link RefundDestination}. One function so the
|
|
7
|
+
* envelope is written in one place and no caller can flatten it, and so the
|
|
8
|
+
* `msisdn` goes to the wire byte for byte as the merchant wrote it — this SDK
|
|
9
|
+
* does not canonicalise a payee's number (the server does, and it requires a
|
|
10
|
+
* leading `+`).
|
|
11
|
+
*/
|
|
12
|
+
function destinationForm(destination) {
|
|
13
|
+
return { [destination.payment_method_type]: { msisdn: destination.msisdn } };
|
|
14
|
+
}
|
|
15
|
+
/** `client.refunds` — see the module doc. */
|
|
16
|
+
export class RefundsResource {
|
|
17
|
+
#http;
|
|
18
|
+
constructor(http) {
|
|
19
|
+
this.#http = http;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `POST /v1/refunds`.
|
|
23
|
+
*
|
|
24
|
+
* Field order is the wire order `sdks/rust/tests/resources.rs` pins byte
|
|
25
|
+
* for byte; an unset field is omitted from the body entirely.
|
|
26
|
+
*
|
|
27
|
+
* A `400` naming `destination` for a payee this rail will not take — and
|
|
28
|
+
* for **no** payee at all, since both rails vpay carries need one; a `400`
|
|
29
|
+
* naming `amount` for a partial refund on a rail that has no partials; a
|
|
30
|
+
* `409` for an intent with nothing left to refund.
|
|
31
|
+
*
|
|
32
|
+
* See the module doc for what a `200` here does and does not mean.
|
|
33
|
+
*/
|
|
34
|
+
async create(params, options) {
|
|
35
|
+
if (params.amount !== undefined) {
|
|
36
|
+
assertIntegerAmount(params.amount);
|
|
37
|
+
}
|
|
38
|
+
const body = {
|
|
39
|
+
payment_intent: params.payment_intent,
|
|
40
|
+
};
|
|
41
|
+
if (params.amount !== undefined) {
|
|
42
|
+
body["amount"] = params.amount;
|
|
43
|
+
}
|
|
44
|
+
if (params.reason !== undefined) {
|
|
45
|
+
body["reason"] = params.reason;
|
|
46
|
+
}
|
|
47
|
+
if (params.destination !== undefined) {
|
|
48
|
+
body["destination"] = destinationForm(params.destination);
|
|
49
|
+
}
|
|
50
|
+
if (params.metadata !== undefined) {
|
|
51
|
+
body["metadata"] = params.metadata;
|
|
52
|
+
}
|
|
53
|
+
return this.#http.request("POST", "/refunds", body, options);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `GET /v1/refunds/{id}`.
|
|
57
|
+
*
|
|
58
|
+
* A refund is asynchronous on this rail — `RefundStatus` has a
|
|
59
|
+
* non-terminal `"pending"` — and webhook delivery is at-least-once and
|
|
60
|
+
* unordered, so this is the authoritative read a reconciliation job falls
|
|
61
|
+
* back to when a `charge.refund.updated` was missed or arrived out of
|
|
62
|
+
* order.
|
|
63
|
+
*/
|
|
64
|
+
async retrieve(id) {
|
|
65
|
+
return this.#http.request("GET", `/refunds/${encodeURIComponent(id)}`);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `POST /v1/refunds/{id}` — the **metadata** update, and nothing else.
|
|
69
|
+
*
|
|
70
|
+
* See {@link UpdateRefundParams}: the other four fields of a refund are
|
|
71
|
+
* fixed once it exists, the merge is key-wise, and an empty value deletes a
|
|
72
|
+
* key. A refund whose metadata actually changes emits
|
|
73
|
+
* `charge.refund.updated`; one sent no `metadata` is read back unchanged
|
|
74
|
+
* and emits nothing.
|
|
75
|
+
*
|
|
76
|
+
* A `404` for a refund this merchant has none of, and a `400` naming the
|
|
77
|
+
* parameter for anything but `metadata` — which this params type has no way
|
|
78
|
+
* to send.
|
|
79
|
+
*/
|
|
80
|
+
async update(id, params, options) {
|
|
81
|
+
const body = {};
|
|
82
|
+
if (params.metadata !== undefined) {
|
|
83
|
+
body["metadata"] = params.metadata;
|
|
84
|
+
}
|
|
85
|
+
return this.#http.request("POST", `/refunds/${encodeURIComponent(id)}`, body, options);
|
|
86
|
+
}
|
|
87
|
+
/** `GET /v1/refunds` — this merchant's refunds, newest first. */
|
|
88
|
+
async list(params) {
|
|
89
|
+
return this.#http.request("GET", "/refunds", params);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* `POST /v1/refunds/{id}/cancel` — **while it is still pending**. No
|
|
93
|
+
* request fields.
|
|
94
|
+
*
|
|
95
|
+
* The reservation this refund holds against its intent is released and
|
|
96
|
+
* `charge.refund.updated` is written. A `404` for a refund this merchant
|
|
97
|
+
* has none of, and a `409` naming the current status for one that is no
|
|
98
|
+
* longer `"pending"`.
|
|
99
|
+
*
|
|
100
|
+
* Nothing here reaches a rail: a cancel is a vpay-side release, because no
|
|
101
|
+
* rail was ever told to start. See the module doc.
|
|
102
|
+
*/
|
|
103
|
+
async cancel(id, options) {
|
|
104
|
+
return this.#http.request("POST", `/refunds/${encodeURIComponent(id)}/cancel`, {}, options);
|
|
105
|
+
}
|
|
106
|
+
}
|