@asteby/storefront 0.11.2 → 0.13.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
@@ -263,6 +263,9 @@ const result = await sf.checkouts.pay(checkout.checkout.id, { method: 'card', pr
263
263
  { idempotencyKey: paymentIdempotencyKey(checkout.checkout.id, attemptId) }) // idempotencyKey is REQUIRED (throws before calling)
264
264
  const outcome = paymentOutcome(result, 'es-MX') // approved | in_review | voucher | challenge | redirect | rejected | expired | cancelled
265
265
  paymentErrorView(error) // network (retry same attempt) | price_mismatch | payment_in_progress | expired | too_many_attempts
266
+ classifyPaymentError(error) // { kind, retryable } WITHOUT texts; same rules in the server and the browser
267
+ // kinds: transient | in_progress | rate_limited | merchant_config | provider_declined | stale_checkout | expired | price_mismatch | store_closed | validation | other
268
+ // honours OPS `retryable:false` and the stable codes payment_provider_rejected / payment_provider_misconfigured / unavailable / invalid_state
266
269
  ```
267
270
 
268
271
  - `declineMessage(code, locale)` / `paymentMessages(locale, overrides)`: actionable es/en texts; unknown codes become `declined`.
@@ -443,6 +446,8 @@ connectStore().keepFresh() // heartbeat; rotates (POST /api/conne
443
446
  missing and where to repair them (`OPS → Conexiones y API → <conexión> → Reparar permisos`), showing `sk_…a91f` and never
444
447
  the key. Repairing permissions does not change the key.
445
448
 
449
+ `StorefrontError` also keeps what OPS sends in `error.*`: `retryable`, `retryAfter`, `checkoutId`, `cost` (of a `price_mismatch`) and `messageEn`.
450
+
446
451
  Errors: a `403 insufficient_scope` is a `StorefrontError` with `code: 'insufficient_scope'`, `status: 403` and
447
452
  `requiredScopes: ['customer:write']`. `customer.capabilities()` reports `password: false`, `unavailable_reason:
448
453
  'missing_scope'` and `missing_scopes` when the key cannot create accounts. The mock rehearses all of it with
package/dist/cli.js CHANGED
@@ -15,6 +15,16 @@ var StorefrontError = class extends Error {
15
15
  url;
16
16
  /** Scopes a `403 insufficient_scope` says the route needs (empty for any other error). */
17
17
  requiredScopes;
18
+ /** `true`: repeating the request (same idempotency key) can succeed. `false`: not until someone fixes something. `undefined`: OPS did not say. */
19
+ retryable;
20
+ /** Seconds OPS asks to wait before retrying. */
21
+ retryAfter;
22
+ /** Checkout that a refused cart write can resume. */
23
+ checkoutId;
24
+ /** Current cart cost sent with a `price_mismatch` (untrusted shape). */
25
+ cost;
26
+ /** English version of `message`, when OPS sent one. */
27
+ messageEn;
18
28
  constructor(init) {
19
29
  super(init.message, init.cause === void 0 ? void 0 : { cause: init.cause });
20
30
  this.status = init.status;
@@ -23,6 +33,11 @@ var StorefrontError = class extends Error {
23
33
  this.details = init.details;
24
34
  this.url = init.url;
25
35
  this.requiredScopes = init.requiredScopes ?? [];
36
+ this.retryable = init.retryable;
37
+ this.retryAfter = init.retryAfter;
38
+ this.checkoutId = init.checkoutId;
39
+ this.cost = init.cost;
40
+ this.messageEn = init.messageEn;
26
41
  }
27
42
  /**
28
43
  * Affected lines of a `409 insufficient_stock` (empty for any other error or a
@@ -41,7 +56,10 @@ var StorefrontError = class extends Error {
41
56
  code: this.code,
42
57
  message: this.message,
43
58
  field: this.field,
44
- ...this.requiredScopes.length ? { requiredScopes: this.requiredScopes } : {}
59
+ ...this.requiredScopes.length ? { requiredScopes: this.requiredScopes } : {},
60
+ ...this.retryable !== void 0 ? { retryable: this.retryable } : {},
61
+ ...this.retryAfter !== void 0 ? { retryAfter: this.retryAfter } : {},
62
+ ...this.checkoutId ? { checkoutId: this.checkoutId } : {}
45
63
  };
46
64
  }
47
65
  };
@@ -169,6 +187,11 @@ async function readError(res, url) {
169
187
  message: e?.message ?? `Storefront API responded ${res.status}${res.statusText ? " " + res.statusText : ""}`,
170
188
  ...e?.field ? { field: e.field } : {},
171
189
  ...e?.details !== void 0 ? { details: e.details } : {},
190
+ ...typeof e?.retryable === "boolean" ? { retryable: e.retryable } : {},
191
+ ...typeof e?.retry_after === "number" && e.retry_after > 0 ? { retryAfter: e.retry_after } : {},
192
+ ...typeof e?.checkout_id === "string" && e.checkout_id ? { checkoutId: e.checkout_id } : {},
193
+ ...e?.cost !== void 0 ? { cost: e.cost } : {},
194
+ ...typeof e?.message_en === "string" && e.message_en ? { messageEn: e.message_en } : {},
172
195
  url
173
196
  });
174
197
  }
@@ -464,7 +487,7 @@ function repairPath(connectionName) {
464
487
  import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeSync } from "fs";
465
488
  import { dirname, join, resolve } from "path";
466
489
  import { hostname } from "os";
467
- var SDK_VERSION = true ? "0.11.2" : "dev";
490
+ var SDK_VERSION = true ? "0.13.0" : "dev";
468
491
  function writeFileAtomic(file, content, mode = 384) {
469
492
  const dir = dirname(file);
470
493
  mkdirSync(dir, { recursive: true });
@@ -1,5 +1,5 @@
1
- import { U as FiscalDocumentSource, aR as RegionFiscalEntry, Z as IdentityDocumentSpec, T as FiscalDocumentEvidence, aS as RegionFiscalId, $ as IdentityRulesV1 } from './types-BQ7k6DwG.cjs';
2
- export { Y as IdentityCapture, _ as IdentityFieldRule, a0 as IdentityStrategy } from './types-BQ7k6DwG.cjs';
1
+ import { U as FiscalDocumentSource, aR as RegionFiscalEntry, Z as IdentityDocumentSpec, T as FiscalDocumentEvidence, aS as RegionFiscalId, $ as IdentityRulesV1 } from './types-BQ3S6VjF.cjs';
2
+ export { Y as IdentityCapture, _ as IdentityFieldRule, a0 as IdentityStrategy } from './types-BQ3S6VjF.cjs';
3
3
  import './geo.cjs';
4
4
 
5
5
  /** A value read from a document: text, or the codes of a catalog (`multiple` fields). */
@@ -1,5 +1,5 @@
1
- import { U as FiscalDocumentSource, aR as RegionFiscalEntry, Z as IdentityDocumentSpec, T as FiscalDocumentEvidence, aS as RegionFiscalId, $ as IdentityRulesV1 } from './types-D8nLoKyk.js';
2
- export { Y as IdentityCapture, _ as IdentityFieldRule, a0 as IdentityStrategy } from './types-D8nLoKyk.js';
1
+ import { U as FiscalDocumentSource, aR as RegionFiscalEntry, Z as IdentityDocumentSpec, T as FiscalDocumentEvidence, aS as RegionFiscalId, $ as IdentityRulesV1 } from './types-C_rsJRCl.js';
2
+ export { Y as IdentityCapture, _ as IdentityFieldRule, a0 as IdentityStrategy } from './types-C_rsJRCl.js';
3
3
  import './geo.js';
4
4
 
5
5
  /** A value read from a document: text, or the codes of a catalog (`multiple` fields). */
package/dist/index.cjs CHANGED
@@ -10,6 +10,16 @@ var StorefrontError = class extends Error {
10
10
  url;
11
11
  /** Scopes a `403 insufficient_scope` says the route needs (empty for any other error). */
12
12
  requiredScopes;
13
+ /** `true`: repeating the request (same idempotency key) can succeed. `false`: not until someone fixes something. `undefined`: OPS did not say. */
14
+ retryable;
15
+ /** Seconds OPS asks to wait before retrying. */
16
+ retryAfter;
17
+ /** Checkout that a refused cart write can resume. */
18
+ checkoutId;
19
+ /** Current cart cost sent with a `price_mismatch` (untrusted shape). */
20
+ cost;
21
+ /** English version of `message`, when OPS sent one. */
22
+ messageEn;
13
23
  constructor(init) {
14
24
  super(init.message, init.cause === void 0 ? void 0 : { cause: init.cause });
15
25
  this.status = init.status;
@@ -18,6 +28,11 @@ var StorefrontError = class extends Error {
18
28
  this.details = init.details;
19
29
  this.url = init.url;
20
30
  this.requiredScopes = init.requiredScopes ?? [];
31
+ this.retryable = init.retryable;
32
+ this.retryAfter = init.retryAfter;
33
+ this.checkoutId = init.checkoutId;
34
+ this.cost = init.cost;
35
+ this.messageEn = init.messageEn;
21
36
  }
22
37
  /**
23
38
  * Affected lines of a `409 insufficient_stock` (empty for any other error or a
@@ -36,7 +51,10 @@ var StorefrontError = class extends Error {
36
51
  code: this.code,
37
52
  message: this.message,
38
53
  field: this.field,
39
- ...this.requiredScopes.length ? { requiredScopes: this.requiredScopes } : {}
54
+ ...this.requiredScopes.length ? { requiredScopes: this.requiredScopes } : {},
55
+ ...this.retryable !== void 0 ? { retryable: this.retryable } : {},
56
+ ...this.retryAfter !== void 0 ? { retryAfter: this.retryAfter } : {},
57
+ ...this.checkoutId ? { checkoutId: this.checkoutId } : {}
40
58
  };
41
59
  }
42
60
  };
@@ -201,6 +219,11 @@ async function readError(res, url) {
201
219
  message: e?.message ?? `Storefront API responded ${res.status}${res.statusText ? " " + res.statusText : ""}`,
202
220
  ...e?.field ? { field: e.field } : {},
203
221
  ...e?.details !== void 0 ? { details: e.details } : {},
222
+ ...typeof e?.retryable === "boolean" ? { retryable: e.retryable } : {},
223
+ ...typeof e?.retry_after === "number" && e.retry_after > 0 ? { retryAfter: e.retry_after } : {},
224
+ ...typeof e?.checkout_id === "string" && e.checkout_id ? { checkoutId: e.checkout_id } : {},
225
+ ...e?.cost !== void 0 ? { cost: e.cost } : {},
226
+ ...typeof e?.message_en === "string" && e.message_en ? { messageEn: e.message_en } : {},
204
227
  url
205
228
  });
206
229
  }
@@ -482,6 +505,35 @@ function storefrontDomainHeader(domain, market) {
482
505
  return code && /^[a-z0-9_-]{1,32}$/.test(code) ? `${host}#${code}` : host;
483
506
  }
