@solvapay/server 2.5.0 → 2.6.0-preview-253dcb103840555309a5ff5e4ccfa6a07f7ca6ae

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/edge.d.ts CHANGED
@@ -826,9 +826,13 @@ interface components {
826
826
  LimitBalanceDto: {
827
827
  /** @description Credit balance in credits (100 credits = 1 minor currency unit) */
828
828
  creditBalance: number;
829
+ /** @description Credits per minor unit of `currency` (typically 100) */
830
+ creditsPerMinorUnit?: number;
829
831
  /** @description Credits deducted per metered item (wallet coverage: remainingUnits = balance / creditsPerUnit) */
830
832
  creditsPerUnit: number;
831
833
  currency: string;
834
+ /** @description USD → display-currency rate used with creditsPerMinorUnit */
835
+ displayExchangeRate?: number;
832
836
  /** @description How many metered items the credit balance still covers (`balance / creditsPerUnit`) */
833
837
  remainingUnits?: number;
834
838
  };
@@ -1201,17 +1205,17 @@ interface components {
1201
1205
  };
1202
1206
  ProcessPaymentCancelled: {
1203
1207
  /**
1204
- * discriminator enum property added by openapi-typescript
1208
+ * @example cancelled
1205
1209
  * @enum {string}
1206
1210
  */
1207
- status: 'ProcessPaymentCancelled';
1211
+ status: 'cancelled';
1208
1212
  };
1209
1213
  ProcessPaymentFailed: {
1210
1214
  /**
1211
- * discriminator enum property added by openapi-typescript
1215
+ * @example failed
1212
1216
  * @enum {string}
1213
1217
  */
1214
- status: 'ProcessPaymentFailed';
1218
+ status: 'failed';
1215
1219
  };
1216
1220
  ProcessPaymentIntentDto: {
1217
1221
  customerRef: string;
@@ -1220,25 +1224,25 @@ interface components {
1220
1224
  };
1221
1225
  ProcessPaymentProcessing: {
1222
1226
  /**
1223
- * discriminator enum property added by openapi-typescript
1227
+ * @example processing
1224
1228
  * @enum {string}
1225
1229
  */
1226
- status: 'ProcessPaymentProcessing';
1230
+ status: 'processing';
1227
1231
  };
1228
1232
  ProcessPaymentSucceededBare: {
1229
1233
  /**
1230
- * discriminator enum property added by openapi-typescript
1234
+ * @example succeeded
1231
1235
  * @enum {string}
1232
1236
  */
1233
- status: 'ProcessPaymentSucceededBare';
1237
+ status: 'succeeded';
1234
1238
  };
1235
1239
  ProcessPaymentSucceededOneTime: {
1236
1240
  oneTimePurchase: components['schemas']['OneTimePurchaseInfo'];
1237
1241
  /**
1238
- * discriminator enum property added by openapi-typescript
1242
+ * @example succeeded
1239
1243
  * @enum {string}
1240
1244
  */
1241
- status: 'ProcessPaymentSucceededOneTime';
1245
+ status: 'succeeded';
1242
1246
  /**
1243
1247
  * @example one-time
1244
1248
  * @enum {string}
@@ -1248,10 +1252,10 @@ interface components {
1248
1252
  ProcessPaymentSucceededRecurring: {
1249
1253
  purchase: components['schemas']['SdkPurchaseResponse'];
1250
1254
  /**
1251
- * discriminator enum property added by openapi-typescript
1255
+ * @example succeeded
1252
1256
  * @enum {string}
1253
1257
  */
1254
- status: 'ProcessPaymentSucceededRecurring';
1258
+ status: 'succeeded';
1255
1259
  /**
1256
1260
  * @example recurring
1257
1261
  * @enum {string}
@@ -1262,10 +1266,10 @@ interface components {
1262
1266
  /** @description Detail message describing the timeout */
1263
1267
  message?: string;
1264
1268
  /**
1265
- * discriminator enum property added by openapi-typescript
1269
+ * @example timeout
1266
1270
  * @enum {string}
1267
1271
  */
1268
- status: 'ProcessPaymentTimeout';
1272
+ status: 'timeout';
1269
1273
  };
1270
1274
  ProductConfigDto: {
1271
1275
  /**
@@ -3207,10 +3211,10 @@ interface operations {
3207
3211
  content: {
3208
3212
  'application/json': {
3209
3213
  /**
3210
- * Outcome of the delete: hard delete in sandbox, soft-delete or deactivation in live depending on existing purchases
3214
+ * Outcome of the delete: deactivation when purchases exist, otherwise soft-delete. Same in sandbox and live.
3211
3215
  * @enum {string}
3212
3216
  */
3213
- action?: 'deleted' | 'deactivated' | 'soft_deleted';
3217
+ action?: 'deactivated' | 'soft_deleted';
3214
3218
  success?: boolean;
3215
3219
  };
3216
3220
  };
@@ -3949,38 +3953,20 @@ type OneTimePurchaseInfo = components['schemas']['OneTimePurchaseInfo'];
3949
3953
  /**
3950
3954
  * Result from processing a payment intent.
3951
3955
  *
3952
- * Mirrors the backend's discriminated `oneOf` response. The `succeeded`
3953
- * branches are further discriminated on `type` so consumers can route to
3954
- * recurring vs one-time purchase handling without guarding against
3955
- * `purchase === undefined`. A bare `{ status: 'succeeded' }` is returned
3956
- * when the webhook race means the backend can't yet enrich the response
3957
- * with the created purchase — callers should fall back to refetching.
3958
- *
3959
- * `failed` and `cancelled` are returned by the backend when the Stripe
3960
- * PaymentIntent is in a terminal non-success state and are routed to
3961
- * `onError` by `reconcilePayment`. `timeout` carries a retry hint and is
3962
- * routed to the timeout branch.
3956
+ * Derived from the generated `/v1/sdk/payment-intents/{id}/process` 200
3957
+ * response. The `succeeded` branches further discriminate on `type` so
3958
+ * consumers can route to recurring vs one-time handling without guarding
3959
+ * against `purchase === undefined`. A bare `{ status: 'succeeded' }` is
3960
+ * returned when the webhook race means the backend can't yet enrich the
3961
+ * response with the created purchase — callers should fall back to
3962
+ * refetching.
3963
+ *
3964
+ * `failed` and `cancelled` are returned when the Stripe PaymentIntent is
3965
+ * in a terminal non-success state and are routed to `onError` by
3966
+ * `reconcilePayment`. `timeout` carries a retry hint and is routed to
3967
+ * the timeout branch.
3963
3968
  */
3964
- type ProcessPaymentResult = {
3965
- status: 'succeeded';
3966
- type: 'recurring';
3967
- purchase: PurchaseInfo;
3968
- } | {
3969
- status: 'succeeded';
3970
- type: 'one-time';
3971
- oneTimePurchase: OneTimePurchaseInfo;
3972
- } | {
3973
- status: 'succeeded';
3974
- } | {
3975
- status: 'processing';
3976
- } | {
3977
- status: 'timeout';
3978
- message?: string;
3979
- } | {
3980
- status: 'failed';
3981
- } | {
3982
- status: 'cancelled';
3983
- };
3969
+ type ProcessPaymentResult = operations['PaymentIntentSdkController_processPaymentIntent']['responses']['200']['content']['application/json'];
3984
3970
  /**
3985
3971
  * Result from processing a credit-topup payment intent.
3986
3972
  *
@@ -4291,6 +4277,10 @@ type PaywallGateRecoveryFields = {
4291
4277
  shortfallCredits?: number;
4292
4278
  /** How many calls the current allowance or wallet still covers. */
4293
4279
  remainingCalls?: number;
4280
+ /** Credits per minor currency unit, from the limits balance peg. */
4281
+ creditsPerMinorUnit?: number;
4282
+ /** USD → display-currency rate used with `creditsPerMinorUnit`. */
4283
+ displayExchangeRate?: number;
4294
4284
  /** Active purchase reference when the customer already holds a purchase. */
4295
4285
  purchaseRef?: string;
4296
4286
  /** Purchase/plan status when the backend sent one. */
@@ -5354,6 +5344,12 @@ interface SolvaPay {
5354
5344
  */
5355
5345
  paywall: {
5356
5346
  decide<TArgs extends PaywallArgs>(args: TArgs, metadata?: PaywallMetadata, getCustomerRef?: (args: TArgs) => string): Promise<PaywallDecision<TArgs>>;
5347
+ /**
5348
+ * Drop cached `checkLimits` snapshots for this customer (optionally
5349
+ * scoped to one product). Call after `activate_plan`, a completed
5350
+ * checkout, or a top-up so the next payable call refetches.
5351
+ */
5352
+ invalidateLimits(customerRef: string, product?: string): void;
5357
5353
  };
5358
5354
  /**
5359
5355
  * Direct access to the API client for advanced operations.
@@ -6144,7 +6140,13 @@ declare function processTopupPaymentIntentCore(request: Request, body: {
6144
6140
  declare function createCheckoutSessionCore(request: Request, body: {
6145
6141
  productRef: string;
6146
6142
  planRef?: string;
6147
- returnUrl?: string;
6143
+ /**
6144
+ * URL to send the customer back to after payment. Pass `null` to omit
6145
+ * the field entirely — skips `options.returnUrl` and the request-origin
6146
+ * fallback. Use this when the caller has no browsable page to return to
6147
+ * (MCP servers whose public URL is an API endpoint, not a website).
6148
+ */
6149
+ returnUrl?: string | null;
6148
6150
  purpose?: 'credit_topup';
6149
6151
  }, options?: {
6150
6152
  solvaPay?: SolvaPay;
package/dist/edge.js CHANGED
@@ -683,9 +683,8 @@ async function verifyProductConfiguration(options) {
683
683
  }
684
684
 
685
685
  // src/paywall-state.ts
686
- import { minorUnitsPerMajor } from "@solvapay/core";
686
+ import { creditsToDisplayMinorUnits, minorUnitsPerMajor } from "@solvapay/core";
687
687
  var CHECKOUT_SESSION_TTL_MINUTES = 15;
688
- var DOCS_HINT = "See docs://solvapay/overview.md.";
689
688
  function creditSignals(limits) {
690
689
  if (!limits) return { isCreditBased: false };
691
690
  const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
@@ -769,6 +768,24 @@ function formatMinor(amountMinor, currency) {
769
768
  function formatCredits(value) {
770
769
  return value.toLocaleString("en-US");
771
770
  }
771
+ function formatCreditsWithMoney(credits, gate) {
772
+ const amount = formatCredits(credits);
773
+ const peg = gate.creditsPerMinorUnit;
774
+ const currency = gate.currency;
775
+ const rate = gate.displayExchangeRate;
776
+ if (peg && peg > 0 && currency && rate != null && rate > 0) {
777
+ const minor = creditsToDisplayMinorUnits({
778
+ credits,
779
+ creditsPerMinorUnit: peg,
780
+ displayExchangeRate: rate,
781
+ displayCurrency: currency
782
+ });
783
+ if (minor != null) {
784
+ return `${amount} credits (~${formatMinor(minor, currency)})`;
785
+ }
786
+ }
787
+ return `${amount} credits`;
788
+ }
772
789
  function meterLabel(gate) {
773
790
  if (!gate.meterName) return "units";
774
791
  return gate.meterName.replace(/_/g, " ");
@@ -812,49 +829,51 @@ function buildGateMessage(state, gate) {
812
829
  const nextLine = price ? ` The next call is ${price}.` : "";
813
830
  const planName = gate.planName ?? "This plan";
814
831
  const antiTrap = gate.creditBalance !== void 0 && gate.creditBalance > 0 && (gate.creditsPerCall === void 0 || gate.creditsPerCall === 0) ? ` Adding credits will not help, because ${planName} does not spend them.` : "";
815
- const switchLine = ladder ? ` Or switch plan: ${ladder}.` : recoverClause(url, "continue", "checkout");
816
- return `${usedLine}${nextLine}${antiTrap}${switchLine} ${DOCS_HINT}`;
832
+ const switchLine = ladder ? ` Or switch plan: ${ladder}. ${callViewer("account").replace(/^c/, "C")} for usage and recovery.` : recoverClause(url, "continue", "checkout");
833
+ return `${usedLine}${nextLine}${antiTrap}${switchLine}`;
817
834
  }
818
835
  case "activation_required":
819
- return `Your plan needs activation.${recoverClause(url, "activate", "checkout")} Or call \`activate_plan\` with a \`planRef\`. ${DOCS_HINT}`;
836
+ return `Your plan needs activation.${recoverClause(url, "activate", "checkout")} Or call \`activate_plan\` with a \`planRef\`.`;
820
837
  case "topup_required": {
821
838
  const balance = gate.creditBalance;
822
839
  const cost = gate.creditsPerCall;
823
840
  const shortfall = gate.shortfallCredits;
824
841
  let lead;
825
842
  if (balance !== void 0 && cost !== void 0 && shortfall !== void 0) {
826
- lead = `Out of credits for this call. Balance ${formatCredits(balance)} credits; this call costs ${formatCredits(cost)} credits \u2014 ${formatCredits(shortfall)} short.`;
843
+ lead = `Out of credits for this call. Balance ${formatCreditsWithMoney(balance, gate)}; this call costs ${formatCreditsWithMoney(cost, gate)} \u2014 ${formatCredits(shortfall)} short.`;
827
844
  } else {
828
845
  lead = "Included usage is exhausted.";
829
846
  }
830
847
  const topup = recoverClause(url, "add credits", "topup", "Add credits");
831
- const auto = autoRechargeDisabled(gate) ? " Auto-recharge is off \u2014 turn it on from the account tool to avoid this next time." : "";
832
- const switchLine = ladder ? ` Or switch plan: ${ladder}.` : "";
833
- return `${lead}${topup}${auto}${switchLine} ${DOCS_HINT}`;
848
+ const auto = autoRechargeDisabled(gate) ? ` Auto-recharge is off \u2014 ${callViewer("account")} to turn it on and avoid this next time.` : "";
849
+ const switchLine = ladder ? ` Or switch plan: ${ladder}.` : hasActivePlan(gate) ? "" : ` Or ${callViewer("checkout")} to pick a plan.`;
850
+ return `${lead}${topup}${auto}${switchLine}`;
834
851
  }
835
852
  case "upgrade_required": {
836
853
  if (hasActivePlan(gate)) {
837
854
  const planName = gate.planName ?? "This plan";
838
855
  const lead = `${planName} is active but its included usage is exhausted, and the automatic switch to the next plan did not complete.`;
839
856
  if (ladder) {
840
- return `${lead} Switch here: ${ladder}. ${DOCS_HINT}`;
857
+ return `${lead} Switch here: ${ladder}. ${callViewer("account").replace(/^c/, "C")} for usage and recovery.`;
841
858
  }
842
- return `${lead}${recoverClause(url, "switch plan", "checkout")} ${DOCS_HINT}`;
859
+ return `${lead}${recoverClause(url, "switch plan", "checkout")}`;
843
860
  }
844
861
  if (ladder) {
845
- return `You don't have an active plan for this tool. Pick a plan to use this tool: ${ladder} (links expire in ${CHECKOUT_SESSION_TTL_MINUTES} minutes), or ${callViewer("checkout")}. ${DOCS_HINT}`;
862
+ return `You don't have an active plan for this tool. Pick a plan to use this tool: ${ladder} (links expire in ${CHECKOUT_SESSION_TTL_MINUTES} minutes), or ${callViewer("checkout")}. ${callViewer("account").replace(/^c/, "C")} for usage and recovery.`;
846
863
  }
847
- return `You don't have an active plan for this tool.${recoverClause(url, "pick a plan", "checkout")} ${DOCS_HINT}`;
864
+ return `You don't have an active plan for this tool.${recoverClause(url, "pick a plan", "checkout")}`;
848
865
  }
849
866
  case "reactivation_required":
850
- return `Your previous plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it, or ${callViewer("checkout")} to pick a new plan. ${DOCS_HINT}`;
867
+ return `Your previous plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it, or ${callViewer("checkout")} to pick a new plan.`;
851
868
  }
852
869
  }
853
870
  function buildNudgeMessage(state, limits) {
854
871
  const url = limits?.checkoutUrl && limits.checkoutUrl.length > 0 ? limits.checkoutUrl : null;
855
872
  const visitClause = url ? `, or ${namedCheckoutMarkdown(url)}` : "";
856
- const remaining = creditSignals(limits).remainingCalls;
857
- switch (state.kind) {
873
+ const signals = creditSignals(limits);
874
+ const remaining = signals.remainingCalls;
875
+ const kind = state.kind === "topup_required" && limits && !signals.isCreditBased ? "limit_reached" : state.kind;
876
+ switch (kind) {
858
877
  case "topup_required":
859
878
  if (remaining === 0) {
860
879
  return `Heads up \u2014 0 calls left \u2014 the next call needs a top-up. ${callViewer("topup").replace(/^c/, "C")} to add more${visitClause}.`;
@@ -939,6 +958,8 @@ function recoveryFields(limits, state) {
939
958
  ...signals.creditsPerCall !== void 0 ? { creditsPerCall: signals.creditsPerCall } : {},
940
959
  ...signals.shortfallCredits !== void 0 ? { shortfallCredits: signals.shortfallCredits } : {},
941
960
  ...signals.remainingCalls !== void 0 ? { remainingCalls: signals.remainingCalls } : {},
961
+ ...limits.balance?.creditsPerMinorUnit !== void 0 ? { creditsPerMinorUnit: limits.balance.creditsPerMinorUnit } : {},
962
+ ...limits.balance?.displayExchangeRate !== void 0 ? { displayExchangeRate: limits.balance.displayExchangeRate } : {},
942
963
  ...limits.purchaseRef !== void 0 ? { purchaseRef: limits.purchaseRef } : {},
943
964
  ...limits.autoRecharge !== void 0 ? { autoRecharge: limits.autoRecharge } : {},
944
965
  ...links ? { links } : {}
@@ -1195,6 +1216,23 @@ var SolvaPayPaywall = class {
1195
1216
  console.log(...args);
1196
1217
  }
1197
1218
  }
1219
+ /**
1220
+ * Drop cached limits for `customerRef`. When `product` is omitted every
1221
+ * meter/product entry for that customer is removed. Matches both the
1222
+ * backend `cus_…` ref and an unmapped external ref used as a cache key.
1223
+ */
1224
+ invalidateLimits(customerRef, product) {
1225
+ const aliases = /* @__PURE__ */ new Set([customerRef]);
1226
+ const mapped = this.customerRefMapping.get(customerRef);
1227
+ if (mapped) aliases.add(mapped);
1228
+ if (!customerRef.startsWith("cus_")) aliases.add(`cus_${customerRef}`);
1229
+ for (const key of [...this.limitsCache.keys()]) {
1230
+ const [ref, prod] = key.split(":");
1231
+ if (!ref || !aliases.has(ref)) continue;
1232
+ if (product && prod !== product) continue;
1233
+ this.limitsCache.delete(key);
1234
+ }
1235
+ }
1198
1236
  resolveProduct(metadata) {
1199
1237
  return requireProductRef(metadata.product);
1200
1238
  }
@@ -2214,6 +2252,9 @@ function createSolvaPay(config) {
2214
2252
  paywall: {
2215
2253
  decide(args, metadata, getCustomerRef) {
2216
2254
  return paywall.decide(args, metadata, getCustomerRef);
2255
+ },
2256
+ invalidateLimits(customerRef, product) {
2257
+ paywall.invalidateLimits(customerRef, product);
2217
2258
  }
2218
2259
  },
2219
2260
  // Common API methods exposed directly for convenience
@@ -2282,7 +2323,7 @@ function createSolvaPay(config) {
2282
2323
  customerRef: params.customerRef,
2283
2324
  productRef: params.productRef,
2284
2325
  planRef: params.planRef,
2285
- returnUrl: params.returnUrl,
2326
+ ...params.returnUrl ? { returnUrl: params.returnUrl } : {},
2286
2327
  ...params.purpose ? { purpose: params.purpose } : {}
2287
2328
  });
2288
2329
  },
@@ -2981,12 +3022,15 @@ async function createCheckoutSessionCore(request, body, options = {}) {
2981
3022
  return customerResult;
2982
3023
  }
2983
3024
  const customerRef = customerResult;
2984
- let returnUrl = body.returnUrl || options.returnUrl;
2985
- if (!returnUrl) {
2986
- try {
2987
- const url = new URL(request.url);
2988
- returnUrl = url.origin;
2989
- } catch {
3025
+ let returnUrl;
3026
+ if (body.returnUrl !== null) {
3027
+ returnUrl = body.returnUrl || options.returnUrl;
3028
+ if (!returnUrl) {
3029
+ try {
3030
+ const url = new URL(request.url);
3031
+ returnUrl = url.origin;
3032
+ } catch {
3033
+ }
2990
3034
  }
2991
3035
  }
2992
3036
  const solvaPay = options.solvaPay || createSolvaPay();
@@ -2994,7 +3038,7 @@ async function createCheckoutSessionCore(request, body, options = {}) {
2994
3038
  productRef: body.productRef,
2995
3039
  customerRef,
2996
3040
  planRef: body.planRef || void 0,
2997
- returnUrl,
3041
+ ...returnUrl ? { returnUrl } : {},
2998
3042
  ...body.purpose ? { purpose: body.purpose } : {}
2999
3043
  });
3000
3044
  return {
@@ -841,7 +841,6 @@ function createSolvaPayClient(opts) {
841
841
  // src/paywall-state.ts
842
842
  var import_core3 = require("@solvapay/core");
843
843
  var CHECKOUT_SESSION_TTL_MINUTES = 15;
844
- var DOCS_HINT = "See docs://solvapay/overview.md.";
845
844
  function creditSignals(limits) {
846
845
  if (!limits) return { isCreditBased: false };
847
846
  const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
@@ -925,6 +924,24 @@ function formatMinor(amountMinor, currency) {
925
924
  function formatCredits(value) {
926
925
  return value.toLocaleString("en-US");
927
926
  }
927
+ function formatCreditsWithMoney(credits, gate) {
928
+ const amount = formatCredits(credits);
929
+ const peg = gate.creditsPerMinorUnit;
930
+ const currency = gate.currency;
931
+ const rate = gate.displayExchangeRate;
932
+ if (peg && peg > 0 && currency && rate != null && rate > 0) {
933
+ const minor = (0, import_core3.creditsToDisplayMinorUnits)({
934
+ credits,
935
+ creditsPerMinorUnit: peg,
936
+ displayExchangeRate: rate,
937
+ displayCurrency: currency
938
+ });
939
+ if (minor != null) {
940
+ return `${amount} credits (~${formatMinor(minor, currency)})`;
941
+ }
942
+ }
943
+ return `${amount} credits`;
944
+ }
928
945
  function meterLabel(gate) {
929
946
  if (!gate.meterName) return "units";
930
947
  return gate.meterName.replace(/_/g, " ");
@@ -968,42 +985,42 @@ function buildGateMessage(state, gate) {
968
985
  const nextLine = price ? ` The next call is ${price}.` : "";
969
986
  const planName = gate.planName ?? "This plan";
970
987
  const antiTrap = gate.creditBalance !== void 0 && gate.creditBalance > 0 && (gate.creditsPerCall === void 0 || gate.creditsPerCall === 0) ? ` Adding credits will not help, because ${planName} does not spend them.` : "";
971
- const switchLine = ladder ? ` Or switch plan: ${ladder}.` : recoverClause(url, "continue", "checkout");
972
- return `${usedLine}${nextLine}${antiTrap}${switchLine} ${DOCS_HINT}`;
988
+ const switchLine = ladder ? ` Or switch plan: ${ladder}. ${callViewer("account").replace(/^c/, "C")} for usage and recovery.` : recoverClause(url, "continue", "checkout");
989
+ return `${usedLine}${nextLine}${antiTrap}${switchLine}`;
973
990
  }
974
991
  case "activation_required":
975
- return `Your plan needs activation.${recoverClause(url, "activate", "checkout")} Or call \`activate_plan\` with a \`planRef\`. ${DOCS_HINT}`;
992
+ return `Your plan needs activation.${recoverClause(url, "activate", "checkout")} Or call \`activate_plan\` with a \`planRef\`.`;
976
993
  case "topup_required": {
977
994
  const balance = gate.creditBalance;
978
995
  const cost = gate.creditsPerCall;
979
996
  const shortfall = gate.shortfallCredits;
980
997
  let lead;
981
998
  if (balance !== void 0 && cost !== void 0 && shortfall !== void 0) {
982
- lead = `Out of credits for this call. Balance ${formatCredits(balance)} credits; this call costs ${formatCredits(cost)} credits \u2014 ${formatCredits(shortfall)} short.`;
999
+ lead = `Out of credits for this call. Balance ${formatCreditsWithMoney(balance, gate)}; this call costs ${formatCreditsWithMoney(cost, gate)} \u2014 ${formatCredits(shortfall)} short.`;
983
1000
  } else {
984
1001
  lead = "Included usage is exhausted.";
985
1002
  }
986
1003
  const topup = recoverClause(url, "add credits", "topup", "Add credits");
987
- const auto = autoRechargeDisabled(gate) ? " Auto-recharge is off \u2014 turn it on from the account tool to avoid this next time." : "";
988
- const switchLine = ladder ? ` Or switch plan: ${ladder}.` : "";
989
- return `${lead}${topup}${auto}${switchLine} ${DOCS_HINT}`;
1004
+ const auto = autoRechargeDisabled(gate) ? ` Auto-recharge is off \u2014 ${callViewer("account")} to turn it on and avoid this next time.` : "";
1005
+ const switchLine = ladder ? ` Or switch plan: ${ladder}.` : hasActivePlan(gate) ? "" : ` Or ${callViewer("checkout")} to pick a plan.`;
1006
+ return `${lead}${topup}${auto}${switchLine}`;
990
1007
  }
991
1008
  case "upgrade_required": {
992
1009
  if (hasActivePlan(gate)) {
993
1010
  const planName = gate.planName ?? "This plan";
994
1011
  const lead = `${planName} is active but its included usage is exhausted, and the automatic switch to the next plan did not complete.`;
995
1012
  if (ladder) {
996
- return `${lead} Switch here: ${ladder}. ${DOCS_HINT}`;
1013
+ return `${lead} Switch here: ${ladder}. ${callViewer("account").replace(/^c/, "C")} for usage and recovery.`;
997
1014
  }
998
- return `${lead}${recoverClause(url, "switch plan", "checkout")} ${DOCS_HINT}`;
1015
+ return `${lead}${recoverClause(url, "switch plan", "checkout")}`;
999
1016
  }
1000
1017
  if (ladder) {
1001
- return `You don't have an active plan for this tool. Pick a plan to use this tool: ${ladder} (links expire in ${CHECKOUT_SESSION_TTL_MINUTES} minutes), or ${callViewer("checkout")}. ${DOCS_HINT}`;
1018
+ return `You don't have an active plan for this tool. Pick a plan to use this tool: ${ladder} (links expire in ${CHECKOUT_SESSION_TTL_MINUTES} minutes), or ${callViewer("checkout")}. ${callViewer("account").replace(/^c/, "C")} for usage and recovery.`;
1002
1019
  }
1003
- return `You don't have an active plan for this tool.${recoverClause(url, "pick a plan", "checkout")} ${DOCS_HINT}`;
1020
+ return `You don't have an active plan for this tool.${recoverClause(url, "pick a plan", "checkout")}`;
1004
1021
  }
1005
1022
  case "reactivation_required":
1006
- return `Your previous plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it, or ${callViewer("checkout")} to pick a new plan. ${DOCS_HINT}`;
1023
+ return `Your previous plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it, or ${callViewer("checkout")} to pick a new plan.`;
1007
1024
  }
1008
1025
  }
1009
1026
 
@@ -1073,6 +1090,8 @@ function recoveryFields(limits, state) {
1073
1090
  ...signals.creditsPerCall !== void 0 ? { creditsPerCall: signals.creditsPerCall } : {},
1074
1091
  ...signals.shortfallCredits !== void 0 ? { shortfallCredits: signals.shortfallCredits } : {},
1075
1092
  ...signals.remainingCalls !== void 0 ? { remainingCalls: signals.remainingCalls } : {},
1093
+ ...limits.balance?.creditsPerMinorUnit !== void 0 ? { creditsPerMinorUnit: limits.balance.creditsPerMinorUnit } : {},
1094
+ ...limits.balance?.displayExchangeRate !== void 0 ? { displayExchangeRate: limits.balance.displayExchangeRate } : {},
1076
1095
  ...limits.purchaseRef !== void 0 ? { purchaseRef: limits.purchaseRef } : {},
1077
1096
  ...limits.autoRecharge !== void 0 ? { autoRecharge: limits.autoRecharge } : {},
1078
1097
  ...links ? { links } : {}
@@ -1329,6 +1348,23 @@ var SolvaPayPaywall = class {
1329
1348
  console.log(...args);
1330
1349
  }
1331
1350
  }
1351
+ /**
1352
+ * Drop cached limits for `customerRef`. When `product` is omitted every
1353
+ * meter/product entry for that customer is removed. Matches both the
1354
+ * backend `cus_…` ref and an unmapped external ref used as a cache key.
1355
+ */
1356
+ invalidateLimits(customerRef, product) {
1357
+ const aliases = /* @__PURE__ */ new Set([customerRef]);
1358
+ const mapped = this.customerRefMapping.get(customerRef);
1359
+ if (mapped) aliases.add(mapped);
1360
+ if (!customerRef.startsWith("cus_")) aliases.add(`cus_${customerRef}`);
1361
+ for (const key of [...this.limitsCache.keys()]) {
1362
+ const [ref, prod] = key.split(":");
1363
+ if (!ref || !aliases.has(ref)) continue;
1364
+ if (product && prod !== product) continue;
1365
+ this.limitsCache.delete(key);
1366
+ }
1367
+ }
1332
1368
  resolveProduct(metadata) {
1333
1369
  return requireProductRef(metadata.product);
1334
1370
  }
@@ -2348,6 +2384,9 @@ function createSolvaPay(config) {
2348
2384
  paywall: {
2349
2385
  decide(args, metadata, getCustomerRef) {
2350
2386
  return paywall.decide(args, metadata, getCustomerRef);
2387
+ },
2388
+ invalidateLimits(customerRef, product) {
2389
+ paywall.invalidateLimits(customerRef, product);
2351
2390
  }
2352
2391
  },
2353
2392
  // Common API methods exposed directly for convenience
@@ -2416,7 +2455,7 @@ function createSolvaPay(config) {
2416
2455
  customerRef: params.customerRef,
2417
2456
  productRef: params.productRef,
2418
2457
  planRef: params.planRef,
2419
- returnUrl: params.returnUrl,
2458
+ ...params.returnUrl ? { returnUrl: params.returnUrl } : {},
2420
2459
  ...params.purpose ? { purpose: params.purpose } : {}
2421
2460
  });
2422
2461
  },
@@ -2762,12 +2801,15 @@ async function createCheckoutSessionCore(request, body, options = {}) {
2762
2801
  return customerResult;
2763
2802
  }
2764
2803
  const customerRef = customerResult;
2765
- let returnUrl = body.returnUrl || options.returnUrl;
2766
- if (!returnUrl) {
2767
- try {
2768
- const url = new URL(request.url);
2769
- returnUrl = url.origin;
2770
- } catch {
2804
+ let returnUrl;
2805
+ if (body.returnUrl !== null) {
2806
+ returnUrl = body.returnUrl || options.returnUrl;
2807
+ if (!returnUrl) {
2808
+ try {
2809
+ const url = new URL(request.url);
2810
+ returnUrl = url.origin;
2811
+ } catch {
2812
+ }
2771
2813
  }
2772
2814
  }
2773
2815
  const solvaPay = options.solvaPay || createSolvaPay();
@@ -2775,7 +2817,7 @@ async function createCheckoutSessionCore(request, body, options = {}) {
2775
2817
  productRef: body.productRef,
2776
2818
  customerRef,
2777
2819
  planRef: body.planRef || void 0,
2778
- returnUrl,
2820
+ ...returnUrl ? { returnUrl } : {},
2779
2821
  ...body.purpose ? { purpose: body.purpose } : {}
2780
2822
  });
2781
2823
  return {
@@ -823,9 +823,13 @@ interface components {
823
823
  LimitBalanceDto: {
824
824
  /** @description Credit balance in credits (100 credits = 1 minor currency unit) */
825
825
  creditBalance: number;
826
+ /** @description Credits per minor unit of `currency` (typically 100) */
827
+ creditsPerMinorUnit?: number;
826
828
  /** @description Credits deducted per metered item (wallet coverage: remainingUnits = balance / creditsPerUnit) */
827
829
  creditsPerUnit: number;
828
830
  currency: string;
831
+ /** @description USD → display-currency rate used with creditsPerMinorUnit */
832
+ displayExchangeRate?: number;
829
833
  /** @description How many metered items the credit balance still covers (`balance / creditsPerUnit`) */
830
834
  remainingUnits?: number;
831
835
  };
@@ -1198,17 +1202,17 @@ interface components {
1198
1202
  };
1199
1203
  ProcessPaymentCancelled: {
1200
1204
  /**
1201
- * discriminator enum property added by openapi-typescript
1205
+ * @example cancelled
1202
1206
  * @enum {string}
1203
1207
  */
1204
- status: 'ProcessPaymentCancelled';
1208
+ status: 'cancelled';
1205
1209
  };
1206
1210
  ProcessPaymentFailed: {
1207
1211
  /**
1208
- * discriminator enum property added by openapi-typescript
1212
+ * @example failed
1209
1213
  * @enum {string}
1210
1214
  */
1211
- status: 'ProcessPaymentFailed';
1215
+ status: 'failed';
1212
1216
  };
1213
1217
  ProcessPaymentIntentDto: {
1214
1218
  customerRef: string;
@@ -1217,25 +1221,25 @@ interface components {
1217
1221
  };
1218
1222
  ProcessPaymentProcessing: {
1219
1223
  /**
1220
- * discriminator enum property added by openapi-typescript
1224
+ * @example processing
1221
1225
  * @enum {string}
1222
1226
  */
1223
- status: 'ProcessPaymentProcessing';
1227
+ status: 'processing';
1224
1228
  };
1225
1229
  ProcessPaymentSucceededBare: {
1226
1230
  /**
1227
- * discriminator enum property added by openapi-typescript
1231
+ * @example succeeded
1228
1232
  * @enum {string}
1229
1233
  */
1230
- status: 'ProcessPaymentSucceededBare';
1234
+ status: 'succeeded';
1231
1235
  };
1232
1236
  ProcessPaymentSucceededOneTime: {
1233
1237
  oneTimePurchase: components['schemas']['OneTimePurchaseInfo'];
1234
1238
  /**
1235
- * discriminator enum property added by openapi-typescript
1239
+ * @example succeeded
1236
1240
  * @enum {string}
1237
1241
  */
1238
- status: 'ProcessPaymentSucceededOneTime';
1242
+ status: 'succeeded';
1239
1243
  /**
1240
1244
  * @example one-time
1241
1245
  * @enum {string}
@@ -1245,10 +1249,10 @@ interface components {
1245
1249
  ProcessPaymentSucceededRecurring: {
1246
1250
  purchase: components['schemas']['SdkPurchaseResponse'];
1247
1251
  /**
1248
- * discriminator enum property added by openapi-typescript
1252
+ * @example succeeded
1249
1253
  * @enum {string}
1250
1254
  */
1251
- status: 'ProcessPaymentSucceededRecurring';
1255
+ status: 'succeeded';
1252
1256
  /**
1253
1257
  * @example recurring
1254
1258
  * @enum {string}
@@ -1259,10 +1263,10 @@ interface components {
1259
1263
  /** @description Detail message describing the timeout */
1260
1264
  message?: string;
1261
1265
  /**
1262
- * discriminator enum property added by openapi-typescript
1266
+ * @example timeout
1263
1267
  * @enum {string}
1264
1268
  */
1265
- status: 'ProcessPaymentTimeout';
1269
+ status: 'timeout';
1266
1270
  };
1267
1271
  ProductConfigDto: {
1268
1272
  /**