@asteby/storefront 0.11.1 → 0.12.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.1" : "dev";
490
+ var SDK_VERSION = true ? "0.12.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-bsdlAumU.cjs';
2
- export { Y as IdentityCapture, _ as IdentityFieldRule, a0 as IdentityStrategy } from './types-bsdlAumU.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-B1E3GySa.js';
2
- export { Y as IdentityCapture, _ as IdentityFieldRule, a0 as IdentityStrategy } from './types-B1E3GySa.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)";
@@ -944,7 +996,9 @@ function paymentErrorView(err, locale, overrides) {
944
996
  if (status === 429 || code === "too_many_attempts") {
945
997
  return { kind: "too_many_attempts", message: m["error.too_many_attempts"], retrySameAttempt: false, canRetry: false };
946
998
  }
947
- if (status === 0 || status === 502 || status === 503 || status === 504 || code === "network" || code === "timeout") {
999
+ const declared = e.retryable;
1000
+ if (status >= 500 && declared === false) return { kind: "generic", message: m["error.generic"], retrySameAttempt: false, canRetry: true };
1001
+ if (status === 0 || status === 502 || status === 503 || status === 504 || code === "network" || code === "timeout" || status >= 500) {
948
1002
  return { kind: "network", message: m["error.network"], retrySameAttempt: true, canRetry: true };
949
1003
  }
950
1004
  return { kind: "generic", message: m["error.generic"], retrySameAttempt: false, canRetry: true };
@@ -1247,6 +1301,7 @@ exports.CLIENT_IP_HEADER = CLIENT_IP_HEADER;
1247
1301
  exports.COUNTRY_HEADERS = COUNTRY_HEADERS;
1248
1302
  exports.MISSING_WEIGHT = MISSING_WEIGHT;
1249
1303
  exports.NOTE_MAX_LENGTH = NOTE_MAX_LENGTH;
1304
+ exports.PAYMENT_CHOICE_REFUSED_FIELD = PAYMENT_CHOICE_REFUSED_FIELD;
1250
1305
  exports.StorefrontError = StorefrontError;
1251
1306
  exports.appendParams = appendParams;
1252
1307
  exports.availabilityLocale = availabilityLocale;
@@ -1254,6 +1309,7 @@ exports.availabilityMessages = availabilityMessages;
1254
1309
  exports.availabilityView = availabilityView;
1255
1310
  exports.cartLineProblemView = cartLineProblemView;
1256
1311
  exports.clampPercent = clampPercent;
1312
+ exports.classifyPaymentError = classifyPaymentError;
1257
1313
  exports.createStorefrontClient = createStorefrontClient;
1258
1314
  exports.declineMessage = declineMessage;
1259
1315
  exports.declineMessageKey = declineMessageKey;
@@ -1271,6 +1327,7 @@ exports.maskToken = maskToken;
1271
1327
  exports.negotiateLocale = negotiateLocale;
1272
1328
  exports.normalizeClientIp = normalizeClientIp;
1273
1329
  exports.normalizeNote = normalizeNote;
1330
+ exports.opsSaysNotRetryable = opsSaysNotRetryable;
1274
1331
  exports.parseDelivery = parseDelivery;
1275
1332
  exports.parseMoney = parseMoney;
1276
1333
  exports.parseProductFilters = parseProductFilters;