@vaam-apps/vpay-sdk 0.5.0 → 0.6.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/README.md CHANGED
@@ -727,8 +727,21 @@ manual check, run on demand — it needs a Rust toolchain, it is not part of
727
727
  it having been run.
728
728
 
729
729
  What this does **not** prove: that any of this works against a real vpay
730
- deployment. Nothing in this package has been run against `vpay-server`; the
730
+ deployment. ~~Nothing in this package has been run against `vpay-server`; the
731
731
  `issuer`/`tokenEndpoint` defaults agree with `vpay_api::op::issuer_for` by
732
732
  inspection, not by a test in this repository that exercises them together,
733
733
  and the resource routes every method here calls do not exist server-side at
734
- all — see [`docs/status.md`](../../docs/status.md).
734
+ all~~ — see [`docs/status.md`](../../docs/status.md).
735
+
736
+ _(Struck 2026-09-23. All three clauses had gone stale. **Run against
737
+ `vpay-server`:** since 2026-09-10, `src/invoices.live.test.ts`, joined by
738
+ `src/refunds.live.test.ts` on 2026-09-16, drives a real `vpay-server` over a
739
+ socket, and CI's `e2e` job runs them (`pnpm --filter @vaam-apps/vpay-sdk
740
+ test:live`). **The defaults:** those suites' preflight
741
+ (`src/testing/live-preflight.ts`) builds its client from `baseUrl` alone, so
742
+ every live run exercises the default `issuer` and `tokenEndpoint` against the
743
+ server's own OP. **The routes:** every method's route is mounted except
744
+ `balance.retrieve`, which answers `404` by design
745
+ ([`docs/sdks/parity.md`](../../docs/sdks/parity.md)). What still stands is the
746
+ sentence this paragraph opens with. That stack is a compose stack with
747
+ WireMock rails, not a deployment.)_
package/dist/index.d.ts CHANGED
@@ -11,5 +11,5 @@ export { verifyWebhook, type VerifyWebhookOptions } from "./webhooks.js";
11
11
  /** This package's own version, as sent in the `vpay-sdk-node/<version>` User-Agent. */
12
12
  export { SDK_VERSION } from "./version.js";
13
13
  export { VpayError, VpayApiError, VpayAuthError, VpayUnexpectedResponseError, VpayTransportError, VpayConfigError, WebhookSignatureError, } from "./errors.js";
14
- export type { AccountHolder, RetrieveAccountHolderParams, CheckoutSession, CheckoutSessionStatus, CheckoutSessionUiMode, CheckoutPaymentStatus, CreateCheckoutSessionParams, ListCheckoutSessionsParams, Customer, DeletedCustomer, Address, AddressParams, CreateCustomerParams, UpdateCustomerParams, ListCustomersParams, Invoice, InvoiceLine, InvoiceStatus, InvoiceStatusTransitions, DeletedInvoice, DeletedInvoiceItem, CreateInvoiceParams, UpdateInvoiceParams, ListInvoicesParams, PayInvoiceParams, CreateInvoiceItemParams, UpdateInvoiceItemParams, PaymentIntent, PaymentIntentStatus, PaymentMethodType, NextAction, LastPaymentError, FailureCode, Refund, RefundStatus, Event, KnownEventType, Balance, BalanceAmount, List, ListParams, ListEventsParams, CreatePaymentIntentParams, ConfirmPaymentIntentParams, MtnMomoPaymentMethodData, OrangeMoneyPaymentMethodData, CreateRefundParams, RefundDestination, UpdateRefundParams, ListRefundsParams, RequestOptions, } from "./types.js";
14
+ export type { AccountHolder, RetrieveAccountHolderParams, CheckoutSession, CheckoutSessionStatus, CheckoutSessionUiMode, CheckoutPaymentStatus, CreateCheckoutSessionParams, ListCheckoutSessionsParams, Customer, DeletedCustomer, Address, AddressParams, CreateCustomerParams, UpdateCustomerParams, ListCustomersParams, Invoice, InvoiceLine, InvoiceStatus, InvoiceStatusTransitions, DeletedInvoice, DeletedInvoiceItem, CreateInvoiceParams, UpdateInvoiceParams, ListInvoicesParams, PayInvoiceParams, HostedPayInvoiceParams, OutOfBandPayInvoiceParams, OutOfBandParams, OutOfBandMethod, OutOfBandPayment, CreateInvoiceItemParams, UpdateInvoiceItemParams, PaymentIntent, PaymentIntentStatus, PaymentMethodType, NextAction, LastPaymentError, FailureCode, Refund, RefundStatus, Event, KnownEventType, Balance, BalanceAmount, List, ListParams, ListPaymentIntentsParams, ListEventsParams, CreatePaymentIntentParams, ConfirmPaymentIntentParams, MtnMomoPaymentMethodData, OrangeMoneyPaymentMethodData, CreateRefundParams, RefundDestination, UpdateRefundParams, ListRefundsParams, RequestOptions, } from "./types.js";
15
15
  export { isCheckoutSessionEvent, isInvoiceEvent, isPaymentIntentEvent, isRefundEvent, } from "./types.js";
@@ -147,6 +147,13 @@ export declare class InvoicesResource {
147
147
  *
148
148
  * A `500 checkout_not_configured` when this deployment serves no checkout
149
149
  * page.
150
+ *
151
+ * **With `outOfBand`** it records instead that the invoice was settled
152
+ * outside vpay (RFC-0004 § 6): `open` → `paid` on the merchant's word,
153
+ * `invoice.paid` emitted, no intent, no checkout, no URL, and nothing
154
+ * verified or posted to any ledger. While an intent that is not canceled is
155
+ * attached it is the same `409` as `void`; cancel the intent first. A URL
156
+ * beside `outOfBand` is refused with a `TypeError` before any request.
150
157
  */
151
158
  pay(id: string, params: PayInvoiceParams, options?: RequestOptions): Promise<Invoice>;
152
159
  }
@@ -183,9 +183,40 @@ export class InvoicesResource {
183
183
  *
184
184
  * A `500 checkout_not_configured` when this deployment serves no checkout
185
185
  * page.
186
+ *
187
+ * **With `outOfBand`** it records instead that the invoice was settled
188
+ * outside vpay (RFC-0004 § 6): `open` → `paid` on the merchant's word,
189
+ * `invoice.paid` emitted, no intent, no checkout, no URL, and nothing
190
+ * verified or posted to any ledger. While an intent that is not canceled is
191
+ * attached it is the same `409` as `void`; cancel the intent first. A URL
192
+ * beside `outOfBand` is refused with a `TypeError` before any request.
186
193
  */
187
194
  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);
195
+ const outOfBand = params.outOfBand;
196
+ if (outOfBand !== undefined) {
197
+ // The type forbids this; a plain-JS caller can still send it, and the
198
+ // server would answer a `400`. Refused here for parity with
199
+ // `sdks/rust`, which refuses the same combination as `InvalidParams`.
200
+ for (const param of ["success_url", "cancel_url"]) {
201
+ const value = params[param];
202
+ if (value !== undefined && value !== "") {
203
+ throw new TypeError(`${param} has no meaning when recording a payment made outside vpay (outOfBand is set)`);
204
+ }
205
+ }
206
+ }
207
+ // Field order is the wire order `sdks/rust` pins too: the flag, then
208
+ // `out_of_band[method]`, `[reference]`, `[received_at]`.
209
+ const body = outOfBand !== undefined
210
+ ? {
211
+ paid_out_of_band: true,
212
+ out_of_band: {
213
+ method: outOfBand.method,
214
+ reference: outOfBand.reference,
215
+ received_at: outOfBand.receivedAt,
216
+ },
217
+ }
218
+ : { success_url: params.success_url, cancel_url: params.cancel_url };
219
+ return this.#http.request("POST", `/invoices/${encodeURIComponent(id)}/pay`, body, options);
189
220
  }
190
221
  /**
191
222
  * The three transitions that take no parameters, which are one shape:
@@ -1,5 +1,5 @@
1
1
  import type { HttpClient } from "../http.js";
2
- import type { ConfirmPaymentIntentParams, CreatePaymentIntentParams, List, ListParams, PaymentIntent, RequestOptions } from "../types.js";
2
+ import type { ConfirmPaymentIntentParams, CreatePaymentIntentParams, List, ListPaymentIntentsParams, PaymentIntent, RequestOptions } from "../types.js";
3
3
  /** `/v1/payment_intents` — docs/flows/merchant-auth.md, "Resources". */
4
4
  export declare class PaymentIntentsResource {
5
5
  #private;
@@ -8,5 +8,6 @@ export declare class PaymentIntentsResource {
8
8
  retrieve(id: string): Promise<PaymentIntent>;
9
9
  confirm(id: string, params: ConfirmPaymentIntentParams, options?: RequestOptions): Promise<PaymentIntent>;
10
10
  cancel(id: string, options?: RequestOptions): Promise<PaymentIntent>;
11
- list(params?: ListParams): Promise<List<PaymentIntent>>;
11
+ /** `GET /v1/payment_intents`, newest first; `customer` narrows it. */
12
+ list(params?: ListPaymentIntentsParams): Promise<List<PaymentIntent>>;
12
13
  }
@@ -38,6 +38,7 @@ export class PaymentIntentsResource {
38
38
  async cancel(id, options) {
39
39
  return this.#http.request("POST", `/payment_intents/${encodeURIComponent(id)}/cancel`, {}, options);
40
40
  }
41
+ /** `GET /v1/payment_intents`, newest first; `customer` narrows it. */
41
42
  async list(params) {
42
43
  return this.#http.request("GET", "/payment_intents", params);
43
44
  }
package/dist/types.d.ts CHANGED
@@ -345,6 +345,11 @@ export interface CheckoutSession {
345
345
  * The `cus_…` this session is for, or `null` (S4a). Copied from the
346
346
  * session's intent at create when the intent has one. The id, never the
347
347
  * expanded object — {@link PaymentIntent.customer}'s reason, unchanged.
348
+ *
349
+ * The other direction, from 2026-09-23 (ADR-0025): a session created with
350
+ * a customer for an intent that had none wrote it onto the intent too, so
351
+ * the two agree. A session created that way before then, or by a server
352
+ * without ADR-0025, may name a customer its intent does not.
348
353
  */
349
354
  customer?: string | null;
350
355
  /** Unix seconds. 24 h from create. */
@@ -459,6 +464,46 @@ export type ListParams = {
459
464
  starting_after?: string | undefined;
460
465
  ending_before?: string | undefined;
461
466
  };
467
+ /**
468
+ * `GET /v1/payment_intents` query parameters: {@link ListParams} plus the
469
+ * `customer` filter (RFC-0004 § 5).
470
+ *
471
+ * Written flat rather than as `ListParams & { … }` for {@link ListParams}'
472
+ * index-signature reason. `ListParams` itself stays exported and unchanged —
473
+ * it is public, and every `ListParams` value is still a valid one of these.
474
+ */
475
+ export type ListPaymentIntentsParams = {
476
+ limit?: number | undefined;
477
+ starting_after?: string | undefined;
478
+ ending_before?: string | undefined;
479
+ /**
480
+ * Only intents for this `cus_…` (the intent's own `customer`).
481
+ *
482
+ * Held to the Customer id shape by the server, which answers `400` naming
483
+ * `customer` for anything else. An id of the right shape that names
484
+ * nothing, or another merchant's customer, is an **empty page** and not a
485
+ * `404`, so the filter cannot tell you which customers exist under some
486
+ * other account.
487
+ *
488
+ * **A checkout session's customer is seen here, from 2026-09-23 on.** A
489
+ * session created with `customer` on an intent that has none now writes
490
+ * that customer onto the intent (ADR-0025,
491
+ * `docs/adr/0025-session-customer-onto-intent.md`), so the payment it
492
+ * collects is in this list. **Historical rows are not:** a session created
493
+ * that way before the change, or against a server without it, stored the
494
+ * customer on the session only, those intents were not backfilled, and
495
+ * only {@link ListCheckoutSessionsParams}' `customer` finds their payment.
496
+ * _(Until ADR-0025 this said the session's customer was never seen here,
497
+ * and called the question open question 3 of ADR-0024.)_
498
+ *
499
+ * **Needs a vpay server that has this filter**: no release up to and
500
+ * including 0.5.0 has it (as of 2026-09-23). A
501
+ * self-hosted server at 0.5.0 or earlier does not know this parameter: it
502
+ * ignores it and answers the **unfiltered** list — every customer's rows,
503
+ * with no error. Check the server's version before relying on it.
504
+ */
505
+ customer?: string | undefined;
506
+ };
462
507
  /**
463
508
  * A postal address on a {@link Customer} — Stripe's six formal components
464
509
  * **and** the GPS point (issue #67).
@@ -764,7 +809,8 @@ export interface InvoiceStatusTransitions {
764
809
  marked_uncollectible_at: number | null;
765
810
  }
766
811
  /**
767
- * An `invoice` (S4b): a merchant's bill to one customer. **Eighteen keys.**
812
+ * An `invoice` (S4b): a merchant's bill to one customer. **Twenty-one keys** (this
813
+ * said "Eighteen" through two additions; corrected 2026-09-23).
768
814
  *
769
815
  * # `lines` is always expanded, and always unpaged
770
816
  *
@@ -826,6 +872,16 @@ export interface Invoice {
826
872
  * a server that predates migration `0042`, where the key is absent.
827
873
  */
828
874
  amount_refunded?: number;
875
+ /**
876
+ * Stripe's key: `true` exactly when the merchant recorded that this invoice
877
+ * was settled outside vpay (`client.invoices.pay` with `outOfBand`).
878
+ * Always `false` on a bill a payer paid through
879
+ * a rail. Optional in the type so a client of this version keeps compiling
880
+ * against a server that predates migration `0049`.
881
+ */
882
+ paid_out_of_band?: boolean;
883
+ /** The record behind `paid_out_of_band`, or `null`. Present exactly when the flag is `true`. */
884
+ out_of_band_payment?: OutOfBandPayment | null;
829
885
  /**
830
886
  * Unix **seconds**, or `null`. **Advisory**: nothing in vpay reads it —
831
887
  * there is no dunning, no reminder and no automatic transition.
@@ -965,10 +1021,68 @@ export type ListInvoicesParams = {
965
1021
  * carry the literal `{CHECKOUT_SESSION_ID}` — and this SDK deliberately does
966
1022
  * not duplicate them, for {@link CreateCheckoutSessionParams}' reason.
967
1023
  */
968
- export type PayInvoiceParams = {
1024
+ export type PayInvoiceParams = HostedPayInvoiceParams | OutOfBandPayInvoiceParams;
1025
+ /** The hosted-checkout payment: both URLs, and no `outOfBand`. */
1026
+ export interface HostedPayInvoiceParams {
969
1027
  success_url: string;
970
1028
  cancel_url: string;
971
- };
1029
+ outOfBand?: undefined;
1030
+ }
1031
+ /**
1032
+ * Records the invoice as settled **outside vpay** (RFC-0004 § 6). The
1033
+ * presence of `outOfBand` is the flag: it puts Stripe's
1034
+ * `paid_out_of_band=true` plus vpay's `out_of_band[…]` on the wire. The
1035
+ * invoice becomes `"paid"` on the merchant's word, with no intent and no
1036
+ * checkout, and **nothing is verified or posted to any ledger**. No URL may
1037
+ * go with it — the type forbids one, and `client.invoices.pay` refuses one at
1038
+ * runtime for a caller the type did not reach — and none needs configuring.
1039
+ *
1040
+ * `outOfBand` and its keys are camelCase, unlike the snake_case URL fields
1041
+ * beside them, by the maintainer's decision of 2026-09-23 (ADR-0024).
1042
+ */
1043
+ export interface OutOfBandPayInvoiceParams {
1044
+ outOfBand: OutOfBandParams;
1045
+ success_url?: undefined;
1046
+ cancel_url?: undefined;
1047
+ }
1048
+ /** How a merchant says an out-of-band payment arrived. vpay's own words. */
1049
+ export type OutOfBandMethod = "cash" | "cheque" | "bank_transfer" | "other";
1050
+ /**
1051
+ * What the merchant says about a payment made outside vpay. Every key is
1052
+ * optional: `{}` is Stripe's bare `paid_out_of_band=true`, which the server
1053
+ * records with the method `"other"`.
1054
+ */
1055
+ export interface OutOfBandParams {
1056
+ /** Sent as `out_of_band[method]`; omitted, the server records `"other"`. */
1057
+ method?: OutOfBandMethod | undefined;
1058
+ /**
1059
+ * Sent as `out_of_band[reference]`. A cheque number, a transfer reference;
1060
+ * at most 500 characters. **Treat it as personal data**: vpay does — a
1061
+ * reference routinely names the payer — and replaces it with `[redacted]`
1062
+ * when the invoice's customer is erased, and refuses one on an erased
1063
+ * customer's invoice.
1064
+ */
1065
+ reference?: string | undefined;
1066
+ /**
1067
+ * Sent as `out_of_band[received_at]`, unix **seconds**. The server defaults
1068
+ * it to now, and refuses one more than 30 seconds in the future or earlier
1069
+ * than the second the invoice was finalized in.
1070
+ */
1071
+ receivedAt?: number | undefined;
1072
+ }
1073
+ /**
1074
+ * `invoice.out_of_band_payment` — the merchant's own statement, echoed back.
1075
+ * **Nothing in vpay verified it.** Four keys and no `object`.
1076
+ */
1077
+ export interface OutOfBandPayment {
1078
+ /** `mp_…`. */
1079
+ id: string;
1080
+ method: OutOfBandMethod;
1081
+ /** `null`, or `"[redacted]"` once the invoice's customer has been erased. */
1082
+ reference: string | null;
1083
+ /** Unix **seconds**: when the merchant says the money arrived. */
1084
+ received_at: number;
1085
+ }
972
1086
  /**
973
1087
  * `POST /v1/invoice_items` request fields (S4b).
974
1088
  *
@@ -1079,8 +1193,19 @@ export interface CreateCheckoutSessionParams {
1079
1193
  * The `cus_…` this session is for (issue #70), or omit to inherit the
1080
1194
  * intent's customer — see {@link CheckoutSession.customer}.
1081
1195
  *
1082
- * Refused `409` naming `customer` when the session's PaymentIntent already
1196
+ * Refused `400` naming `customer` when the session's PaymentIntent already
1083
1197
  * names a *different* customer; the server never lets the two disagree.
1198
+ * _(This said `409` until 2026-09-23. The server has always answered
1199
+ * `400` (`invalid_param`), which
1200
+ * `a_sessions_customer_is_inherited_supplied_or_a_refused_contradiction` in
1201
+ * `backends/tests/integration/tests/customers.rs` asserts.)_
1202
+ *
1203
+ * When the intent has **no** customer, a server with ADR-0025 (2026-09-23,
1204
+ * `docs/adr/0025-session-customer-onto-intent.md`) writes this one onto
1205
+ * the intent, in the same transaction as the session. So the intent's own
1206
+ * `customer` and the intent and refund list filters see it, and a later
1207
+ * session on that intent cannot name somebody else. A server without it
1208
+ * stores the customer on the session only.
1084
1209
  */
1085
1210
  customer?: string | undefined;
1086
1211
  }
@@ -1094,6 +1219,34 @@ export type ListCheckoutSessionsParams = {
1094
1219
  ending_before?: string | undefined;
1095
1220
  /** Only sessions for this `pi_…`. */
1096
1221
  payment_intent?: string | undefined;
1222
+ /**
1223
+ * Only sessions for this `cus_…` — the session's own `customer`, which is
1224
+ * the one it renders.
1225
+ *
1226
+ * Held to the Customer id shape by the server, which answers `400` naming
1227
+ * `customer` for anything else. An id of the right shape that names
1228
+ * nothing, or another merchant's customer, is an **empty page** and not a
1229
+ * `404`, so the filter cannot tell you which customers exist under some
1230
+ * other account.
1231
+ *
1232
+ * This is the **session's** customer, and it includes a session created
1233
+ * with `customer` on an intent that has none. From 2026-09-23 on such a
1234
+ * session also writes its customer onto the intent (ADR-0025,
1235
+ * `docs/adr/0025-session-customer-onto-intent.md`), so
1236
+ * {@link ListPaymentIntentsParams}' and {@link ListRefundsParams}'
1237
+ * `customer` find its payment too. A session created that way **before**
1238
+ * the change, or against a server without it, has its customer on the
1239
+ * session only; its intent was not backfilled, and this filter is the only
1240
+ * one that finds its payment. _(Until ADR-0025 this said that of every such
1241
+ * session, and called the question open question 3 of ADR-0024.)_
1242
+ *
1243
+ * **Needs a vpay server that has this filter**: no release up to and
1244
+ * including 0.5.0 has it (as of 2026-09-23). A
1245
+ * self-hosted server at 0.5.0 or earlier does not know this parameter: it
1246
+ * ignores it and answers the **unfiltered** list — every customer's rows,
1247
+ * with no error. Check the server's version before relying on it.
1248
+ */
1249
+ customer?: string | undefined;
1097
1250
  };
1098
1251
  /**
1099
1252
  * Where a refund's money goes, on a rail that cannot send it back the way it
@@ -1249,6 +1402,34 @@ export type ListRefundsParams = {
1249
1402
  * page** and not a `404`.
1250
1403
  */
1251
1404
  payment_intent?: string | undefined;
1405
+ /**
1406
+ * Only refunds of payments by this `cus_…` — matched through each refund's
1407
+ * PaymentIntent, since a refund carries no customer of its own.
1408
+ *
1409
+ * Held to the Customer id shape by the server, which answers `400` naming
1410
+ * `customer` for anything else. An id of the right shape that names
1411
+ * nothing, or another merchant's customer, is an **empty page** and not a
1412
+ * `404`, so the filter cannot tell you which customers exist under some
1413
+ * other account.
1414
+ *
1415
+ * **A checkout session's customer is seen here, from 2026-09-23 on.** A
1416
+ * session created with `customer` on an intent that has none now writes
1417
+ * that customer onto the intent (ADR-0025,
1418
+ * `docs/adr/0025-session-customer-onto-intent.md`), and this filter reads
1419
+ * the refund's intent's `customer`, so refunds of its payment are in this
1420
+ * list. **Historical rows are not:** a session created that way before the
1421
+ * change, or against a server without it, stored the customer on the
1422
+ * session only, and those intents were not backfilled. _(Until ADR-0025
1423
+ * this said the session's customer was never seen here, and called the
1424
+ * question open question 3 of ADR-0024.)_
1425
+ *
1426
+ * **Needs a vpay server that has this filter**: no release up to and
1427
+ * including 0.5.0 has it (as of 2026-09-23). A
1428
+ * self-hosted server at 0.5.0 or earlier does not know this parameter: it
1429
+ * ignores it and answers the **unfiltered** list — every customer's rows,
1430
+ * with no error. Check the server's version before relying on it.
1431
+ */
1432
+ customer?: string | undefined;
1252
1433
  };
1253
1434
  /**
1254
1435
  * Written flat rather than as `ListParams & { … }` for the same
package/dist/version.d.ts CHANGED
@@ -9,4 +9,4 @@
9
9
  * releases. A hand-written constant has no such landmine — it just needs to
10
10
  * be bumped alongside `package.json`'s own `version` field.
11
11
  */
12
- export declare const SDK_VERSION = "0.5.0";
12
+ export declare const SDK_VERSION = "0.6.0";
package/dist/version.js CHANGED
@@ -9,4 +9,4 @@
9
9
  * releases. A hand-written constant has no such landmine — it just needs to
10
10
  * be bumped alongside `package.json`'s own `version` field.
11
11
  */
12
- export const SDK_VERSION = "0.5.0"; // x-release-please-version
12
+ export const SDK_VERSION = "0.6.0"; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaam-apps/vpay-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "The Node.js merchant SDK for vpay's /v1 API: OAuth2 private_key_jwt, payment intents, checkout sessions, refunds, events and webhook verification.",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {