@vonpay/checkout-node 1.1.1 → 2.0.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/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { VonPayCheckoutConfig, CreateSessionParams, CheckoutSession, SessionStatus, DryRunResult, WebhookEvent, HealthStatus, RequestOptions, ReturnOutcome, ReturnParams, PaymentIntent, CreatePaymentIntentParams, CapturePaymentIntentParams, Capabilities, Refund, CreateRefundParams, Token, CreateTokenParams, WebhookSubscription, ListWebhookSubscriptionsParams, WebhookSubscriptionsList, WebhookEventRecord } from "./types.js";
1
+ import type { VonPayCheckoutConfig, CreateSessionParams, CheckoutSession, SessionStatus, DryRunResult, WebhookEvent, HealthStatus, RequestOptions, ReturnOutcome, ReturnParams, PaymentIntent, CreatePaymentIntentParams, CapturePaymentIntentParams, Capabilities, Refund, CreateRefundParams, Token, CreateTokenParams, WebhookSubscription, ListWebhookSubscriptionsParams, WebhookSubscriptionsList, WebhookEventRecord, Buyer, UpsertBuyerParams, UpdateBuyerParams, FindBuyerParams } from "./types.js";
2
2
  export declare const KNOWN_RETURN_SCHEMES: readonly ["v1", "v2"];
3
3
  export type ReturnScheme = (typeof KNOWN_RETURN_SCHEMES)[number];
4
4
  export declare const DEFAULT_RETURN_SCHEMES: readonly ReturnScheme[];
@@ -165,6 +165,129 @@ export declare class VonPayCheckout {
165
165
  * the rest of `/v1/*` on this engine); the SDK exposes responses
166
166
  * pass-through with no key conversion.
167
167
  */
168
+ /**
169
+ * Stored shopper profiles. `vp_sk_*` Bearer required.
170
+ *
171
+ * The same profile the checkout flows write via `buyerId` / `buyerEmail`, so
172
+ * a buyer created here is the one a later payment attaches to.
173
+ *
174
+ * ⛔ **The erase is a separate method on purpose.** `PATCH /v1/buyers/:id`
175
+ * accepts `email: null` to permanently erase the address — irreversibly, and
176
+ * it removes the copy held at the payment provider too. The API's own docs
177
+ * warn that several common JSON libraries serialise absent optional fields as
178
+ * `null`, so an ordinary update can wipe a shopper's email with nobody
179
+ * intending it.
180
+ *
181
+ * This SDK refuses to expose that shape. `update()` cannot express `null` —
182
+ * it is a type error — and {@link eraseEmail} is the only path that sends it.
183
+ * An accidental erase is therefore not writable in TypeScript.
184
+ */
185
+ /**
186
+ * ⚠️ **No idempotency key on this resource, deliberately.**
187
+ *
188
+ * The canonical spec declares `Idempotency-Key` on `POST /v1/sessions` and
189
+ * `POST /v1/refunds`, and documents the replay-mismatch contract for them. It
190
+ * declares NOTHING for either buyers route. An earlier draft of this resource
191
+ * offered the option anyway, and that was wrong: shipping an
192
+ * unconfirmed safety knob on an IRREVERSIBLE, provider-propagated deletion is
193
+ * the wrong default — a caller would reasonably read it as the same
194
+ * replay-safety guarantee the other endpoints give, and we cannot back that.
195
+ *
196
+ * Removed until server support is confirmed and the API contract declares
197
+ * it. If you need replay safety on these calls today, guard at your own call
198
+ * site.
199
+ */
200
+ buyers: {
201
+ /**
202
+ * `POST /v1/buyers` — create or update a buyer.
203
+ *
204
+ * ⚠️ **This is an UPSERT.** The write is keyed on `externalId` when
205
+ * present, else on `email`. Calling it twice with the same key UPDATES;
206
+ * it does not fail with a duplicate error. The server answers `201` when it
207
+ * created and `200` when it updated, and the body is the full stored
208
+ * profile either way — so what you read back is what was persisted
209
+ * (including `metadata` after its append-merge).
210
+ *
211
+ * At least one of `externalId` / `email` is required: a profile with no
212
+ * lookup key could never be retrieved or charged against. That is checked
213
+ * here rather than spent on a round-trip to a `400`.
214
+ */
215
+ upsert: (params: UpsertBuyerParams) => Promise<Buyer>;
216
+ /**
217
+ * `GET /v1/buyers/:id` — fetch a stored profile by id.
218
+ *
219
+ * A cross-merchant id returns an opaque 404, never a 403.
220
+ */
221
+ retrieve: (buyerId: string) => Promise<Buyer>;
222
+ /**
223
+ * `GET /v1/buyers` — exact lookup by your reference or by email.
224
+ *
225
+ * Returns the profile or `null`. There is deliberately no unfiltered
226
+ * listing in v1, so at least one key is required; when both are supplied
227
+ * `externalId` wins, matching the upsert's identity-resolution order.
228
+ *
229
+ * Unwraps the server's `{ data: [...] }` envelope, which holds zero or one
230
+ * profile — handing back a one-element array to destructure would be a
231
+ * worse API for a lookup that can never return two.
232
+ */
233
+ find: (params: FindBuyerParams) => Promise<Buyer | null>;
234
+ /**
235
+ * `PATCH /v1/buyers/:id` — update a stored profile.
236
+ *
237
+ * Omitted fields are left unchanged. `metadata` merges with append
238
+ * semantics. `externalId` is not patchable — the identity key is fixed at
239
+ * creation; upsert a new profile instead.
240
+ *
241
+ * ⛔ **This method cannot erase.** `email` is typed `string`, so passing
242
+ * `null` is a compile error, and the body is built with an explicit
243
+ * `!== undefined` test rather than a truthiness check — a truthy test would
244
+ * also drop a legitimate empty-string value and, worse, would let a `null`
245
+ * through unnoticed if the type were ever widened. Use
246
+ * {@link eraseEmail} when you actually mean to erase.
247
+ */
248
+ update: (buyerId: string, params: UpdateBuyerParams) => Promise<Buyer>;
249
+ /**
250
+ * **Permanently erase this buyer's email address.** `PATCH /v1/buyers/:id`
251
+ * with `email: null`.
252
+ *
253
+ * It removes the address from the buyer profile. Afterwards the buyer
254
+ * cannot be found by email.
255
+ *
256
+ * ⚠️ **We also ASK your payment provider to drop their copy — but a
257
+ * successful response here does not confirm that happened.** That request
258
+ * is dispatched after we answer you, is never awaited, and reports no
259
+ * failure back: a provider error, a timeout, or an open circuit breaker
260
+ * all pass silently. And for a buyer we only ever knew by email, whose
261
+ * provider record we had not separately recorded, the retraction cannot be
262
+ * performed at all — the key we would need was derived from the address
263
+ * that was just erased. Do not tell a shopper the provider copy is gone on
264
+ * the strength of a `200`.
265
+ *
266
+ * ⛔ **This is NOT a complete deletion on its own, and must not be
267
+ * described to a shopper as one.** It clears the profile's address. Other
268
+ * copies live outside this record, under their own
269
+ * retention rules, and this call does not touch them — including the
270
+ * settlement record kept for financial audit, checkout sessions from the
271
+ * recent past, and any address already forwarded to a connected store,
272
+ * which is an external system with no retraction path.
273
+ *
274
+ * ⚠️ Read that as examples, **not as an inventory.** It is the set we have
275
+ * confirmed, not a completed audit of every place a copy can exist — your
276
+ * own systems may hold one we sent you earlier. Treat this call as the
277
+ * profile half of a deletion request, not the whole answer.
278
+ *
279
+ * ⚠️ **It cannot be undone**, and a buyer identified only by email becomes
280
+ * unreachable by lookup afterwards — the record and every payment attached
281
+ * to it survive, but `find({ email })` will no longer locate it. Keep your
282
+ * own reference in `externalId` if you need to find them again.
283
+ *
284
+ * No other profile field is erasable this way; `email` is the only one the
285
+ * API declares nullable.
286
+ *
287
+ * @returns the stored profile as it is after the erase, with `email: null`.
288
+ */
289
+ eraseEmail: (buyerId: string) => Promise<Buyer>;
290
+ };
168
291
  webhookSubscriptions: {
169
292
  /**
170
293
  * `GET /v1/webhook_subscriptions` — Stripe-shaped list envelope.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -9,11 +9,34 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
10
10
  const SDK_VERSION = pkg.version;
11
11
  const DEFAULT_BASE_URL = "https://checkout.vonpay.com";
12
- // Sent as `Von-Pay-Version` on every request. Bump alongside spec changes
13
- // that introduce new endpoints (e.g. SDK 0.6.x added capture/void/refunds/
14
- // tokens at 2026-05-05). The server treats this header as advisory routing
15
- // information; no SDK-side gating depends on it.
16
- const DEFAULT_API_VERSION = "2026-05-05";
12
+ // Sent as `Von-Pay-Version` on every request.
13
+ //
14
+ // THIS WAS "2026-05-05" AND THE API HAS NEVER ACCEPTED THAT VALUE.
15
+ // The server keeps a one-member allowlist (`SUPPORTED_VERSIONS` in checkout's
16
+ // proxy) containing only "2026-04-14", and silently substitutes it for anything
17
+ // else — no error, no warning. So every request this SDK has ever sent was
18
+ // served as 2026-04-14 while announcing something different, and the response
19
+ // header said so.
20
+ //
21
+ // The old value was not a premature version. It was the DATE THE 0.6.x SDK
22
+ // SCAFFOLD WAS WRITTEN (commit 1eb30ee, 2026-05-05), recorded as though it were
23
+ // a contract. Nothing on the server ever implemented it; the only occurrences
24
+ // of that string in checkout are comment dates.
25
+ //
26
+ // ⚠️ WHY IT MATTERED EVEN THOUGH NOTHING BROKE: a merchant copying this default
27
+ // and pinning `Von-Pay-Version: 2026-05-05` explicitly believed they had pinned
28
+ // a NEWER contract. They had pinned nothing and silently got the older one.
29
+ // Version pinning that silently no-ops is the exact failure pinning exists to
30
+ // prevent. Reported by vonpay-docs, 2026-09-08.
31
+ //
32
+ // This correction is RUNTIME-NEUTRAL: the server was already serving
33
+ // 2026-04-14: only the announced value changes, to the one actually honoured.
34
+ //
35
+ // ⛔ Do NOT bump this to a new date because a new SDK surface shipped. It is not
36
+ // a changelog. Bump it only when checkout ADDS the value to SUPPORTED_VERSIONS,
37
+ // and keep it in lock-step with the Python constant
38
+ // (`sdk/api-version-bump-both-sdks`).
39
+ const DEFAULT_API_VERSION = "2026-04-14";
17
40
  const DEFAULT_MAX_RETRIES = 2;
18
41
  const DEFAULT_TIMEOUT = 30_000;
19
42
  const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
@@ -571,6 +594,35 @@ function paymentIntentFromWire(raw) {
571
594
  metadata: metadata ?? {},
572
595
  };
573
596
  }
597
+ /**
598
+ * Parse a server buyer response into the camelCase {@link Buyer} type.
599
+ *
600
+ * ⛔ WITHOUT THIS, THE RESOURCE'S OWN PRIMARY FIELD IS UNDEFINED. `request()`
601
+ * returns `response.json()` raw — it does NOT convert case — and every other
602
+ * resource here calls a `*FromWire` helper on the way out. The first version of
603
+ * `buyers` called none, so `externalId`, `createdAt`, `lastSeenAt` and every
604
+ * nested `address.*` field came back `undefined` from a correct server
605
+ * response. `externalId` is the field this SDK's own erase docs tell an
606
+ * integrator to keep hold of.
607
+ *
608
+ * Worth recording WHY this was easy to miss:
609
+ * the one committed test that read a returned field read `id`, which is spelled
610
+ * identically in both conventions and therefore passed by coincidence. A test
611
+ * that only ever asserts a convention-invariant field cannot detect a
612
+ * conversion bug.
613
+ *
614
+ * `metadata` is passed through verbatim — integrator-defined keys must NEVER be
615
+ * case-converted — they are the merchant's own keys and must round-trip
616
+ * byte-for-byte.
617
+ */
618
+ function buyerFromWire(raw) {
619
+ const { metadata, ...rest } = raw;
620
+ const camel = toCamelKeysDeep(rest);
621
+ return {
622
+ ...camel,
623
+ metadata: metadata ?? {},
624
+ };
625
+ }
574
626
  // Server-issued resource IDs match this shape (vps_*, vpi_*, vpr_*, vp_pmt_*,
575
627
  // etc). encodeURIComponent alone is sufficient for path safety on Node fetch,
576
628
  // but a strict allowlist closes the contract — runtime-dependent encoding of
@@ -1084,7 +1136,32 @@ export class VonPayCheckout {
1084
1136
  capabilities = {
1085
1137
  get: async () => {
1086
1138
  const { data } = await this.request("GET", "/v1/capabilities", { reporterMethod: "capabilities.get" });
1087
- return toCamelKeysDeep(data);
1139
+ const caps = toCamelKeysDeep(data);
1140
+ // ⛔ NORMALISE `disputeReporting` — do NOT trust the pass-through here.
1141
+ //
1142
+ // Everything else on this response is converted generically, which is why
1143
+ // the value reached callers even before it was declared. But the type
1144
+ // promises a non-optional `"tracked" | "not_tracked"`, and a generic
1145
+ // conversion cannot deliver that: against a server older than the field
1146
+ // (or any future regression) the property is simply absent, and a caller
1147
+ // doing an exhaustive check on a value the type says is always present
1148
+ // gets `undefined`. Python already defaults this; Node promised the same
1149
+ // guarantee and did not enforce it (found 2026-08-29).
1150
+ //
1151
+ // Unknown or missing both resolve to `"not_tracked"`, which is the
1152
+ // fail-safe direction and the whole point of the field: it can only mean
1153
+ // "we cannot promise we would see a dispute", and a server that does not
1154
+ // send it genuinely cannot. Resolving toward `"tracked"` would invent a
1155
+ // reassurance nobody gave and let a merchant read a `succeeded` payment
1156
+ // as dispute-free while nothing is watching.
1157
+ const reported = caps.supportedOperations?.disputeReporting;
1158
+ return {
1159
+ ...caps,
1160
+ supportedOperations: {
1161
+ ...caps.supportedOperations,
1162
+ disputeReporting: reported === "tracked" ? "tracked" : "not_tracked",
1163
+ },
1164
+ };
1088
1165
  },
1089
1166
  };
1090
1167
  sessions = {
@@ -1274,6 +1351,258 @@ export class VonPayCheckout {
1274
1351
  * the rest of `/v1/*` on this engine); the SDK exposes responses
1275
1352
  * pass-through with no key conversion.
1276
1353
  */
1354
+ /**
1355
+ * Stored shopper profiles. `vp_sk_*` Bearer required.
1356
+ *
1357
+ * The same profile the checkout flows write via `buyerId` / `buyerEmail`, so
1358
+ * a buyer created here is the one a later payment attaches to.
1359
+ *
1360
+ * ⛔ **The erase is a separate method on purpose.** `PATCH /v1/buyers/:id`
1361
+ * accepts `email: null` to permanently erase the address — irreversibly, and
1362
+ * it removes the copy held at the payment provider too. The API's own docs
1363
+ * warn that several common JSON libraries serialise absent optional fields as
1364
+ * `null`, so an ordinary update can wipe a shopper's email with nobody
1365
+ * intending it.
1366
+ *
1367
+ * This SDK refuses to expose that shape. `update()` cannot express `null` —
1368
+ * it is a type error — and {@link eraseEmail} is the only path that sends it.
1369
+ * An accidental erase is therefore not writable in TypeScript.
1370
+ */
1371
+ /**
1372
+ * ⚠️ **No idempotency key on this resource, deliberately.**
1373
+ *
1374
+ * The canonical spec declares `Idempotency-Key` on `POST /v1/sessions` and
1375
+ * `POST /v1/refunds`, and documents the replay-mismatch contract for them. It
1376
+ * declares NOTHING for either buyers route. An earlier draft of this resource
1377
+ * offered the option anyway, and that was wrong: shipping an
1378
+ * unconfirmed safety knob on an IRREVERSIBLE, provider-propagated deletion is
1379
+ * the wrong default — a caller would reasonably read it as the same
1380
+ * replay-safety guarantee the other endpoints give, and we cannot back that.
1381
+ *
1382
+ * Removed until server support is confirmed and the API contract declares
1383
+ * it. If you need replay safety on these calls today, guard at your own call
1384
+ * site.
1385
+ */
1386
+ buyers = {
1387
+ /**
1388
+ * `POST /v1/buyers` — create or update a buyer.
1389
+ *
1390
+ * ⚠️ **This is an UPSERT.** The write is keyed on `externalId` when
1391
+ * present, else on `email`. Calling it twice with the same key UPDATES;
1392
+ * it does not fail with a duplicate error. The server answers `201` when it
1393
+ * created and `200` when it updated, and the body is the full stored
1394
+ * profile either way — so what you read back is what was persisted
1395
+ * (including `metadata` after its append-merge).
1396
+ *
1397
+ * At least one of `externalId` / `email` is required: a profile with no
1398
+ * lookup key could never be retrieved or charged against. That is checked
1399
+ * here rather than spent on a round-trip to a `400`.
1400
+ */
1401
+ upsert: async (params) => {
1402
+ // Snapshot before checking, for the same reason `update()` does — a
1403
+ // getter that answers differently on a second read must not be able to
1404
+ // change what we validated. Also means the body we send is exactly the
1405
+ // object we inspected.
1406
+ const snapshot = { ...(params ?? {}) };
1407
+ if (snapshot.externalId === undefined && snapshot.email === undefined) {
1408
+ throw new TypeError("buyers.upsert requires at least one of externalId or email — a buyer " +
1409
+ "with no lookup key can never be retrieved or charged against. " +
1410
+ "externalId is your own customer reference and is the preferred key.");
1411
+ }
1412
+ // ⚠️ Refuse a null email here too, even though only PATCH gives null the
1413
+ // erase meaning. The argument for `update()`'s runtime guard — a
1414
+ // JavaScript caller has no type system, and their serialiser may emit
1415
+ // null for a field they left out — applies identically to this call, so
1416
+ // guarding one and not the other would be arbitrary. The spec types
1417
+ // this field non-nullable, so the server would answer 400; failing here
1418
+ // with a message that names the right method is a better answer than a
1419
+ // round-trip to a validation error.
1420
+ if (snapshot.email === null) {
1421
+ throw new TypeError("buyers.upsert does not accept a null email. Omit the field to leave it " +
1422
+ "unset, or pass an address. To erase an existing buyer's email, call " +
1423
+ "buyers.eraseEmail(buyerId) — that is the only method that erases.");
1424
+ }
1425
+ const { data } = await this.request("POST", "/v1/buyers", {
1426
+ // buildSnakeBodyOpaqueMetadata, not toSnakeKeysDeep: the latter recurses
1427
+ // into `metadata` and rewrites the MERCHANT'S OWN keys — `orderId`
1428
+ // silently becomes `order_id`, which they can never match on again
1429
+ // (review rule sdk/metadata-keys-opaque). Every other resource in this
1430
+ // file uses the helper; buyers did not.
1431
+ body: buildSnakeBodyOpaqueMetadata(snapshot),
1432
+ reporterMethod: "buyers.upsert",
1433
+ });
1434
+ return buyerFromWire(data);
1435
+ },
1436
+ /**
1437
+ * `GET /v1/buyers/:id` — fetch a stored profile by id.
1438
+ *
1439
+ * A cross-merchant id returns an opaque 404, never a 403.
1440
+ */
1441
+ retrieve: async (buyerId) => {
1442
+ assertResourceId(buyerId, "buyerId");
1443
+ const { data } = await this.request("GET", `/v1/buyers/${encodeURIComponent(buyerId)}`, { reporterMethod: "buyers.retrieve" });
1444
+ return buyerFromWire(data);
1445
+ },
1446
+ /**
1447
+ * `GET /v1/buyers` — exact lookup by your reference or by email.
1448
+ *
1449
+ * Returns the profile or `null`. There is deliberately no unfiltered
1450
+ * listing in v1, so at least one key is required; when both are supplied
1451
+ * `externalId` wins, matching the upsert's identity-resolution order.
1452
+ *
1453
+ * Unwraps the server's `{ data: [...] }` envelope, which holds zero or one
1454
+ * profile — handing back a one-element array to destructure would be a
1455
+ * worse API for a lookup that can never return two.
1456
+ */
1457
+ find: async (params) => {
1458
+ if (params?.externalId === undefined && params?.email === undefined) {
1459
+ throw new TypeError("buyers.find requires at least one of externalId or email — there is " +
1460
+ "no unfiltered buyer listing in v1.");
1461
+ }
1462
+ const query = {};
1463
+ if (params.externalId !== undefined)
1464
+ query.external_id = params.externalId;
1465
+ if (params.email !== undefined)
1466
+ query.email = params.email;
1467
+ const { data } = await this.request("GET", "/v1/buyers", {
1468
+ query,
1469
+ reporterMethod: "buyers.find",
1470
+ });
1471
+ const first = data.data[0];
1472
+ return first ? buyerFromWire(first) : null;
1473
+ },
1474
+ /**
1475
+ * `PATCH /v1/buyers/:id` — update a stored profile.
1476
+ *
1477
+ * Omitted fields are left unchanged. `metadata` merges with append
1478
+ * semantics. `externalId` is not patchable — the identity key is fixed at
1479
+ * creation; upsert a new profile instead.
1480
+ *
1481
+ * ⛔ **This method cannot erase.** `email` is typed `string`, so passing
1482
+ * `null` is a compile error, and the body is built with an explicit
1483
+ * `!== undefined` test rather than a truthiness check — a truthy test would
1484
+ * also drop a legitimate empty-string value and, worse, would let a `null`
1485
+ * through unnoticed if the type were ever widened. Use
1486
+ * {@link eraseEmail} when you actually mean to erase.
1487
+ */
1488
+ update: async (buyerId, params) => {
1489
+ assertResourceId(buyerId, "buyerId");
1490
+ // ⛔ REFUSE AN EXPLICIT NULL AT RUNTIME, not only in the type.
1491
+ //
1492
+ // The type makes this unwritable in TypeScript, and that is worth having
1493
+ // — but it protects exactly the callers who were never the risk. The
1494
+ // API's own warning is about JSON libraries that serialise an ABSENT
1495
+ // optional field as `null`, and a caller in plain JavaScript has no type
1496
+ // system at all. For both of them the type is not present at runtime and
1497
+ // the erase would go through on an ordinary update.
1498
+ //
1499
+ // So the guarantee is enforced where it is actually load-bearing. There
1500
+ // is exactly one way to erase in this SDK, and it says so in its name.
1501
+ // ⛔ READ EVERY FIELD EXACTLY ONCE, BEFORE ANY OF THEM IS CHECKED.
1502
+ //
1503
+ // The first version of this guard read `params.email` three times — once
1504
+ // to test for null, twice more to build the body. That is a
1505
+ // check-then-use gap: an object whose `email` is a GETTER (or a Proxy)
1506
+ // can return `undefined` to the check and `null` to the body-builder,
1507
+ // and the erase goes through `update()` with no exception at all.
1508
+ // Reproduced against the real code, so this is not theoretical.
1509
+ //
1510
+ // That is a check-then-use gap, and it defeats the entire point of the
1511
+ // guard. Objects with lazily-evaluated properties are ordinary — ORM
1512
+ // models, framework request binders, validation wrappers — so this needs
1513
+ // no adversary, just a caller passing something that is not a plain
1514
+ // object literal. Snapshot first; decide afterwards.
1515
+ const email = params?.email;
1516
+ const name = params?.name;
1517
+ const phone = params?.phone;
1518
+ const company = params?.company;
1519
+ const address = params?.address;
1520
+ const metadata = params?.metadata;
1521
+ if (email === null) {
1522
+ throw new TypeError("buyers.update cannot erase an email address. Sending `email: null` on " +
1523
+ "this endpoint permanently erases it — including the copy held at your " +
1524
+ "payment provider — and it cannot be undone. If you meant to erase it, " +
1525
+ "call buyers.eraseEmail(buyerId). If you did not, check whether your " +
1526
+ "JSON serialiser is emitting null for fields you simply left out; " +
1527
+ "several common libraries do that by default. To leave the address " +
1528
+ "unchanged, omit the field entirely.");
1529
+ }
1530
+ const patch = {};
1531
+ // Explicit `!== undefined`, never truthiness: "" is a legitimate value to
1532
+ // send for a name, and truthiness would silently drop it.
1533
+ if (email !== undefined)
1534
+ patch.email = email;
1535
+ if (name !== undefined)
1536
+ patch.name = name;
1537
+ if (phone !== undefined)
1538
+ patch.phone = phone;
1539
+ if (company !== undefined)
1540
+ patch.company = company;
1541
+ if (address !== undefined)
1542
+ patch.address = address;
1543
+ if (metadata !== undefined)
1544
+ patch.metadata = metadata;
1545
+ // Same helper as every other resource: snake-cases `address.addressLine1`
1546
+ // and friends while leaving the merchant's own metadata keys untouched.
1547
+ const body = buildSnakeBodyOpaqueMetadata(patch);
1548
+ const { data } = await this.request("PATCH", `/v1/buyers/${encodeURIComponent(buyerId)}`, {
1549
+ body,
1550
+ reporterMethod: "buyers.update",
1551
+ });
1552
+ return buyerFromWire(data);
1553
+ },
1554
+ /**
1555
+ * **Permanently erase this buyer's email address.** `PATCH /v1/buyers/:id`
1556
+ * with `email: null`.
1557
+ *
1558
+ * It removes the address from the buyer profile. Afterwards the buyer
1559
+ * cannot be found by email.
1560
+ *
1561
+ * ⚠️ **We also ASK your payment provider to drop their copy — but a
1562
+ * successful response here does not confirm that happened.** That request
1563
+ * is dispatched after we answer you, is never awaited, and reports no
1564
+ * failure back: a provider error, a timeout, or an open circuit breaker
1565
+ * all pass silently. And for a buyer we only ever knew by email, whose
1566
+ * provider record we had not separately recorded, the retraction cannot be
1567
+ * performed at all — the key we would need was derived from the address
1568
+ * that was just erased. Do not tell a shopper the provider copy is gone on
1569
+ * the strength of a `200`.
1570
+ *
1571
+ * ⛔ **This is NOT a complete deletion on its own, and must not be
1572
+ * described to a shopper as one.** It clears the profile's address. Other
1573
+ * copies live outside this record, under their own
1574
+ * retention rules, and this call does not touch them — including the
1575
+ * settlement record kept for financial audit, checkout sessions from the
1576
+ * recent past, and any address already forwarded to a connected store,
1577
+ * which is an external system with no retraction path.
1578
+ *
1579
+ * ⚠️ Read that as examples, **not as an inventory.** It is the set we have
1580
+ * confirmed, not a completed audit of every place a copy can exist — your
1581
+ * own systems may hold one we sent you earlier. Treat this call as the
1582
+ * profile half of a deletion request, not the whole answer.
1583
+ *
1584
+ * ⚠️ **It cannot be undone**, and a buyer identified only by email becomes
1585
+ * unreachable by lookup afterwards — the record and every payment attached
1586
+ * to it survive, but `find({ email })` will no longer locate it. Keep your
1587
+ * own reference in `externalId` if you need to find them again.
1588
+ *
1589
+ * No other profile field is erasable this way; `email` is the only one the
1590
+ * API declares nullable.
1591
+ *
1592
+ * @returns the stored profile as it is after the erase, with `email: null`.
1593
+ */
1594
+ eraseEmail: async (buyerId) => {
1595
+ assertResourceId(buyerId, "buyerId");
1596
+ const { data } = await this.request("PATCH", `/v1/buyers/${encodeURIComponent(buyerId)}`, {
1597
+ // The one place in this SDK that sends an explicit null. Written as a
1598
+ // literal rather than routed through the params builder so it cannot
1599
+ // be reached by any other call path.
1600
+ body: { email: null },
1601
+ reporterMethod: "buyers.eraseEmail",
1602
+ });
1603
+ return buyerFromWire(data);
1604
+ },
1605
+ };
1277
1606
  webhookSubscriptions = {
1278
1607
  /**
1279
1608
  * `GET /v1/webhook_subscriptions` — Stripe-shaped list envelope.