@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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +732 -0
  3. package/dist/auth.d.ts +180 -0
  4. package/dist/auth.js +297 -0
  5. package/dist/client.d.ts +75 -0
  6. package/dist/client.js +79 -0
  7. package/dist/errors.d.ts +91 -0
  8. package/dist/errors.js +112 -0
  9. package/dist/form.d.ts +47 -0
  10. package/dist/form.js +127 -0
  11. package/dist/http.d.ts +33 -0
  12. package/dist/http.js +115 -0
  13. package/dist/index.d.ts +15 -0
  14. package/dist/index.js +14 -0
  15. package/dist/resources/account-holders.d.ts +36 -0
  16. package/dist/resources/account-holders.js +46 -0
  17. package/dist/resources/balance.d.ts +8 -0
  18. package/dist/resources/balance.js +10 -0
  19. package/dist/resources/checkout-sessions.d.ts +55 -0
  20. package/dist/resources/checkout-sessions.js +127 -0
  21. package/dist/resources/customers.d.ts +97 -0
  22. package/dist/resources/customers.js +157 -0
  23. package/dist/resources/events.d.ts +8 -0
  24. package/dist/resources/events.js +10 -0
  25. package/dist/resources/invoices.d.ts +193 -0
  26. package/dist/resources/invoices.js +275 -0
  27. package/dist/resources/payment-intents.d.ts +12 -0
  28. package/dist/resources/payment-intents.js +44 -0
  29. package/dist/resources/refunds.d.ts +92 -0
  30. package/dist/resources/refunds.js +106 -0
  31. package/dist/stripe-auth.d.ts +177 -0
  32. package/dist/stripe-auth.js +192 -0
  33. package/dist/types.d.ts +1268 -0
  34. package/dist/types.js +48 -0
  35. package/dist/validate.d.ts +16 -0
  36. package/dist/validate.js +20 -0
  37. package/dist/version.d.ts +12 -0
  38. package/dist/version.js +12 -0
  39. package/dist/webhooks.d.ts +22 -0
  40. package/dist/webhooks.js +94 -0
  41. package/package.json +63 -0
  42. package/scripts/mint-assertion.mjs +73 -0