484
507
 
508
+ // src/payment-errors.ts
509
+ function opsSaysNotRetryable(err) {
510
+ if (err.retryable === false) return true;
511
+ const d = err.details;
512
+ return typeof d === "object" && d !== null && d.retryable === false;
513
+ }
514
+ var PAYMENT_CHOICE_REFUSED_FIELD = "payment_choice";
515
+ var MERCHANT_CONFIG_CODE = /credential|not_configured|unconfigured|invalid_access|access_token|provider_config|merchant|invalid_public_key|unauthorized/i;
516
+ function classifyPaymentError(err) {
517
+ const status = typeof err.status === "number" ? err.status : 0;
518
+ const code = typeof err.code === "string" ? err.code : "";
519
+ if (code === "store_not_ready") return { kind: "store_closed", retryable: false };
520
+ if (code === "price_mismatch" || status === 422 && err.field === "expected_total") return { kind: "price_mismatch", retryable: false };
521
+ if (status === 409 && (code === "payment_in_progress" || err.field === "payment_in_progress")) return { kind: "in_progress", retryable: true };
522
+ if (status === 409 && code === "expired") return { kind: "expired", retryable: false };
523
+ if (status === 429 || code === "too_many_attempts" || code === "rate_limited") return { kind: "rate_limited", retryable: true };
524
+ if (status === 422 && code === "invalid_state") {
525
+ if (err.field === PAYMENT_CHOICE_REFUSED_FIELD) return { kind: "other", retryable: false };
526
+ return { kind: "stale_checkout", retryable: false };
527
+ }
528
+ if (code === "payment_provider_misconfigured") return { kind: "merchant_config", retryable: false };
529
+ if (code === "payment_provider_rejected") return { kind: "provider_declined", retryable: false };
530
+ if (MERCHANT_CONFIG_CODE.test(code) || status === 401 || status === 403) return { kind: "merchant_config", retryable: false };
531
+ if (status >= 500 && opsSaysNotRetryable(err)) return { kind: "other", retryable: false };
532
+ if (status === 0 || status >= 500 || code === "network" || code === "timeout" || code === "unavailable") return { kind: "transient", retryable: true };
533
+ if (status === 400 || code === "validation") return { kind: "validation", retryable: false };
534
+ return { kind: "other", retryable: false };
535
+ }
536
+
485
537
  // src/diagnose.ts
