@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 +1 -1
- package/dist/index.cjs +73 -8
- package/dist/index.d.cts +330 -13
- package/dist/index.d.ts +330 -13
- package/dist/index.js +69 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -87,27 +87,33 @@ function pickActivePromo(promotions, targetKey, locale, cycle, today = /* @__PUR
|
|
|
87
87
|
return matches[0] ?? null;
|
|
88
88
|
}
|
|
89
89
|
function applyPromo(promo, basePrice) {
|
|
90
|
+
const result = boundedPromo(promo, basePrice);
|
|
91
|
+
return result && result.discounted < result.original ? result : null;
|
|
92
|
+
}
|
|
93
|
+
function boundedPromo(promo, basePrice) {
|
|
90
94
|
if (!promo || basePrice === null || basePrice === void 0) return null;
|
|
91
95
|
if (promo.type === "percent" && typeof promo.value === "number") {
|
|
96
|
+
const pct = withinBounds(promo.value, 100);
|
|
92
97
|
return {
|
|
93
98
|
kind: "percent",
|
|
94
|
-
discounted: Math.round(basePrice * (100 -
|
|
99
|
+
discounted: Math.round(basePrice * (100 - pct)) / 100,
|
|
95
100
|
original: basePrice,
|
|
96
|
-
pct
|
|
101
|
+
pct
|
|
97
102
|
};
|
|
98
103
|
}
|
|
99
104
|
if (promo.type === "amount" && typeof promo.value === "number") {
|
|
105
|
+
const saved = withinBounds(promo.value, basePrice);
|
|
100
106
|
return {
|
|
101
107
|
kind: "amount",
|
|
102
|
-
discounted: Math.
|
|
108
|
+
discounted: Math.round((basePrice - saved) * 100) / 100,
|
|
103
109
|
original: basePrice,
|
|
104
|
-
saved
|
|
110
|
+
saved
|
|
105
111
|
};
|
|
106
112
|
}
|
|
107
|
-
if (promo.type === "intro" && typeof promo.value === "object") {
|
|
113
|
+
if (promo.type === "intro" && typeof promo.value === "object" && promo.value !== null) {
|
|
108
114
|
return {
|
|
109
115
|
kind: "intro",
|
|
110
|
-
discounted: promo.value.price,
|
|
116
|
+
discounted: withinBounds(promo.value.price, basePrice),
|
|
111
117
|
original: basePrice,
|
|
112
118
|
months: promo.value.months
|
|
113
119
|
};
|
|
@@ -117,6 +123,14 @@ function applyPromo(promo, basePrice) {
|
|
|
117
123
|
}
|
|
118
124
|
return null;
|
|
119
125
|
}
|
|
126
|
+
function withinBounds(value, upper) {
|
|
127
|
+
return Math.min(Math.max(value, 0), Math.max(upper, 0));
|
|
128
|
+
}
|
|
129
|
+
function promotionOnPrice(promotions, targetKey, locale, cycle, basePrice, today = /* @__PURE__ */ new Date(), targetType = "PLAN") {
|
|
130
|
+
const promotion = pickActivePromo(promotions, targetKey, locale, cycle, today, targetType);
|
|
131
|
+
const result = applyPromo(promotion, basePrice);
|
|
132
|
+
return promotion && result ? { promotion, result } : null;
|
|
133
|
+
}
|
|
120
134
|
|
|
121
135
|
// src/quota-value.ts
|
|
122
136
|
function readQuotaValue(value) {
|
|
@@ -413,6 +427,12 @@ var CATALOG_ERROR_CODES = {
|
|
|
413
427
|
FEATURE_NOT_FOUND: "FEATURE_NOT_FOUND",
|
|
414
428
|
QUOTA_NOT_FOUND: "QUOTA_NOT_FOUND",
|
|
415
429
|
PROMOTION_NOT_FOUND: "PROMOTION_NOT_FOUND",
|
|
430
|
+
/**
|
|
431
|
+
* A promotion's value is not one its type takes: a percentage above 0 and
|
|
432
|
+
* at most 100, an amount above 0, an intro price of at least 0 for a whole
|
|
433
|
+
* number of months, or a whole number of free months.
|
|
434
|
+
*/
|
|
435
|
+
PROMOTION_VALUE_INVALID: "PROMOTION_VALUE_INVALID",
|
|
416
436
|
MARKETING_PROJECTION_NOT_FOUND: "MARKETING_PROJECTION_NOT_FOUND",
|
|
417
437
|
// ── already exists ──
|
|
418
438
|
PLAN_ALREADY_EXISTS: "PLAN_ALREADY_EXISTS",
|
|
@@ -572,6 +592,13 @@ var CONTRACT_ERROR_CODES = {
|
|
|
572
592
|
SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH",
|
|
573
593
|
SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT: "SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT",
|
|
574
594
|
SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH",
|
|
595
|
+
/**
|
|
596
|
+
* The lines do not add up to a total the contract states, counted as often
|
|
597
|
+
* as each falls due in one period. `field` names the total.
|
|
598
|
+
*/
|
|
599
|
+
SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP: "SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP",
|
|
600
|
+
/** Something that takes money off states a negative amount. */
|
|
601
|
+
SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE",
|
|
575
602
|
SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START",
|
|
576
603
|
// ── stage 3 ──
|
|
577
604
|
CHECKOUT_OFFER_NOT_FOUND: "CHECKOUT_OFFER_NOT_FOUND",
|
|
@@ -1114,6 +1141,30 @@ function oneOf(allowed, value, column, id) {
|
|
|
1114
1141
|
}
|
|
1115
1142
|
return match;
|
|
1116
1143
|
}
|
|
1144
|
+
var FOREIGN_PAYMENT_METHOD_REFERENCE = "FOREIGN_PAYMENT_METHOD_REFERENCE";
|
|
1145
|
+
var ForeignPaymentMethodReferenceError = class extends Error {
|
|
1146
|
+
constructor(gatewayAccount, paymentMethodRef) {
|
|
1147
|
+
super(
|
|
1148
|
+
`Payment method '${paymentMethodRef}' of account '${gatewayAccount}' belongs to another subscriber. A reference belongs to exactly one, so it is not handed out.`
|
|
1149
|
+
);
|
|
1150
|
+
this.gatewayAccount = gatewayAccount;
|
|
1151
|
+
this.paymentMethodRef = paymentMethodRef;
|
|
1152
|
+
this.name = "ForeignPaymentMethodReferenceError";
|
|
1153
|
+
}
|
|
1154
|
+
gatewayAccount;
|
|
1155
|
+
paymentMethodRef;
|
|
1156
|
+
code = FOREIGN_PAYMENT_METHOD_REFERENCE;
|
|
1157
|
+
};
|
|
1158
|
+
function isForeignPaymentMethodReferenceError(error) {
|
|
1159
|
+
return error instanceof Error && error.code === FOREIGN_PAYMENT_METHOD_REFERENCE;
|
|
1160
|
+
}
|
|
1161
|
+
function refuseForeignPaymentMethodReference(recorded, subscriberId) {
|
|
1162
|
+
if (recorded.subscriberId === subscriberId) return;
|
|
1163
|
+
throw new ForeignPaymentMethodReferenceError(
|
|
1164
|
+
recorded.gatewayAccount,
|
|
1165
|
+
recorded.paymentMethodRef
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1117
1168
|
|
|
1118
1169
|
// src/subscription-contract-mapping.ts
|
|
1119
1170
|
function toSubscriptionContractRecord(row, lineItems) {
|
|
@@ -1324,6 +1375,7 @@ var ERROR_MESSAGES_EN = {
|
|
|
1324
1375
|
FEATURE_NOT_FOUND: "Feature '{featureKey}' not found",
|
|
1325
1376
|
QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found",
|
|
1326
1377
|
PROMOTION_NOT_FOUND: "Promotion '{promotionId}' not found",
|
|
1378
|
+
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.",
|
|
1327
1379
|
MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' not found",
|
|
1328
1380
|
PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists",
|
|
1329
1381
|
BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists",
|
|
@@ -1378,7 +1430,7 @@ var ERROR_MESSAGES_EN = {
|
|
|
1378
1430
|
// ── promo ──
|
|
1379
1431
|
PROMO_CODE_NOT_FOUND: "Code not found",
|
|
1380
1432
|
PROMO_CODE_ALREADY_EXISTS: "The code already exists.",
|
|
1381
|
-
PROMO_CODE_HAS_REDEMPTIONS: "The code already has redemptions \u2014 it cannot be soft-deleted. Pause it instead.",
|
|
1433
|
+
PROMO_CODE_HAS_REDEMPTIONS: "The code already has redemptions, or checkouts holding one \u2014 it cannot be soft-deleted. Pause it instead.",
|
|
1382
1434
|
PROMO_CODE_NOT_REDEEMABLE: "Code cannot be redeemed: {reason}",
|
|
1383
1435
|
PROMO_CODE_FORMAT_INVALID: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
|
|
1384
1436
|
PROMO_PERCENT_OUT_OF_RANGE: "The percentage must be between 0 and 100.",
|
|
@@ -1408,6 +1460,8 @@ var ERROR_MESSAGES_EN = {
|
|
|
1408
1460
|
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.",
|
|
1409
1461
|
SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil must be after the effectiveFrom of the contract.",
|
|
1410
1462
|
SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "A line item must be booked in the currency its contract was priced in.",
|
|
1463
|
+
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}.",
|
|
1464
|
+
SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "A subscription contract states a discount of {amount} at {field}; a discount takes money off and is never negative.",
|
|
1411
1465
|
CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' not found",
|
|
1412
1466
|
CHECKOUT_OFFER_EXPIRED: "Checkout offer '{offerId}' has expired and cannot be {action}",
|
|
1413
1467
|
CHECKOUT_OFFER_ALREADY_CONSUMED: "Checkout offer '{offerId}' has already been consumed and cannot be {action}",
|
|
@@ -1531,6 +1585,7 @@ var ERROR_MESSAGES_DE = {
|
|
|
1531
1585
|
FEATURE_NOT_FOUND: "Feature '{featureKey}' nicht gefunden",
|
|
1532
1586
|
QUOTA_NOT_FOUND: "Quota '{quotaKey}' nicht gefunden",
|
|
1533
1587
|
PROMOTION_NOT_FOUND: "Promotion '{promotionId}' nicht gefunden",
|
|
1588
|
+
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.",
|
|
1534
1589
|
MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' nicht gefunden",
|
|
1535
1590
|
PLAN_ALREADY_EXISTS: "Plan '{planKey}' gibt es bereits",
|
|
1536
1591
|
BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' gibt es bereits",
|
|
@@ -1585,7 +1640,7 @@ var ERROR_MESSAGES_DE = {
|
|
|
1585
1640
|
// ── promo ──
|
|
1586
1641
|
PROMO_CODE_NOT_FOUND: "Code nicht gefunden",
|
|
1587
1642
|
PROMO_CODE_ALREADY_EXISTS: "Diesen Code gibt es bereits.",
|
|
1588
|
-
PROMO_CODE_HAS_REDEMPTIONS: "Der Code wurde bereits eingel\xF6st \u2014 er l\xE4sst sich nicht per Soft-Delete entfernen. Pausiere ihn stattdessen.",
|
|
1643
|
+
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.",
|
|
1589
1644
|
PROMO_CODE_NOT_REDEEMABLE: "Der Code l\xE4sst sich nicht einl\xF6sen: {reason}",
|
|
1590
1645
|
PROMO_CODE_FORMAT_INVALID: 'Der Code darf nur Gro\xDFbuchstaben, Ziffern, "-" und "_" enthalten (4\u201332 Zeichen).',
|
|
1591
1646
|
PROMO_PERCENT_OUT_OF_RANGE: "Der Prozentwert muss zwischen 0 und 100 liegen.",
|
|
@@ -1614,6 +1669,8 @@ var ERROR_MESSAGES_DE = {
|
|
|
1614
1669
|
SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH: "taxAmount einer Position muss exakt priceGross minus priceNet sein.",
|
|
1615
1670
|
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.",
|
|
1616
1671
|
SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH: "Eine Position muss in der W\xE4hrung gebucht sein, in der ihr Vertrag bepreist ist.",
|
|
1672
|
+
SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP: "Die Positionen eines Abo-Vertrags ergeben {lines} f\xFCr {field}, der Vertrag nennt aber {stated}.",
|
|
1673
|
+
SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE: "Ein Abo-Vertrag nennt bei {field} einen Rabatt von {amount}; ein Rabatt zieht Geld ab und ist nie negativ.",
|
|
1617
1674
|
SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil muss nach dem effectiveFrom des Vertrags liegen.",
|
|
1618
1675
|
CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' nicht gefunden",
|
|
1619
1676
|
CHECKOUT_OFFER_EXPIRED: "Checkout-Angebot '{offerId}' ist abgelaufen \u2014 '{action}' ist nicht mehr m\xF6glich",
|
|
@@ -1670,6 +1727,7 @@ export {
|
|
|
1670
1727
|
ERROR_MESSAGES_DE,
|
|
1671
1728
|
ERROR_MESSAGES_EN,
|
|
1672
1729
|
FEATURE_NOT_LICENSED,
|
|
1730
|
+
ForeignPaymentMethodReferenceError,
|
|
1673
1731
|
LEGAL_IDENTITY_FIELDS,
|
|
1674
1732
|
MARKETING_PRIORITY_MAX,
|
|
1675
1733
|
MARKETING_PRIORITY_MIN,
|
|
@@ -1714,6 +1772,7 @@ export {
|
|
|
1714
1772
|
formatErrorMessage,
|
|
1715
1773
|
identityCorrectionDelta,
|
|
1716
1774
|
isBundleRedundant,
|
|
1775
|
+
isForeignPaymentMethodReferenceError,
|
|
1717
1776
|
isPaymentCallbackRejectedError,
|
|
1718
1777
|
isPlatformUserExistsError,
|
|
1719
1778
|
isVersionEditable,
|
|
@@ -1725,9 +1784,11 @@ export {
|
|
|
1725
1784
|
planCatalogSettingsOf,
|
|
1726
1785
|
previousUtcDay,
|
|
1727
1786
|
promoStatus,
|
|
1787
|
+
promotionOnPrice,
|
|
1728
1788
|
readQuotaRecord,
|
|
1729
1789
|
readQuotaValue,
|
|
1730
1790
|
recordedIssuerIdentity,
|
|
1791
|
+
refuseForeignPaymentMethodReference,
|
|
1731
1792
|
resolveBundleAvailability,
|
|
1732
1793
|
resolveErrorMessage,
|
|
1733
1794
|
sameLegalIdentity,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasicat/core",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.21",
|
|
4
4
|
"description": "TypeScript contracts for SaaSiCat — capabilities, catalogs, subscriptions, entitlements, admin manifests, persistence ports — and the pure domain logic both server and browser run: error catalogues, plan-diff classification, promo arithmetic, feature requirements.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|