@@ -0,0 +1,1268 @@
1
+ /**
2
+ * Wire types for vpay's `/v1` resource contract
3
+ * (docs/api/README.md, docs/flows/merchant-auth.md's "Objects" table).
4
+ */
5
+ /**
6
+ * The five-value state machine `vpay_core::state::IntentStatus` defines
7
+ * (docs/flows/payment-lifecycle.md). There is deliberately no `failed`
8
+ * status — a rail failure returns the intent to
9
+ * `requires_payment_method` with {@link LastPaymentError} populated.
10
+ */
11
+ export type PaymentIntentStatus = "requires_payment_method" | "requires_action" | "processing" | "succeeded" | "canceled";
12
+ /** Rail codes this SDK's `payment_method_types` values may hold. */
13
+ export type PaymentMethodType = "mtn_momo" | "orange_money";
14
+ /** Redirect-rail next step, Stripe's own `next_action.redirect_to_url` shape. */
15
+ export interface NextAction {
16
+ type: "redirect_to_url";
17
+ redirect_to_url: {
18
+ url: string;
19
+ return_url: string | null;
20
+ };
21
+ }
22
+ /**
23
+ * The closed failure-code vocabulary `vpay_core::failure` owns
24
+ * (docs/flows/failures.md). Adapters map rail-specific error strings into
25
+ * this list; merchants integrate against it once.
26
+ *
27
+ * **Not every rail can produce every code.** The list is the *core's*, and it
28
+ * does not shrink when a rail is added — but a merchant branching on all
29
+ * eleven should know that on the two MVP rails, MTN MoMo reaches all of them
30
+ * and Orange Money reaches three (`payer_timeout`,
31
+ * `provider_account_blocked`, `provider_error`). Orange documents five
32
+ * statuses and no sub-reason for `FAILED`, so its protocol has no way to say
33
+ * "not enough funds"; a payer who cancels on its hosted page arrives as
34
+ * `payer_timeout`, because the rail does not distinguish that from an
35
+ * abandoned page. The table, code by code with the rail reason that produces
36
+ * it and the conformance case that proves it, is
37
+ * `docs/flows/failures.md` § "Which rail can produce which code".
38
+ *
39
+ * `payer_declined` in particular was produced by **no** adapter until
40
+ * 2026-09-10 (vpay issue #59), while being typed here and given buyer copy by
41
+ * `examples/shop`. It is now MTN's `PAYMENT_NOT_APPROVED` /
42
+ * `APPROVAL_REJECTED`.
43
+ */
44
+ export type FailureCode = "insufficient_funds" | "payer_timeout" | "payer_declined" | "invalid_payer" | "payer_limit_reached" | "payer_account_blocked" | "invalid_payee" | "payee_account_blocked" | "provider_account_blocked" | "provider_unavailable" | "provider_error";
45
+ export interface LastPaymentError {
46
+ code: FailureCode;
47
+ message: string;
48
+ }
49
+ export interface PaymentIntent {
50
+ id: string;
51
+ object: "payment_intent";
52
+ /** Integer minor units. XAF is zero-decimal: 5000 means 5,000 FCFA. */
53
+ amount: number;
54
+ /** Lowercase ISO 4217 code, e.g. `xaf`. */
55
+ currency: string;
56
+ status: PaymentIntentStatus;
57
+ payment_method_types: string[];
58
+ next_action: NextAction | null;
59
+ last_payment_error: LastPaymentError | null;
60
+ metadata: Record<string, string>;
61
+ description: string | null;
62
+ /**
63
+ * The `cus_…` this intent is for, or `null` (S4a).
64
+ *
65
+ * The **id**, never the expanded object: vpay does not implement `expand`,
66
+ * and rendering the customer unasked would put a payer's name, email and
67
+ * phone number into every `payment_intent.*` webhook body. Read it with
68
+ * `client.customers.retrieve`.
69
+ *
70
+ * `?` because a vpay predating 2026-09-06 omits the key entirely — the
71
+ * same reason `Refund.fee` carries one, and with a *weaker* consequence:
72
+ * unlike `fee`, `undefined` and `null` mean the same thing here ("no
73
+ * customer"), so nothing is lost by conflating them.
74
+ */
75
+ customer?: string | null;
76
+ /** Unix seconds. */
77
+ created: number;
78
+ livemode: boolean;
79
+ /**
80
+ * `pi_…_secret_…` — the payer credential `/v1/browser` accepts to confirm
81
+ * this intent from a browser (hand it to `@vaam-apps/vpay-stripe-js`).
82
+ *
83
+ * Present **only** on `create()` and `retrieve()` responses (Step 5c's
84
+ * D2, `vpay_api::model::PaymentIntentWithSecret`); absent — this property
85
+ * is missing entirely, not `null` — on every `list()` item and on
86
+ * `event.data.object`, so it never reaches a merchant's listing view or a
87
+ * webhook body that's stored and forwarded at-least-once. Never log this
88
+ * value.
89
+ */
90
+ client_secret?: string;
91
+ }
92
+ export type RefundStatus = "pending" | "succeeded" | "failed" | "canceled";
93
+ export interface Refund {
94
+ id: string;
95
+ object: "refund";
96
+ /**
97
+ * Integer minor units of {@link Refund.currency}. **The payer's money** —
98
+ * never net of {@link Refund.fee}, which is a separate cost with a
99
+ * different owner.
100
+ */
101
+ amount: number;
102
+ currency: string;
103
+ payment_intent: string;
104
+ status: RefundStatus;
105
+ reason: string | null;
106
+ metadata: Record<string, string>;
107
+ created: number;
108
+ /**
109
+ * What the rail charged to execute this refund, in minor units of
110
+ * {@link Refund.currency} — never a second currency, never a float.
111
+ *
112
+ * **Three answers, and they are not interchangeable.** `0` means the rail
113
+ * reported the movement was free. `null` means it reported no fee at all.
114
+ * `undefined` — the key absent — means the vpay that answered predates the
115
+ * field (added by issue #46). The last two are both *unknown*; only `0` is
116
+ * a measured zero, and substituting it for either is how an invented number
117
+ * reaches a merchant's settlement statement.
118
+ *
119
+ * Optional **and** `| null` on purpose: it is the `?` that puts
120
+ * `undefined` in the read type, so `Refund["fee"]` is exactly
121
+ * `number | null | undefined` and all three answers survive. That is
122
+ * pinned by a type-level assertion — see `types fee so that absent, null
123
+ * and a measured zero stay three different answers` in `types.test.ts`,
124
+ * which fails if either the `?` or the `| null` is dropped.
125
+ *
126
+ * This package's `exactOptionalPropertyTypes` is **not** what creates the
127
+ * distinction, although an earlier version of this comment said it was.
128
+ * Measured 2026-09-05 with the repo's own `tsc`: the read type is
129
+ * `number | null | undefined` with the flag and without it, and an absent
130
+ * property never reads as `null` in either. What the flag actually adds is
131
+ * narrower — `{ fee: undefined }` stops being a legal way to spell
132
+ * "absent".
133
+ *
134
+ * Narrow with `typeof refund.fee === "number"`; `refund.fee ?? 0` and
135
+ * `refund.fee || 0` are both the bug.
136
+ *
137
+ * **It is `null` from every vpay deployment today.** Neither rail reports a
138
+ * refund fee. `orange_money::refund` is an unbuilt `NotImplemented` token —
139
+ * since 2026-09-15 an Orange refund is an outbound transfer this repository
140
+ * has no specification for (RFC-0003 section 5). `mtn_momo::refund` is
141
+ * written, the Disbursements `transfer` call, and reports no fee either:
142
+ * MTN answers `202 ACCEPTED` with an empty body and documents no fee field,
143
+ * and vpay has never called that product. So nothing populates it. (This
144
+ * read "Orange has no refund API" until 2026-09-15, and "both `refund`
145
+ * implementations are unbuilt tokens" until MTN's was written the same
146
+ * day.) See `docs/status.md`.
147
+ */
148
+ fee?: number | null;
149
+ }
150
+ /**
151
+ * The real Stripe event types docs/flows/webhooks.md commits to. A custom
152
+ * type would be silently dropped by a merchant's exhaustive `switch` over
153
+ * Stripe's own typed union — this SDK types `type` as `string` rather than
154
+ * this literal union so an event carrying a type it does not yet know about
155
+ * is still deliverable, and narrows with the exported guards below instead.
156
+ *
157
+ * `checkout.session.expired` is emitted when vpay's hourly sweep moves a
158
+ * session past its 24-hour horizon (Step 9's D10); its `data.object` is a
159
+ * {@link CheckoutSession}, which makes it the only member here whose payload
160
+ * is neither a {@link PaymentIntent} nor a {@link Refund}. Narrow it with
161
+ * {@link isCheckoutSessionEvent}.
162
+ */
163
+ export type KnownEventType = "payment_intent.created" | "payment_intent.processing" | "payment_intent.succeeded" | "payment_intent.payment_failed" | "payment_intent.canceled" | "charge.refunded" | "charge.refund.updated" | "checkout.session.expired"
164
+ /**
165
+ * A Customer was created by `POST /v1/customers`. `data.object` is the
166
+ * {@link Customer} as the insert stored it — the canonical `phone`, and
167
+ * `metadata` as sent. Written in the same transaction as the row (vpay
168
+ * issue #66, 2026-09-10).
169
+ */
170
+ | "customer.created"
171
+ /**
172
+ * A Customer was changed by `POST /v1/customers/{id}`. `data.object` is the
173
+ * {@link Customer} **after** the change, including the key-wise `metadata`
174
+ * merge — which vpay computes under the row's lock, so two concurrent
175
+ * updates produce two events describing the two committed states rather
176
+ * than one describing a merge that was lost.
177
+ *
178
+ * A request that changes nothing — a bodiless `POST`, which is what "touch
179
+ * this object" is on the wire — emits **no** event.
180
+ */
181
+ | "customer.updated"
182
+ /**
183
+ * A Customer was deleted — by `DELETE /v1/customers/{id}` or by vpay's
184
+ * twelve-month retention sweep (S4a). `data.object` is a
185
+ * {@link Customer} as it stood immediately before the delete, which is the
186
+ * **only** way to learn its `name`, `email` and `phone`: the row is gone,
187
+ * and a `GET` afterwards is byte-identical to one for an id that never
188
+ * existed.
189
+ */
190
+ | "customer.deleted"
191
+ /**
192
+ * A draft invoice was created by `POST /v1/invoices` (S4b). The insert and
193
+ * this event are one transaction, so an event you received describes a row
194
+ * that committed. `data.object` is an {@link Invoice}.
195
+ */
196
+ | "invoice.created"
197
+ /**
198
+ * A draft was issued: it has its {@link Invoice.number}, its lines are
199
+ * frozen and its amounts are final. `data.object` is an {@link Invoice} in
200
+ * `"open"`.
201
+ */
202
+ | "invoice.finalized"
203
+ /**
204
+ * A settlement paid an invoice in full. `data.object` is an
205
+ * {@link Invoice} in `"paid"`.
206
+ *
207
+ * Emitted inside the settlement transaction itself, **beside** the
208
+ * `payment_intent.succeeded` for the same money — a handler receives both
209
+ * and must not read them as two payments.
210
+ */
211
+ | "invoice.paid"
212
+ /**
213
+ * The merchant cancelled an issued invoice. `data.object` is an
214
+ * {@link Invoice} in `"void"`, **keeping** its number.
215
+ *
216
+ * There is deliberately no `invoice.marked_uncollectible` beside this one:
217
+ * vpay does not write it, so a union entry would be a claim about vpay that
218
+ * is false. A write-off is learned from
219
+ * `client.invoices.list({ status: "uncollectible" })`. The same goes for
220
+ * Stripe's `invoice.payment_failed`, which a merchant meets as
221
+ * `payment_intent.payment_failed`.
222
+ */
223
+ | "invoice.voided";
224
+ export interface Event {
225
+ id: string;
226
+ object: "event";
227
+ type: string;
228
+ /** Unix seconds. */
229
+ created: number;
230
+ livemode: boolean;
231
+ /**
232
+ * Kept as raw JSON with typed accessors below, rather than a typed union,
233
+ * so an event carrying an object shape this SDK does not model is still
234
+ * deliverable (docs/flows/merchant-auth.md, "Objects").
235
+ */
236
+ data: {
237
+ object: unknown;
238
+ };
239
+ }
240
+ /** Narrows a {@link PaymentIntent}-shaped value out of `event.data.object`. */
241
+ export declare function isPaymentIntentEvent(event: Event): event is Event & {
242
+ data: {
243
+ object: PaymentIntent;
244
+ };
245
+ };
246
+ /** Narrows a {@link Refund}-shaped value out of `event.data.object`. */
247
+ export declare function isRefundEvent(event: Event): event is Event & {
248
+ data: {
249
+ object: Refund;
250
+ };
251
+ };
252
+ /**
253
+ * Narrows a {@link CheckoutSession}-shaped value out of `event.data.object`.
254
+ *
255
+ * Matched on the `checkout.session.` prefix rather than on the one literal
256
+ * type vpay emits today, exactly as the two guards above are: a later
257
+ * `checkout.session.*` type carries the same object, and a guard that had to
258
+ * be edited for each one is a guard that will be out of date before the
259
+ * `switch` that calls it is.
260
+ *
261
+ * The session on an event **never** carries `client_secret`, and its `url` is
262
+ * always `null`: both would put a live payer credential in a body that is
263
+ * stored, delivered at-least-once and replayed
264
+ * (`vpay_api::model::CheckoutSessionObject::expired_snapshot`). So
265
+ * {@link CheckoutSession.url} being `null` here does not mean the session was
266
+ * embedded — read {@link CheckoutSession.ui_mode} for that.
267
+ */
268
+ export declare function isCheckoutSessionEvent(event: Event): event is Event & {
269
+ data: {
270
+ object: CheckoutSession;
271
+ };
272
+ };
273
+ /**
274
+ * Narrows an {@link Invoice}-shaped value out of `event.data.object`, for one
275
+ * of the four `invoice.*` events (S4b).
276
+ *
277
+ * Matched on the `invoice.` prefix rather than on the four literals, as every
278
+ * guard above is.
279
+ *
280
+ * **The invoice on an event carries no lines.** `data.object.lines.data` is
281
+ * empty on every `invoice.*` body — the event is rendered inside the
282
+ * transition's own transaction, where the lines are not read — so
283
+ * {@link Invoice.lines} here is an empty {@link List}, and that is not the
284
+ * same statement as "this invoice has no lines". A handler that needs them
285
+ * calls `client.invoices.retrieve`, whose response always carries them.
286
+ */
287
+ export declare function isInvoiceEvent(event: Event): event is Event & {
288
+ data: {
289
+ object: Invoice;
290
+ };
291
+ };
292
+ /**
293
+ * A Checkout Session's lifecycle state (Step 9's D10) — vpay's own three
294
+ * values, not Stripe's. There is no `failed`: a session whose intent
295
+ * reached a terminal non-success state is `expired` with
296
+ * {@link CheckoutSession.payment_status} `failed`.
297
+ */
298
+ export type CheckoutSessionStatus = "open" | "complete" | "expired";
299
+ /** Whether the session's intent has been paid. D10's second axis. */
300
+ export type CheckoutPaymentStatus = "unpaid" | "paid" | "failed";
301
+ /**
302
+ * Which surface the session is rendered on. `hosted` gets a
303
+ * {@link CheckoutSession.url} to send the payer to; `embedded` gets a
304
+ * `client_secret` to hand `@vaam-apps/vpay-stripe-js`'s `initEmbeddedCheckout`.
305
+ */
306
+ export type CheckoutSessionUiMode = "hosted" | "embedded";
307
+ /**
308
+ * A `/v1/checkout/sessions` object (Step 9's D1).
309
+ *
310
+ * The session **references** an existing PaymentIntent; it never creates
311
+ * one. Amount, currency and the allowed rails stay on the intent, where
312
+ * every existing invariant already guards them — which is why there is no
313
+ * `line_items`, no `mode` and no `amount_total` here, however Stripe-shaped
314
+ * the rest of the field names are.
315
+ */
316
+ export interface CheckoutSession {
317
+ /** `cs_…`. */
318
+ id: string;
319
+ object: "checkout.session";
320
+ livemode: boolean;
321
+ /**
322
+ * The `pi_…` this session drives — an **id**, on every `/v1` route.
323
+ *
324
+ * `@vaam-apps/vpay-stripe-js`'s `CheckoutSession` types the same field as the whole
325
+ * expanded {@link PaymentIntent}, because the *browser* session read
326
+ * expands it (the checkout page confirms and polls the intent through the
327
+ * browser routes and cannot fetch it separately). That is a deliberate
328
+ * per-route difference, ruled on 2026-09-04, not a skew between the
329
+ * SDKs: nothing on `/v1` ever expands it, so a merchant integration reads
330
+ * an id here and calls `paymentIntents.retrieve` if it wants the object.
331
+ */
332
+ payment_intent: string;
333
+ ui_mode: CheckoutSessionUiMode;
334
+ status: CheckoutSessionStatus;
335
+ payment_status: CheckoutPaymentStatus;
336
+ /** Hosted mode only; `null` when embedded. May carry `{CHECKOUT_SESSION_ID}` (D5). */
337
+ success_url: string | null;
338
+ /** Hosted mode only; `null` when embedded. */
339
+ cancel_url: string | null;
340
+ /** Embedded mode only; `null` when hosted. */
341
+ return_url: string | null;
342
+ /** The page to send the payer to, hosted mode only; `null` when embedded. */
343
+ url: string | null;
344
+ /**
345
+ * The `cus_…` this session is for, or `null` (S4a). Copied from the
346
+ * session's intent at create when the intent has one. The id, never the
347
+ * expanded object — {@link PaymentIntent.customer}'s reason, unchanged.
348
+ */
349
+ customer?: string | null;
350
+ /** Unix seconds. 24 h from create. */
351
+ expires_at: number;
352
+ /** Unix seconds. */
353
+ created: number;
354
+ /**
355
+ * `cs_…_secret_…` — the payer credential the browser presents to read
356
+ * this session, and what `initEmbeddedCheckout`'s `fetchClientSecret`
357
+ * must return.
358
+ *
359
+ * Present **only** on `create()` and `retrieve()` responses; absent —
360
+ * missing entirely, not `null` — on every `list()` item. It is a
361
+ * different credential from the intent's `client_secret`: it authorises
362
+ * reading this session, never confirming the intent.
363
+ *
364
+ * Never log this value. Unlike {@link PaymentIntent}, whose plain
365
+ * interface prints its secret through `console.log`, every
366
+ * `CheckoutSession` this SDK returns carries a custom
367
+ * `util.inspect` representation that redacts this field — see
368
+ * `src/resources/checkout-sessions.ts`. `JSON.stringify` is deliberately
369
+ * left faithful: an embedded integration has to serialise the secret to
370
+ * get it to the browser at all.
371
+ */
372
+ client_secret?: string;
373
+ }
374
+ export interface BalanceAmount {
375
+ amount: number;
376
+ currency: string;
377
+ }
378
+ export interface Balance {
379
+ object: "balance";
380
+ available: BalanceAmount[];
381
+ pending: BalanceAmount[];
382
+ }
383
+ /**
384
+ * Who a mobile-money number is registered to, or the fact that the rail has
385
+ * no record of it — `GET /v1/account_holders` (issue #47).
386
+ *
387
+ * `name: null` means **the rail answered and does not know this number**. It
388
+ * does not mean "we could not ask": that throws. A caller matching a
389
+ * nominated refund destination against a buyer's verified name must refuse
390
+ * on both — but only one of them is the buyer's problem.
391
+ *
392
+ * A name and nothing else, by construction: the rail's body carries more (a
393
+ * birthdate, a locale, a gender) and vpay projects it away before it can be
394
+ * logged or stored. There is no `id`, because nothing is stored to address
395
+ * later.
396
+ */
397
+ export interface AccountHolder {
398
+ object: "account_holder";
399
+ /**
400
+ * The rail the question was put to, echoed back.
401
+ *
402
+ * `string` and not {@link PaymentMethodType}, for the reason
403
+ * `PaymentIntent.payment_method_types` is `string[]`: the request side is
404
+ * closed and the response side must stay readable for a rail this SDK
405
+ * version predates.
406
+ */
407
+ payment_method_type: string;
408
+ /** The registered holder's name, or `null` when the rail has no record. */
409
+ name: string | null;
410
+ /**
411
+ * `true` exactly when `name` is present.
412
+ *
413
+ * Not a claim that anything was cryptographically verified — it says the
414
+ * rail named a holder.
415
+ */
416
+ verified: boolean;
417
+ }
418
+ /**
419
+ * `GET /v1/account_holders` request fields.
420
+ *
421
+ * A `type` alias for {@link ListParams}' index-signature reason. Both fields
422
+ * are required and neither is optional: a lookup with no number or no rail
423
+ * is not a narrower query, it is not a query.
424
+ *
425
+ * **`payment_method_type`, snake_case, like every other params type in this
426
+ * package** — the wire spelling, which is what the encoder walks and what
427
+ * `sdks/rust/tests/resources.rs` pins byte for byte. A camelCase
428
+ * `paymentMethodType` would be the only camelCase request field in the SDK
429
+ * and would need a translation step the others do not have.
430
+ */
431
+ export type RetrieveAccountHolderParams = {
432
+ /**
433
+ * The mobile-money number, in any of the three shapes vpay accepts:
434
+ * `+2376XXXXXXXX`, `2376XXXXXXXX` or the national `6XXXXXXXX`. Validated
435
+ * by the server, not here — see `AccountHoldersResource.retrieve`.
436
+ */
437
+ msisdn: string;
438
+ /** Which rail to ask. Closed on the request side, as a create's list is. */
439
+ payment_method_type: PaymentMethodType;
440
+ };
441
+ export interface List<T> {
442
+ object: "list";
443
+ data: T[];
444
+ has_more: boolean;
445
+ url: string;
446
+ }
447
+ /**
448
+ * A `type` alias, not an `interface`, on purpose: TypeScript gives an
449
+ * anonymous object type an *implicit index signature* and an interface none,
450
+ * so only this form is assignable to the encoder's
451
+ * `Record<string, FormValue>` without an `as unknown as` cast.
452
+ *
453
+ * Every optional property is written `?: T | undefined` rather than `?: T`,
454
+ * so that a consumer compiling with `exactOptionalPropertyTypes` (as this
455
+ * repo does) can pass a variable that is legitimately `T | undefined`.
456
+ */
457
+ export type ListParams = {
458
+ limit?: number | undefined;
459
+ starting_after?: string | undefined;
460
+ ending_before?: string | undefined;
461
+ };
462
+ /**
463
+ * A postal address on a {@link Customer} — Stripe's six formal components
464
+ * **and** the GPS point (issue #67).
465
+ *
466
+ * # vpay's address is both halves, and this is where Stripe's is not
467
+ *
468
+ * `latitude_microdeg` and `longitude_microdeg` have no counterpart on
469
+ * Stripe's `address`. They are here because vpay's address *means* both
470
+ * halves (the maintainer, 2026-09-11): formal addressing is unreliable
471
+ * across the markets vpay serves, and a coordinate is how a place is actually
472
+ * found. A merchant porting Stripe code to vpay gains two fields; one porting
473
+ * the other way loses them, and should know that before they discover it.
474
+ *
475
+ * Every component is nullable and the server renders all eight even when they
476
+ * are `null`: an address is one fact about a payer, and a key that appeared
477
+ * and disappeared would change the shape of a signed `customer.*` webhook
478
+ * body per payer. The object as a whole is `null` when there is no address.
479
+ *
480
+ * The same shape is sent and received. On {@link UpdateCustomerParams} an
481
+ * address **replaces** the stored one rather than merging with it — a request
482
+ * that names `line1` and not `city` clears the city — because an address
483
+ * assembled by the server out of two requests is an address that was never
484
+ * anybody's. Sending `null` removes it entirely.
485
+ */
486
+ export interface Address {
487
+ line1: string | null;
488
+ line2: string | null;
489
+ city: string | null;
490
+ state: string | null;
491
+ postal_code: string | null;
492
+ /**
493
+ * ISO 3166-1 alpha-2 — `CM`, `FR`, `NG`. Lower case is accepted and stored
494
+ * **upper case**, so what comes back may not be what was sent: the same
495
+ * wire contract {@link Customer.phone}'s canonicalisation is. A
496
+ * three-letter code or a country name is a `400` naming `address`.
497
+ */
498
+ country: string | null;
499
+ /**
500
+ * Latitude in **microdegrees** — millionths of a degree, so 4.061°N is
501
+ * `4061000`.
502
+ *
503
+ * A whole number, always. vpay has no floating-point coordinate anywhere —
504
+ * not on the wire, not in its database — which is what the unit in the
505
+ * field name is for: a field called `latitude` would be read as degrees,
506
+ * and `4.061` is a value that cannot be stored without rounding it. The
507
+ * server answers `400` naming `address` for a decimal rather than
508
+ * approximating it.
509
+ *
510
+ * `null` unless {@link Address.longitude_microdeg} is also set: half a
511
+ * coordinate names no place, and the server refuses one half.
512
+ *
513
+ * On an **erased** customer this is `null` while the six formal components
514
+ * are `[redacted]`. That asymmetry is deliberate: there is no integer that
515
+ * is not a possible place, so the marker is not a value this field can
516
+ * take. See {@link Customer.deleted}.
517
+ */
518
+ latitude_microdeg: number | null;
519
+ /** Longitude in **microdegrees**. See {@link Address.latitude_microdeg}. */
520
+ longitude_microdeg: number | null;
521
+ }
522
+ /**
523
+ * A `customer` — the merchant-owned record of a payer they expect to see
524
+ * again (S4a).
525
+ *
526
+ * Nine keys, plus `deleted` on an erased one. `last_used_at` — the clock
527
+ * vpay's twelve-month retention sweep reads — is deliberately **not** on the
528
+ * wire.
529
+ *
530
+ * At least one of `name`, `email` and `phone` is always present. A phone
531
+ * number **alone** is a complete customer, which is what the object is for
532
+ * on a mobile money rail.
533
+ */
534
+ export interface Customer {
535
+ id: string;
536
+ object: "customer";
537
+ name: string | null;
538
+ email: string | null;
539
+ /**
540
+ * Echoed back **canonicalised** (`2376XXXXXXXX`, no `+`) rather than as it
541
+ * was sent, which is a wire contract and not a quirk: it is the value a
542
+ * rail is given, so a merchant comparing this against a charge's payer
543
+ * reference is comparing the same string.
544
+ */
545
+ phone: string | null;
546
+ /**
547
+ * The payer's postal address **and** GPS point, or `null`. Two of its keys
548
+ * have no counterpart on Stripe's address — see {@link Address}.
549
+ */
550
+ address: Address | null;
551
+ metadata: Record<string, string>;
552
+ /** Unix seconds. */
553
+ created: number;
554
+ livemode: boolean;
555
+ /**
556
+ * `true` on a customer vpay has **erased**, and absent on a live one — the
557
+ * server omits the key rather than sending `false`.
558
+ *
559
+ * `DELETE /v1/customers/{id}` removes a customer with no payment history
560
+ * outright, and a later retrieve is a `404`. A customer an intent, a
561
+ * checkout session or an invoice references cannot be removed — vpay never
562
+ * detaches a payment from the payer it was taken from — so it is
563
+ * **anonymised** instead: the row and the payment record stay, and every
564
+ * identifier on it comes back as `[redacted]`. `metadata` is untouched,
565
+ * because that is the merchant's own data.
566
+ *
567
+ * So a `cus_…` in your own records goes on resolving after an erasure,
568
+ * which is why this key exists. Such a customer cannot be updated or
569
+ * attached to a new payment — both answer `409`. See
570
+ * `docs/flows/customers.md`.
571
+ */
572
+ deleted?: true | undefined;
573
+ }
574
+ /**
575
+ * What `client.customers.del(…)` answers with — Stripe's deleted-object
576
+ * shape.
577
+ *
578
+ * Deliberately **not** the customer that was removed: a merchant confirming
579
+ * an erasure is the caller most likely to log the whole response, and
580
+ * returning the payer's details in it would write them into a log *because*
581
+ * they were deleted.
582
+ */
583
+ export interface DeletedCustomer {
584
+ id: string;
585
+ object: "customer";
586
+ deleted: true;
587
+ }
588
+ export interface CreateCustomerParams {
589
+ /**
590
+ * At least one of `name`, `email` and `phone` must be present, and this
591
+ * SDK deliberately does not check that: which identifiers a customer needs
592
+ * is a *market* rule vpay owns and may widen — the same line
593
+ * `RetrieveAccountHolderParams.msisdn` takes on the MSISDN — so a copy here
594
+ * would refuse offline a customer a later server version accepts. The
595
+ * server answers `400` naming the parameter. `sdks/rust` takes the
596
+ * identical line.
597
+ */
598
+ name?: string | undefined;
599
+ email?: string | undefined;
600
+ /**
601
+ * In any spelling vpay's canonicaliser accepts (`+237 6 …`,
602
+ * `237600000200`, `600000200`). Stored and echoed back **canonical**, so
603
+ * what comes back may not be what was sent — see {@link Customer.phone}.
604
+ */
605
+ phone?: string | undefined;
606
+ /**
607
+ * The payer's postal address, sent as `address[line1]=…`. An address alone
608
+ * does not name anybody, so it does not satisfy the one-of rule above: a
609
+ * create carrying only an address is refused exactly as one carrying
610
+ * nothing is.
611
+ */
612
+ address?: AddressParams | undefined;
613
+ metadata?: Record<string, string> | undefined;
614
+ }
615
+ /**
616
+ * The address components, in the shape the wire spells them — Stripe's six
617
+ * **and** the GPS point.
618
+ *
619
+ * A `type` alias rather than an `interface` for {@link ListParams}' reason:
620
+ * only this form is assignable to the form encoder's
621
+ * `Record<string, FormValue>` without a cast.
622
+ *
623
+ * Separate from {@link Address} — which the *response* carries — because what
624
+ * a merchant may send and what the server returns are two contracts, and the
625
+ * second one grows keys the first must not accept. They happen to have the
626
+ * same eight fields today. This SDK deliberately does not validate `country`
627
+ * locally, exactly as it does not validate an MSISDN: which codes vpay
628
+ * accepts is a rule vpay owns and may widen. The same line is taken on the
629
+ * coordinate's range and its pair rule.
630
+ */
631
+ export type AddressParams = {
632
+ line1?: string | undefined;
633
+ line2?: string | undefined;
634
+ city?: string | undefined;
635
+ state?: string | undefined;
636
+ postal_code?: string | undefined;
637
+ country?: string | undefined;
638
+ /**
639
+ * Latitude in **microdegrees** — `4061000` for 4.061°N, never `4.061`. The
640
+ * range (±90,000,000) and the rule that it is sent with
641
+ * {@link AddressParams.longitude_microdeg} or not at all are the server's,
642
+ * and are not checked here for the reason `country` is not: they are
643
+ * vpay's to widen, and a copy in this SDK would refuse offline an address a
644
+ * later server accepts.
645
+ */
646
+ latitude_microdeg?: number | undefined;
647
+ /** Longitude in **microdegrees**. See {@link AddressParams.latitude_microdeg}. */
648
+ longitude_microdeg?: number | undefined;
649
+ };
650
+ /**
651
+ * `POST /v1/customers/{id}` request fields (S4a).
652
+ *
653
+ * # Why the three scalars are `string | null | undefined`
654
+ *
655
+ * An update has three answers per field and a merchant depends on all three:
656
+ * *leave it alone* (`undefined`, or the key absent), *set it* (a string), and
657
+ * **clear it** (`null`, which this SDK sends as `name=`). A `string |
658
+ * undefined` field collapses the first and the third, and a payer's email
659
+ * would then be unclearable through the API that documents how to clear it.
660
+ *
661
+ * It is the same three-state shape `UpdateCustomerParams` carries in
662
+ * `sdks/rust` (`Option<Option<String>>`) and `vpay_db::CustomerPatch` carries
663
+ * on the server, so all three layers express the distinction rather than two
664
+ * of them preserving it and one losing it.
665
+ *
666
+ * `metadata` has two states rather than three, and that is the wire contract:
667
+ * it is **merged** key-wise by the server, and a key whose value is the empty
668
+ * string is removed. So there is no separate "clear all metadata" — send each
669
+ * key empty.
670
+ */
671
+ export interface UpdateCustomerParams {
672
+ name?: string | null | undefined;
673
+ email?: string | null | undefined;
674
+ phone?: string | null | undefined;
675
+ /**
676
+ * The address, in the same three states — with one difference worth
677
+ * stating, because it is the one a merchant can get wrong silently.
678
+ *
679
+ * `undefined` leaves the stored address alone. `null` removes it (sent as
680
+ * `address=`). An object **replaces** it whole: every component the request
681
+ * does not name is cleared, so correcting a street means sending the city
682
+ * again. vpay does not merge components, deliberately — an address
683
+ * assembled out of two requests is an address that was never anybody's, and
684
+ * its failure mode is a plausible wrong address rather than a visible one.
685
+ */
686
+ address?: AddressParams | null | undefined;
687
+ metadata?: Record<string, string> | undefined;
688
+ }
689
+ /**
690
+ * `GET /v1/customers` query parameters.
691
+ *
692
+ * There is no `email` filter, and that is the server's shape rather than an
693
+ * omission here: a filter on a payer identifier turns the list into a lookup.
694
+ * See `docs/flows/customers.md`.
695
+ */
696
+ export type ListCustomersParams = {
697
+ limit?: number | undefined;
698
+ starting_after?: string | undefined;
699
+ ending_before?: string | undefined;
700
+ };
701
+ /**
702
+ * An invoice's lifecycle state (S4b).
703
+ *
704
+ * Exactly the five labels the server's `invoices_status_enum_check` allows,
705
+ * and the transitions between them are the server's: `draft → open → paid |
706
+ * void | uncollectible`. A closed union rather than `string`, and
707
+ * `sdks/rust` spells the same five as a closed `enum` — a sixth label is a
708
+ * schema migration and a breaking wire change, not something a merchant's
709
+ * deployment can meet by surprise. That is the opposite of the split
710
+ * {@link KnownEventType} makes, and deliberately so.
711
+ *
712
+ * `"uncollectible"` is the one a merchant filters on: a write-off emits **no
713
+ * event**, so `invoices.list({ status: "uncollectible" })` is how they are
714
+ * found.
715
+ */
716
+ export type InvoiceStatus = "draft" | "open" | "paid" | "void" | "uncollectible";
717
+ /**
718
+ * One line on an invoice — the `line_item` object.
719
+ *
720
+ * # Why the type is `InvoiceLine` and the resource is `client.invoiceItems`
721
+ *
722
+ * The wire has one object under two names and both are load-bearing: the
723
+ * `object` field is `"line_item"` and the route that creates, reads, changes
724
+ * and removes it is `/v1/invoice_items`. Stripe has two objects there
725
+ * (`invoiceitem` and `line_item`); vpay has one. This type is named for the
726
+ * object it decodes and the resource for the route it calls, so neither name
727
+ * is invented — `sdks/rust` spells them `InvoiceLine` and
728
+ * `client.invoice_items()` for the same reason.
729
+ *
730
+ * No price, no product, no proration and no period: a line is a description,
731
+ * a quantity and a unit amount.
732
+ */
733
+ export interface InvoiceLine {
734
+ /** `ii_…` — the id `/v1/invoice_items/{id}` addresses. */
735
+ id: string;
736
+ /** Always `"line_item"`, and **not** `"invoice_item"`. See above. */
737
+ object: "line_item";
738
+ description: string;
739
+ quantity: number;
740
+ /** The price of one, in integer minor units. */
741
+ unit_amount: number;
742
+ /**
743
+ * `quantity * unit_amount`, in integer minor units.
744
+ *
745
+ * Computed and checked by the database, never sent: `amount` is not a
746
+ * parameter on {@link CreateInvoiceItemParams} or
747
+ * {@link UpdateInvoiceItemParams}, ever.
748
+ */
749
+ amount: number;
750
+ /** Lower-case ISO-4217, always the parent invoice's. */
751
+ currency: string;
752
+ livemode: boolean;
753
+ }
754
+ /**
755
+ * When each of an invoice's transitions happened, in unix **seconds**.
756
+ *
757
+ * Every field is `null` until the transition happens, and at most two are
758
+ * ever non-null — `finalized_at` plus whichever terminal one applies.
759
+ */
760
+ export interface InvoiceStatusTransitions {
761
+ finalized_at: number | null;
762
+ paid_at: number | null;
763
+ voided_at: number | null;
764
+ marked_uncollectible_at: number | null;
765
+ }
766
+ /**
767
+ * An `invoice` (S4b): a merchant's bill to one customer. **Eighteen keys.**
768
+ *
769
+ * # `lines` is always expanded, and always unpaged
770
+ *
771
+ * vpay does not implement Stripe's `expand[]` at all, and an invoice without
772
+ * its lines is a total with no explanation — so `lines` arrives populated on
773
+ * every `/v1/invoices` response, with `has_more` always `false`. Its `url` is
774
+ * `/v1/invoice_items`, a route that exists, rather than Stripe's
775
+ * `/v1/invoices/{id}/lines`, which vpay does not serve.
776
+ *
777
+ * **Inside an `invoice.*` webhook body `lines.data` is empty**, and that is
778
+ * the one place the shape differs: the event is rendered inside the
779
+ * transition's own transaction. Re-read the invoice with
780
+ * `client.invoices.retrieve` if a handler needs the lines.
781
+ *
782
+ * # `hosted_invoice_url` is a checkout session, not an invoice page
783
+ *
784
+ * vpay has no invoice page. A payer following it meets the existing hosted
785
+ * checkout, and it is `null` until `client.invoices.pay` has run.
786
+ */
787
+ export interface Invoice {
788
+ /** `in_…`. The id an API call uses; {@link Invoice.number} is the one a human quotes. */
789
+ id: string;
790
+ object: "invoice";
791
+ /**
792
+ * The `cus_…` this invoice bills.
793
+ *
794
+ * `string` and not `string | null`, unlike {@link PaymentIntent.customer},
795
+ * and that is the wire contract rather than an assumption: the column is
796
+ * `NOT NULL`, because an invoice naming no payer is one nobody can be asked
797
+ * to pay.
798
+ */
799
+ customer: string;
800
+ currency: string;
801
+ status: InvoiceStatus;
802
+ /**
803
+ * `{prefix}-{000001}` from this merchant's own sequence, or `null`
804
+ * **exactly** while the invoice is a `"draft"`.
805
+ *
806
+ * Assigned at finalize, never reused, and kept by a voided invoice.
807
+ */
808
+ number: string | null;
809
+ /** The total, in integer minor units. Frozen at finalize. */
810
+ amount_due: number;
811
+ /** `0`, or {@link Invoice.amount_due}. Never between the two. */
812
+ amount_paid: number;
813
+ /** `amount_due - amount_paid`, and what `pay` mints an intent for. */
814
+ amount_remaining: number;
815
+ /**
816
+ * What has been given back out of {@link Invoice.amount_paid}, as a
817
+ * **gross** total.
818
+ *
819
+ * It is not subtracted from `amount_paid` and does not raise
820
+ * `amount_remaining`: a refunded invoice is still `"paid"` with nothing
821
+ * remaining, and vpay has no credit note object. `0` on every invoice today
822
+ * — no vpay rail can refund yet, so nothing can move it. Do not read a
823
+ * non-zero value as money being owed again.
824
+ *
825
+ * Optional in the type so a client of this version keeps compiling against
826
+ * a server that predates migration `0042`, where the key is absent.
827
+ */
828
+ amount_refunded?: number;
829
+ /**
830
+ * Unix **seconds**, or `null`. **Advisory**: nothing in vpay reads it —
831
+ * there is no dunning, no reminder and no automatic transition.
832
+ */
833
+ due_date: number | null;
834
+ description: string | null;
835
+ metadata: Record<string, string>;
836
+ /** The `pi_…` paying, or that paid, this invoice — `null` until `pay`. */
837
+ payment_intent: string | null;
838
+ /** The checkout session for {@link Invoice.payment_intent}. See above. */
839
+ hosted_invoice_url: string | null;
840
+ /** Every line, in the order they were added. See above for when this is empty. */
841
+ lines: List<InvoiceLine>;
842
+ status_transitions: InvoiceStatusTransitions;
843
+ /** Unix **seconds** — not milliseconds, and not RFC 3339. */
844
+ created: number;
845
+ livemode: boolean;
846
+ }
847
+ /**
848
+ * What `client.invoices.del(…)` answers with — Stripe's deleted-object shape.
849
+ *
850
+ * A **draft** is deleted and an issued invoice is voided: a voided draft
851
+ * would be a non-draft row with no number, a state the database refuses
852
+ * outright.
853
+ */
854
+ export interface DeletedInvoice {
855
+ id: string;
856
+ object: "invoice";
857
+ deleted: true;
858
+ }
859
+ /**
860
+ * What `client.invoiceItems.del(…)` answers with.
861
+ *
862
+ * Its `object` is `"line_item"` and not `"invoice_item"`, matching
863
+ * {@link InvoiceLine.object} — the route and the object are named
864
+ * differently and both spellings are the wire's.
865
+ */
866
+ export interface DeletedInvoiceItem {
867
+ id: string;
868
+ object: "line_item";
869
+ deleted: true;
870
+ }
871
+ /**
872
+ * `POST /v1/invoices` request fields (S4b).
873
+ *
874
+ * `customer` is the only required one, and it is required for a reason worth
875
+ * stating: an invoice is a bill to somebody, and one that names no payer is
876
+ * one nobody can be asked to pay. The server answers `400` naming `customer`
877
+ * for an absent, unknown *or other merchant's* `cus_…` — always the same
878
+ * sentence, so the parameter cannot be used to discover which customers exist
879
+ * under some other account.
880
+ *
881
+ * A created invoice is always a `"draft"` with no number, no lines and zero
882
+ * amounts. Add lines with `client.invoiceItems.create`, then issue it with
883
+ * `client.invoices.finalize`.
884
+ */
885
+ export interface CreateInvoiceParams {
886
+ /** The `cus_…` this invoice bills. */
887
+ customer: string;
888
+ /**
889
+ * Lower-cased at encode time regardless of how it was supplied, as an
890
+ * intent's is.
891
+ *
892
+ * **Required**, as it is on an intent. This field was optional until
893
+ * 2026-09-08 and documented as letting "the server apply this deployment's
894
+ * own default"; measured against a running vpay, a create with no
895
+ * `currency` is `400 A three-letter \`currency\` code is required.` —
896
+ * there is no such default. A type that can express only sendable requests
897
+ * is why {@link CreatePaymentIntentParams.currency} is required too.
898
+ */
899
+ currency: string;
900
+ /** At most 1000 characters, which the server checks. */
901
+ description?: string | undefined;
902
+ /**
903
+ * Unix **seconds**, as Stripe spells it — not milliseconds.
904
+ *
905
+ * **Advisory**: nothing in vpay reads it, so setting this changes nothing
906
+ * about what vpay does. It is a field the merchant's own systems may read
907
+ * back off {@link Invoice.due_date}.
908
+ */
909
+ due_date?: number | undefined;
910
+ metadata?: Record<string, string> | undefined;
911
+ }
912
+ /**
913
+ * `POST /v1/invoices/{id}` request fields (S4b) — **draft only**.
914
+ *
915
+ * # Three states per field, exactly as {@link UpdateCustomerParams} has
916
+ *
917
+ * `undefined` (or an absent key) leaves the field alone, a value sets it, and
918
+ * **`null` clears it** — which this SDK sends as `description=`. Collapsing
919
+ * `null` and `undefined` makes a due date set by mistake unremovable, and
920
+ * nothing else notices. `sdks/rust` spells the same three states
921
+ * `Option<Option<T>>`.
922
+ *
923
+ * `customer` and `currency` are **not** patchable and deliberately absent
924
+ * from this type: an invoice that changed who it bills, or what it is
925
+ * denominated in, is a different document.
926
+ *
927
+ * `metadata` has two states rather than three and that is the wire contract:
928
+ * it is merged key-wise, and a key whose value is the empty string is
929
+ * removed.
930
+ */
931
+ export interface UpdateInvoiceParams {
932
+ description?: string | null | undefined;
933
+ /** Unix **seconds**. `null` clears it. */
934
+ due_date?: number | null | undefined;
935
+ metadata?: Record<string, string> | undefined;
936
+ }
937
+ /**
938
+ * `GET /v1/invoices` query parameters (S4b).
939
+ *
940
+ * A `type` alias for {@link ListParams}' index-signature reason.
941
+ */
942
+ export type ListInvoicesParams = {
943
+ limit?: number | undefined;
944
+ starting_after?: string | undefined;
945
+ ending_before?: string | undefined;
946
+ customer?: string | undefined;
947
+ /**
948
+ * Only invoices in this state.
949
+ *
950
+ * Typed as the closed {@link InvoiceStatus} rather than `string`, and this
951
+ * is the one filter where that earns its place: the server answers `400`
952
+ * naming `status` for a label it does not have rather than silently
953
+ * returning the whole first page, so a typo would cost a round trip and
954
+ * read like an empty result.
955
+ */
956
+ status?: InvoiceStatus | undefined;
957
+ };
958
+ /**
959
+ * `POST /v1/invoices/{id}/pay` request fields (S4b).
960
+ *
961
+ * **Both URLs are required**, unlike on a checkout session where the pair is
962
+ * required only in hosted mode: `pay` mints a *hosted* session and there is
963
+ * no other mode to be in. They take the same rules a session's do — http(s),
964
+ * at most 2048 characters, `https` only under livemode, and `success_url` may
965
+ * carry the literal `{CHECKOUT_SESSION_ID}` — and this SDK deliberately does
966
+ * not duplicate them, for {@link CreateCheckoutSessionParams}' reason.
967
+ */
968
+ export type PayInvoiceParams = {
969
+ success_url: string;
970
+ cancel_url: string;
971
+ };
972
+ /**
973
+ * `POST /v1/invoice_items` request fields (S4b).
974
+ *
975
+ * There is no `currency` parameter and no `amount` one, and both absences are
976
+ * the wire contract rather than an omission here: a line is always in its
977
+ * invoice's currency (copied off the parent in the statement that writes the
978
+ * row), and `amount` is `quantity * unit_amount` computed by the database. A
979
+ * merchant who could send `amount` could send one that did not match its own
980
+ * factors.
981
+ */
982
+ export interface CreateInvoiceItemParams {
983
+ /**
984
+ * The `in_…` to add this line to, and it must be one of your **drafts** —
985
+ * a `400` naming `invoice` otherwise, including for a finalized invoice and
986
+ * for another merchant's.
987
+ */
988
+ invoice: string;
989
+ /** At most 1000 characters. */
990
+ description: string;
991
+ /**
992
+ * At least 1. Omitted from the body when absent, so the server applies its
993
+ * own default of 1 rather than this SDK sending one.
994
+ */
995
+ quantity?: number | undefined;
996
+ /**
997
+ * The price of one, in integer minor units. A non-integer, a negative, or
998
+ * anything past `Number.MAX_SAFE_INTEGER` throws `TypeError` before any
999
+ * request — `sdks/rust` refuses the identical set.
1000
+ */
1001
+ unit_amount: number;
1002
+ }
1003
+ /**
1004
+ * `POST /v1/invoice_items/{id}` request fields (S4b) — **draft parent only**.
1005
+ *
1006
+ * # Why these are two-state and {@link UpdateInvoiceParams}' are three-state
1007
+ *
1008
+ * All three columns are `NOT NULL`, so there is no "clear it" state to carry:
1009
+ * an absent key leaves the field alone and a value sets it. Sending
1010
+ * `description=` is a `400` naming the parameter rather than a clear, which
1011
+ * is why this type cannot express it — no `| null`.
1012
+ *
1013
+ * `amount` is not here, ever — see {@link CreateInvoiceItemParams}.
1014
+ */
1015
+ export interface UpdateInvoiceItemParams {
1016
+ description?: string | undefined;
1017
+ quantity?: number | undefined;
1018
+ unit_amount?: number | undefined;
1019
+ }
1020
+ export interface CreatePaymentIntentParams {
1021
+ /**
1022
+ * Integer minor units. A non-integer, a negative, or anything past
1023
+ * `Number.MAX_SAFE_INTEGER` throws `TypeError` before any request.
1024
+ */
1025
+ amount: number;
1026
+ currency: string;
1027
+ payment_method_types: PaymentMethodType[];
1028
+ metadata?: Record<string, string> | undefined;
1029
+ description?: string | undefined;
1030
+ /**
1031
+ * The `cus_…` this intent is for (S4a).
1032
+ *
1033
+ * Accepted and **dropped** by any vpay predating 2026-09-06, which is worth
1034
+ * knowing before relying on it: an older server answers `200` with
1035
+ * `customer: null` rather than refusing. Check the response.
1036
+ */
1037
+ customer?: string | undefined;
1038
+ }
1039
+ /** A `type` alias for the same index-signature reason as {@link ListParams}. */
1040
+ export type MtnMomoPaymentMethodData = {
1041
+ type: "mtn_momo";
1042
+ mtn_momo: {
1043
+ msisdn: string;
1044
+ };
1045
+ };
1046
+ /** A `type` alias for the same index-signature reason as {@link ListParams}. */
1047
+ export type OrangeMoneyPaymentMethodData = {
1048
+ type: "orange_money";
1049
+ };
1050
+ export type ConfirmPaymentIntentParams = {
1051
+ payment_method_data: MtnMomoPaymentMethodData;
1052
+ } | {
1053
+ payment_method_data: OrangeMoneyPaymentMethodData;
1054
+ return_url: string;
1055
+ };
1056
+ /**
1057
+ * `POST /v1/checkout/sessions` request fields.
1058
+ *
1059
+ * The URL rules are the server's and are not duplicated here: `success_url`
1060
+ * and `cancel_url` are required for `hosted` and refused for `embedded`,
1061
+ * `return_url` the other way round, all http(s) and at most 2048
1062
+ * characters, `https` only under livemode. This SDK sends what it is given
1063
+ * and lets the server say no — a second copy of those rules here would be a
1064
+ * second thing to keep in step with them, and would refuse a combination a
1065
+ * later server version allows.
1066
+ */
1067
+ export interface CreateCheckoutSessionParams {
1068
+ /** The `pi_…` to drive. Required; the session never creates one. */
1069
+ payment_intent: string;
1070
+ /** Defaults to `hosted` server-side when omitted. */
1071
+ ui_mode?: CheckoutSessionUiMode | undefined;
1072
+ /** Where to send a payer who paid. Hosted mode. May contain `{CHECKOUT_SESSION_ID}`. */
1073
+ success_url?: string | undefined;
1074
+ /** Where to send a payer who gave up. Hosted mode. */
1075
+ cancel_url?: string | undefined;
1076
+ /** Where vpay's framed page forwards the payer at the end. Embedded mode. */
1077
+ return_url?: string | undefined;
1078
+ /**
1079
+ * The `cus_…` this session is for (issue #70), or omit to inherit the
1080
+ * intent's customer — see {@link CheckoutSession.customer}.
1081
+ *
1082
+ * Refused `409` naming `customer` when the session's PaymentIntent already
1083
+ * names a *different* customer; the server never lets the two disagree.
1084
+ */
1085
+ customer?: string | undefined;
1086
+ }
1087
+ /**
1088
+ * `GET /v1/checkout/sessions` query parameters. A `type` alias for the same
1089
+ * index-signature reason as {@link ListParams}.
1090
+ */
1091
+ export type ListCheckoutSessionsParams = {
1092
+ limit?: number | undefined;
1093
+ starting_after?: string | undefined;
1094
+ ending_before?: string | undefined;
1095
+ /** Only sessions for this `pi_…`. */
1096
+ payment_intent?: string | undefined;
1097
+ };
1098
+ /**
1099
+ * Where a refund's money goes, on a rail that cannot send it back the way it
1100
+ * came.
1101
+ *
1102
+ * # The rail's code is the outer key, and that is the whole point
1103
+ *
1104
+ * On the wire this is `destination[<payment_method_type>][…]` — a
1105
+ * rail-agnostic container with a rail-specific interior, exactly as
1106
+ * `payment_method_data[<payment_method_type>][…]` is on the confirm path. The
1107
+ * server strips the outer key and hands the interior to that rail's own
1108
+ * adapter, which is the only thing that knows what a payee looks like there.
1109
+ * An SDK that flattened this, or that wrote one rail's code into the body
1110
+ * whatever rail the charge was on, would be deciding a rail's payee shape
1111
+ * from the client.
1112
+ *
1113
+ * The container is built for you: give the rail and the number, and
1114
+ * `refunds.create` writes `destination[<rail>][msisdn]`.
1115
+ *
1116
+ * # Which rail, and why you do not choose it
1117
+ *
1118
+ * A refund goes back on the rail the charge was made on. The server reads
1119
+ * that off the charge and refuses a `destination` naming any other rail with
1120
+ * a `400` naming `destination`, so `payment_method_type` here must be the one
1121
+ * the intent was confirmed on — not a preference.
1122
+ *
1123
+ * # The `+` is required, and this SDK does not add it for you
1124
+ *
1125
+ * The server canonicalises the number and **refuses one that does not start
1126
+ * with `+`**: `"+237600000200"` is a payee and the bare national
1127
+ * `"600000200"` is a `400` naming `destination`. That is not the rule
1128
+ * `GET /v1/account_holders` applies — that route knows it is in Cameroon and
1129
+ * the refund path deliberately does not, because read as an international
1130
+ * number `600000200` names a different country. It is asymmetric in the safe
1131
+ * direction: a lookup that guesses wrong returns the wrong name, a transfer
1132
+ * that guesses wrong sends the money.
1133
+ *
1134
+ * This SDK sends the string it is given, unchanged: it does not add a `+`,
1135
+ * strip one, or rewrite the number into any other form, for the reason
1136
+ * {@link RetrieveAccountHolderParams} gives about validating numbers locally
1137
+ * — the rule is the server's and it may widen it. Separators a caller types
1138
+ * (`"+237 600 000 200"`, `"+237-600-000-200"`) are accepted by the server and
1139
+ * are not this type's business either.
1140
+ *
1141
+ * # Do not log it
1142
+ *
1143
+ * The payee is a third party and the number is their personal data. vpay's
1144
+ * own handler logs it masked and never raw, and renders it in no response and
1145
+ * in no webhook body.
1146
+ *
1147
+ * A single-member union rather than an object type, so that a destination
1148
+ * which is not a phone number is a sibling member — discriminated on `kind`
1149
+ * rather than on the rail, because the rail is a *value* here and not a
1150
+ * shape.
1151
+ */
1152
+ export type RefundDestination = {
1153
+ /**
1154
+ * The shape of this payee. `"mobile_money"` is the only one today; it is
1155
+ * present so that a future non-phone destination narrows rather than
1156
+ * widening this one.
1157
+ */
1158
+ kind: "mobile_money";
1159
+ /** The rail the charge was made on — its code becomes the outer key. */
1160
+ payment_method_type: PaymentMethodType;
1161
+ /**
1162
+ * The payee's number, **in international form, starting with `+`**. Sent as
1163
+ * `destination[<payment_method_type>][msisdn]`.
1164
+ */
1165
+ msisdn: string;
1166
+ };
1167
+ /**
1168
+ * `POST /v1/refunds` request fields.
1169
+ *
1170
+ * # No refund this creates has ever moved money
1171
+ *
1172
+ * The route is real and so is every refusal it makes, but nothing on the
1173
+ * other side of it pays anybody: `orange_money`'s refund is a declared
1174
+ * `NotImplemented` token, `mtn_momo`'s is MTN's Disbursements `transfer`
1175
+ * which is WireMock-proven and for which **no real MTN credential exists in
1176
+ * this project** — the only `disbursement_subscription_key` anywhere is the
1177
+ * stub the e2e/demo stack points at a WireMock container, and the product has
1178
+ * never been called —
1179
+ * and **nothing settles a `pending` refund** — there is no refund poll
1180
+ * ladder, so a refund this creates stays `"pending"` until an operator moves
1181
+ * it. `docs/status.md` carries all three. Do not read a `200` here as money
1182
+ * on its way back.
1183
+ */
1184
+ export interface CreateRefundParams {
1185
+ payment_intent: string;
1186
+ /**
1187
+ * Integer minor units. Omit for a full refund — `amount=` and no `amount`
1188
+ * are different requests, and only the second means "all of it".
1189
+ *
1190
+ * A rail that refunds in full only refuses a partial amount with a `400`
1191
+ * naming `amount`, and it measures "in full" against the intent's own
1192
+ * amount rather than against what is left.
1193
+ */
1194
+ amount?: number | undefined;
1195
+ reason?: string | undefined;
1196
+ /**
1197
+ * The payee, on a rail that cannot return money to the instrument that
1198
+ * paid. See {@link RefundDestination}.
1199
+ *
1200
+ * Optional because the port allows a rail that refunds to the instrument
1201
+ * that paid, and such a rail refuses a `destination` with a `400`.
1202
+ * **Neither rail vpay carries today is one**: `mtn_momo` and `orange_money`
1203
+ * both declare that a refund needs a payee, so a create with no
1204
+ * `destination` is a `400` naming `destination` against every deployment
1205
+ * this repository can build.
1206
+ */
1207
+ destination?: RefundDestination | undefined;
1208
+ metadata?: Record<string, string> | undefined;
1209
+ }
1210
+ /**
1211
+ * `POST /v1/refunds/{id}` request fields — **metadata, and nothing else**.
1212
+ *
1213
+ * There is no `amount`, no `reason`, no `payment_intent` and no `destination`
1214
+ * here, and their absence is this type saying what the server says: all four
1215
+ * are refused with a `400` naming the parameter once a refund exists. A
1216
+ * refund whose amount or payee is wrong is cancelled while it is still
1217
+ * `"pending"` and created again.
1218
+ *
1219
+ * # The merge is key-wise, and an empty value deletes a key
1220
+ *
1221
+ * Stripe's rule, which vpay follows: the map sent here is merged into the
1222
+ * stored one key by key — keys not mentioned are left alone — and a key sent
1223
+ * with an **empty string** value is removed. Sending no `metadata` at all is
1224
+ * a no-op read: the refund comes back unchanged and **no event is written**.
1225
+ */
1226
+ export interface UpdateRefundParams {
1227
+ /** The keys to set, and the keys to delete (an empty value deletes). */
1228
+ metadata?: Record<string, string> | undefined;
1229
+ }
1230
+ /**
1231
+ * `GET /v1/refunds` query parameters.
1232
+ *
1233
+ * Written flat rather than as `ListParams & { … }` for the same
1234
+ * index-signature reason as {@link ListParams}.
1235
+ */
1236
+ export type ListRefundsParams = {
1237
+ limit?: number | undefined;
1238
+ /** Cursor: refunds after this `re_…`. */
1239
+ starting_after?: string | undefined;
1240
+ /** Cursor: refunds before this `re_…`. */
1241
+ ending_before?: string | undefined;
1242
+ /**
1243
+ * Only refunds against this `pi_…`.
1244
+ *
1245
+ * Held to the PaymentIntent id shape by the server, which answers `400`
1246
+ * naming `payment_intent` for anything else — a `re_…` sent here is the
1247
+ * easy mistake, since both ids are on the refund object. An id of the right
1248
+ * shape that names nothing, or another merchant's intent, is an **empty
1249
+ * page** and not a `404`.
1250
+ */
1251
+ payment_intent?: string | undefined;
1252
+ };
1253
+ /**
1254
+ * Written flat rather than as `ListParams & { … }` for the same
1255
+ * index-signature reason as {@link ListParams}: only an anonymous object type
1256
+ * gets an implicit index signature, and an intersection of aliases does not.
1257
+ */
1258
+ export type ListEventsParams = {
1259
+ limit?: number | undefined;
1260
+ starting_after?: string | undefined;
1261
+ ending_before?: string | undefined;
1262
+ type?: string | undefined;
1263
+ };
1264
+ /** Per-call options accepted by every resource method that issues a `POST`. */
1265
+ export interface RequestOptions {
1266
+ /** Caller-supplied idempotency key; a fresh UUIDv4 is generated when omitted. */
1267
+ idempotencyKey?: string | undefined;
1268
+ }