@saasicat/core 1.0.0-rc.20 → 1.0.0-rc.21

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/.build-stamp CHANGED
@@ -1 +1 @@
1
- 52f37c89214b0d0e2647b9735ce3fb3773f0f8e9e8d1f6d301e4df83160dbd85
1
+ 369e8bc186ed4a646394a1f9b7c7dcc8605204b4bae0e8c40edc4f3b5dd5e378
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ __export(index_exports, {
30
30
  ERROR_MESSAGES_DE: () => ERROR_MESSAGES_DE,
31
31
  ERROR_MESSAGES_EN: () => ERROR_MESSAGES_EN,
32
32
  FEATURE_NOT_LICENSED: () => FEATURE_NOT_LICENSED,
33
+ ForeignPaymentMethodReferenceError: () => ForeignPaymentMethodReferenceError,
33
34
  LEGAL_IDENTITY_FIELDS: () => LEGAL_IDENTITY_FIELDS,
34
35
  MARKETING_PRIORITY_MAX: () => MARKETING_PRIORITY_MAX,
35
36
  MARKETING_PRIORITY_MIN: () => MARKETING_PRIORITY_MIN,
@@ -74,6 +75,7 @@ __export(index_exports, {
74
75
  formatErrorMessage: () => formatErrorMessage,
75
76
  identityCorrectionDelta: () => identityCorrectionDelta,
76
77
  isBundleRedundant: () => isBundleRedundant,
78
+ isForeignPaymentMethodReferenceError: () => isForeignPaymentMethodReferenceError,
77
79
  isPaymentCallbackRejectedError: () => isPaymentCallbackRejectedError,
78
80
  isPlatformUserExistsError: () => isPlatformUserExistsError,
79
81
  isVersionEditable: () => isVersionEditable,
@@ -85,9 +87,11 @@ __export(index_exports, {
85
87
  planCatalogSettingsOf: () => planCatalogSettingsOf,
86
88
  previousUtcDay: () => previousUtcDay,
87
89
  promoStatus: () => promoStatus,
90
+ promotionOnPrice: () => promotionOnPrice,
88
91
  readQuotaRecord: () => readQuotaRecord,
89
92
  readQuotaValue: () => readQuotaValue,
90
93
  recordedIssuerIdentity: () => recordedIssuerIdentity,
94
+ refuseForeignPaymentMethodReference: () => refuseForeignPaymentMethodReference,
91
95
  resolveBundleAvailability: () => resolveBundleAvailability,
92
96
  resolveErrorMessage: () => resolveErrorMessage,
93
97
  sameLegalIdentity: () => sameLegalIdentity,
@@ -197,27 +201,33 @@ function pickActivePromo(promotions, targetKey, locale, cycle, today = /* @__PUR
197
201
  return matches[0] ?? null;
198
202
  }
199
203
  function applyPromo(promo, basePrice) {
204
+ const result = boundedPromo(promo, basePrice);
205
+ return result && result.discounted < result.original ? result : null;
206
+ }
207
+ function boundedPromo(promo, basePrice) {
200
208
  if (!promo || basePrice === null || basePrice === void 0) return null;
201
209
  if (promo.type === "percent" && typeof promo.value === "number") {
210
+ const pct = withinBounds(promo.value, 100);
202
211
  return {
203
212
  kind: "percent",
204
- discounted: Math.round(basePrice * (100 - promo.value)) / 100,
213
+ discounted: Math.round(basePrice * (100 - pct)) / 100,
205
214
  original: basePrice,
206
- pct: promo.value
215
+ pct
207
216
  };
208
217
  }
209
218
  if (promo.type === "amount" && typeof promo.value === "number") {
219
+ const saved = withinBounds(promo.value, basePrice);
210
220
  return {
211
221
  kind: "amount",
212
- discounted: Math.max(0, basePrice - promo.value),
222
+ discounted: Math.round((basePrice - saved) * 100) / 100,
213
223
  original: basePrice,
214
- saved: promo.value
224
+ saved
215
225
  };
216
226
  }
217
- if (promo.type === "intro" && typeof promo.value === "object") {
227
+ if (promo.type === "intro" && typeof promo.value === "object" && promo.value !== null) {
218
228
  return {
219
229
  kind: "intro",
220
- discounted: promo.value.price,
230
+ discounted: withinBounds(promo.value.price, basePrice),
221
231
  original: basePrice,
222
232
  months: promo.value.months
223
233
  };
@@ -227,6 +237,14 @@ function applyPromo(promo, basePrice) {
227
237
  }
228
238
  return null;
229
239
  }
240
+ function withinBounds(value, upper) {
241
+ return Math.min(Math.max(value, 0), Math.max(upper, 0));
242
+ }
243
+ function promotionOnPrice(promotions, targetKey, locale, cycle, basePrice, today = /* @__PURE__ */ new Date(), targetType = "PLAN") {
244
+ const promotion = pickActivePromo(promotions, targetKey, locale, cycle, today, targetType);
245
+ const result = applyPromo(promotion, basePrice);
246
+ return promotion && result ? { promotion, result } : null;
247
+ }
230
248
 
231
249
  // src/quota-value.ts
232
250
  function readQuotaValue(value) {
@@ -523,6 +541,12 @@ var CATALOG_ERROR_CODES = {
523
541
  FEATURE_NOT_FOUND: "FEATURE_NOT_FOUND",
524
542
  QUOTA_NOT_FOUND: "QUOTA_NOT_FOUND",
525
543
  PROMOTION_NOT_FOUND: "PROMOTION_NOT_FOUND",
544
+ /**
545
+ * A promotion's value is not one its type takes: a percentage above 0 and
546
+ * at most 100, an amount above 0, an intro price of at least 0 for a whole
547
+ * number of months, or a whole number of free months.
548
+ */
549
+ PROMOTION_VALUE_INVALID: "PROMOTION_VALUE_INVALID",
526
550
  MARKETING_PROJECTION_NOT_FOUND: "MARKETING_PROJECTION_NOT_FOUND",
527
551
  // ── already exists ──
528
552
  PLAN_ALREADY_EXISTS: "PLAN_ALREADY_EXISTS",
@@ -682,6 +706,13 @@ var CONTRACT_ERROR_CODES = {
682
706
  SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH",
683
707
  SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT: "SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT",
684
708
  SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH",
709
+ /**
710
+ * The lines do not add up to a total the contract states, counted as often
711
+ * as each falls due in one period. `field` names the total.
712
+ */
713
+ SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP: "SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP",
714
+ /** Something that takes money off states a negative amount. */
715
+ SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE",
685
716
  SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START",
686
717
  // ── stage 3 ──
687
718
  CHECKOUT_OFFER_NOT_FOUND: "CHECKOUT_OFFER_NOT_FOUND",
@@ -1224,6 +1255,30 @@ function oneOf(allowed, value, column, id) {
1224
1255
  }
1225
1256
  return match;
1226
1257
  }
1258
+ var FOREIGN_PAYMENT_METHOD_REFERENCE = "FOREIGN_PAYMENT_METHOD_REFERENCE";
1259
+ var ForeignPaymentMethodReferenceError = class extends Error {
1260
+ constructor(gatewayAccount, paymentMethodRef) {
1261
+ super(
1262
+ `Payment method '${paymentMethodRef}' of account '${gatewayAccount}' belongs to another subscriber. A reference belongs to exactly one, so it is not handed out.`
1263
+ );
1264
+ this.gatewayAccount = gatewayAccount;
1265
+ this.paymentMethodRef = paymentMethodRef;
1266
+ this.name = "ForeignPaymentMethodReferenceError";
1267
+ }
1268
+ gatewayAccount;
1269
+ paymentMethodRef;
1270
+ code = FOREIGN_PAYMENT_METHOD_REFERENCE;
1271
+ };
1272
+ function isForeignPaymentMethodReferenceError(error) {
1273
+ return error instanceof Error && error.code === FOREIGN_PAYMENT_METHOD_REFERENCE;
1274
+ }
1275
+ function refuseForeignPaymentMethodReference(recorded, subscriberId) {
1276
+ if (recorded.subscriberId === subscriberId) return;
1277
+ throw new ForeignPaymentMethodReferenceError(
1278
+ recorded.gatewayAccount,
1279
+ recorded.paymentMethodRef
1280
+ );
1281
+ }
1227
1282
 
1228
1283
  // src/subscription-contract-mapping.ts
1229
1284
  function toSubscriptionContractRecord(row, lineItems) {
@@ -1434,6 +1489,7 @@ var ERROR_MESSAGES_EN = {
1434
1489
  FEATURE_NOT_FOUND: "Feature '{featureKey}' not found",
1435
1490
  QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found",
1436
1491
  PROMOTION_NOT_FOUND: "Promotion '{promotionId}' not found",
1492
+ PROMOTION_VALUE_INVALID: "The value of a '{type}' promotion is not one its type takes: a percentage above 0 and at most 100, an amount above 0, an intro price of at least 0 for a whole number of months, or a whole number of free months.",
1437
1493
  MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' not found",
1438
1494
  PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists",
1439
1495
  BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists",
@@ -1488,7 +1544,7 @@ var ERROR_MESSAGES_EN = {
1488
1544
  // ── promo ──
1489
1545
  PROMO_CODE_NOT_FOUND: "Code not found",
1490
1546
  PROMO_CODE_ALREADY_EXISTS: "The code already exists.",
1491
- PROMO_CODE_HAS_REDEMPTIONS: "The code already has redemptions \u2014 it cannot be soft-deleted. Pause it instead.",
1547
+ PROMO_CODE_HAS_REDEMPTIONS: "The code already has redemptions, or checkouts holding one \u2014 it cannot be soft-deleted. Pause it instead.",
1492
1548
  PROMO_CODE_NOT_REDEEMABLE: "Code cannot be redeemed: {reason}",
1493
1549
  PROMO_CODE_FORMAT_INVALID: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
1494
1550
  PROMO_PERCENT_OUT_OF_RANGE: "The percentage must be between 0 and 100.",
@@ -1518,6 +1574,8 @@ var ERROR_MESSAGES_EN = {
1518
1574
  SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT: "A subscription contract states a tax rate of {taxRate} at {field}, which is not a percentage: a rate lies from 0 to 100, and a value between 0 and 1 is refused as a fraction.",
1519
1575
  SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil must be after the effectiveFrom of the contract.",
1520
1576
  SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "A line item must be booked in the currency its contract was priced in.",
1577
+ SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP: "The line items of a subscription contract add up to {lines} for {field}, but the contract states {stated}.",
1578
+ SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "A subscription contract states a discount of {amount} at {field}; a discount takes money off and is never negative.",
1521
1579
  CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' not found",
1522
1580
  CHECKOUT_OFFER_EXPIRED: "Checkout offer '{offerId}' has expired and cannot be {action}",
1523
1581
  CHECKOUT_OFFER_ALREADY_CONSUMED: "Checkout offer '{offerId}' has already been consumed and cannot be {action}",
@@ -1641,6 +1699,7 @@ var ERROR_MESSAGES_DE = {
1641
1699
  FEATURE_NOT_FOUND: "Feature '{featureKey}' nicht gefunden",
1642
1700
  QUOTA_NOT_FOUND: "Quota '{quotaKey}' nicht gefunden",
1643
1701
  PROMOTION_NOT_FOUND: "Promotion '{promotionId}' nicht gefunden",
1702
+ PROMOTION_VALUE_INVALID: "Der Wert einer Aktion vom Typ '{type}' passt nicht zu ihrem Typ: ein Prozentsatz \xFCber 0 und h\xF6chstens 100, ein Betrag \xFCber 0, ein Einf\xFChrungspreis ab 0 f\xFCr eine ganze Zahl von Monaten oder eine ganze Zahl von Gratismonaten.",
1644
1703
  MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' nicht gefunden",
1645
1704
  PLAN_ALREADY_EXISTS: "Plan '{planKey}' gibt es bereits",
1646
1705
  BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' gibt es bereits",
@@ -1695,7 +1754,7 @@ var ERROR_MESSAGES_DE = {
1695
1754
  // ── promo ──
1696
1755
  PROMO_CODE_NOT_FOUND: "Code nicht gefunden",
1697
1756
  PROMO_CODE_ALREADY_EXISTS: "Diesen Code gibt es bereits.",
1698
- PROMO_CODE_HAS_REDEMPTIONS: "Der Code wurde bereits eingel\xF6st \u2014 er l\xE4sst sich nicht per Soft-Delete entfernen. Pausiere ihn stattdessen.",
1757
+ PROMO_CODE_HAS_REDEMPTIONS: "Der Code wurde bereits eingel\xF6st oder ist f\xFCr einen laufenden Checkout reserviert \u2014 er l\xE4sst sich nicht per Soft-Delete entfernen. Pausiere ihn stattdessen.",
1699
1758
  PROMO_CODE_NOT_REDEEMABLE: "Der Code l\xE4sst sich nicht einl\xF6sen: {reason}",
1700
1759
  PROMO_CODE_FORMAT_INVALID: 'Der Code darf nur Gro\xDFbuchstaben, Ziffern, "-" und "_" enthalten (4\u201332 Zeichen).',
1701
1760
  PROMO_PERCENT_OUT_OF_RANGE: "Der Prozentwert muss zwischen 0 und 100 liegen.",
@@ -1724,6 +1783,8 @@ var ERROR_MESSAGES_DE = {
1724
1783
  SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "taxAmount einer Position muss exakt priceGross minus priceNet sein.",
1725
1784
  SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT: "Ein Abo-Vertrag nennt bei {field} einen Steuersatz von {taxRate}, der kein Prozentsatz ist: Ein Satz liegt zwischen 0 und 100, und ein Wert zwischen 0 und 1 wird als Bruch abgelehnt.",
1726
1785
  SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "Eine Position muss in der W\xE4hrung gebucht sein, in der ihr Vertrag bepreist ist.",
1786
+ SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP: "Die Positionen eines Abo-Vertrags ergeben {lines} f\xFCr {field}, der Vertrag nennt aber {stated}.",
1787
+ SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "Ein Abo-Vertrag nennt bei {field} einen Rabatt von {amount}; ein Rabatt zieht Geld ab und ist nie negativ.",
1727
1788
  SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil muss nach dem effectiveFrom des Vertrags liegen.",
1728
1789
  CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' nicht gefunden",
1729
1790
  CHECKOUT_OFFER_EXPIRED: "Checkout-Angebot '{offerId}' ist abgelaufen \u2014 '{action}' ist nicht mehr m\xF6glich",
@@ -1781,6 +1842,7 @@ var ERROR_MESSAGES_DE = {
1781
1842
  ERROR_MESSAGES_DE,
1782
1843
  ERROR_MESSAGES_EN,
1783
1844
  FEATURE_NOT_LICENSED,
1845
+ ForeignPaymentMethodReferenceError,
1784
1846
  LEGAL_IDENTITY_FIELDS,
1785
1847
  MARKETING_PRIORITY_MAX,
1786
1848
  MARKETING_PRIORITY_MIN,
@@ -1825,6 +1887,7 @@ var ERROR_MESSAGES_DE = {
1825
1887
  formatErrorMessage,
1826
1888
  identityCorrectionDelta,
1827
1889
  isBundleRedundant,
1890
+ isForeignPaymentMethodReferenceError,
1828
1891
  isPaymentCallbackRejectedError,
1829
1892
  isPlatformUserExistsError,
1830
1893
  isVersionEditable,
@@ -1836,9 +1899,11 @@ var ERROR_MESSAGES_DE = {
1836
1899
  planCatalogSettingsOf,
1837
1900
  previousUtcDay,
1838
1901
  promoStatus,
1902
+ promotionOnPrice,
1839
1903
  readQuotaRecord,
1840
1904
  readQuotaValue,
1841
1905
  recordedIssuerIdentity,
1906
+ refuseForeignPaymentMethodReference,
1842
1907
  resolveBundleAvailability,
1843
1908
  resolveErrorMessage,
1844
1909
  sameLegalIdentity,