486
538
  function maskToken(token) {
487
539
  if (!token) return "(sin token)";
@@ -781,7 +833,10 @@ var ES2 = {
781
833
  "voucher.open": "Ver o imprimir ficha",
782
834
  "voucher.email": "Tambi\xE9n te la enviamos a tu correo.",
783
835
  "form.label": "Datos de tu tarjeta",
784
- "challenge.title": "Verificaci\xF3n de tu banco"
836
+ "challenge.title": "Verificaci\xF3n de tu banco",
837
+ "challenge.escape": "Cancelar y volver al pago",
838
+ "challenge.escape_hint": "\xBFTarda demasiado? Puedes cerrar la verificaci\xF3n. Si ya la aprobaste en tu banco, el pago a\xFAn puede completarse: te confirmamos aqu\xED antes de dejarte pagar de nuevo.",
839
+ "challenge.left": "Cerraste la verificaci\xF3n. Estamos confirmando con tu banco si el pago se complet\xF3; no vuelvas a pagar todav\xEDa."
785
840
  };
786
841
  var EN2 = {
787
842
  "decline.insufficient_funds": "Your card has insufficient funds. Try another card or pay in cash.",
@@ -825,7 +880,10 @@ var EN2 = {
825
880
  "voucher.open": "View or print voucher",
826
881
  "voucher.email": "We also emailed it to you.",
827
882
  "form.label": "Your card details",
828
- "challenge.title": "Your bank verification"
883
+ "challenge.title": "Your bank verification",
884
+ "challenge.escape": "Cancel and go back to payment",
885
+ "challenge.escape_hint": "Taking too long? You can close the verification. If you already approved it with your bank the payment may still go through: we confirm here before letting you pay again.",
886
+ "challenge.left": "You closed the verification. We are checking with your bank whether the payment went through; do not pay again yet."
829
887
  };
830
888
  var CATALOG2 = { es: ES2, en: EN2 };
831
889
  function paymentMessages(locale, overrides) {
@@ -944,7 +1002,9 @@ function paymentErrorView(err, locale, overrides) {
944
1002
  if (status === 429 || code === "too_many_attempts") {
945
1003
  return { kind: "too_many_attempts", message: m["error.too_many_attempts"], retrySameAttempt: false, canRetry: false };
946
1004
  }
947
- if (status === 0 || status === 502 || status === 503 || status === 504 || code === "network" || code === "timeout") {
1005
+ const declared = e.retryable;
1006
+ if (status >= 500 && declared === false) return { kind: "generic", message: m["error.generic"], retrySameAttempt: false, canRetry: true };
1007
+ if (status === 0 || status === 502 || status === 503 || status === 504 || code === "network" || code === "timeout" || status >= 500) {
948
1008
  return { kind: "network", message: m["error.network"], retrySameAttempt: true, canRetry: true };
949
1009
  }
950
1010
  return { kind: "generic", message: m["error.generic"], retrySameAttempt: false, canRetry: true };
@@ -1247,6 +1307,7 @@ exports.CLIENT_IP_HEADER = CLIENT_IP_HEADER;
1247
1307
  exports.COUNTRY_HEADERS = COUNTRY_HEADERS;
1248
1308
  exports.MISSING_WEIGHT = MISSING_WEIGHT;
1249
1309
  exports.NOTE_MAX_LENGTH = NOTE_MAX_LENGTH;
1310
+ exports.PAYMENT_CHOICE_REFUSED_FIELD = PAYMENT_CHOICE_REFUSED_FIELD;
1250
1311
  exports.StorefrontError = StorefrontError;
1251
1312
  exports.appendParams = appendParams;
1252
1313
  exports.availabilityLocale = availabilityLocale;
@@ -1254,6 +1315,7 @@ exports.availabilityMessages = availabilityMessages;
1254
1315
  exports.availabilityView = availabilityView;
1255
1316
  exports.cartLineProblemView = cartLineProblemView;
1256
1317
  exports.clampPercent = clampPercent;
1318
+ exports.classifyPaymentError = classifyPaymentError;
1257
1319
  exports.createStorefrontClient = createStorefrontClient;
1258
1320
  exports.declineMessage = declineMessage;
1259
1321
  exports.declineMessageKey = declineMessageKey;
@@ -1271,6 +1333,7 @@ exports.maskToken = maskToken;
1271
1333
  exports.negotiateLocale = negotiateLocale;
1272
1334
  exports.normalizeClientIp = normalizeClientIp;
1273
1335
  exports.normalizeNote = normalizeNote;
1336
+ exports.opsSaysNotRetryable = opsSaysNotRetryable;
1274
1337
  exports.parseDelivery = parseDelivery;
1275
1338
  exports.parseMoney = parseMoney;
1276
1339
  exports.parseProductFilters = parseProductFilters;