@saasicat/core 1.0.0-rc.25 → 1.0.0-rc.27
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 +2120 -147
- package/dist/index.d.cts +3525 -182
- package/dist/index.d.ts +3525 -182
- package/dist/index.js +2006 -146
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -157,6 +157,22 @@ function computeIncludedVat(gross, vatRate) {
|
|
|
157
157
|
const rate = exactDecimal(vatRate);
|
|
158
158
|
return amountOf(cents(exactDecimal(gross), rate, sum(HUNDRED, rate)));
|
|
159
159
|
}
|
|
160
|
+
function taxPerRate(lines) {
|
|
161
|
+
const nets = /* @__PURE__ */ new Map();
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
const key = String(line.rate);
|
|
164
|
+
const entry = nets.get(key) ?? { rate: line.rate, terms: [] };
|
|
165
|
+
entry.terms.push(line.net);
|
|
166
|
+
nets.set(key, entry);
|
|
167
|
+
}
|
|
168
|
+
const rates = [...nets.values()].sort((a, b) => a.rate - b.rate).map(({ rate, terms }) => {
|
|
169
|
+
const net2 = sumToCents(...terms);
|
|
170
|
+
return { rate, net: net2, tax: percentOf(net2, rate) };
|
|
171
|
+
});
|
|
172
|
+
const net = sumToCents(...rates.map((entry) => entry.net));
|
|
173
|
+
const tax = sumToCents(...rates.map((entry) => entry.tax));
|
|
174
|
+
return { rates, net, tax, gross: sumToCents(net, tax) };
|
|
175
|
+
}
|
|
160
176
|
|
|
161
177
|
// src/promotion.types.ts
|
|
162
178
|
function promoStatus(promo, today = /* @__PURE__ */ new Date()) {
|
|
@@ -407,7 +423,13 @@ var KINDS = {
|
|
|
407
423
|
"version-retirement-reminder": true,
|
|
408
424
|
"bundle-version-retired": true,
|
|
409
425
|
"bundle-version-retirement-reminder": true,
|
|
410
|
-
"bundle-version-offered": true
|
|
426
|
+
"bundle-version-offered": true,
|
|
427
|
+
"feature-withdrawn": true,
|
|
428
|
+
"feature-withdrawal-lifted": true,
|
|
429
|
+
"ended-at-once": true,
|
|
430
|
+
"collection-failed": true,
|
|
431
|
+
"collection-announced": true,
|
|
432
|
+
"collection-reversed": true
|
|
411
433
|
};
|
|
412
434
|
function toSubscriptionNoticeRecord(row) {
|
|
413
435
|
return {
|
|
@@ -538,6 +560,106 @@ function toBundleVersionRetirementRecord(row) {
|
|
|
538
560
|
};
|
|
539
561
|
}
|
|
540
562
|
|
|
563
|
+
// src/feature-withdrawal.types.ts
|
|
564
|
+
var FEATURE_WITHDRAWAL_REASON_MAX_LENGTH = 1e3;
|
|
565
|
+
function isFeatureWithdrawnAt(withdrawal, at) {
|
|
566
|
+
return withdrawal.effectiveFrom <= at && (withdrawal.liftedFrom === null || at < withdrawal.liftedFrom);
|
|
567
|
+
}
|
|
568
|
+
function featuresWithdrawnAt(withdrawals, at) {
|
|
569
|
+
return new Set(
|
|
570
|
+
withdrawals.filter((withdrawal) => isFeatureWithdrawnAt(withdrawal, at)).map((withdrawal) => withdrawal.featureKey)
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
function nextWithdrawalChange(withdrawals, at) {
|
|
574
|
+
let next = null;
|
|
575
|
+
for (const { effectiveFrom, liftedFrom } of withdrawals) {
|
|
576
|
+
for (const moment of [effectiveFrom, liftedFrom]) {
|
|
577
|
+
if (moment === null || moment <= at) continue;
|
|
578
|
+
if (next === null || moment < next) next = moment;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return next;
|
|
582
|
+
}
|
|
583
|
+
var FEATURE_WITHDRAWAL_CAPABILITY = "features.withdraw";
|
|
584
|
+
function featureWithdrawalRowOf(record) {
|
|
585
|
+
return {
|
|
586
|
+
id: record.id,
|
|
587
|
+
featureKey: record.featureKey,
|
|
588
|
+
reason: record.reason,
|
|
589
|
+
effectiveFrom: record.effectiveFrom.toISOString(),
|
|
590
|
+
liftedFrom: record.liftedFrom?.toISOString() ?? null,
|
|
591
|
+
reductions: record.reductions,
|
|
592
|
+
announcedAt: record.announcedAt.toISOString(),
|
|
593
|
+
announcedBy: record.announcedBy,
|
|
594
|
+
liftedAt: record.liftedAt?.toISOString() ?? null,
|
|
595
|
+
liftedBy: record.liftedBy,
|
|
596
|
+
toldAt: record.toldAt?.toISOString() ?? null
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function withdrawnFeaturesOf(withdrawals, now) {
|
|
600
|
+
return withdrawals.filter(
|
|
601
|
+
({ effectiveFrom, liftedFrom }) => liftedFrom === null || liftedFrom > now && effectiveFrom < liftedFrom
|
|
602
|
+
).sort((a, b) => a.effectiveFrom.getTime() - b.effectiveFrom.getTime()).map((withdrawal) => ({
|
|
603
|
+
featureKey: withdrawal.featureKey,
|
|
604
|
+
reason: withdrawal.reason,
|
|
605
|
+
effectiveFrom: withdrawal.effectiveFrom.toISOString(),
|
|
606
|
+
liftedFrom: withdrawal.liftedFrom?.toISOString() ?? null
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
function endedAtOnceSubjectOf(subscriptionOrBookingId, at) {
|
|
610
|
+
return `${subscriptionOrBookingId}@${at.toISOString()}`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/feature-withdrawal-mapping.ts
|
|
614
|
+
var TABLE2 = "feature_withdrawals";
|
|
615
|
+
var KINDS2 = { plan: true, bundle: true };
|
|
616
|
+
var CYCLES = { MONTHLY: true, YEARLY: true };
|
|
617
|
+
function toFeatureWithdrawalRecord(row) {
|
|
618
|
+
return {
|
|
619
|
+
id: row.id,
|
|
620
|
+
featureKey: row.featureKey,
|
|
621
|
+
reason: row.reason,
|
|
622
|
+
effectiveFrom: row.effectiveFrom,
|
|
623
|
+
liftedFrom: row.liftedFrom,
|
|
624
|
+
reductions: reductionsOf(row),
|
|
625
|
+
announcedAt: row.announcedAt,
|
|
626
|
+
announcedBy: row.announcedBy,
|
|
627
|
+
liftedAt: row.liftedAt,
|
|
628
|
+
liftedBy: row.liftedBy,
|
|
629
|
+
toldAt: row.toldAt
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function reductionsOf(row) {
|
|
633
|
+
if (!Array.isArray(row.reductions)) {
|
|
634
|
+
throw new Error(`${TABLE2} row '${row.id}' holds reductions that are not a list.`);
|
|
635
|
+
}
|
|
636
|
+
return row.reductions.map((entry, index) => {
|
|
637
|
+
const column = `reductions[${index}]`;
|
|
638
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
639
|
+
throw new Error(`${TABLE2} row '${row.id}' holds ${column} that is not an object.`);
|
|
640
|
+
}
|
|
641
|
+
const { kind, key, billingCycle, amountNet } = entry;
|
|
642
|
+
if (typeof kind !== "string" || typeof key !== "string" || typeof billingCycle !== "string" || typeof amountNet !== "number" || !Number.isFinite(amountNet)) {
|
|
643
|
+
throw new Error(
|
|
644
|
+
`${TABLE2} row '${row.id}' holds ${column} without a kind, a key, a billing cycle and a finite amount.`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const source = { table: TABLE2, id: row.id };
|
|
648
|
+
return {
|
|
649
|
+
kind: oneOf(Object.keys(KINDS2), kind, {
|
|
650
|
+
...source,
|
|
651
|
+
column: `${column}.kind`
|
|
652
|
+
}),
|
|
653
|
+
key,
|
|
654
|
+
billingCycle: oneOf(Object.keys(CYCLES), billingCycle, {
|
|
655
|
+
...source,
|
|
656
|
+
column: `${column}.billingCycle`
|
|
657
|
+
}),
|
|
658
|
+
amountNet
|
|
659
|
+
};
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
541
663
|
// src/scheduled-change-after-write.ts
|
|
542
664
|
var NO_SCHEDULED_CHANGE = {
|
|
543
665
|
pendingPlan: null,
|
|
@@ -674,6 +796,37 @@ function parseZonedInstant(text) {
|
|
|
674
796
|
return Number.isNaN(moment.getTime()) ? null : moment;
|
|
675
797
|
}
|
|
676
798
|
|
|
799
|
+
// src/zoned-day.ts
|
|
800
|
+
function dayInZone(at, timeZone) {
|
|
801
|
+
if (typeof timeZone !== "string" || timeZone === "") {
|
|
802
|
+
throw new RangeError("A day is read in a named time zone.");
|
|
803
|
+
}
|
|
804
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
805
|
+
timeZone,
|
|
806
|
+
year: "numeric",
|
|
807
|
+
month: "2-digit",
|
|
808
|
+
day: "2-digit"
|
|
809
|
+
}).formatToParts(at);
|
|
810
|
+
const part = (type) => parts.find((candidate) => candidate.type === type)?.value;
|
|
811
|
+
return `${part("year")}-${part("month")}-${part("day")}`;
|
|
812
|
+
}
|
|
813
|
+
function lastDayInZone(period, timeZone) {
|
|
814
|
+
if (!(period.until.getTime() > period.from.getTime())) {
|
|
815
|
+
throw new RangeError("A period ends after it begins.");
|
|
816
|
+
}
|
|
817
|
+
return dayInZone(new Date(period.until.getTime() - 1), timeZone);
|
|
818
|
+
}
|
|
819
|
+
function addDaysToDay(day, days) {
|
|
820
|
+
if (!Number.isInteger(days)) throw new RangeError("Days are counted in whole days.");
|
|
821
|
+
const [year, month, date] = day.split("-").map(Number);
|
|
822
|
+
if (!year || !month || !date) throw new RangeError(`${day} is not a day.`);
|
|
823
|
+
const moved = new Date(Date.UTC(year, month - 1, date + days));
|
|
824
|
+
return moved.toISOString().slice(0, 10);
|
|
825
|
+
}
|
|
826
|
+
function yearOfDay(day) {
|
|
827
|
+
return Number(day.slice(0, 4));
|
|
828
|
+
}
|
|
829
|
+
|
|
677
830
|
// src/settings-subtree.ts
|
|
678
831
|
var CATALOGUE_KEYS = /* @__PURE__ */ new Set([
|
|
679
832
|
"schemaVersion",
|
|
@@ -1131,6 +1284,80 @@ var BILLING_ERROR_CODES = {
|
|
|
1131
1284
|
* show it instead of guessing.
|
|
1132
1285
|
*/
|
|
1133
1286
|
BUNDLE_VERSION_OFFER_CHANGED: "BUNDLE_VERSION_OFFER_CHANGED",
|
|
1287
|
+
// ── withdrawing a feature for an external reason ──
|
|
1288
|
+
/**
|
|
1289
|
+
* Withdrawing a feature is not offered: the installation's stores cannot
|
|
1290
|
+
* list everybody a withdrawal would reach.
|
|
1291
|
+
*/
|
|
1292
|
+
FEATURE_WITHDRAWAL_UNAVAILABLE: "FEATURE_WITHDRAWAL_UNAVAILABLE",
|
|
1293
|
+
/** The catalogue knows no feature of that key, so there is nothing to withdraw. Carries `featureKey`. */
|
|
1294
|
+
FEATURE_WITHDRAWAL_FEATURE_UNKNOWN: "FEATURE_WITHDRAWAL_FEATURE_UNKNOWN",
|
|
1295
|
+
/**
|
|
1296
|
+
* The date names a moment before now. A feature is withdrawn from the
|
|
1297
|
+
* moment it is announced or from a later date: the days before were
|
|
1298
|
+
* delivered.
|
|
1299
|
+
*/
|
|
1300
|
+
FEATURE_WITHDRAWAL_DATE_IN_PAST: "FEATURE_WITHDRAWAL_DATE_IN_PAST",
|
|
1301
|
+
/** A withdrawal of the feature is not lifted yet; one at a time. Carries `featureKey`. */
|
|
1302
|
+
FEATURE_WITHDRAWAL_OPEN: "FEATURE_WITHDRAWAL_OPEN",
|
|
1303
|
+
/**
|
|
1304
|
+
* A withdrawal of the feature is lifted only from a date after the one
|
|
1305
|
+
* named, so the two would overlap. Carries `featureKey` and the `date` a
|
|
1306
|
+
* new one may begin on.
|
|
1307
|
+
*/
|
|
1308
|
+
FEATURE_WITHDRAWAL_OVERLAPS: "FEATURE_WITHDRAWAL_OVERLAPS",
|
|
1309
|
+
/**
|
|
1310
|
+
* A reduction names a plan or an add-on, in a rhythm, that no line the
|
|
1311
|
+
* withdrawal reaches is. Carries `kind`, `key` and `billingCycle`.
|
|
1312
|
+
*/
|
|
1313
|
+
FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED: "FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED",
|
|
1314
|
+
/** A reduction is named twice. Carries `kind`, `key` and `billingCycle`. */
|
|
1315
|
+
FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE: "FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE",
|
|
1316
|
+
/**
|
|
1317
|
+
* A reduction is more than the lowest price among the lines it reduces:
|
|
1318
|
+
* a price is reduced to nothing at most. Carries `key`, `billingCycle` and
|
|
1319
|
+
* that `price`.
|
|
1320
|
+
*/
|
|
1321
|
+
FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE: "FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE",
|
|
1322
|
+
/**
|
|
1323
|
+
* The subscriptions the withdrawal reaches changed since the preview the
|
|
1324
|
+
* operator looked at. Nothing was announced; carries the current `preview`.
|
|
1325
|
+
*/
|
|
1326
|
+
FEATURE_WITHDRAWAL_PREVIEW_CHANGED: "FEATURE_WITHDRAWAL_PREVIEW_CHANGED",
|
|
1327
|
+
/** No withdrawal has that id. Carries `withdrawalId`. */
|
|
1328
|
+
FEATURE_WITHDRAWAL_NOT_FOUND: "FEATURE_WITHDRAWAL_NOT_FOUND",
|
|
1329
|
+
/** The withdrawal was lifted already. Carries the `date` it is lifted from. */
|
|
1330
|
+
FEATURE_WITHDRAWAL_ALREADY_LIFTED: "FEATURE_WITHDRAWAL_ALREADY_LIFTED",
|
|
1331
|
+
/**
|
|
1332
|
+
* Nobody the withdrawal reaches has been told of it yet, and telling them
|
|
1333
|
+
* failed just now: a withdrawal is lifted only once they were. Nothing was
|
|
1334
|
+
* lifted; the next try, or the quarter-hourly run, tells them first.
|
|
1335
|
+
*/
|
|
1336
|
+
FEATURE_WITHDRAWAL_NOT_TOLD_YET: "FEATURE_WITHDRAWAL_NOT_TOLD_YET",
|
|
1337
|
+
/**
|
|
1338
|
+
* The date a withdrawal would be lifted from lies before now: what was
|
|
1339
|
+
* withdrawn until then was withdrawn.
|
|
1340
|
+
*/
|
|
1341
|
+
FEATURE_WITHDRAWAL_LIFT_IN_PAST: "FEATURE_WITHDRAWAL_LIFT_IN_PAST",
|
|
1342
|
+
/**
|
|
1343
|
+
* The withdrawal does not take its feature away now — its date is ahead,
|
|
1344
|
+
* or it was lifted — so nothing ends at once because of it. Carries
|
|
1345
|
+
* `featureKey`.
|
|
1346
|
+
*/
|
|
1347
|
+
FEATURE_WITHDRAWAL_NOT_IN_EFFECT: "FEATURE_WITHDRAWAL_NOT_IN_EFFECT",
|
|
1348
|
+
/**
|
|
1349
|
+
* The withdrawal did not reach the subscription, or the line to end: it
|
|
1350
|
+
* was not running at the announcement, or no longer holds the feature.
|
|
1351
|
+
*/
|
|
1352
|
+
FEATURE_WITHDRAWAL_DOES_NOT_REACH: "FEATURE_WITHDRAWAL_DOES_NOT_REACH",
|
|
1353
|
+
/** What was to be ended at once has ended already. */
|
|
1354
|
+
FEATURE_WITHDRAWAL_ALREADY_ENDED: "FEATURE_WITHDRAWAL_ALREADY_ENDED",
|
|
1355
|
+
/**
|
|
1356
|
+
* A cancellation is declared for a later date, and the installation's
|
|
1357
|
+
* subscription write or booking store cannot bring it forward (`endNow`).
|
|
1358
|
+
* It ends as declared. Carries that `date`.
|
|
1359
|
+
*/
|
|
1360
|
+
FEATURE_WITHDRAWAL_END_NOW_UNSUPPORTED: "FEATURE_WITHDRAWAL_END_NOW_UNSUPPORTED",
|
|
1134
1361
|
// ── the preview routes' own blockers and warnings ──
|
|
1135
1362
|
//
|
|
1136
1363
|
// These travel inside a 200 response, in `blockers[]` and `warnings[]`, not
|
|
@@ -1288,6 +1515,92 @@ var CONTRACT_ERROR_CODES = {
|
|
|
1288
1515
|
*/
|
|
1289
1516
|
SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED: "SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED"
|
|
1290
1517
|
};
|
|
1518
|
+
var INVOICE_ERROR_CODES = {
|
|
1519
|
+
/**
|
|
1520
|
+
* A charge already stands on an invoice: a second run issuing the same
|
|
1521
|
+
* charges lost the race. Nothing is written and no number is drawn.
|
|
1522
|
+
*/
|
|
1523
|
+
SUBSCRIPTION_INVOICE_CHARGE_INVOICED: "SUBSCRIPTION_INVOICE_CHARGE_INVOICED",
|
|
1524
|
+
/**
|
|
1525
|
+
* The invoice lacks content the tax adapter requires of it — `missing`
|
|
1526
|
+
* names the fields, `adapter` the adapter. It draws no number and waits
|
|
1527
|
+
* until what is missing is supplied (`SC-PRIC-027`).
|
|
1528
|
+
*/
|
|
1529
|
+
SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE: "SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE",
|
|
1530
|
+
/**
|
|
1531
|
+
* The contract's parties were copied by a migration and the operator has
|
|
1532
|
+
* not confirmed them yet; its charges are not invoiced until then
|
|
1533
|
+
* (`SC-AUD-012`).
|
|
1534
|
+
*/
|
|
1535
|
+
SUBSCRIPTION_INVOICE_PARTIES_UNCONFIRMED: "SUBSCRIPTION_INVOICE_PARTIES_UNCONFIRMED",
|
|
1536
|
+
/**
|
|
1537
|
+
* The subscriber's direct debit names no mandate reference
|
|
1538
|
+
* (`paymentMethodId`), which has to be announced before it is debited
|
|
1539
|
+
* (`SC-PRIC-028`). An invoice collected by it draws no number, and one
|
|
1540
|
+
* planned with it is not announced, until the payment method is replaced.
|
|
1541
|
+
*/
|
|
1542
|
+
SUBSCRIPTION_INVOICE_MANDATE_MISSING: "SUBSCRIPTION_INVOICE_MANDATE_MISSING",
|
|
1543
|
+
/**
|
|
1544
|
+
* The subscriber (`subscriberId`) has no payment method in use, so the
|
|
1545
|
+
* invoice is not collected until one is (`SC-PRIC-082`).
|
|
1546
|
+
*/
|
|
1547
|
+
SUBSCRIPTION_INVOICE_NO_PAYMENT_METHOD: "SUBSCRIPTION_INVOICE_NO_PAYMENT_METHOD",
|
|
1548
|
+
/**
|
|
1549
|
+
* A direct debit announced again after a failed collection (`attemptKey`)
|
|
1550
|
+
* waits until the subscriber has been told of that failure
|
|
1551
|
+
* (`SC-PRIC-080`).
|
|
1552
|
+
*/
|
|
1553
|
+
SUBSCRIPTION_INVOICE_FAILURE_NOT_TOLD: "SUBSCRIPTION_INVOICE_FAILURE_NOT_TOLD",
|
|
1554
|
+
/**
|
|
1555
|
+
* A file of the invoice's document has another checksum than the one
|
|
1556
|
+
* recorded or archived for it (`invoiceId`, `recorded`, `rendered`), and
|
|
1557
|
+
* the first stays. The run takes the archived file as the one issued, or —
|
|
1558
|
+
* where nothing is archived — records the new rendering in place of the
|
|
1559
|
+
* old (`SC-AUD-019`); the invoice waits only where another run changed the
|
|
1560
|
+
* record or the archive in between, and the next run settles it.
|
|
1561
|
+
*/
|
|
1562
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_CHANGED: "SUBSCRIPTION_INVOICE_DOCUMENT_CHANGED",
|
|
1563
|
+
/**
|
|
1564
|
+
* The tax adapter refuses the rendered document — `missing` names what it
|
|
1565
|
+
* lacks or disagrees with, `adapter` the adapter. The invoice keeps its
|
|
1566
|
+
* number and waits for a document that passes (`SC-PRIC-042`).
|
|
1567
|
+
*/
|
|
1568
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_REFUSED: "SUBSCRIPTION_INVOICE_DOCUMENT_REFUSED",
|
|
1569
|
+
/**
|
|
1570
|
+
* The archived document does not match the checksum recorded when it was
|
|
1571
|
+
* rendered (`invoiceId`). It is not handed out (`SC-AUD-019`).
|
|
1572
|
+
*/
|
|
1573
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH: "SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH",
|
|
1574
|
+
/** The invoice's document is not archived yet (`invoiceId`); nothing is offered until it is (`SC-AUD-014`). */
|
|
1575
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED: "SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED",
|
|
1576
|
+
/** No invoice with this id (`invoiceId`). */
|
|
1577
|
+
SUBSCRIPTION_INVOICE_NOT_FOUND: "SUBSCRIPTION_INVOICE_NOT_FOUND",
|
|
1578
|
+
/**
|
|
1579
|
+
* The subscriber's record holds no invoice email (`subscriberId`), so the
|
|
1580
|
+
* archived invoice cannot be sent (`SC-PRIC-047`); it is sent once one is
|
|
1581
|
+
* entered.
|
|
1582
|
+
*/
|
|
1583
|
+
SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL: "SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL",
|
|
1584
|
+
/** No credit under this id for the tenant's subscriber (`creditId`). */
|
|
1585
|
+
SUBSCRIBER_CREDIT_NOT_FOUND: "SUBSCRIBER_CREDIT_NOT_FOUND",
|
|
1586
|
+
/**
|
|
1587
|
+
* A payout of more than the credit still owes (`creditId`, `open`): nothing
|
|
1588
|
+
* is paid out beyond it (`SC-PRIC-083`), and nothing is recorded.
|
|
1589
|
+
*/
|
|
1590
|
+
SUBSCRIBER_PAYOUT_EXCEEDS_CREDIT: "SUBSCRIBER_PAYOUT_EXCEEDS_CREDIT",
|
|
1591
|
+
/**
|
|
1592
|
+
* A payout on a day still to come (`paidOn`, `today` in the installation's
|
|
1593
|
+
* zone): a payout is recorded once it left.
|
|
1594
|
+
*/
|
|
1595
|
+
SUBSCRIBER_PAYOUT_NOT_MADE_YET: "SUBSCRIBER_PAYOUT_NOT_MADE_YET",
|
|
1596
|
+
/** No case to reconcile under this id (`caseId`). */
|
|
1597
|
+
SUBSCRIBER_RECONCILIATION_CASE_NOT_FOUND: "SUBSCRIBER_RECONCILIATION_CASE_NOT_FOUND",
|
|
1598
|
+
/**
|
|
1599
|
+
* A case closed before (`caseId`, `closedAt`), by the operator or by
|
|
1600
|
+
* SaaSiCat; its note stands, and nothing is written (`SC-ADM-033`).
|
|
1601
|
+
*/
|
|
1602
|
+
SUBSCRIBER_RECONCILIATION_CASE_CLOSED: "SUBSCRIBER_RECONCILIATION_CASE_CLOSED"
|
|
1603
|
+
};
|
|
1291
1604
|
var SUBSCRIBER_ERROR_CODES = {
|
|
1292
1605
|
/**
|
|
1293
1606
|
* A contract, a plan change or a booking for a tenant that has no
|
|
@@ -1468,6 +1781,7 @@ var PLATFORM_ERROR_CODES = {
|
|
|
1468
1781
|
...CATALOG_ERROR_CODES,
|
|
1469
1782
|
...BILLING_ERROR_CODES,
|
|
1470
1783
|
...CONTRACT_ERROR_CODES,
|
|
1784
|
+
...INVOICE_ERROR_CODES,
|
|
1471
1785
|
...SUBSCRIBER_ERROR_CODES,
|
|
1472
1786
|
...REGISTRATION_ERROR_CODES,
|
|
1473
1787
|
...PAYMENT_ERROR_CODES,
|
|
@@ -1521,7 +1835,45 @@ var OPTIONAL_CANONICAL_MODELS = {
|
|
|
1521
1835
|
SubscriptionNotice: ["tenantBilling.subscriptionNotices"],
|
|
1522
1836
|
VersionRetirement: ["tenantBilling.versionRetirements"],
|
|
1523
1837
|
BundleVersionRetirement: ["tenantBilling.bundleVersionRetirements"],
|
|
1838
|
+
FeatureWithdrawal: ["tenantBilling.featureWithdrawals"],
|
|
1524
1839
|
SubscriberLedgerEntry: ["entitlement.subscriberLedgerRepository"],
|
|
1840
|
+
SubscriptionInvoice: ["entitlement.subscriptionInvoiceRepository"],
|
|
1841
|
+
SubscriptionInvoiceLine: ["entitlement.subscriptionInvoiceRepository"],
|
|
1842
|
+
SubscriptionInvoiceNumber: ["entitlement.subscriptionInvoiceRepository"],
|
|
1843
|
+
SubscriptionInvoiceDocument: ["entitlement.subscriptionInvoiceRepository"],
|
|
1844
|
+
SubscriptionInvoiceArchive: ["entitlement.subscriptionInvoiceArchive"],
|
|
1845
|
+
SubscriptionInvoiceCollection: [
|
|
1846
|
+
"entitlement.invoiceCollectionRepository",
|
|
1847
|
+
"entitlement.subscriberPaymentRepository"
|
|
1848
|
+
],
|
|
1849
|
+
SubscriberPayment: [
|
|
1850
|
+
"entitlement.subscriberPaymentRepository",
|
|
1851
|
+
"entitlement.invoiceCollectionRepository"
|
|
1852
|
+
],
|
|
1853
|
+
SubscriptionInvoiceSettlement: [
|
|
1854
|
+
"entitlement.subscriberPaymentRepository",
|
|
1855
|
+
"entitlement.invoiceCollectionRepository"
|
|
1856
|
+
],
|
|
1857
|
+
SubscriberCredit: [
|
|
1858
|
+
"entitlement.subscriberPaymentRepository",
|
|
1859
|
+
"entitlement.invoiceCollectionRepository"
|
|
1860
|
+
],
|
|
1861
|
+
SubscriberCreditEntry: [
|
|
1862
|
+
"entitlement.subscriberPaymentRepository",
|
|
1863
|
+
"entitlement.invoiceCollectionRepository"
|
|
1864
|
+
],
|
|
1865
|
+
SubscriberRefund: [
|
|
1866
|
+
"entitlement.subscriberPaymentRepository",
|
|
1867
|
+
"entitlement.invoiceCollectionRepository"
|
|
1868
|
+
],
|
|
1869
|
+
SubscriberReversal: [
|
|
1870
|
+
"entitlement.subscriberPaymentRepository",
|
|
1871
|
+
"entitlement.invoiceCollectionRepository"
|
|
1872
|
+
],
|
|
1873
|
+
SubscriberReconciliationCase: [
|
|
1874
|
+
"entitlement.subscriberPaymentRepository",
|
|
1875
|
+
"entitlement.invoiceCollectionRepository"
|
|
1876
|
+
],
|
|
1525
1877
|
PromoCodeHold: ["promo.holdRepository"]
|
|
1526
1878
|
};
|
|
1527
1879
|
function membersLeftOut(notAdopted = []) {
|
|
@@ -1704,6 +2056,23 @@ var ERROR_MESSAGES_EN = {
|
|
|
1704
2056
|
BUNDLE_RETIREMENT_REINSTATE_SUBSCRIPTION_ENDS: "Version {version} of {bundleKey} is being retired, and this booking ends before it would move. Your subscription ends by then as well.",
|
|
1705
2057
|
BUNDLE_RETIREMENT_SWITCH_PLAN_CHANGES: "{bundleName} moves to its new version on {date}, and your plan changes before then, so the price a switch would keep is not known yet. Switch once your plan has changed, or let it move on that date.",
|
|
1706
2058
|
BUNDLE_VERSION_OFFER_CHANGED: "The offer changed since it was shown. Look at the current one before switching.",
|
|
2059
|
+
FEATURE_WITHDRAWAL_UNAVAILABLE: "Withdrawing a feature is not offered here: the stores cannot list everybody a withdrawal reaches.",
|
|
2060
|
+
FEATURE_WITHDRAWAL_FEATURE_UNKNOWN: "The catalogue knows no feature {featureKey}.",
|
|
2061
|
+
FEATURE_WITHDRAWAL_DATE_IN_PAST: "A feature is withdrawn from now on or from a later date, never from a date already past.",
|
|
2062
|
+
FEATURE_WITHDRAWAL_OPEN: "{featureKey} is withdrawn already, and that withdrawal is not lifted. Lift it before announcing another.",
|
|
2063
|
+
FEATURE_WITHDRAWAL_OVERLAPS: "{featureKey} is withdrawn until {date}. Another withdrawal can begin on that date at the earliest.",
|
|
2064
|
+
FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED: "The reduction for {key} billed {billingCycle} names nothing this withdrawal reaches.",
|
|
2065
|
+
FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE: "The reduction for {key} billed {billingCycle} is named twice.",
|
|
2066
|
+
FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE: "The reduction for {key} billed {billingCycle} is more than the lowest price it reduces, {price}.",
|
|
2067
|
+
FEATURE_WITHDRAWAL_PREVIEW_CHANGED: "The subscriptions this withdrawal reaches changed since they were shown. Look at them again before announcing it.",
|
|
2068
|
+
FEATURE_WITHDRAWAL_NOT_FOUND: "Feature withdrawal {withdrawalId} not found.",
|
|
2069
|
+
FEATURE_WITHDRAWAL_ALREADY_LIFTED: "This withdrawal is lifted already, from {date}.",
|
|
2070
|
+
FEATURE_WITHDRAWAL_NOT_TOLD_YET: "Nobody has been told of this withdrawal yet, and it is not lifted before they are. Try again in a moment.",
|
|
2071
|
+
FEATURE_WITHDRAWAL_LIFT_IN_PAST: "A withdrawal is lifted from now on or from a later date, never from a date already past.",
|
|
2072
|
+
FEATURE_WITHDRAWAL_NOT_IN_EFFECT: "{featureKey} is not withdrawn now, so nothing can be ended at once because of it.",
|
|
2073
|
+
FEATURE_WITHDRAWAL_DOES_NOT_REACH: "This withdrawal does not reach your subscription, or no longer this booking.",
|
|
2074
|
+
FEATURE_WITHDRAWAL_ALREADY_ENDED: "This has ended already.",
|
|
2075
|
+
FEATURE_WITHDRAWAL_END_NOW_UNSUPPORTED: "Your cancellation for {date} is recorded, and it cannot be brought forward here. It ends on that date, and until then the reduction applies.",
|
|
1707
2076
|
RETIREMENT_SWITCH_NOT_PENDING: "No retirement of your version is waiting for its date, so there is nothing to switch to.",
|
|
1708
2077
|
RETIREMENT_SWITCH_IN_TRIAL: "The switch opens when your trial ends.",
|
|
1709
2078
|
RETIREMENT_SWITCH_BUNDLE_CANNOT_FOLLOW: "{bundleName} cannot run on {planName} and runs until {until} at the earliest. Once it is cancelled, the switch can be made from that day.",
|
|
@@ -1746,6 +2115,23 @@ var ERROR_MESSAGES_EN = {
|
|
|
1746
2115
|
PROMO_MAX_REDEMPTIONS_LOWERED: "maxRedemptions cannot be lowered.",
|
|
1747
2116
|
// ── contract ──
|
|
1748
2117
|
SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED: "The contract states {stated} % in {field}, and the tax adapter decides {decided} %.",
|
|
2118
|
+
SUBSCRIPTION_INVOICE_CHARGE_INVOICED: "Charge '{chargeId}' already stands on an invoice.",
|
|
2119
|
+
SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE: "The invoice lacks what the tax adapter {adapter} requires of it: {missing}. It is issued once that is supplied.",
|
|
2120
|
+
SUBSCRIPTION_INVOICE_PARTIES_UNCONFIRMED: "Contract {contractId} names no confirmed issuer and subscriber \u2014 a migration copied its parties, or it names no issuer \u2014 so its charges are not invoiced.",
|
|
2121
|
+
SUBSCRIPTION_INVOICE_MANDATE_MISSING: "The direct debit {paymentMethodId} names no mandate reference, which has to be announced before it is debited. The invoice waits until the payment method is replaced.",
|
|
2122
|
+
SUBSCRIPTION_INVOICE_NO_PAYMENT_METHOD: "The subscriber {subscriberId} has no payment method in use. The invoice is collected once one is.",
|
|
2123
|
+
SUBSCRIPTION_INVOICE_FAILURE_NOT_TOLD: "The direct debit is announced once the subscriber has been told that collection {attemptKey} failed.",
|
|
2124
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_CHANGED: "The document of invoice {invoiceId} differs from the one recorded or archived for it ({rendered} instead of {recorded}); the first stays, and the next run settles which one the invoice keeps.",
|
|
2125
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_REFUSED: "The tax adapter {adapter} refuses the rendered invoice: {missing}. The invoice keeps its number and waits for a document that passes.",
|
|
2126
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH: "The archived document of invoice {invoiceId} does not match the checksum recorded when it was rendered, and is not handed out.",
|
|
2127
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED: "The document of invoice {invoiceId} is not archived yet.",
|
|
2128
|
+
SUBSCRIPTION_INVOICE_NOT_FOUND: "No invoice '{invoiceId}'.",
|
|
2129
|
+
SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL: "Subscriber {subscriberId} has no invoice email, so its invoice cannot be sent; it is sent once one is entered.",
|
|
2130
|
+
SUBSCRIBER_CREDIT_NOT_FOUND: "No credit '{creditId}' of this tenant's subscriber.",
|
|
2131
|
+
SUBSCRIBER_PAYOUT_EXCEEDS_CREDIT: "The credit {creditId} still owes {open}; nothing beyond that is paid out, and nothing was recorded.",
|
|
2132
|
+
SUBSCRIBER_PAYOUT_NOT_MADE_YET: "A payout is recorded once it left: {paidOn} is after today, {today}.",
|
|
2133
|
+
SUBSCRIBER_RECONCILIATION_CASE_NOT_FOUND: "No case to reconcile '{caseId}'.",
|
|
2134
|
+
SUBSCRIBER_RECONCILIATION_CASE_CLOSED: "The case {caseId} was closed on {closedAt}; its note stands, and nothing was recorded.",
|
|
1749
2135
|
CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "A checkout offer can yield only one contract, and only once its line items are frozen.",
|
|
1750
2136
|
CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "A checkout offer requires a frozen plan line item.",
|
|
1751
2137
|
CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Every selected bundle version requires a frozen bundle line item.",
|
|
@@ -1831,11 +2217,21 @@ var ERROR_MESSAGES_EN = {
|
|
|
1831
2217
|
TAX_VAT_ID_CHECK_NOT_COMPLETED: "The VAT identification number could not be checked just now ({reason}). Please try again later.",
|
|
1832
2218
|
TAX_VAT_ID_CHECK_NOT_AVAILABLE: "This installation names no tax adapter, so no VAT identification number is checked."
|
|
1833
2219
|
};
|
|
2220
|
+
function parameterText(value) {
|
|
2221
|
+
if (typeof value === "string") return value;
|
|
2222
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
2223
|
+
return String(value);
|
|
2224
|
+
}
|
|
2225
|
+
if (value instanceof Date) return String(value);
|
|
2226
|
+
if (!Array.isArray(value)) return null;
|
|
2227
|
+
const items = value.map(parameterText);
|
|
2228
|
+
return items.every((item) => item !== null) ? items.join(",") : null;
|
|
2229
|
+
}
|
|
1834
2230
|
function formatErrorMessage(template, params = {}) {
|
|
1835
|
-
return template.replace(
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2231
|
+
return template.replace(
|
|
2232
|
+
/\{(\w+)\}/g,
|
|
2233
|
+
(match, name) => parameterText(params[name]) ?? match
|
|
2234
|
+
);
|
|
1839
2235
|
}
|
|
1840
2236
|
function resolveErrorMessage(body, overrides = {}, defaults = ERROR_MESSAGES_EN) {
|
|
1841
2237
|
const code = typeof body.code === "string" ? body.code : void 0;
|
|
@@ -2013,6 +2409,13 @@ function selectChargeableBundles(planFeatures, selectedBundles) {
|
|
|
2013
2409
|
}
|
|
2014
2410
|
|
|
2015
2411
|
// src/payment-gateway.types.ts
|
|
2412
|
+
var RETRYABLE_FAILURES = /* @__PURE__ */ new Set([
|
|
2413
|
+
"insufficient-funds",
|
|
2414
|
+
"declined"
|
|
2415
|
+
]);
|
|
2416
|
+
function isRetryableCollectionFailure(reason) {
|
|
2417
|
+
return RETRYABLE_FAILURES.has(reason);
|
|
2418
|
+
}
|
|
2016
2419
|
var PAYMENT_CALLBACK_REJECTED = "PAYMENT_CALLBACK_REJECTED";
|
|
2017
2420
|
var PaymentCallbackRejectedError = class extends Error {
|
|
2018
2421
|
code = PAYMENT_CALLBACK_REJECTED;
|
|
@@ -2188,17 +2591,20 @@ var ORIGINS = {
|
|
|
2188
2591
|
bundleBooking: true,
|
|
2189
2592
|
bundleChange: true,
|
|
2190
2593
|
credit: true,
|
|
2191
|
-
correction: true
|
|
2594
|
+
correction: true,
|
|
2595
|
+
reductionTakenBack: true
|
|
2192
2596
|
};
|
|
2193
2597
|
var SOURCES = {
|
|
2194
2598
|
plan: true,
|
|
2195
2599
|
bundle: true,
|
|
2196
2600
|
discount: true
|
|
2197
2601
|
};
|
|
2198
|
-
var
|
|
2602
|
+
var SUBSCRIBER_CHARGE_ORIGINS = keysOf(ORIGINS);
|
|
2603
|
+
var SUBSCRIBER_CHARGE_SOURCES = keysOf(SOURCES);
|
|
2604
|
+
var TABLE3 = "subscriber_ledger_entries";
|
|
2199
2605
|
var CENT_TOLERANCE = 1e-6;
|
|
2200
2606
|
function toSubscriberChargeRecord(row) {
|
|
2201
|
-
const where = (column) => ({ table:
|
|
2607
|
+
const where = (column) => ({ table: TABLE3, column, id: row.id });
|
|
2202
2608
|
return {
|
|
2203
2609
|
id: row.id,
|
|
2204
2610
|
subscriberId: row.subscriberId,
|
|
@@ -2206,8 +2612,8 @@ function toSubscriberChargeRecord(row) {
|
|
|
2206
2612
|
subscriptionId: row.subscriptionId,
|
|
2207
2613
|
contractId: row.contractId,
|
|
2208
2614
|
contractLineItemId: row.contractLineItemId,
|
|
2209
|
-
origin: oneOf(
|
|
2210
|
-
source: oneOf(
|
|
2615
|
+
origin: oneOf(SUBSCRIBER_CHARGE_ORIGINS, row.origin, where("origin")),
|
|
2616
|
+
source: oneOf(SUBSCRIBER_CHARGE_SOURCES, row.source, where("source")),
|
|
2211
2617
|
sourceRef: row.sourceRef,
|
|
2212
2618
|
periodStart: row.periodStart,
|
|
2213
2619
|
periodEnd: row.periodEnd,
|
|
@@ -2247,6 +2653,96 @@ function keysOf(record) {
|
|
|
2247
2653
|
// src/subscriber-ledger.types.ts
|
|
2248
2654
|
var SUBSCRIBER_ACCOUNT_CAPABILITY = "charges.read";
|
|
2249
2655
|
|
|
2656
|
+
// src/subscription-invoice.types.ts
|
|
2657
|
+
var INVOICE_SEQUENCE_DIGITS = 6;
|
|
2658
|
+
function formatInvoiceNumber(prefix, year, sequence) {
|
|
2659
|
+
if (!Number.isInteger(sequence) || sequence < 1) {
|
|
2660
|
+
throw new RangeError(`An invoice number's sequence starts at 1; ${sequence} is not one.`);
|
|
2661
|
+
}
|
|
2662
|
+
return `${prefix}-${year}-${String(sequence).padStart(INVOICE_SEQUENCE_DIGITS, "0")}`;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// src/column-day.ts
|
|
2666
|
+
var DAY_LENGTH = "YYYY-MM-DD".length;
|
|
2667
|
+
function dayOf2(value, where) {
|
|
2668
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
2669
|
+
return value.toISOString().slice(0, DAY_LENGTH);
|
|
2670
|
+
}
|
|
2671
|
+
if (typeof value === "string" && value.length === DAY_LENGTH && !Number.isNaN(Date.parse(value))) {
|
|
2672
|
+
return value;
|
|
2673
|
+
}
|
|
2674
|
+
throw new Error(`${where.table}.${where.column} of ${where.id} is not a day.`);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// src/subscriber-payment-method-mapping.ts
|
|
2678
|
+
var PAYMENT_METHOD_TYPES = ["card", "sepa_debit"];
|
|
2679
|
+
var STATUSES = ["ACTIVE", "REPLACED"];
|
|
2680
|
+
var TABLE4 = "subscriber_payment_methods";
|
|
2681
|
+
function toSubscriberPaymentMethodRecord(row) {
|
|
2682
|
+
return {
|
|
2683
|
+
id: row.id,
|
|
2684
|
+
subscriberId: row.subscriberId,
|
|
2685
|
+
gatewayAccount: row.gatewayAccount,
|
|
2686
|
+
provider: row.provider,
|
|
2687
|
+
customerRef: row.customerRef,
|
|
2688
|
+
paymentMethodRef: row.paymentMethodRef,
|
|
2689
|
+
type: oneOf(PAYMENT_METHOD_TYPES, row.type, { table: TABLE4, column: "type", id: row.id }),
|
|
2690
|
+
brand: row.brand,
|
|
2691
|
+
last4: row.last4,
|
|
2692
|
+
expiryMonth: row.expiryMonth,
|
|
2693
|
+
expiryYear: row.expiryYear,
|
|
2694
|
+
country: row.country,
|
|
2695
|
+
bankCode: row.bankCode,
|
|
2696
|
+
mandateReference: row.mandateReference,
|
|
2697
|
+
status: oneOf(STATUSES, row.status, { table: TABLE4, column: "status", id: row.id }),
|
|
2698
|
+
confirmedAt: row.confirmedAt,
|
|
2699
|
+
replacedAt: row.replacedAt,
|
|
2700
|
+
createdAt: row.createdAt
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
function subscriberPaymentMethodColumns(data) {
|
|
2704
|
+
return {
|
|
2705
|
+
subscriberId: data.subscriberId,
|
|
2706
|
+
gatewayAccount: data.gatewayAccount,
|
|
2707
|
+
provider: data.provider,
|
|
2708
|
+
customerRef: data.customerRef,
|
|
2709
|
+
paymentMethodRef: data.paymentMethodRef,
|
|
2710
|
+
type: data.type,
|
|
2711
|
+
brand: data.brand,
|
|
2712
|
+
last4: data.last4,
|
|
2713
|
+
expiryMonth: data.expiryMonth,
|
|
2714
|
+
expiryYear: data.expiryYear,
|
|
2715
|
+
country: data.country,
|
|
2716
|
+
bankCode: data.bankCode,
|
|
2717
|
+
mandateReference: data.mandateReference,
|
|
2718
|
+
confirmedAt: data.confirmedAt
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
var FOREIGN_PAYMENT_METHOD_REFERENCE = "FOREIGN_PAYMENT_METHOD_REFERENCE";
|
|
2722
|
+
var ForeignPaymentMethodReferenceError = class extends Error {
|
|
2723
|
+
constructor(gatewayAccount, paymentMethodRef) {
|
|
2724
|
+
super(
|
|
2725
|
+
`Payment method '${paymentMethodRef}' of account '${gatewayAccount}' belongs to another subscriber. A reference belongs to exactly one, so it is not handed out.`
|
|
2726
|
+
);
|
|
2727
|
+
this.gatewayAccount = gatewayAccount;
|
|
2728
|
+
this.paymentMethodRef = paymentMethodRef;
|
|
2729
|
+
this.name = "ForeignPaymentMethodReferenceError";
|
|
2730
|
+
}
|
|
2731
|
+
gatewayAccount;
|
|
2732
|
+
paymentMethodRef;
|
|
2733
|
+
code = FOREIGN_PAYMENT_METHOD_REFERENCE;
|
|
2734
|
+
};
|
|
2735
|
+
function isForeignPaymentMethodReferenceError(error) {
|
|
2736
|
+
return error instanceof Error && error.code === FOREIGN_PAYMENT_METHOD_REFERENCE;
|
|
2737
|
+
}
|
|
2738
|
+
function refuseForeignPaymentMethodReference(recorded, subscriberId) {
|
|
2739
|
+
if (recorded.subscriberId === subscriberId) return;
|
|
2740
|
+
throw new ForeignPaymentMethodReferenceError(
|
|
2741
|
+
recorded.gatewayAccount,
|
|
2742
|
+
recorded.paymentMethodRef
|
|
2743
|
+
);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2250
2746
|
// src/subscriber.types.ts
|
|
2251
2747
|
var SUBSCRIBER_INVOICE_ADDRESS_FIELDS = [
|
|
2252
2748
|
"addressLine1",
|
|
@@ -2378,89 +2874,1375 @@ function textEntries(value) {
|
|
|
2378
2874
|
return entries;
|
|
2379
2875
|
}
|
|
2380
2876
|
|
|
2381
|
-
// src/
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
function
|
|
2877
|
+
// src/subscription-invoice-mapping.ts
|
|
2878
|
+
var INVOICES = "subscription_invoices";
|
|
2879
|
+
var LINES = "subscription_invoice_lines";
|
|
2880
|
+
var CENT_TOLERANCE2 = 1e-6;
|
|
2881
|
+
function toSubscriptionInvoiceRecord(row, lines) {
|
|
2882
|
+
const where = (column) => ({ table: INVOICES, column, id: row.id });
|
|
2883
|
+
const treatment = toTaxTreatment(row.taxTreatment);
|
|
2884
|
+
if (!treatment) {
|
|
2885
|
+
throw new Error(`${INVOICES}.taxTreatment of ${row.id} is not a tax treatment.`);
|
|
2886
|
+
}
|
|
2887
|
+
const dueDate = dayOf2(row.dueDate, where("dueDate"));
|
|
2386
2888
|
return {
|
|
2387
2889
|
id: row.id,
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2890
|
+
number: row.number,
|
|
2891
|
+
numberPrefix: row.numberPrefix,
|
|
2892
|
+
numberYear: row.numberYear,
|
|
2893
|
+
numberSequence: row.numberSequence,
|
|
2894
|
+
tenantId: row.tenantId,
|
|
2895
|
+
subscriberId: row.subscriberId,
|
|
2896
|
+
subscriptionId: row.subscriptionId,
|
|
2897
|
+
contractId: row.contractId,
|
|
2898
|
+
issuedAt: row.issuedAt,
|
|
2899
|
+
issueDate: dayOf2(row.issueDate, where("issueDate")),
|
|
2900
|
+
dueDate,
|
|
2901
|
+
servicePeriodFrom: dayOf2(row.servicePeriodFrom, where("servicePeriodFrom")),
|
|
2902
|
+
servicePeriodUntil: dayOf2(row.servicePeriodUntil, where("servicePeriodUntil")),
|
|
2903
|
+
...collectionOf(row.payment, row.collectionDate, dueDate, where),
|
|
2904
|
+
currency: row.currency,
|
|
2905
|
+
issuer: issuerOf(row.issuerParty, where("issuerParty")),
|
|
2906
|
+
subscriber: subscriberOf(row.subscriberParty, where("subscriberParty")),
|
|
2907
|
+
footer: footerOf(row.footer, where("footer")),
|
|
2908
|
+
taxTreatment: treatment,
|
|
2909
|
+
taxes: taxesOf(row.taxes, where("taxes")),
|
|
2910
|
+
totalNet: amountOf2(row.totalNet, where("totalNet")),
|
|
2911
|
+
totalTax: amountOf2(row.totalTax, where("totalTax")),
|
|
2912
|
+
totalGross: amountOf2(row.totalGross, where("totalGross")),
|
|
2913
|
+
lines: [...lines].sort((a, b) => a.position - b.position).map((line) => toSubscriptionInvoiceLineRecord(line)),
|
|
2914
|
+
createdAt: row.createdAt
|
|
2403
2915
|
};
|
|
2404
2916
|
}
|
|
2405
|
-
function
|
|
2917
|
+
function toSubscriptionInvoiceLineRecord(row) {
|
|
2918
|
+
const where = (column) => ({ table: LINES, column, id: row.id });
|
|
2406
2919
|
return {
|
|
2407
2920
|
id: row.id,
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2921
|
+
invoiceId: row.invoiceId,
|
|
2922
|
+
position: row.position,
|
|
2923
|
+
chargeId: row.chargeId,
|
|
2924
|
+
contractLineItemId: row.contractLineItemId,
|
|
2925
|
+
title: row.title,
|
|
2926
|
+
origin: oneOf(SUBSCRIBER_CHARGE_ORIGINS, row.origin, where("origin")),
|
|
2927
|
+
source: oneOf(SUBSCRIBER_CHARGE_SOURCES, row.source, where("source")),
|
|
2928
|
+
periodFrom: dayOf2(row.periodFrom, where("periodFrom")),
|
|
2929
|
+
periodUntil: dayOf2(row.periodUntil, where("periodUntil")),
|
|
2930
|
+
amountNet: amountOf2(row.amountNet, where("amountNet")),
|
|
2931
|
+
taxRate: amountOf2(row.taxRate, where("taxRate"))
|
|
2414
2932
|
};
|
|
2415
2933
|
}
|
|
2416
|
-
function
|
|
2934
|
+
function subscriptionInvoiceColumns(invoice, drawn) {
|
|
2417
2935
|
return {
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2936
|
+
number: drawn.number,
|
|
2937
|
+
numberPrefix: invoice.numberPrefix,
|
|
2938
|
+
numberYear: invoice.numberYear,
|
|
2939
|
+
numberSequence: drawn.numberSequence,
|
|
2940
|
+
tenantId: invoice.tenantId,
|
|
2941
|
+
subscriberId: invoice.subscriberId,
|
|
2942
|
+
subscriptionId: invoice.subscriptionId,
|
|
2943
|
+
contractId: invoice.contractId,
|
|
2944
|
+
issuedAt: invoice.issuedAt,
|
|
2945
|
+
issueDate: invoice.issueDate,
|
|
2946
|
+
dueDate: invoice.dueDate,
|
|
2947
|
+
servicePeriodFrom: invoice.servicePeriodFrom,
|
|
2948
|
+
servicePeriodUntil: invoice.servicePeriodUntil,
|
|
2949
|
+
payment: invoice.payment ? { ...invoice.payment } : null,
|
|
2950
|
+
collectionDate: invoice.collectionDate,
|
|
2951
|
+
currency: invoice.currency,
|
|
2952
|
+
issuerParty: { ...invoice.issuer },
|
|
2953
|
+
subscriberParty: { ...invoice.subscriber },
|
|
2954
|
+
footer: [...invoice.footer],
|
|
2955
|
+
taxTreatment: taxTreatmentToJson(invoice.taxTreatment),
|
|
2956
|
+
taxes: invoice.taxes.map((entry) => ({ rate: entry.rate, net: entry.net, tax: entry.tax })),
|
|
2957
|
+
totalNet: centsText(invoice.totalNet, "totalNet"),
|
|
2958
|
+
totalTax: centsText(invoice.totalTax, "totalTax"),
|
|
2959
|
+
totalGross: centsText(invoice.totalGross, "totalGross")
|
|
2425
2960
|
};
|
|
2426
2961
|
}
|
|
2427
|
-
function
|
|
2428
|
-
const previous = {};
|
|
2429
|
-
const next = {};
|
|
2430
|
-
for (const field of LEGAL_IDENTITY_FIELDS) {
|
|
2431
|
-
const value = corrected[field];
|
|
2432
|
-
if (value === void 0 || value === current[field]) continue;
|
|
2433
|
-
previous[field] = current[field];
|
|
2434
|
-
next[field] = value;
|
|
2435
|
-
}
|
|
2962
|
+
function subscriptionInvoiceLineColumns(line, invoiceId) {
|
|
2436
2963
|
return {
|
|
2437
|
-
|
|
2438
|
-
|
|
2964
|
+
invoiceId,
|
|
2965
|
+
position: line.position,
|
|
2966
|
+
chargeId: line.chargeId,
|
|
2967
|
+
contractLineItemId: line.contractLineItemId,
|
|
2968
|
+
title: line.title,
|
|
2969
|
+
origin: line.origin,
|
|
2970
|
+
source: line.source,
|
|
2971
|
+
periodFrom: line.periodFrom,
|
|
2972
|
+
periodUntil: line.periodUntil,
|
|
2973
|
+
amountNet: centsText(line.amountNet, "amountNet"),
|
|
2974
|
+
taxRate: centsText(line.taxRate, "taxRate")
|
|
2439
2975
|
};
|
|
2440
2976
|
}
|
|
2441
|
-
function
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2977
|
+
function utcMidnightOf(day) {
|
|
2978
|
+
return /* @__PURE__ */ new Date(`${day}T00:00:00.000Z`);
|
|
2979
|
+
}
|
|
2980
|
+
function collectionOf(payment, collectionDate, dueDate, where) {
|
|
2981
|
+
const paymentAt = where("payment");
|
|
2982
|
+
if (payment === void 0 || collectionDate === void 0) {
|
|
2983
|
+
throw new Error(
|
|
2984
|
+
`${paymentAt.table} row ${paymentAt.id} was read without its payment or its collection date.`
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
if (payment === null) {
|
|
2988
|
+
if (collectionDate !== null) {
|
|
2989
|
+
throw new Error(
|
|
2990
|
+
`${paymentAt.table}.collectionDate of ${paymentAt.id} is set on an invoice that is not collected.`
|
|
2991
|
+
);
|
|
2992
|
+
}
|
|
2993
|
+
return { payment: null, collectionDate: null };
|
|
2994
|
+
}
|
|
2995
|
+
if (typeof payment !== "object" || Array.isArray(payment)) {
|
|
2996
|
+
throw new Error(`${paymentAt.table}.payment of ${paymentAt.id} is not a payment method.`);
|
|
2997
|
+
}
|
|
2998
|
+
const required = (key) => {
|
|
2999
|
+
const value = textOrNull2(payment, key, paymentAt);
|
|
3000
|
+
if (value === null) {
|
|
3001
|
+
throw new Error(`${paymentAt.table}.payment of ${paymentAt.id} names no ${key}.`);
|
|
3002
|
+
}
|
|
3003
|
+
return value;
|
|
2452
3004
|
};
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
3005
|
+
const type = oneOf(PAYMENT_METHOD_TYPES, required("type"), {
|
|
3006
|
+
...paymentAt,
|
|
3007
|
+
column: "payment.type"
|
|
3008
|
+
});
|
|
3009
|
+
const recorded = {
|
|
3010
|
+
paymentMethodId: required("paymentMethodId"),
|
|
3011
|
+
gatewayAccount: required("gatewayAccount"),
|
|
3012
|
+
type,
|
|
3013
|
+
brand: textOrNull2(payment, "brand", paymentAt),
|
|
3014
|
+
last4: required("last4"),
|
|
3015
|
+
mandateReference: type === "sepa_debit" ? required("mandateReference") : null
|
|
2457
3016
|
};
|
|
3017
|
+
if (type !== "sepa_debit") {
|
|
3018
|
+
if (collectionDate !== null) {
|
|
3019
|
+
throw new Error(
|
|
3020
|
+
`${paymentAt.table}.collectionDate of ${paymentAt.id} is set on an invoice paid by ${type}.`
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
return { payment: recorded, collectionDate: null };
|
|
3024
|
+
}
|
|
3025
|
+
const collected = dayOf2(collectionDate, where("collectionDate"));
|
|
3026
|
+
if (collected !== dueDate) {
|
|
3027
|
+
throw new Error(
|
|
3028
|
+
`${paymentAt.table}.dueDate of ${paymentAt.id} is ${dueDate}, and a direct debit falls due on the day it is collected, ${collected}.`
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
return { payment: recorded, collectionDate: collected };
|
|
2458
3032
|
}
|
|
2459
|
-
function
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
3033
|
+
function footerOf(value, where) {
|
|
3034
|
+
if (!Array.isArray(value) || !value.every((line) => typeof line === "string")) {
|
|
3035
|
+
throw new Error(`${where.table}.${where.column} of ${where.id} is not a list of lines.`);
|
|
3036
|
+
}
|
|
3037
|
+
return [...value];
|
|
3038
|
+
}
|
|
3039
|
+
function amountOf2(value, where) {
|
|
3040
|
+
const amount = Number(value === null || value === void 0 ? NaN : String(value));
|
|
3041
|
+
if (!Number.isFinite(amount)) {
|
|
3042
|
+
throw new Error(`${where.table}.${where.column} of ${where.id} is not an amount.`);
|
|
3043
|
+
}
|
|
3044
|
+
return amount;
|
|
3045
|
+
}
|
|
3046
|
+
function centsText(amount, field) {
|
|
3047
|
+
const cents2 = amount * 100;
|
|
3048
|
+
if (!Number.isFinite(cents2) || Math.abs(cents2 - Math.round(cents2)) > CENT_TOLERANCE2) {
|
|
3049
|
+
throw new Error(
|
|
3050
|
+
`An invoice's ${field} of ${amount} is not a whole number of cents; it is rounded once, where the invoice is drafted, before it is written.`
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
return amount.toFixed(2);
|
|
3054
|
+
}
|
|
3055
|
+
function fieldOf(source, key) {
|
|
3056
|
+
const field = Object.getOwnPropertyDescriptor(source, key)?.value;
|
|
3057
|
+
return field;
|
|
3058
|
+
}
|
|
3059
|
+
function textOrNull2(source, key, where) {
|
|
3060
|
+
const value = fieldOf(source, key);
|
|
3061
|
+
if (value === null || value === void 0) return null;
|
|
3062
|
+
if (typeof value !== "string") {
|
|
3063
|
+
throw new Error(`${where.table}.${where.column}.${key} of ${where.id} is not text.`);
|
|
3064
|
+
}
|
|
3065
|
+
return value;
|
|
3066
|
+
}
|
|
3067
|
+
function partyOf(value, where) {
|
|
3068
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
3069
|
+
throw new Error(`${where.table}.${where.column} of ${where.id} is not a party.`);
|
|
3070
|
+
}
|
|
3071
|
+
const source = value;
|
|
3072
|
+
const legalName = textOrNull2(source, "legalName", where);
|
|
3073
|
+
if (legalName === null) {
|
|
3074
|
+
throw new Error(`${where.table}.${where.column} of ${where.id} names no legal name.`);
|
|
3075
|
+
}
|
|
3076
|
+
return {
|
|
3077
|
+
legalName,
|
|
3078
|
+
vatId: textOrNull2(source, "vatId", where),
|
|
3079
|
+
taxNumber: textOrNull2(source, "taxNumber", where),
|
|
3080
|
+
addressLine1: textOrNull2(source, "addressLine1", where),
|
|
3081
|
+
addressLine2: textOrNull2(source, "addressLine2", where),
|
|
3082
|
+
postalCode: textOrNull2(source, "postalCode", where),
|
|
3083
|
+
city: textOrNull2(source, "city", where),
|
|
3084
|
+
country: textOrNull2(source, "country", where)
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
function issuerOf(value, where) {
|
|
3088
|
+
return partyOf(value, where);
|
|
3089
|
+
}
|
|
3090
|
+
function subscriberOf(value, where) {
|
|
3091
|
+
const party = partyOf(value, where);
|
|
3092
|
+
const customerNumber = textOrNull2(value, "customerNumber", where);
|
|
3093
|
+
if (customerNumber === null) {
|
|
3094
|
+
throw new Error(`${where.table}.${where.column} of ${where.id} names no customer number.`);
|
|
3095
|
+
}
|
|
3096
|
+
return { ...party, customerNumber };
|
|
3097
|
+
}
|
|
3098
|
+
function taxesOf(value, where) {
|
|
3099
|
+
if (!Array.isArray(value)) {
|
|
3100
|
+
throw new Error(`${where.table}.${where.column} of ${where.id} is not a list of rates.`);
|
|
3101
|
+
}
|
|
3102
|
+
return value.map((entry) => {
|
|
3103
|
+
const item = entry ?? {};
|
|
3104
|
+
return {
|
|
3105
|
+
rate: amountOf2(item.rate, where),
|
|
3106
|
+
net: amountOf2(item.net, where),
|
|
3107
|
+
tax: amountOf2(item.tax, where)
|
|
3108
|
+
};
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
// src/subscription-invoice-refusals.ts
|
|
3113
|
+
function subscriptionInvoiceChargeInvoiced(chargeId) {
|
|
3114
|
+
return new PersistenceRefusal(
|
|
3115
|
+
INVOICE_ERROR_CODES.SUBSCRIPTION_INVOICE_CHARGE_INVOICED,
|
|
3116
|
+
"moved",
|
|
3117
|
+
`Charge '${chargeId}' already stands on an invoice.`,
|
|
3118
|
+
{ chargeId }
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
function subscriptionInvoiceDocumentChanged(invoiceId, recorded, rendered) {
|
|
3122
|
+
return new PersistenceRefusal(
|
|
3123
|
+
INVOICE_ERROR_CODES.SUBSCRIPTION_INVOICE_DOCUMENT_CHANGED,
|
|
3124
|
+
"moved",
|
|
3125
|
+
`The document rendered for invoice '${invoiceId}' differs from the one recorded for it.`,
|
|
3126
|
+
{ invoiceId, recorded, rendered }
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
// src/subscription-invoice-draft.ts
|
|
3131
|
+
function invoiceGroupsOf(charges) {
|
|
3132
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3133
|
+
for (const charge of charges) {
|
|
3134
|
+
const key = `${charge.contractId}|${charge.bookedAt.toISOString()}`;
|
|
3135
|
+
const group = groups.get(key);
|
|
3136
|
+
if (group) group.charges.push(charge);
|
|
3137
|
+
else
|
|
3138
|
+
groups.set(key, {
|
|
3139
|
+
contractId: charge.contractId,
|
|
3140
|
+
bookedAt: charge.bookedAt,
|
|
3141
|
+
charges: [charge]
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
return [...groups.values()].filter((group) => group.charges.some((charge) => charge.amountNet !== 0)).sort(
|
|
3145
|
+
(a, b) => a.bookedAt.getTime() - b.bookedAt.getTime() || a.contractId.localeCompare(b.contractId)
|
|
3146
|
+
);
|
|
3147
|
+
}
|
|
3148
|
+
var DIRECT_DEBIT_LEAD_DAYS = 14;
|
|
3149
|
+
function directDebitLeadDaysOf(invoicing) {
|
|
3150
|
+
return invoicing.directDebitLeadDays ?? DIRECT_DEBIT_LEAD_DAYS;
|
|
3151
|
+
}
|
|
3152
|
+
function invoicePaymentOf(method) {
|
|
3153
|
+
if (!method) return null;
|
|
3154
|
+
if (method.type === "sepa_debit" && method.mandateReference === null) return null;
|
|
3155
|
+
return {
|
|
3156
|
+
paymentMethodId: method.id,
|
|
3157
|
+
gatewayAccount: method.gatewayAccount,
|
|
3158
|
+
type: method.type,
|
|
3159
|
+
brand: method.brand,
|
|
3160
|
+
last4: method.last4,
|
|
3161
|
+
mandateReference: method.type === "sepa_debit" ? method.mandateReference : null
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
function draftSubscriptionInvoice(input) {
|
|
3165
|
+
const { group, treatment, timeZone } = input;
|
|
3166
|
+
const lines = linesOf(input);
|
|
3167
|
+
const issueDate = dayInZone(input.issuedAt, timeZone);
|
|
3168
|
+
const tax = input.tax(lines.map((line) => ({ net: line.amountNet, rate: line.taxRate })));
|
|
3169
|
+
const payment = tax.gross > 0 ? input.payment : null;
|
|
3170
|
+
if (payment?.type === "sepa_debit" && payment.mandateReference === null) {
|
|
3171
|
+
throw new Error(
|
|
3172
|
+
`Payment method ${payment.paymentMethodId} is a direct debit without a mandate reference, which an invoice collected by it has to announce.`
|
|
3173
|
+
);
|
|
3174
|
+
}
|
|
3175
|
+
const collectionDate = payment?.type === "sepa_debit" ? addDaysToDay(issueDate, input.directDebitLeadDays) : null;
|
|
3176
|
+
return {
|
|
3177
|
+
numberPrefix: input.numberPrefix,
|
|
3178
|
+
numberYear: yearOfDay(issueDate),
|
|
3179
|
+
tenantId: input.tenantId,
|
|
3180
|
+
subscriberId: input.subscriberId,
|
|
3181
|
+
subscriptionId: group.charges[0].subscriptionId,
|
|
3182
|
+
contractId: group.contractId,
|
|
3183
|
+
issuedAt: input.issuedAt,
|
|
3184
|
+
issueDate,
|
|
3185
|
+
dueDate: collectionDate ?? addDaysToDay(issueDate, input.paymentTermDays),
|
|
3186
|
+
payment: payment ? { ...payment } : null,
|
|
3187
|
+
collectionDate,
|
|
3188
|
+
servicePeriodFrom: lines.map((line) => line.periodFrom).sort()[0],
|
|
3189
|
+
servicePeriodUntil: lines.map((line) => line.periodUntil).sort().at(-1),
|
|
3190
|
+
currency: currencyOf(group),
|
|
3191
|
+
issuer: input.issuer,
|
|
3192
|
+
subscriber: input.subscriber,
|
|
3193
|
+
footer: [...input.footer],
|
|
3194
|
+
taxTreatment: treatment,
|
|
3195
|
+
taxes: tax.rates,
|
|
3196
|
+
totalNet: tax.net,
|
|
3197
|
+
totalTax: tax.tax,
|
|
3198
|
+
totalGross: tax.gross,
|
|
3199
|
+
lines
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
function invoiceContentDraftOf(invoice) {
|
|
3203
|
+
return {
|
|
3204
|
+
issuer: invoice.issuer,
|
|
3205
|
+
subscriber: invoice.subscriber,
|
|
3206
|
+
treatment: invoice.taxTreatment,
|
|
3207
|
+
issueDate: invoice.issueDate,
|
|
3208
|
+
dueDate: invoice.dueDate,
|
|
3209
|
+
servicePeriod: { from: invoice.servicePeriodFrom, until: invoice.servicePeriodUntil },
|
|
3210
|
+
lines: invoice.lines.map((line) => ({
|
|
3211
|
+
title: line.title,
|
|
3212
|
+
net: line.amountNet,
|
|
3213
|
+
rate: line.taxRate
|
|
3214
|
+
}))
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
function linesOf(input) {
|
|
3218
|
+
const placeOf = new Map(input.contractLines.map((line, index) => [line.id, index]));
|
|
3219
|
+
const place = (charge) => {
|
|
3220
|
+
const index = placeOf.get(charge.contractLineItemId);
|
|
3221
|
+
if (index === void 0) {
|
|
3222
|
+
throw new Error(
|
|
3223
|
+
`Charge ${charge.id} names contract line ${charge.contractLineItemId}, which contract ${input.group.contractId} does not have.`
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
return index;
|
|
3227
|
+
};
|
|
3228
|
+
return [...input.group.charges].sort(
|
|
3229
|
+
(a, b) => place(a) - place(b) || a.periodStart.getTime() - b.periodStart.getTime() || a.origin.localeCompare(b.origin)
|
|
3230
|
+
).map((charge, index) => ({
|
|
3231
|
+
position: index + 1,
|
|
3232
|
+
chargeId: charge.id,
|
|
3233
|
+
contractLineItemId: charge.contractLineItemId,
|
|
3234
|
+
title: input.contractLines[place(charge)].titleSnapshot,
|
|
3235
|
+
origin: charge.origin,
|
|
3236
|
+
source: charge.source,
|
|
3237
|
+
periodFrom: dayInZone(charge.periodStart, input.timeZone),
|
|
3238
|
+
periodUntil: lastDayInZone(
|
|
3239
|
+
{ from: charge.periodStart, until: charge.periodEnd },
|
|
3240
|
+
input.timeZone
|
|
3241
|
+
),
|
|
3242
|
+
amountNet: charge.amountNet,
|
|
3243
|
+
taxRate: input.treatment.rate
|
|
3244
|
+
}));
|
|
3245
|
+
}
|
|
3246
|
+
function currencyOf(group) {
|
|
3247
|
+
const currencies = new Set(group.charges.map((charge) => charge.currency));
|
|
3248
|
+
if (currencies.size !== 1) {
|
|
3249
|
+
throw new Error(
|
|
3250
|
+
`The charges of contract ${group.contractId} booked at ${group.bookedAt.toISOString()} are in ${[...currencies].join(" and ")}; an invoice is in one currency.`
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3253
|
+
return [...currencies][0];
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
// src/invoice-issuer.ts
|
|
3257
|
+
function invoiceIssuerOf(copy, configured, history) {
|
|
3258
|
+
const target = configured && identityOf2(configured);
|
|
3259
|
+
let reached = identityOf2(copy);
|
|
3260
|
+
if (!target || !reached) return { ...copy };
|
|
3261
|
+
for (const step of history) {
|
|
3262
|
+
if (sameLegalIdentity(reached, target)) break;
|
|
3263
|
+
const change = classifyIssuerChange(
|
|
3264
|
+
recordedIssuerIdentity(step.previous),
|
|
3265
|
+
issuerBlockOf(step.current)
|
|
3266
|
+
);
|
|
3267
|
+
if (change.kind === "corrected" && sameLegalIdentity(change.recorded, reached)) {
|
|
3268
|
+
reached = change.current;
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
return sameLegalIdentity(reached, target) ? { ...configured } : { ...copy };
|
|
3272
|
+
}
|
|
3273
|
+
function invoiceFooterOf(issuer, configured, footer) {
|
|
3274
|
+
const named = identityOf2(issuer);
|
|
3275
|
+
const target = configured && identityOf2(configured);
|
|
3276
|
+
return named && target && sameLegalIdentity(named, target) ? [...footer] : [];
|
|
3277
|
+
}
|
|
3278
|
+
function identityOf2(party) {
|
|
3279
|
+
return recordedIssuerIdentity({ issuer: party });
|
|
3280
|
+
}
|
|
3281
|
+
function issuerBlockOf(values) {
|
|
3282
|
+
const issuer = values.issuer;
|
|
3283
|
+
if (issuer === null || typeof issuer !== "object" || Array.isArray(issuer)) return void 0;
|
|
3284
|
+
return issuer;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
// src/invoice-document.types.ts
|
|
3288
|
+
var INVOICE_DOCUMENT_CAPABILITY = "invoices.read";
|
|
3289
|
+
|
|
3290
|
+
// src/invoice-document-mapping.ts
|
|
3291
|
+
var DOCUMENTS = "subscription_invoice_documents";
|
|
3292
|
+
var ARCHIVE = "subscription_invoice_archive";
|
|
3293
|
+
var CHECKSUM = /^[0-9a-f]{64}$/;
|
|
3294
|
+
function isInvoiceDocumentChecksum(value) {
|
|
3295
|
+
return typeof value === "string" && CHECKSUM.test(value);
|
|
3296
|
+
}
|
|
3297
|
+
function toSubscriptionInvoiceDocumentRecord(row) {
|
|
3298
|
+
if (!isInvoiceDocumentChecksum(row.checksum)) {
|
|
3299
|
+
throw new Error(`${DOCUMENTS}.checksum of ${row.invoiceId} is not a SHA-256 digest.`);
|
|
3300
|
+
}
|
|
3301
|
+
const size = Number(row.size);
|
|
3302
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
3303
|
+
throw new Error(`${DOCUMENTS}.size of ${row.invoiceId} is not a size in bytes.`);
|
|
3304
|
+
}
|
|
3305
|
+
return {
|
|
3306
|
+
invoiceId: row.invoiceId,
|
|
3307
|
+
checksum: row.checksum,
|
|
3308
|
+
mediaType: row.mediaType,
|
|
3309
|
+
fileName: row.fileName,
|
|
3310
|
+
size,
|
|
3311
|
+
renderedAt: row.renderedAt,
|
|
3312
|
+
archivedAt: row.archivedAt,
|
|
3313
|
+
deliveryClaimedAt: row.deliveryClaimedAt,
|
|
3314
|
+
deliveredAt: row.deliveredAt,
|
|
3315
|
+
deliveredTo: row.deliveredTo
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
function toArchivedInvoiceDocument(row) {
|
|
3319
|
+
if (!isInvoiceDocumentChecksum(row.checksum)) {
|
|
3320
|
+
throw new Error(`${ARCHIVE}.checksum of ${row.invoiceId} is not a SHA-256 digest.`);
|
|
3321
|
+
}
|
|
3322
|
+
if (!(row.content instanceof Uint8Array)) {
|
|
3323
|
+
throw new Error(`${ARCHIVE}.content of ${row.invoiceId} is not a file.`);
|
|
3324
|
+
}
|
|
3325
|
+
return {
|
|
3326
|
+
invoiceId: row.invoiceId,
|
|
3327
|
+
checksum: row.checksum,
|
|
3328
|
+
mediaType: row.mediaType,
|
|
3329
|
+
fileName: row.fileName,
|
|
3330
|
+
// A copy that owns its bytes: a driver's `Buffer` may be a view into a
|
|
3331
|
+
// pooled allocation, and a caller handing it on must not see that pool.
|
|
3332
|
+
content: new Uint8Array(row.content),
|
|
3333
|
+
archivedAt: row.archivedAt
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
var INVOICE_ARCHIVE_TRIGGERS = [
|
|
3337
|
+
"subscription_invoice_archive_is_kept",
|
|
3338
|
+
"subscription_invoice_archive_is_not_emptied"
|
|
3339
|
+
];
|
|
3340
|
+
function invoiceArchiveProtectionProblem(enabledTriggers) {
|
|
3341
|
+
const missing = INVOICE_ARCHIVE_TRIGGERS.filter((name) => !enabledTriggers.includes(name));
|
|
3342
|
+
if (missing.length === 0) return null;
|
|
3343
|
+
return `${ARCHIVE} lacks ${missing.join(" and ")}, which keep an archived invoice document as it was issued: run @saasicat/spec/sql/1.0-an-invoice-document-is-archived.postgres.sql after db push, and enable the triggers where they were disabled.`;
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/invoice-collection.types.ts
|
|
3347
|
+
function sentCollectionOf(attempt) {
|
|
3348
|
+
const { submittedAt, heldAt } = attempt;
|
|
3349
|
+
if (submittedAt === null || heldAt === null) {
|
|
3350
|
+
throw new Error(`Collection attempt ${attempt.attemptKey} has not been sent.`);
|
|
3351
|
+
}
|
|
3352
|
+
return { ...attempt, submittedAt, heldAt };
|
|
3353
|
+
}
|
|
3354
|
+
var INVOICE_COLLECTION_LEASE_MS = 15 * 6e4;
|
|
3355
|
+
var INVOICE_COLLECTION_ASK_EVERY_MS = 60 * 6e4;
|
|
3356
|
+
function collectionAttemptKey(invoiceId, attempt) {
|
|
3357
|
+
if (!Number.isInteger(attempt) || attempt < 1) {
|
|
3358
|
+
throw new RangeError(`A collection attempt counts from 1; ${attempt} is not one.`);
|
|
3359
|
+
}
|
|
3360
|
+
return `saasicat:collect:${invoiceId}:${attempt}`;
|
|
3361
|
+
}
|
|
3362
|
+
function collectionStatusOf(outcome) {
|
|
3363
|
+
if (outcome.status === "succeeded") return "SUCCEEDED";
|
|
3364
|
+
if (outcome.status === "pending") return "PENDING";
|
|
3365
|
+
return "FAILED";
|
|
3366
|
+
}
|
|
3367
|
+
var COLLECTION_NOT_SENT_PAID = {
|
|
3368
|
+
status: "failed",
|
|
3369
|
+
paymentRef: null,
|
|
3370
|
+
reason: "other",
|
|
3371
|
+
gatewayCode: "saasicat_invoice_paid"
|
|
3372
|
+
};
|
|
3373
|
+
var COLLECTION_NOT_SENT_REVERSED = {
|
|
3374
|
+
status: "failed",
|
|
3375
|
+
paymentRef: null,
|
|
3376
|
+
reason: "other",
|
|
3377
|
+
gatewayCode: "saasicat_payment_method_reversed"
|
|
3378
|
+
};
|
|
3379
|
+
|
|
3380
|
+
// src/invoice-collection-mapping.ts
|
|
3381
|
+
var TABLE5 = "subscription_invoice_collections";
|
|
3382
|
+
var INVOICE_COLLECTION_STATUSES = [
|
|
3383
|
+
"SCHEDULED",
|
|
3384
|
+
"SUBMITTED",
|
|
3385
|
+
"PENDING",
|
|
3386
|
+
"SUCCEEDED",
|
|
3387
|
+
"FAILED",
|
|
3388
|
+
"WITHDRAWN",
|
|
3389
|
+
"REVERSED"
|
|
3390
|
+
];
|
|
3391
|
+
var UNSETTLED_COLLECTION_STATUSES = [
|
|
3392
|
+
"SUBMITTED",
|
|
3393
|
+
"PENDING"
|
|
3394
|
+
];
|
|
3395
|
+
var BLOCKING_COLLECTION_STATUSES = [
|
|
3396
|
+
"SCHEDULED",
|
|
3397
|
+
...UNSETTLED_COLLECTION_STATUSES,
|
|
3398
|
+
"SUCCEEDED"
|
|
3399
|
+
];
|
|
3400
|
+
var FOLLOWED_UP_COLLECTION_STATUSES = [
|
|
3401
|
+
"FAILED",
|
|
3402
|
+
"REVERSED"
|
|
3403
|
+
];
|
|
3404
|
+
var REVERSIBLE_COLLECTION_STATUSES = [
|
|
3405
|
+
"SUCCEEDED",
|
|
3406
|
+
"FAILED"
|
|
3407
|
+
];
|
|
3408
|
+
function lastCollectionOf(earlier) {
|
|
3409
|
+
return earlier.reduce((last, row) => Math.max(last, row.attempt), 0);
|
|
3410
|
+
}
|
|
3411
|
+
function refusesFirstAttempt(earlier) {
|
|
3412
|
+
return earlier.length > 0;
|
|
3413
|
+
}
|
|
3414
|
+
function refusesPlannedAttempt(earlier, after) {
|
|
3415
|
+
return earlier.some((row) => BLOCKING.has(row.status)) || lastCollectionOf(earlier) !== after;
|
|
3416
|
+
}
|
|
3417
|
+
var BLOCKING = new Set(BLOCKING_COLLECTION_STATUSES);
|
|
3418
|
+
var NOT_SENT = /* @__PURE__ */ new Set(["SCHEDULED", "WITHDRAWN"]);
|
|
3419
|
+
var COLLECTION_FAILURE_REASONS = [
|
|
3420
|
+
"insufficient-funds",
|
|
3421
|
+
"declined",
|
|
3422
|
+
"authentication-required",
|
|
3423
|
+
"payment-method-unusable",
|
|
3424
|
+
"mandate-invalid",
|
|
3425
|
+
"other"
|
|
3426
|
+
];
|
|
3427
|
+
function toInvoiceCollectionRecord(row) {
|
|
3428
|
+
const where = (column) => ({ table: TABLE5, column, id: row.id });
|
|
3429
|
+
const amount = Number(
|
|
3430
|
+
// oxlint-disable-next-line typescript/no-base-to-string -- Prisma hands a Decimal, whose text is the number; any other object becomes NaN and is refused below
|
|
3431
|
+
row.amount === null || row.amount === void 0 ? NaN : String(row.amount)
|
|
3432
|
+
);
|
|
3433
|
+
if (!Number.isFinite(amount)) {
|
|
3434
|
+
throw new Error(`${TABLE5}.amount of ${row.id} is not an amount.`);
|
|
3435
|
+
}
|
|
3436
|
+
const status = oneOf(INVOICE_COLLECTION_STATUSES, row.status, where("status"));
|
|
3437
|
+
const sent = !NOT_SENT.has(status);
|
|
3438
|
+
if (sent !== (row.submittedAt !== null) || sent !== (row.heldAt !== null)) {
|
|
3439
|
+
throw new Error(
|
|
3440
|
+
`${TABLE5}.submittedAt and heldAt of ${row.id} do not fit its status ${status}: an attempt that left carries both, and one that did not carries neither.`
|
|
3441
|
+
);
|
|
3442
|
+
}
|
|
3443
|
+
const collectOn = row.collectOn === null || row.collectOn === void 0 ? null : dayOf2(row.collectOn, where("collectOn"));
|
|
3444
|
+
if (status === "SCHEDULED" && collectOn === null) {
|
|
3445
|
+
throw new Error(`${TABLE5}.collectOn of ${row.id} is missing from a planned attempt.`);
|
|
3446
|
+
}
|
|
3447
|
+
return {
|
|
3448
|
+
id: row.id,
|
|
3449
|
+
invoiceId: row.invoiceId,
|
|
3450
|
+
attempt: row.attempt,
|
|
3451
|
+
attemptKey: row.attemptKey,
|
|
3452
|
+
gatewayAccount: row.gatewayAccount,
|
|
3453
|
+
paymentMethodId: row.paymentMethodId,
|
|
3454
|
+
paymentMethodType: oneOf(
|
|
3455
|
+
PAYMENT_METHOD_TYPES,
|
|
3456
|
+
row.paymentMethodType,
|
|
3457
|
+
where("paymentMethodType")
|
|
3458
|
+
),
|
|
3459
|
+
customerRef: row.customerRef,
|
|
3460
|
+
paymentMethodRef: row.paymentMethodRef,
|
|
3461
|
+
invoiceNumber: row.invoiceNumber,
|
|
3462
|
+
amount,
|
|
3463
|
+
currency: row.currency,
|
|
3464
|
+
status,
|
|
3465
|
+
paymentRef: row.paymentRef,
|
|
3466
|
+
failureReason: row.failureReason === null ? null : oneOf(COLLECTION_FAILURE_REASONS, row.failureReason, where("failureReason")),
|
|
3467
|
+
failureCode: row.failureCode,
|
|
3468
|
+
submittedAt: row.submittedAt,
|
|
3469
|
+
heldAt: row.heldAt,
|
|
3470
|
+
resolvedAt: row.resolvedAt,
|
|
3471
|
+
collectOn,
|
|
3472
|
+
announcedAt: row.announcedAt,
|
|
3473
|
+
followedUpAt: row.followedUpAt
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
function invoiceCollectionColumns(attempt, number) {
|
|
3477
|
+
return {
|
|
3478
|
+
invoiceId: attempt.invoiceId,
|
|
3479
|
+
attempt: number,
|
|
3480
|
+
attemptKey: collectionAttemptKey(attempt.invoiceId, number),
|
|
3481
|
+
gatewayAccount: attempt.gatewayAccount,
|
|
3482
|
+
paymentMethodId: attempt.paymentMethodId,
|
|
3483
|
+
paymentMethodType: attempt.paymentMethodType,
|
|
3484
|
+
customerRef: attempt.customerRef,
|
|
3485
|
+
paymentMethodRef: attempt.paymentMethodRef,
|
|
3486
|
+
invoiceNumber: attempt.invoiceNumber,
|
|
3487
|
+
amount: attempt.amount.toFixed(2),
|
|
3488
|
+
currency: attempt.currency,
|
|
3489
|
+
status: "SUBMITTED",
|
|
3490
|
+
submittedAt: attempt.at,
|
|
3491
|
+
heldAt: attempt.at,
|
|
3492
|
+
collectOn: null
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
function plannedCollectionColumns(plan, number) {
|
|
3496
|
+
return {
|
|
3497
|
+
invoiceId: plan.invoiceId,
|
|
3498
|
+
attempt: number,
|
|
3499
|
+
attemptKey: collectionAttemptKey(plan.invoiceId, number),
|
|
3500
|
+
gatewayAccount: plan.gatewayAccount,
|
|
3501
|
+
paymentMethodId: plan.paymentMethodId,
|
|
3502
|
+
paymentMethodType: plan.paymentMethodType,
|
|
3503
|
+
customerRef: plan.customerRef,
|
|
3504
|
+
paymentMethodRef: plan.paymentMethodRef,
|
|
3505
|
+
invoiceNumber: plan.invoiceNumber,
|
|
3506
|
+
amount: plan.amount.toFixed(2),
|
|
3507
|
+
currency: plan.currency,
|
|
3508
|
+
status: "SCHEDULED",
|
|
3509
|
+
submittedAt: null,
|
|
3510
|
+
heldAt: null,
|
|
3511
|
+
collectOn: plan.collectOn
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
function collectionOutcomeColumns(outcome, at) {
|
|
3515
|
+
const failed = outcome.status === "failed" ? outcome : null;
|
|
3516
|
+
return {
|
|
3517
|
+
status: collectionStatusOf(outcome),
|
|
3518
|
+
paymentRef: outcome.paymentRef,
|
|
3519
|
+
failureReason: failed?.reason ?? null,
|
|
3520
|
+
failureCode: failed?.gatewayCode ?? null,
|
|
3521
|
+
resolvedAt: outcome.status === "pending" ? null : at
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
// src/invoice-collection-plan.ts
|
|
3526
|
+
var COLLECTION_RETRY_AFTER_DAYS = 7;
|
|
3527
|
+
function isChargedAgain(failed, attempts) {
|
|
3528
|
+
if (failed.status !== "FAILED" || failed.failureReason === null) return false;
|
|
3529
|
+
if (!isRetryableCollectionFailure(failed.failureReason)) return false;
|
|
3530
|
+
return !attempts.some(
|
|
3531
|
+
(other) => other.id !== failed.id && other.status === "FAILED" && other.paymentMethodId === failed.paymentMethodId
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
3534
|
+
function chargedAgainOn(failedOn) {
|
|
3535
|
+
return addDaysToDay(failedOn, COLLECTION_RETRY_AFTER_DAYS);
|
|
3536
|
+
}
|
|
3537
|
+
function announcedCollectionDay(earliest2, announcedOn, leadDays) {
|
|
3538
|
+
const afterLead = addDaysToDay(announcedOn, leadDays);
|
|
3539
|
+
return afterLead > earliest2 ? afterLead : earliest2;
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
// src/invoice-collection-notice-mapping.ts
|
|
3543
|
+
var TABLE6 = "subscription_notices";
|
|
3544
|
+
var NEXT_ACTIONS = [
|
|
3545
|
+
"charged-again",
|
|
3546
|
+
"announced-again",
|
|
3547
|
+
"replace-payment-method"
|
|
3548
|
+
];
|
|
3549
|
+
var AFTER_REVERSAL_ACTIONS = [
|
|
3550
|
+
"collected-with-payment-method-in-use",
|
|
3551
|
+
"replace-payment-method"
|
|
3552
|
+
];
|
|
3553
|
+
var NoticeFields = class _NoticeFields {
|
|
3554
|
+
constructor(content, id) {
|
|
3555
|
+
this.content = content;
|
|
3556
|
+
this.id = id;
|
|
3557
|
+
}
|
|
3558
|
+
content;
|
|
3559
|
+
id;
|
|
3560
|
+
where(column) {
|
|
3561
|
+
return { table: TABLE6, column: `content.${column}`, id: this.id };
|
|
3562
|
+
}
|
|
3563
|
+
value(name) {
|
|
3564
|
+
if (this.content === null || typeof this.content !== "object") {
|
|
3565
|
+
throw new Error(`${TABLE6}.content of ${this.id} is not a notice.`);
|
|
3566
|
+
}
|
|
3567
|
+
return Object.getOwnPropertyDescriptor(this.content, name)?.value;
|
|
3568
|
+
}
|
|
3569
|
+
text(name) {
|
|
3570
|
+
const value = this.value(name);
|
|
3571
|
+
if (typeof value !== "string" || value === "") {
|
|
3572
|
+
throw new Error(`${TABLE6}.content.${name} of ${this.id} is not a text.`);
|
|
3573
|
+
}
|
|
3574
|
+
return value;
|
|
3575
|
+
}
|
|
3576
|
+
amount(name) {
|
|
3577
|
+
const value = this.value(name);
|
|
3578
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3579
|
+
throw new Error(`${TABLE6}.content.${name} of ${this.id} is not an amount.`);
|
|
3580
|
+
}
|
|
3581
|
+
return value;
|
|
3582
|
+
}
|
|
3583
|
+
day(name) {
|
|
3584
|
+
return dayOf2(this.value(name), this.where(name));
|
|
3585
|
+
}
|
|
3586
|
+
oneOf(allowed, name) {
|
|
3587
|
+
return oneOf(allowed, this.text(name), this.where(name));
|
|
3588
|
+
}
|
|
3589
|
+
nested(name) {
|
|
3590
|
+
return new _NoticeFields(this.value(name), this.id);
|
|
3591
|
+
}
|
|
3592
|
+
invoice() {
|
|
3593
|
+
return {
|
|
3594
|
+
tenantId: this.text("tenantId"),
|
|
3595
|
+
subscriptionId: this.text("subscriptionId"),
|
|
3596
|
+
subscriberId: this.text("subscriberId"),
|
|
3597
|
+
invoiceId: this.text("invoiceId"),
|
|
3598
|
+
invoiceNumber: this.text("invoiceNumber"),
|
|
3599
|
+
amount: this.amount("amount"),
|
|
3600
|
+
currency: this.text("currency")
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
};
|
|
3604
|
+
function toCollectionFailedNotice(content, id) {
|
|
3605
|
+
const fields = new NoticeFields(content, id);
|
|
3606
|
+
const next = fields.nested("next");
|
|
3607
|
+
const action = next.oneOf(NEXT_ACTIONS, "action");
|
|
3608
|
+
return {
|
|
3609
|
+
kind: fields.oneOf(["collection-failed"], "kind"),
|
|
3610
|
+
...fields.invoice(),
|
|
3611
|
+
paymentMethodType: fields.oneOf(PAYMENT_METHOD_TYPES, "paymentMethodType"),
|
|
3612
|
+
reason: fields.oneOf(COLLECTION_FAILURE_REASONS, "reason"),
|
|
3613
|
+
next: action === "charged-again" ? { action, on: next.day("on") } : { action }
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
function toCollectionAnnouncedNotice(content, id) {
|
|
3617
|
+
const fields = new NoticeFields(content, id);
|
|
3618
|
+
return {
|
|
3619
|
+
kind: fields.oneOf(["collection-announced"], "kind"),
|
|
3620
|
+
...fields.invoice(),
|
|
3621
|
+
collectOn: fields.day("collectOn"),
|
|
3622
|
+
mandateReference: fields.text("mandateReference"),
|
|
3623
|
+
last4: fields.text("last4")
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
function toCollectionReversedNotice(content, id) {
|
|
3627
|
+
const fields = new NoticeFields(content, id);
|
|
3628
|
+
return {
|
|
3629
|
+
kind: fields.oneOf(["collection-reversed"], "kind"),
|
|
3630
|
+
...fields.invoice(),
|
|
3631
|
+
paymentMethodType: fields.oneOf(PAYMENT_METHOD_TYPES, "paymentMethodType"),
|
|
3632
|
+
owed: fields.amount("owed"),
|
|
3633
|
+
next: { action: fields.nested("next").oneOf(AFTER_REVERSAL_ACTIONS, "action") }
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
// src/subscriber-payment.types.ts
|
|
3638
|
+
var SUBSCRIBER_CREDIT_CAPABILITY = "credits.read";
|
|
3639
|
+
var SUBSCRIBER_PAYOUT_CAPABILITY = "credits.payout";
|
|
3640
|
+
|
|
3641
|
+
// src/column-amount.ts
|
|
3642
|
+
function columnAmountOf(value, where) {
|
|
3643
|
+
const amount = Number(value === null || value === void 0 ? NaN : String(value));
|
|
3644
|
+
if (!Number.isFinite(amount)) {
|
|
3645
|
+
throw new Error(`${where.table}.${where.column} of ${where.id} is not an amount.`);
|
|
3646
|
+
}
|
|
3647
|
+
return amount;
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
// src/subscriber-payment-mapping.ts
|
|
3651
|
+
var PAYMENTS = "subscriber_payments";
|
|
3652
|
+
var CREDITS = "subscriber_credits";
|
|
3653
|
+
var CREDIT_ENTRIES = "subscriber_credit_entries";
|
|
3654
|
+
var REFUNDS = "subscriber_refunds";
|
|
3655
|
+
var SETTLEMENTS = "subscription_invoice_settlements";
|
|
3656
|
+
var REVERSALS = "subscriber_reversals";
|
|
3657
|
+
var INVOICE_SETTLEMENT_KINDS = [
|
|
3658
|
+
"PAYMENT",
|
|
3659
|
+
"REVERSAL",
|
|
3660
|
+
"REINSTATEMENT"
|
|
3661
|
+
];
|
|
3662
|
+
var REOPENING_SETTLEMENT_KINDS = ["REVERSAL"];
|
|
3663
|
+
var SUBSCRIBER_CREDIT_ENTRY_KINDS = [
|
|
3664
|
+
"REFUND",
|
|
3665
|
+
"REFUND_FAILED",
|
|
3666
|
+
"PAYOUT",
|
|
3667
|
+
"REVERSAL",
|
|
3668
|
+
"REVERSAL_TAKEN_BACK"
|
|
3669
|
+
];
|
|
3670
|
+
var REFUND_ENTRY_KINDS = /* @__PURE__ */ new Set([
|
|
3671
|
+
"REFUND",
|
|
3672
|
+
"REFUND_FAILED"
|
|
3673
|
+
]);
|
|
3674
|
+
var REVERSAL_ENTRY_KINDS = /* @__PURE__ */ new Set([
|
|
3675
|
+
"REVERSAL",
|
|
3676
|
+
"REVERSAL_TAKEN_BACK"
|
|
3677
|
+
]);
|
|
3678
|
+
var SUBSCRIBER_REFUND_STATUSES = [
|
|
3679
|
+
"PENDING",
|
|
3680
|
+
"SUCCEEDED",
|
|
3681
|
+
"FAILED",
|
|
3682
|
+
"CANCELED"
|
|
3683
|
+
];
|
|
3684
|
+
var ENDED_REFUND_STATUSES = ["FAILED", "CANCELED"];
|
|
3685
|
+
function isEndedRefund(status) {
|
|
3686
|
+
return ENDED_REFUND_STATUSES.includes(status);
|
|
3687
|
+
}
|
|
3688
|
+
function toSubscriberPaymentRecord(row) {
|
|
3689
|
+
return {
|
|
3690
|
+
id: row.id,
|
|
3691
|
+
subscriberId: row.subscriberId,
|
|
3692
|
+
tenantId: row.tenantId,
|
|
3693
|
+
invoiceId: row.invoiceId,
|
|
3694
|
+
collectionId: row.collectionId,
|
|
3695
|
+
gatewayAccount: row.gatewayAccount,
|
|
3696
|
+
provider: row.provider,
|
|
3697
|
+
gatewayRef: row.gatewayRef,
|
|
3698
|
+
amount: columnAmountOf(row.amount, { table: PAYMENTS, column: "amount", id: row.id }),
|
|
3699
|
+
currency: row.currency,
|
|
3700
|
+
recordedAt: row.recordedAt
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
function subscriberPaymentColumns(payment, invoice) {
|
|
3704
|
+
return {
|
|
3705
|
+
subscriberId: invoice.subscriberId,
|
|
3706
|
+
tenantId: invoice.tenantId,
|
|
3707
|
+
invoiceId: payment.invoiceId,
|
|
3708
|
+
collectionId: payment.collectionId,
|
|
3709
|
+
gatewayAccount: payment.gatewayAccount,
|
|
3710
|
+
provider: payment.provider,
|
|
3711
|
+
gatewayRef: payment.gatewayRef,
|
|
3712
|
+
amount: toDecimalString(payment.amount),
|
|
3713
|
+
currency: payment.currency,
|
|
3714
|
+
recordedAt: payment.at
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
function settledAmountOf(row) {
|
|
3718
|
+
const kind = oneOf(INVOICE_SETTLEMENT_KINDS, row.kind, {
|
|
3719
|
+
table: SETTLEMENTS,
|
|
3720
|
+
column: "kind",
|
|
3721
|
+
id: row.id
|
|
3722
|
+
});
|
|
3723
|
+
const amount = columnAmountOf(row.amount, { table: SETTLEMENTS, column: "amount", id: row.id });
|
|
3724
|
+
return REOPENING_SETTLEMENT_KINDS.includes(kind) ? -amount : amount;
|
|
3725
|
+
}
|
|
3726
|
+
function toSubscriberCreditRecord(row) {
|
|
3727
|
+
return {
|
|
3728
|
+
id: row.id,
|
|
3729
|
+
subscriberId: row.subscriberId,
|
|
3730
|
+
tenantId: row.tenantId,
|
|
3731
|
+
amount: columnAmountOf(row.amount, { table: CREDITS, column: "amount", id: row.id }),
|
|
3732
|
+
currency: row.currency,
|
|
3733
|
+
origin: creditOriginOf(row),
|
|
3734
|
+
createdAt: row.createdAt
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
function creditOriginOf(row) {
|
|
3738
|
+
const origins = [];
|
|
3739
|
+
if (row.originPaymentId !== null) {
|
|
3740
|
+
origins.push({ kind: "payment", paymentId: row.originPaymentId });
|
|
3741
|
+
}
|
|
3742
|
+
if (row.originInvoiceId !== null) {
|
|
3743
|
+
origins.push({ kind: "invoice", invoiceId: row.originInvoiceId });
|
|
3744
|
+
}
|
|
3745
|
+
if (row.originReversalId !== null) {
|
|
3746
|
+
origins.push({ kind: "reversal", reversalId: row.originReversalId });
|
|
3747
|
+
}
|
|
3748
|
+
const [origin] = origins;
|
|
3749
|
+
if (origin && origins.length === 1) return origin;
|
|
3750
|
+
throw new Error(
|
|
3751
|
+
`${CREDITS} row '${row.id}' names ${origins.length === 0 ? "none" : String(origins.length)} of a payment, an invoice and a reversal it comes from; a credit comes from exactly one.`
|
|
3752
|
+
);
|
|
3753
|
+
}
|
|
3754
|
+
function toSubscriberCreditEntryRecord(row) {
|
|
3755
|
+
const where = (column) => ({ table: CREDIT_ENTRIES, column, id: row.id });
|
|
3756
|
+
const kind = oneOf(SUBSCRIBER_CREDIT_ENTRY_KINDS, row.kind, where("kind"));
|
|
3757
|
+
const paidOn = row.paidOn === null || row.paidOn === void 0 ? null : dayOf2(row.paidOn, where("paidOn"));
|
|
3758
|
+
if (REFUND_ENTRY_KINDS.has(kind) !== (row.refundId !== null)) {
|
|
3759
|
+
throw new Error(
|
|
3760
|
+
`${CREDIT_ENTRIES} row '${row.id}' is a ${kind} ${row.refundId === null ? "without" : "with"} a refund; the entries of a refund name it, and no other entry does.`
|
|
3761
|
+
);
|
|
3762
|
+
}
|
|
3763
|
+
if (REVERSAL_ENTRY_KINDS.has(kind) !== (row.reversalId !== null)) {
|
|
3764
|
+
throw new Error(
|
|
3765
|
+
`${CREDIT_ENTRIES} row '${row.id}' is a ${kind} ${row.reversalId === null ? "without" : "with"} a reversal; the entries of a reversal name it, and no other entry does.`
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
return {
|
|
3769
|
+
id: row.id,
|
|
3770
|
+
creditId: row.creditId,
|
|
3771
|
+
kind,
|
|
3772
|
+
amount: columnAmountOf(row.amount, where("amount")),
|
|
3773
|
+
refundId: row.refundId,
|
|
3774
|
+
reversalId: row.reversalId,
|
|
3775
|
+
reference: row.reference,
|
|
3776
|
+
paidOn,
|
|
3777
|
+
recordedBy: row.recordedBy,
|
|
3778
|
+
note: row.note,
|
|
3779
|
+
recordedAt: row.recordedAt
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
function refundEntryColumns(creditId, refund, kind, amount, at) {
|
|
3783
|
+
return {
|
|
3784
|
+
creditId,
|
|
3785
|
+
kind,
|
|
3786
|
+
amount: toDecimalString(amount),
|
|
3787
|
+
refundId: refund.id,
|
|
3788
|
+
reversalId: null,
|
|
3789
|
+
reference: refund.gatewayRef,
|
|
3790
|
+
paidOn: null,
|
|
3791
|
+
recordedBy: null,
|
|
3792
|
+
note: null,
|
|
3793
|
+
recordedAt: at
|
|
3794
|
+
};
|
|
3795
|
+
}
|
|
3796
|
+
function payoutEntryColumns(payout) {
|
|
3797
|
+
return {
|
|
3798
|
+
creditId: payout.creditId,
|
|
3799
|
+
kind: "PAYOUT",
|
|
3800
|
+
amount: toDecimalString(payout.amount),
|
|
3801
|
+
refundId: null,
|
|
3802
|
+
reversalId: null,
|
|
3803
|
+
reference: payout.reference,
|
|
3804
|
+
paidOn: payout.paidOn,
|
|
3805
|
+
recordedBy: payout.recordedBy,
|
|
3806
|
+
note: payout.note,
|
|
3807
|
+
recordedAt: payout.at
|
|
3808
|
+
};
|
|
3809
|
+
}
|
|
3810
|
+
function toSubscriberRefundRecord(row) {
|
|
3811
|
+
const where = (column) => ({ table: REFUNDS, column, id: row.id });
|
|
3812
|
+
const status = oneOf(SUBSCRIBER_REFUND_STATUSES, row.status, where("status"));
|
|
3813
|
+
if (isEndedRefund(status) !== (row.endedAt !== null)) {
|
|
3814
|
+
throw new Error(
|
|
3815
|
+
`${REFUNDS}.endedAt of ${row.id} does not fit its status ${status}: a refund that failed or was canceled carries when, and one that pays out does not.`
|
|
3816
|
+
);
|
|
3817
|
+
}
|
|
3818
|
+
return {
|
|
3819
|
+
id: row.id,
|
|
3820
|
+
subscriberId: row.subscriberId,
|
|
3821
|
+
paymentId: row.paymentId,
|
|
3822
|
+
gatewayAccount: row.gatewayAccount,
|
|
3823
|
+
gatewayRef: row.gatewayRef,
|
|
3824
|
+
amount: columnAmountOf(row.amount, where("amount")),
|
|
3825
|
+
currency: row.currency,
|
|
3826
|
+
status,
|
|
3827
|
+
recordedAt: row.recordedAt,
|
|
3828
|
+
endedAt: row.endedAt
|
|
3829
|
+
};
|
|
3830
|
+
}
|
|
3831
|
+
function subscriberRefundColumns(refund, payment) {
|
|
3832
|
+
return {
|
|
3833
|
+
subscriberId: payment.subscriberId,
|
|
3834
|
+
paymentId: payment.id,
|
|
3835
|
+
gatewayAccount: refund.gatewayAccount,
|
|
3836
|
+
gatewayRef: refund.gatewayRef,
|
|
3837
|
+
amount: toDecimalString(refund.amount),
|
|
3838
|
+
currency: refund.currency,
|
|
3839
|
+
status: refund.status,
|
|
3840
|
+
recordedAt: refund.at,
|
|
3841
|
+
endedAt: isEndedRefund(refund.status) ? refund.at : null
|
|
3842
|
+
};
|
|
3843
|
+
}
|
|
3844
|
+
function reversalEntryColumns(creditId, reversal, kind, amount, at) {
|
|
3845
|
+
return {
|
|
3846
|
+
creditId,
|
|
3847
|
+
kind,
|
|
3848
|
+
amount: toDecimalString(amount),
|
|
3849
|
+
refundId: null,
|
|
3850
|
+
reversalId: reversal.id,
|
|
3851
|
+
reference: reversal.gatewayRef,
|
|
3852
|
+
paidOn: null,
|
|
3853
|
+
recordedBy: null,
|
|
3854
|
+
note: null,
|
|
3855
|
+
recordedAt: at
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3858
|
+
function toSubscriberReversalRecord(row) {
|
|
3859
|
+
const where = (column) => ({ table: REVERSALS, column, id: row.id });
|
|
3860
|
+
const amount = columnAmountOf(row.amount, where("amount"));
|
|
3861
|
+
const fromCredit = columnAmountOf(row.fromCredit, where("fromCredit"));
|
|
3862
|
+
const reinstated = columnAmountOf(row.reinstated, where("reinstated"));
|
|
3863
|
+
const within = (part) => part >= 0 && toCents(part) <= toCents(amount);
|
|
3864
|
+
if (!within(fromCredit) || !within(reinstated)) {
|
|
3865
|
+
throw new Error(
|
|
3866
|
+
`${REVERSALS} row '${row.id}' takes ${fromCredit} from the credit and is given back ${reinstated} of the ${amount} it withdrew; each is a part of it.`
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3869
|
+
if (reinstated > 0 !== (row.reinstatedAt !== null)) {
|
|
3870
|
+
throw new Error(
|
|
3871
|
+
`${REVERSALS}.reinstatedAt of ${row.id} does not fit what was given back of it: a reversal given something back carries when, and one given nothing does not.`
|
|
3872
|
+
);
|
|
3873
|
+
}
|
|
3874
|
+
return {
|
|
3875
|
+
id: row.id,
|
|
3876
|
+
subscriberId: row.subscriberId,
|
|
3877
|
+
tenantId: row.tenantId,
|
|
3878
|
+
paymentId: row.paymentId,
|
|
3879
|
+
gatewayAccount: row.gatewayAccount,
|
|
3880
|
+
gatewayRef: row.gatewayRef,
|
|
3881
|
+
amount,
|
|
3882
|
+
currency: row.currency,
|
|
3883
|
+
fromCredit,
|
|
3884
|
+
reinstated,
|
|
3885
|
+
recordedAt: row.recordedAt,
|
|
3886
|
+
reinstatedAt: row.reinstatedAt
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3889
|
+
function subscriberReversalColumns(reversal, payment) {
|
|
3890
|
+
return {
|
|
3891
|
+
subscriberId: payment.subscriberId,
|
|
3892
|
+
tenantId: payment.tenantId,
|
|
3893
|
+
paymentId: payment.id,
|
|
3894
|
+
gatewayAccount: reversal.gatewayAccount,
|
|
3895
|
+
gatewayRef: reversal.gatewayRef,
|
|
3896
|
+
amount: toDecimalString(0),
|
|
3897
|
+
currency: reversal.currency,
|
|
3898
|
+
fromCredit: toDecimalString(0),
|
|
3899
|
+
reinstated: toDecimalString(0),
|
|
3900
|
+
recordedAt: reversal.at,
|
|
3901
|
+
reinstatedAt: null
|
|
3902
|
+
};
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
// src/subscriber-payment-rules.ts
|
|
3906
|
+
function owedOn(gross, settled) {
|
|
3907
|
+
const paid = settled.reduce((sum2, amount) => sum2 + toCents(amount), 0);
|
|
3908
|
+
return (toCents(gross) - paid) / 100;
|
|
3909
|
+
}
|
|
3910
|
+
function paymentSettlement(amount, owed) {
|
|
3911
|
+
const brought = toCents(amount);
|
|
3912
|
+
const settles = Math.min(brought, Math.max(0, toCents(owed)));
|
|
3913
|
+
return { settles: settles / 100, credits: (brought - settles) / 100 };
|
|
3914
|
+
}
|
|
3915
|
+
var GIVING_BACK_CREDIT_ENTRY_KINDS = [
|
|
3916
|
+
"REFUND_FAILED",
|
|
3917
|
+
"REVERSAL_TAKEN_BACK"
|
|
3918
|
+
];
|
|
3919
|
+
var GIVING_BACK = new Set(GIVING_BACK_CREDIT_ENTRY_KINDS);
|
|
3920
|
+
function paidOutBy(entry) {
|
|
3921
|
+
const cents2 = toCents(entry.amount);
|
|
3922
|
+
return GIVING_BACK.has(entry.kind) ? -cents2 : cents2;
|
|
3923
|
+
}
|
|
3924
|
+
function openOfCredit(amount, entries) {
|
|
3925
|
+
const paidOut = entries.reduce((sum2, entry) => sum2 + paidOutBy(entry), 0);
|
|
3926
|
+
return Math.max(0, toCents(amount) - paidOut) / 100;
|
|
3927
|
+
}
|
|
3928
|
+
function refundParts(amount, paymentId, credits) {
|
|
3929
|
+
const ownFirst = [...credits].sort(
|
|
3930
|
+
(a, b) => rankFor(a, paymentId) - rankFor(b, paymentId) || a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id, "en")
|
|
3931
|
+
);
|
|
3932
|
+
let left = toCents(amount);
|
|
3933
|
+
const parts = [];
|
|
3934
|
+
for (const credit of ownFirst) {
|
|
3935
|
+
if (left === 0) break;
|
|
3936
|
+
const taken = Math.min(left, toCents(credit.open));
|
|
3937
|
+
if (taken <= 0) continue;
|
|
3938
|
+
parts.push({ creditId: credit.id, amount: taken / 100 });
|
|
3939
|
+
left -= taken;
|
|
3940
|
+
}
|
|
3941
|
+
return { parts, unmatched: left / 100 };
|
|
3942
|
+
}
|
|
3943
|
+
function rankFor(credit, paymentId) {
|
|
3944
|
+
return credit.origin.kind === "payment" && credit.origin.paymentId === paymentId ? 0 : 1;
|
|
3945
|
+
}
|
|
3946
|
+
function withdrawalParts(amount, credit, invoices, covering = []) {
|
|
3947
|
+
let left = toCents(amount);
|
|
3948
|
+
const share = credit ? Math.min(left, Math.max(0, toCents(credit.left))) : 0;
|
|
3949
|
+
const open = credit ? Math.min(share, Math.max(0, toCents(credit.open))) : 0;
|
|
3950
|
+
left -= share;
|
|
3951
|
+
const opening = [];
|
|
3952
|
+
for (const invoice of invoices) {
|
|
3953
|
+
if (left === 0) break;
|
|
3954
|
+
const taken = Math.min(left, Math.max(0, toCents(invoice.left)));
|
|
3955
|
+
if (taken === 0) continue;
|
|
3956
|
+
opening.push({ invoiceId: invoice.invoiceId, cents: taken });
|
|
3957
|
+
left -= taken;
|
|
3958
|
+
}
|
|
3959
|
+
let toCover = opening.reduce((sum2, each) => sum2 + each.cents, 0);
|
|
3960
|
+
const covered = [];
|
|
3961
|
+
for (const other of covering) {
|
|
3962
|
+
if (toCover === 0) break;
|
|
3963
|
+
const taken = Math.min(toCover, Math.max(0, toCents(other.open)));
|
|
3964
|
+
if (taken === 0) continue;
|
|
3965
|
+
covered.push({ creditId: other.creditId, amount: taken / 100 });
|
|
3966
|
+
toCover -= taken;
|
|
3967
|
+
}
|
|
3968
|
+
let coveredCents = opening.reduce((sum2, each) => sum2 + each.cents, 0) - toCover;
|
|
3969
|
+
const opened = [];
|
|
3970
|
+
for (const each of opening) {
|
|
3971
|
+
const kept = Math.min(each.cents, coveredCents);
|
|
3972
|
+
coveredCents -= kept;
|
|
3973
|
+
if (each.cents > kept) {
|
|
3974
|
+
opened.push({ invoiceId: each.invoiceId, amount: (each.cents - kept) / 100 });
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
return {
|
|
3978
|
+
fromCredit: share / 100,
|
|
3979
|
+
credit: credit && open > 0 ? { creditId: credit.creditId, amount: open / 100 } : null,
|
|
3980
|
+
covering: covered,
|
|
3981
|
+
invoices: opened,
|
|
3982
|
+
unmatched: (share - open + left) / 100
|
|
3983
|
+
};
|
|
3984
|
+
}
|
|
3985
|
+
function reinstatementParts(amount, took) {
|
|
3986
|
+
let left = toCents(amount);
|
|
3987
|
+
const covered = Math.min(left, Math.max(0, toCents(took.unmatched)));
|
|
3988
|
+
left -= covered;
|
|
3989
|
+
const credits = [];
|
|
3990
|
+
for (const credit of took.credits) {
|
|
3991
|
+
const given = Math.min(left, Math.max(0, toCents(credit.left)));
|
|
3992
|
+
if (given === 0) continue;
|
|
3993
|
+
credits.push({ creditId: credit.creditId, amount: given / 100 });
|
|
3994
|
+
left -= given;
|
|
3995
|
+
}
|
|
3996
|
+
const invoices = [];
|
|
3997
|
+
let credited = 0;
|
|
3998
|
+
for (const invoice of took.invoices) {
|
|
3999
|
+
const given = Math.min(left, Math.max(0, toCents(invoice.left)));
|
|
4000
|
+
if (given === 0) continue;
|
|
4001
|
+
const settles = Math.min(given, Math.max(0, toCents(invoice.owed)));
|
|
4002
|
+
if (settles > 0) invoices.push({ invoiceId: invoice.invoiceId, amount: settles / 100 });
|
|
4003
|
+
credited += given - settles;
|
|
4004
|
+
left -= given;
|
|
4005
|
+
}
|
|
4006
|
+
return {
|
|
4007
|
+
covered: covered / 100,
|
|
4008
|
+
credits,
|
|
4009
|
+
invoices,
|
|
4010
|
+
credited: credited / 100,
|
|
4011
|
+
beyond: left / 100
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
4014
|
+
function netByInvoice(entries) {
|
|
4015
|
+
const net = /* @__PURE__ */ new Map();
|
|
4016
|
+
for (const entry of entries) {
|
|
4017
|
+
net.set(entry.invoiceId, (net.get(entry.invoiceId) ?? 0) + toCents(entry.amount));
|
|
4018
|
+
}
|
|
4019
|
+
return [...net].map(([invoiceId, cents2]) => ({ invoiceId, amount: cents2 / 100 }));
|
|
4020
|
+
}
|
|
4021
|
+
function paidOutByCredit(entries) {
|
|
4022
|
+
const paid = /* @__PURE__ */ new Map();
|
|
4023
|
+
for (const entry of entries) {
|
|
4024
|
+
paid.set(entry.creditId, (paid.get(entry.creditId) ?? 0) + paidOutBy(entry));
|
|
4025
|
+
}
|
|
4026
|
+
return [...paid].map(([creditId, cents2]) => ({ creditId, amount: cents2 / 100 }));
|
|
4027
|
+
}
|
|
4028
|
+
function coversOf(entries, own) {
|
|
4029
|
+
const taken = /* @__PURE__ */ new Map();
|
|
4030
|
+
for (const entry of entries) {
|
|
4031
|
+
if (entry.reversalId === null || own.has(entry.reversalId)) continue;
|
|
4032
|
+
taken.set(entry.reversalId, (taken.get(entry.reversalId) ?? 0) + paidOutBy(entry));
|
|
4033
|
+
}
|
|
4034
|
+
return [...taken].filter(([, cents2]) => cents2 > 0).map(([reversalId, cents2]) => ({ reversalId, amount: cents2 / 100 }));
|
|
4035
|
+
}
|
|
4036
|
+
function coversLifted(parts, covers) {
|
|
4037
|
+
let reach = toCents(parts.fromCredit) - toCents(parts.credit?.amount ?? 0);
|
|
4038
|
+
const lifted = [];
|
|
4039
|
+
for (const cover of covers) {
|
|
4040
|
+
if (reach <= 0) break;
|
|
4041
|
+
const taken = Math.min(reach, toCents(cover.amount));
|
|
4042
|
+
if (taken <= 0) continue;
|
|
4043
|
+
lifted.push({ reversalId: cover.reversalId, amount: taken / 100 });
|
|
4044
|
+
reach -= taken;
|
|
4045
|
+
}
|
|
4046
|
+
const liftedCents = lifted.reduce((sum2, each) => sum2 + toCents(each.amount), 0);
|
|
4047
|
+
return { lifted, unmatched: (toCents(parts.unmatched) - liftedCents) / 100 };
|
|
4048
|
+
}
|
|
4049
|
+
function inWithdrawalOrder(parts, credits, paymentId) {
|
|
4050
|
+
const byId = new Map(credits.map((credit) => [credit.id, credit]));
|
|
4051
|
+
const rank = (part) => {
|
|
4052
|
+
const credit = byId.get(part.creditId);
|
|
4053
|
+
if (!credit) return [2, 0, part.creditId];
|
|
4054
|
+
const own = credit.origin.kind === "payment" && credit.origin.paymentId === paymentId;
|
|
4055
|
+
return [own ? 0 : 1, credit.createdAt.getTime(), credit.id];
|
|
4056
|
+
};
|
|
4057
|
+
return [...parts].sort((a, b) => {
|
|
4058
|
+
const [x, y] = [rank(a), rank(b)];
|
|
4059
|
+
return x[0] - y[0] || x[1] - y[1] || x[2].localeCompare(y[2], "en");
|
|
4060
|
+
});
|
|
4061
|
+
}
|
|
4062
|
+
function casesCovered(cases, covered) {
|
|
4063
|
+
const limit = toCents(covered);
|
|
4064
|
+
let running = 0;
|
|
4065
|
+
const ids = [];
|
|
4066
|
+
for (const each of cases) {
|
|
4067
|
+
running += toCents(each.amount);
|
|
4068
|
+
if (running <= limit) ids.push(each.id);
|
|
4069
|
+
}
|
|
4070
|
+
return ids;
|
|
4071
|
+
}
|
|
4072
|
+
|
|
4073
|
+
// src/subscriber-reconciliation.types.ts
|
|
4074
|
+
var RECONCILIATION_READ_CAPABILITY = "reconciliation.read";
|
|
4075
|
+
var RECONCILIATION_CLOSE_CAPABILITY = "reconciliation.close";
|
|
4076
|
+
var SUBSCRIBER_PAYMENTS_CAPABILITY = "payments.read";
|
|
4077
|
+
var RECONCILIATION_NOTE_MAX_LENGTH = 500;
|
|
4078
|
+
|
|
4079
|
+
// src/subscriber-reconciliation-mapping.ts
|
|
4080
|
+
var CASES2 = "subscriber_reconciliation_cases";
|
|
4081
|
+
var RECONCILIATION_CASE_KINDS = [
|
|
4082
|
+
"REFUND_UNMATCHED",
|
|
4083
|
+
"REVERSAL_UNMATCHED"
|
|
4084
|
+
];
|
|
4085
|
+
function toSubscriberReconciliationCaseRecord(row) {
|
|
4086
|
+
const where = (column) => ({ table: CASES2, column, id: row.id });
|
|
4087
|
+
const kind = oneOf(RECONCILIATION_CASE_KINDS, row.kind, where("kind"));
|
|
4088
|
+
const aboutRefund = kind === "REFUND_UNMATCHED";
|
|
4089
|
+
if (aboutRefund !== (row.refundId !== null) || aboutRefund === (row.reversalId !== null)) {
|
|
4090
|
+
throw new Error(
|
|
4091
|
+
`${CASES2} row '${row.id}' is a ${kind} naming ${row.refundId === null ? "no" : "a"} refund and ${row.reversalId === null ? "no" : "a"} reversal; a case names what it is about, and nothing else.`
|
|
4092
|
+
);
|
|
4093
|
+
}
|
|
4094
|
+
if (row.closedAt === null !== (row.note === null)) {
|
|
4095
|
+
throw new Error(
|
|
4096
|
+
`${CASES2} row '${row.id}' is ${row.closedAt === null ? "open with" : "closed without"} a note; a case is closed with one, and an open case carries none.`
|
|
4097
|
+
);
|
|
4098
|
+
}
|
|
4099
|
+
if (row.closedAt === null && row.closedBy !== null) {
|
|
4100
|
+
throw new Error(`${CASES2} row '${row.id}' is open and names who closed it.`);
|
|
4101
|
+
}
|
|
4102
|
+
return {
|
|
4103
|
+
id: row.id,
|
|
4104
|
+
subscriberId: row.subscriberId,
|
|
4105
|
+
tenantId: row.tenantId,
|
|
4106
|
+
kind,
|
|
4107
|
+
amount: columnAmountOf(row.amount, where("amount")),
|
|
4108
|
+
currency: row.currency,
|
|
4109
|
+
paymentId: row.paymentId,
|
|
4110
|
+
refundId: row.refundId,
|
|
4111
|
+
reversalId: row.reversalId,
|
|
4112
|
+
gatewayAccount: row.gatewayAccount,
|
|
4113
|
+
gatewayRef: row.gatewayRef,
|
|
4114
|
+
recordedAt: row.recordedAt,
|
|
4115
|
+
closedAt: row.closedAt,
|
|
4116
|
+
closedBy: row.closedBy,
|
|
4117
|
+
note: row.note
|
|
4118
|
+
};
|
|
4119
|
+
}
|
|
4120
|
+
function reconciliationCaseColumns(opened) {
|
|
4121
|
+
const aboutRefund = opened.kind === "REFUND_UNMATCHED";
|
|
4122
|
+
return {
|
|
4123
|
+
subscriberId: opened.payment.subscriberId,
|
|
4124
|
+
tenantId: opened.payment.tenantId,
|
|
4125
|
+
kind: opened.kind,
|
|
4126
|
+
amount: toDecimalString(opened.amount),
|
|
4127
|
+
currency: opened.currency,
|
|
4128
|
+
paymentId: opened.payment.id,
|
|
4129
|
+
refundId: aboutRefund ? opened.about.id : null,
|
|
4130
|
+
reversalId: aboutRefund ? null : opened.about.id,
|
|
4131
|
+
gatewayAccount: opened.about.gatewayAccount,
|
|
4132
|
+
gatewayRef: opened.about.gatewayRef,
|
|
4133
|
+
recordedAt: opened.at,
|
|
4134
|
+
closedAt: null,
|
|
4135
|
+
closedBy: null,
|
|
4136
|
+
note: null
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
var CASE_CLOSED_REFUND_ENDED = "The refund failed or was canceled, and its money came back.";
|
|
4140
|
+
var CASE_CLOSED_REVERSAL_GIVEN_BACK = "The gateway gave the reversal back, and with it what matched nothing.";
|
|
4141
|
+
|
|
4142
|
+
// src/e-invoice-format.ts
|
|
4143
|
+
var CATEGORY = {
|
|
4144
|
+
standard: "S",
|
|
4145
|
+
reduced: "S",
|
|
4146
|
+
exempt: "E",
|
|
4147
|
+
"small-business": "E",
|
|
4148
|
+
"reverse-charge": "AE",
|
|
4149
|
+
"not-taxable": "O"
|
|
4150
|
+
};
|
|
4151
|
+
function vatCategoryOf(kind) {
|
|
4152
|
+
return CATEGORY[kind];
|
|
4153
|
+
}
|
|
4154
|
+
var NOT_XML = /[^\t\n\r\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/gu;
|
|
4155
|
+
function holdsOnlyXmlCharacters(text) {
|
|
4156
|
+
return text.search(NOT_XML) === -1;
|
|
4157
|
+
}
|
|
4158
|
+
var CARRIAGE_RETURN = /\r\n?/g;
|
|
4159
|
+
function asXmlReadsIt(text) {
|
|
4160
|
+
return text.replace(NOT_XML, "?").replace(CARRIAGE_RETURN, "\n");
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
// src/subscriber-mapping.ts
|
|
4164
|
+
function formatCustomerNumber(prefix, sequence) {
|
|
4165
|
+
return `${prefix}${sequence}`;
|
|
4166
|
+
}
|
|
4167
|
+
function toSubscriberRecord(row, tenantId) {
|
|
4168
|
+
return {
|
|
4169
|
+
id: row.id,
|
|
4170
|
+
customerNumber: formatCustomerNumber(row.customerNumberPrefix, row.customerSequence),
|
|
4171
|
+
legalName: row.legalName,
|
|
4172
|
+
addressLine1: row.addressLine1,
|
|
4173
|
+
addressLine2: row.addressLine2,
|
|
4174
|
+
postalCode: row.postalCode,
|
|
4175
|
+
city: row.city,
|
|
4176
|
+
country: row.country,
|
|
4177
|
+
vatId: row.vatId,
|
|
4178
|
+
taxNumber: row.taxNumber,
|
|
4179
|
+
invoiceEmail: row.invoiceEmail,
|
|
4180
|
+
business: row.business,
|
|
4181
|
+
tenantId,
|
|
4182
|
+
migrated: row.migrated,
|
|
4183
|
+
createdAt: row.createdAt,
|
|
4184
|
+
updatedAt: row.updatedAt
|
|
4185
|
+
};
|
|
4186
|
+
}
|
|
4187
|
+
function toSubscriberCorrectionRecord(row) {
|
|
4188
|
+
return {
|
|
4189
|
+
id: row.id,
|
|
4190
|
+
subscriberId: row.subscriberId,
|
|
4191
|
+
previous: toIdentityValues(row.previous),
|
|
4192
|
+
corrected: toIdentityValues(row.corrected),
|
|
4193
|
+
reason: row.reason,
|
|
4194
|
+
correctedBy: row.correctedBy,
|
|
4195
|
+
correctedAt: row.correctedAt
|
|
4196
|
+
};
|
|
4197
|
+
}
|
|
4198
|
+
function toSubscriberTaxOriginChangeRecord(row) {
|
|
4199
|
+
return {
|
|
4200
|
+
id: row.id,
|
|
4201
|
+
subscriberId: row.subscriberId,
|
|
4202
|
+
previous: toTaxOriginValues(row.previous),
|
|
4203
|
+
changed: toTaxOriginValues(row.changed),
|
|
4204
|
+
changedBy: row.changedBy,
|
|
4205
|
+
changedAt: row.changedAt,
|
|
4206
|
+
reason: row.reason
|
|
4207
|
+
};
|
|
4208
|
+
}
|
|
4209
|
+
function identityCorrectionDelta(current, corrected) {
|
|
4210
|
+
const previous = {};
|
|
4211
|
+
const next = {};
|
|
4212
|
+
for (const field of LEGAL_IDENTITY_FIELDS) {
|
|
4213
|
+
const value = corrected[field];
|
|
4214
|
+
if (value === void 0 || value === current[field]) continue;
|
|
4215
|
+
previous[field] = current[field];
|
|
4216
|
+
next[field] = value;
|
|
4217
|
+
}
|
|
4218
|
+
return {
|
|
4219
|
+
previous,
|
|
4220
|
+
corrected: next
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
function contractPartiesOf(subscriber, issuer) {
|
|
4224
|
+
const party = {
|
|
4225
|
+
customerNumber: subscriber.customerNumber,
|
|
4226
|
+
legalName: subscriber.legalName,
|
|
4227
|
+
vatId: subscriber.vatId,
|
|
4228
|
+
taxNumber: subscriber.taxNumber,
|
|
4229
|
+
addressLine1: subscriber.addressLine1,
|
|
4230
|
+
addressLine2: subscriber.addressLine2,
|
|
4231
|
+
postalCode: subscriber.postalCode,
|
|
4232
|
+
city: subscriber.city,
|
|
4233
|
+
country: subscriber.country
|
|
4234
|
+
};
|
|
4235
|
+
return {
|
|
4236
|
+
subscriberId: subscriber.id,
|
|
4237
|
+
subscriber: party,
|
|
4238
|
+
issuer: issuer ? issuerPartyOf(issuer) : null
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
function issuerPartyOf(issuer) {
|
|
4242
|
+
const identity = issuerIdentityOf(issuer);
|
|
4243
|
+
return {
|
|
4244
|
+
legalName: identity?.legalName ?? "",
|
|
4245
|
+
vatId: identity?.vatId ?? null,
|
|
2464
4246
|
taxNumber: identity?.taxNumber ?? null,
|
|
2465
4247
|
// Settled like the three above rather than copied verbatim: one copy
|
|
2466
4248
|
// whose halves were settled differently is a row that reads two ways,
|
|
@@ -2504,75 +4286,6 @@ function toIdentityValues(value) {
|
|
|
2504
4286
|
return values;
|
|
2505
4287
|
}
|
|
2506
4288
|
|
|
2507
|
-
// src/subscriber-payment-method-mapping.ts
|
|
2508
|
-
var PAYMENT_METHOD_TYPES = ["card", "sepa_debit"];
|
|
2509
|
-
var STATUSES = ["ACTIVE", "REPLACED"];
|
|
2510
|
-
var TABLE3 = "subscriber_payment_methods";
|
|
2511
|
-
function toSubscriberPaymentMethodRecord(row) {
|
|
2512
|
-
return {
|
|
2513
|
-
id: row.id,
|
|
2514
|
-
subscriberId: row.subscriberId,
|
|
2515
|
-
gatewayAccount: row.gatewayAccount,
|
|
2516
|
-
provider: row.provider,
|
|
2517
|
-
customerRef: row.customerRef,
|
|
2518
|
-
paymentMethodRef: row.paymentMethodRef,
|
|
2519
|
-
type: oneOf(PAYMENT_METHOD_TYPES, row.type, { table: TABLE3, column: "type", id: row.id }),
|
|
2520
|
-
brand: row.brand,
|
|
2521
|
-
last4: row.last4,
|
|
2522
|
-
expiryMonth: row.expiryMonth,
|
|
2523
|
-
expiryYear: row.expiryYear,
|
|
2524
|
-
country: row.country,
|
|
2525
|
-
bankCode: row.bankCode,
|
|
2526
|
-
mandateReference: row.mandateReference,
|
|
2527
|
-
status: oneOf(STATUSES, row.status, { table: TABLE3, column: "status", id: row.id }),
|
|
2528
|
-
confirmedAt: row.confirmedAt,
|
|
2529
|
-
replacedAt: row.replacedAt,
|
|
2530
|
-
createdAt: row.createdAt
|
|
2531
|
-
};
|
|
2532
|
-
}
|
|
2533
|
-
function subscriberPaymentMethodColumns(data) {
|
|
2534
|
-
return {
|
|
2535
|
-
subscriberId: data.subscriberId,
|
|
2536
|
-
gatewayAccount: data.gatewayAccount,
|
|
2537
|
-
provider: data.provider,
|
|
2538
|
-
customerRef: data.customerRef,
|
|
2539
|
-
paymentMethodRef: data.paymentMethodRef,
|
|
2540
|
-
type: data.type,
|
|
2541
|
-
brand: data.brand,
|
|
2542
|
-
last4: data.last4,
|
|
2543
|
-
expiryMonth: data.expiryMonth,
|
|
2544
|
-
expiryYear: data.expiryYear,
|
|
2545
|
-
country: data.country,
|
|
2546
|
-
bankCode: data.bankCode,
|
|
2547
|
-
mandateReference: data.mandateReference,
|
|
2548
|
-
confirmedAt: data.confirmedAt
|
|
2549
|
-
};
|
|
2550
|
-
}
|
|
2551
|
-
var FOREIGN_PAYMENT_METHOD_REFERENCE = "FOREIGN_PAYMENT_METHOD_REFERENCE";
|
|
2552
|
-
var ForeignPaymentMethodReferenceError = class extends Error {
|
|
2553
|
-
constructor(gatewayAccount, paymentMethodRef) {
|
|
2554
|
-
super(
|
|
2555
|
-
`Payment method '${paymentMethodRef}' of account '${gatewayAccount}' belongs to another subscriber. A reference belongs to exactly one, so it is not handed out.`
|
|
2556
|
-
);
|
|
2557
|
-
this.gatewayAccount = gatewayAccount;
|
|
2558
|
-
this.paymentMethodRef = paymentMethodRef;
|
|
2559
|
-
this.name = "ForeignPaymentMethodReferenceError";
|
|
2560
|
-
}
|
|
2561
|
-
gatewayAccount;
|
|
2562
|
-
paymentMethodRef;
|
|
2563
|
-
code = FOREIGN_PAYMENT_METHOD_REFERENCE;
|
|
2564
|
-
};
|
|
2565
|
-
function isForeignPaymentMethodReferenceError(error) {
|
|
2566
|
-
return error instanceof Error && error.code === FOREIGN_PAYMENT_METHOD_REFERENCE;
|
|
2567
|
-
}
|
|
2568
|
-
function refuseForeignPaymentMethodReference(recorded, subscriberId) {
|
|
2569
|
-
if (recorded.subscriberId === subscriberId) return;
|
|
2570
|
-
throw new ForeignPaymentMethodReferenceError(
|
|
2571
|
-
recorded.gatewayAccount,
|
|
2572
|
-
recorded.paymentMethodRef
|
|
2573
|
-
);
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
4289
|
// src/subscription-bundle-mapping.ts
|
|
2577
4290
|
var BILLING_CYCLES = ["MONTHLY", "YEARLY"];
|
|
2578
4291
|
function toSubscriptionBundleRecord(row) {
|
|
@@ -2922,6 +4635,23 @@ var ERROR_MESSAGES_DE = {
|
|
|
2922
4635
|
BUNDLE_RETIREMENT_REINSTATE_SUBSCRIPTION_ENDS: "Version {version} von {bundleKey} wird stillgelegt, und diese Buchung endet, bevor sie umziehen w\xFCrde. Ihr Abonnement endet bis dahin ebenfalls.",
|
|
2923
4636
|
BUNDLE_RETIREMENT_SWITCH_PLAN_CHANGES: "{bundleName} zieht am {date} auf seine neue Version um, und Ihr Paket \xE4ndert sich vorher. Der Preis, den ein Wechsel halten w\xFCrde, steht deshalb noch nicht fest. Wechseln Sie, sobald Ihr Paket gewechselt hat, oder lassen Sie es an diesem Tag umziehen.",
|
|
2924
4637
|
BUNDLE_VERSION_OFFER_CHANGED: "Das Angebot hat sich seit der Anzeige ge\xE4ndert. Bitte sieh dir das aktuelle an, bevor du wechselst.",
|
|
4638
|
+
FEATURE_WITHDRAWAL_UNAVAILABLE: "Ein Feature zur\xFCckzuziehen ist hier nicht m\xF6glich: Die Speicher k\xF6nnen nicht alle auflisten, die ein R\xFCckzug erreicht.",
|
|
4639
|
+
FEATURE_WITHDRAWAL_FEATURE_UNKNOWN: "Der Katalog kennt kein Feature {featureKey}.",
|
|
4640
|
+
FEATURE_WITHDRAWAL_DATE_IN_PAST: "Ein Feature wird ab jetzt oder ab einem sp\xE4teren Datum zur\xFCckgezogen, nie ab einem vergangenen.",
|
|
4641
|
+
FEATURE_WITHDRAWAL_OPEN: "{featureKey} ist bereits zur\xFCckgezogen, und dieser R\xFCckzug ist nicht aufgehoben. Heben Sie ihn auf, bevor Sie einen weiteren ank\xFCndigen.",
|
|
4642
|
+
FEATURE_WITHDRAWAL_OVERLAPS: "{featureKey} ist bis zum {date} zur\xFCckgezogen. Ein weiterer R\xFCckzug kann fr\xFChestens an diesem Tag beginnen.",
|
|
4643
|
+
FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED: "Die Minderung f\xFCr {key} bei Abrechnung {billingCycle} betrifft nichts, was dieser R\xFCckzug erreicht.",
|
|
4644
|
+
FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE: "Die Minderung f\xFCr {key} bei Abrechnung {billingCycle} ist doppelt angegeben.",
|
|
4645
|
+
FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE: "Die Minderung f\xFCr {key} bei Abrechnung {billingCycle} ist h\xF6her als der niedrigste Preis, den sie mindert: {price}.",
|
|
4646
|
+
FEATURE_WITHDRAWAL_PREVIEW_CHANGED: "Die Abonnements, die dieser R\xFCckzug erreicht, haben sich seit der Anzeige ge\xE4ndert. Sehen Sie sie erneut an, bevor Sie ihn ank\xFCndigen.",
|
|
4647
|
+
FEATURE_WITHDRAWAL_NOT_FOUND: "R\xFCckzug {withdrawalId} nicht gefunden.",
|
|
4648
|
+
FEATURE_WITHDRAWAL_ALREADY_LIFTED: "Dieser R\xFCckzug ist bereits aufgehoben, ab dem {date}.",
|
|
4649
|
+
FEATURE_WITHDRAWAL_NOT_TOLD_YET: "\xDCber diesen R\xFCckzug ist noch niemand informiert, und er wird nicht aufgehoben, bevor das geschehen ist. Versuchen Sie es gleich noch einmal.",
|
|
4650
|
+
FEATURE_WITHDRAWAL_LIFT_IN_PAST: "Ein R\xFCckzug wird ab jetzt oder ab einem sp\xE4teren Datum aufgehoben, nie ab einem vergangenen.",
|
|
4651
|
+
FEATURE_WITHDRAWAL_NOT_IN_EFFECT: "{featureKey} ist gerade nicht zur\xFCckgezogen. Deshalb kann nichts sofort beendet werden.",
|
|
4652
|
+
FEATURE_WITHDRAWAL_DOES_NOT_REACH: "Dieser R\xFCckzug betrifft Ihr Abonnement nicht, oder diese Buchung nicht mehr.",
|
|
4653
|
+
FEATURE_WITHDRAWAL_ALREADY_ENDED: "Das ist bereits beendet.",
|
|
4654
|
+
FEATURE_WITHDRAWAL_END_NOW_UNSUPPORTED: "Ihre K\xFCndigung zum {date} ist erfasst und kann hier nicht vorgezogen werden. Sie endet an diesem Tag, und bis dahin gilt die Minderung.",
|
|
2925
4655
|
RETIREMENT_SWITCH_NOT_PENDING: "F\xFCr Ihre Version steht keine Einstellung mehr bevor. Es gibt nichts, wohin Sie wechseln k\xF6nnten.",
|
|
2926
4656
|
RETIREMENT_SWITCH_IN_TRIAL: "Der Wechsel ist m\xF6glich, sobald Ihre Testphase endet.",
|
|
2927
4657
|
RETIREMENT_SWITCH_BUNDLE_CANNOT_FOLLOW: "{bundleName} kann im Paket {planName} nicht laufen und l\xE4uft fr\xFChestens bis {until}. Ist es gek\xFCndigt, ist der Wechsel ab diesem Tag m\xF6glich.",
|
|
@@ -2964,6 +4694,23 @@ var ERROR_MESSAGES_DE = {
|
|
|
2964
4694
|
PROMO_MAX_REDEMPTIONS_LOWERED: "maxRedemptions l\xE4sst sich nicht senken.",
|
|
2965
4695
|
// ── contract ──
|
|
2966
4696
|
SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED: "Der Vertrag nennt in {field} {stated} %, der Steueradapter entscheidet {decided} %.",
|
|
4697
|
+
SUBSCRIPTION_INVOICE_CHARGE_INVOICED: "Die Forderung '{chargeId}' steht bereits auf einer Rechnung.",
|
|
4698
|
+
SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE: "Der Rechnung fehlt, was der Steueradapter {adapter} verlangt: {missing}. Sie wird ausgestellt, sobald es erg\xE4nzt ist.",
|
|
4699
|
+
SUBSCRIPTION_INVOICE_PARTIES_UNCONFIRMED: "Der Vertrag {contractId} nennt keinen best\xE4tigten Aussteller und Abonnenten \u2013 eine Migration hat seine Parteien \xFCbernommen, oder er nennt keinen Aussteller \u2013, deshalb werden seine Forderungen nicht in Rechnung gestellt.",
|
|
4700
|
+
SUBSCRIPTION_INVOICE_MANDATE_MISSING: "Die Lastschrift {paymentMethodId} nennt keine Mandatsreferenz, die vor dem Einzug angek\xFCndigt werden muss. Die Rechnung wartet, bis die Zahlungsart ersetzt ist.",
|
|
4701
|
+
SUBSCRIPTION_INVOICE_NO_PAYMENT_METHOD: "Der Abonnent {subscriberId} hat keine Zahlungsart in Gebrauch. Die Rechnung wird eingezogen, sobald er eine hat.",
|
|
4702
|
+
SUBSCRIPTION_INVOICE_FAILURE_NOT_TOLD: "Die Lastschrift wird angek\xFCndigt, sobald dem Abonnenten mitgeteilt ist, dass der Einzug {attemptKey} gescheitert ist.",
|
|
4703
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_CHANGED: "Das Dokument der Rechnung {invoiceId} weicht vom festgehaltenen oder archivierten ab ({rendered} statt {recorded}); das erste bleibt, und der n\xE4chste Lauf kl\xE4rt, welches die Rechnung beh\xE4lt.",
|
|
4704
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_REFUSED: "Der Steueradapter {adapter} weist die gerenderte Rechnung zur\xFCck: {missing}. Die Rechnung beh\xE4lt ihre Nummer und wartet auf ein Dokument, das die Pr\xFCfung besteht.",
|
|
4705
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH: "Das archivierte Dokument der Rechnung {invoiceId} passt nicht zur beim Rendern festgehaltenen Pr\xFCfsumme und wird nicht herausgegeben.",
|
|
4706
|
+
SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED: "Das Dokument der Rechnung {invoiceId} ist noch nicht archiviert.",
|
|
4707
|
+
SUBSCRIPTION_INVOICE_NOT_FOUND: "Keine Rechnung '{invoiceId}'.",
|
|
4708
|
+
SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL: "Der Abonnent {subscriberId} hat keine Rechnungs-E-Mail; seine Rechnung wird versendet, sobald eine eingetragen ist.",
|
|
4709
|
+
SUBSCRIBER_CREDIT_NOT_FOUND: "Kein Guthaben '{creditId}' des Abonnenten dieses Mandanten.",
|
|
4710
|
+
SUBSCRIBER_PAYOUT_EXCEEDS_CREDIT: "Vom Guthaben {creditId} sind noch {open} offen; mehr wird nicht ausgezahlt, und nichts wurde erfasst.",
|
|
4711
|
+
SUBSCRIBER_PAYOUT_NOT_MADE_YET: "Eine Auszahlung wird erfasst, wenn sie erfolgt ist: {paidOn} liegt nach heute, {today}.",
|
|
4712
|
+
SUBSCRIBER_RECONCILIATION_CASE_NOT_FOUND: "Kein abzugleichender Fall '{caseId}'.",
|
|
4713
|
+
SUBSCRIBER_RECONCILIATION_CASE_CLOSED: "Der Fall {caseId} wurde am {closedAt} geschlossen; seine Notiz bleibt, und nichts wurde erfasst.",
|
|
2967
4714
|
CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "Aus einem Checkout-Angebot entsteht nur ein einziger Vertrag, und das erst, wenn seine Positionen eingefroren sind.",
|
|
2968
4715
|
CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "Ein Checkout-Angebot braucht eine eingefrorene Plan-Position.",
|
|
2969
4716
|
CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Jede gew\xE4hlte Bundle-Version braucht eine eingefrorene Bundle-Position.",
|
|
@@ -3053,16 +4800,37 @@ export {
|
|
|
3053
4800
|
ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES,
|
|
3054
4801
|
AUTH_ERROR_CODES,
|
|
3055
4802
|
BILLING_ERROR_CODES,
|
|
4803
|
+
BLOCKING_COLLECTION_STATUSES,
|
|
3056
4804
|
BUNDLE_PRICE_LOOKUP_LIMIT,
|
|
3057
4805
|
BUNDLE_VERSION_RETIREMENT_CAPABILITY,
|
|
4806
|
+
CASE_CLOSED_REFUND_ENDED,
|
|
4807
|
+
CASE_CLOSED_REVERSAL_GIVEN_BACK,
|
|
3058
4808
|
CATALOGUE_KEYS,
|
|
3059
4809
|
CATALOG_ERROR_CODES,
|
|
4810
|
+
COLLECTION_FAILURE_REASONS,
|
|
4811
|
+
COLLECTION_NOT_SENT_PAID,
|
|
4812
|
+
COLLECTION_NOT_SENT_REVERSED,
|
|
4813
|
+
COLLECTION_RETRY_AFTER_DAYS,
|
|
3060
4814
|
CONTRACT_ERROR_CODES,
|
|
3061
4815
|
CUSTOM_LIMITS_KEYS,
|
|
4816
|
+
DIRECT_DEBIT_LEAD_DAYS,
|
|
4817
|
+
ENDED_REFUND_STATUSES,
|
|
3062
4818
|
ERROR_MESSAGES_DE,
|
|
3063
4819
|
ERROR_MESSAGES_EN,
|
|
3064
4820
|
FEATURE_NOT_LICENSED,
|
|
4821
|
+
FEATURE_WITHDRAWAL_CAPABILITY,
|
|
4822
|
+
FEATURE_WITHDRAWAL_REASON_MAX_LENGTH,
|
|
4823
|
+
FOLLOWED_UP_COLLECTION_STATUSES,
|
|
3065
4824
|
ForeignPaymentMethodReferenceError,
|
|
4825
|
+
GIVING_BACK_CREDIT_ENTRY_KINDS,
|
|
4826
|
+
INVOICE_ARCHIVE_TRIGGERS,
|
|
4827
|
+
INVOICE_COLLECTION_ASK_EVERY_MS,
|
|
4828
|
+
INVOICE_COLLECTION_LEASE_MS,
|
|
4829
|
+
INVOICE_COLLECTION_STATUSES,
|
|
4830
|
+
INVOICE_DOCUMENT_CAPABILITY,
|
|
4831
|
+
INVOICE_ERROR_CODES,
|
|
4832
|
+
INVOICE_SEQUENCE_DIGITS,
|
|
4833
|
+
INVOICE_SETTLEMENT_KINDS,
|
|
3066
4834
|
LEGAL_IDENTITY_FIELDS,
|
|
3067
4835
|
MAINTENANCE_CAPABILITY,
|
|
3068
4836
|
MAINTENANCE_ERROR_CODES,
|
|
@@ -3088,24 +4856,41 @@ export {
|
|
|
3088
4856
|
PersistenceCapabilityError,
|
|
3089
4857
|
PersistenceRefusal,
|
|
3090
4858
|
PlatformUserExistsError,
|
|
4859
|
+
RECONCILIATION_CASE_KINDS,
|
|
4860
|
+
RECONCILIATION_CLOSE_CAPABILITY,
|
|
4861
|
+
RECONCILIATION_NOTE_MAX_LENGTH,
|
|
4862
|
+
RECONCILIATION_READ_CAPABILITY,
|
|
3091
4863
|
REGISTRATION_ERROR_CODES,
|
|
3092
4864
|
REGISTRATION_RESUME_TTL_MINUTES,
|
|
3093
4865
|
REGISTRATION_STEP_BY_STATUS,
|
|
4866
|
+
REOPENING_SETTLEMENT_KINDS,
|
|
3094
4867
|
RETIREMENT_REMINDER_LEAD_DAYS,
|
|
4868
|
+
REVERSIBLE_COLLECTION_STATUSES,
|
|
3095
4869
|
SETTINGS_ERROR_CODES,
|
|
3096
4870
|
SETUP_ERROR_CODES,
|
|
3097
4871
|
SUBSCRIBER_ACCOUNT_CAPABILITY,
|
|
3098
4872
|
SUBSCRIBER_ATTENTION_CAPABILITY,
|
|
3099
4873
|
SUBSCRIBER_ATTENTION_PAGE_SIZE,
|
|
4874
|
+
SUBSCRIBER_CHARGE_ORIGINS,
|
|
4875
|
+
SUBSCRIBER_CHARGE_SOURCES,
|
|
3100
4876
|
SUBSCRIBER_CORRECTION_CAPABILITY,
|
|
4877
|
+
SUBSCRIBER_CREDIT_CAPABILITY,
|
|
4878
|
+
SUBSCRIBER_CREDIT_ENTRY_KINDS,
|
|
3101
4879
|
SUBSCRIBER_ERROR_CODES,
|
|
3102
4880
|
SUBSCRIBER_INVOICE_ADDRESS_FIELDS,
|
|
4881
|
+
SUBSCRIBER_PAYMENTS_CAPABILITY,
|
|
4882
|
+
SUBSCRIBER_PAYOUT_CAPABILITY,
|
|
4883
|
+
SUBSCRIBER_REFUND_STATUSES,
|
|
3103
4884
|
SUBSCRIBER_STANDING_CAPABILITY,
|
|
3104
4885
|
SUBSCRIBER_TAX_ORIGIN_FIELDS,
|
|
3105
4886
|
TAX_ERROR_CODES,
|
|
3106
4887
|
TAX_TREATMENT_KINDS,
|
|
4888
|
+
UNSETTLED_COLLECTION_STATUSES,
|
|
3107
4889
|
VERSION_RETIREMENT_CAPABILITY,
|
|
4890
|
+
addDaysToDay,
|
|
4891
|
+
announcedCollectionDay,
|
|
3108
4892
|
applyPromo,
|
|
4893
|
+
asXmlReadsIt,
|
|
3109
4894
|
assertPersistenceCapabilities,
|
|
3110
4895
|
buildActivePlanVersionWhere,
|
|
3111
4896
|
buildActiveVersionWhere,
|
|
@@ -3115,38 +4900,69 @@ export {
|
|
|
3115
4900
|
bundleStemDefaults,
|
|
3116
4901
|
canonicalJson,
|
|
3117
4902
|
canonicalVatId,
|
|
4903
|
+
casesCovered,
|
|
3118
4904
|
catalogDraftExists,
|
|
3119
4905
|
catalogVersionAlreadyPublished,
|
|
3120
4906
|
catalogVersionGone,
|
|
4907
|
+
chargedAgainOn,
|
|
3121
4908
|
classifyBundleVersionDiff,
|
|
3122
4909
|
classifyIssuerChange,
|
|
3123
4910
|
classifyPlanDiff,
|
|
3124
4911
|
classifyVersionOffer,
|
|
3125
4912
|
collectUnsatisfiedRequires,
|
|
4913
|
+
collectionAttemptKey,
|
|
4914
|
+
collectionOutcomeColumns,
|
|
4915
|
+
collectionStatusOf,
|
|
4916
|
+
columnAmountOf,
|
|
3126
4917
|
computeIncludedVat,
|
|
3127
4918
|
concurrencyGate,
|
|
3128
4919
|
concurrencyGateOf,
|
|
3129
4920
|
contractPartiesOf,
|
|
3130
4921
|
coverageExcludingSelf,
|
|
4922
|
+
coversLifted,
|
|
4923
|
+
coversOf,
|
|
3131
4924
|
customLimitsReader,
|
|
4925
|
+
dayInZone,
|
|
3132
4926
|
definedFields,
|
|
3133
4927
|
diffSettings,
|
|
4928
|
+
directDebitLeadDaysOf,
|
|
4929
|
+
draftSubscriptionInvoice,
|
|
4930
|
+
endedAtOnceSubjectOf,
|
|
4931
|
+
featureWithdrawalRowOf,
|
|
4932
|
+
featuresWithdrawnAt,
|
|
3134
4933
|
formatCustomerNumber,
|
|
3135
4934
|
formatErrorMessage,
|
|
4935
|
+
formatInvoiceNumber,
|
|
3136
4936
|
grossFromNet,
|
|
4937
|
+
holdsOnlyXmlCharacters,
|
|
3137
4938
|
identityCorrectionDelta,
|
|
4939
|
+
inWithdrawalOrder,
|
|
4940
|
+
invoiceArchiveProtectionProblem,
|
|
4941
|
+
invoiceCollectionColumns,
|
|
4942
|
+
invoiceContentDraftOf,
|
|
4943
|
+
invoiceFooterOf,
|
|
4944
|
+
invoiceGroupsOf,
|
|
4945
|
+
invoiceIssuerOf,
|
|
4946
|
+
invoicePaymentOf,
|
|
3138
4947
|
isBundleRedundant,
|
|
4948
|
+
isChargedAgain,
|
|
4949
|
+
isEndedRefund,
|
|
4950
|
+
isFeatureWithdrawnAt,
|
|
3139
4951
|
isForeignPaymentMethodReferenceError,
|
|
4952
|
+
isInvoiceDocumentChecksum,
|
|
3140
4953
|
isMaintenanceAnnouncementLapsed,
|
|
3141
4954
|
isMaintenanceOverrun,
|
|
3142
4955
|
isPaymentCallbackRejectedError,
|
|
3143
4956
|
isPersistenceRefusal,
|
|
3144
4957
|
isPlatformUserExistsError,
|
|
4958
|
+
isRetryableCollectionFailure,
|
|
3145
4959
|
isVersionActiveAt,
|
|
3146
4960
|
isVersionEditable,
|
|
3147
4961
|
issuerIdentityOf,
|
|
3148
4962
|
keepOneRecommended,
|
|
3149
4963
|
keepsVatIdCheck,
|
|
4964
|
+
lastCollectionOf,
|
|
4965
|
+
lastDayInZone,
|
|
3150
4966
|
maintenanceRetryAfterSeconds,
|
|
3151
4967
|
maintenanceStatusOf,
|
|
3152
4968
|
maintenanceWindowStatusOf,
|
|
@@ -3155,14 +4971,22 @@ export {
|
|
|
3155
4971
|
membersLeftOut,
|
|
3156
4972
|
missingRequiresFor,
|
|
3157
4973
|
movedIdentityFields,
|
|
4974
|
+
netByInvoice,
|
|
3158
4975
|
netFromGross,
|
|
4976
|
+
nextWithdrawalChange,
|
|
3159
4977
|
noActivePlanVersion,
|
|
4978
|
+
openOfCredit,
|
|
4979
|
+
owedOn,
|
|
4980
|
+
paidOutByCredit,
|
|
3160
4981
|
parseZonedInstant,
|
|
4982
|
+
paymentSettlement,
|
|
4983
|
+
payoutEntryColumns,
|
|
3161
4984
|
percentOf,
|
|
3162
4985
|
pickActivePromo,
|
|
3163
4986
|
planCatalogSettingsOf,
|
|
3164
4987
|
planKeyTaken,
|
|
3165
4988
|
planNotInCatalog,
|
|
4989
|
+
plannedCollectionColumns,
|
|
3166
4990
|
previousUtcDay,
|
|
3167
4991
|
promoCodeTaken,
|
|
3168
4992
|
promoStatus,
|
|
@@ -3171,54 +4995,90 @@ export {
|
|
|
3171
4995
|
readCustomLimits,
|
|
3172
4996
|
readQuotaRecord,
|
|
3173
4997
|
readQuotaValue,
|
|
4998
|
+
reconciliationCaseColumns,
|
|
3174
4999
|
recordedIssuerIdentity,
|
|
5000
|
+
refundEntryColumns,
|
|
5001
|
+
refundParts,
|
|
3175
5002
|
refuseForeignPaymentMethodReference,
|
|
5003
|
+
refusesFirstAttempt,
|
|
5004
|
+
refusesPlannedAttempt,
|
|
5005
|
+
reinstatementParts,
|
|
3176
5006
|
resolveBundleAvailability,
|
|
3177
5007
|
resolveErrorMessage,
|
|
3178
5008
|
retirementCostsTheSubscription,
|
|
3179
5009
|
retirementReminderDueAt,
|
|
3180
5010
|
retirementReminderIsDue,
|
|
3181
5011
|
retirementSwitchTerms,
|
|
5012
|
+
reversalEntryColumns,
|
|
3182
5013
|
roundToCents,
|
|
3183
5014
|
sameLegalIdentity,
|
|
3184
5015
|
scheduledChangeAfterWrite,
|
|
3185
5016
|
selectChargeableBundles,
|
|
5017
|
+
sentCollectionOf,
|
|
3186
5018
|
settingsSubtreeOf,
|
|
5019
|
+
settledAmountOf,
|
|
3187
5020
|
startOfUtcDay,
|
|
3188
5021
|
subscriberChargeColumns,
|
|
3189
5022
|
subscriberFromRegistration,
|
|
5023
|
+
subscriberPaymentColumns,
|
|
3190
5024
|
subscriberPaymentMethodColumns,
|
|
5025
|
+
subscriberRefundColumns,
|
|
5026
|
+
subscriberReversalColumns,
|
|
3191
5027
|
subscriptionBundleAlreadyCancelled,
|
|
3192
5028
|
subscriptionBundleGone,
|
|
3193
5029
|
subscriptionContractGone,
|
|
3194
5030
|
subscriptionGone,
|
|
5031
|
+
subscriptionInvoiceChargeInvoiced,
|
|
5032
|
+
subscriptionInvoiceColumns,
|
|
5033
|
+
subscriptionInvoiceDocumentChanged,
|
|
5034
|
+
subscriptionInvoiceLineColumns,
|
|
3195
5035
|
sumToCents,
|
|
3196
5036
|
taxOriginOf,
|
|
3197
5037
|
taxOriginWrite,
|
|
5038
|
+
taxPerRate,
|
|
3198
5039
|
taxTreatmentToJson,
|
|
5040
|
+
toArchivedInvoiceDocument,
|
|
3199
5041
|
toBundleStemRow,
|
|
3200
5042
|
toBundleVersionRetirementRecord,
|
|
3201
5043
|
toCents,
|
|
5044
|
+
toCollectionAnnouncedNotice,
|
|
5045
|
+
toCollectionFailedNotice,
|
|
5046
|
+
toCollectionReversedNotice,
|
|
3202
5047
|
toContractLineItemRecord,
|
|
3203
5048
|
toDecimalString,
|
|
5049
|
+
toFeatureWithdrawalRecord,
|
|
5050
|
+
toInvoiceCollectionRecord,
|
|
3204
5051
|
toMaintenanceWindowRecord,
|
|
3205
5052
|
toPlanRow,
|
|
3206
5053
|
toPlanVersionRow,
|
|
3207
5054
|
toRunningContractIssuer,
|
|
3208
5055
|
toSubscriberChargeRecord,
|
|
3209
5056
|
toSubscriberCorrectionRecord,
|
|
5057
|
+
toSubscriberCreditEntryRecord,
|
|
5058
|
+
toSubscriberCreditRecord,
|
|
3210
5059
|
toSubscriberPaymentMethodRecord,
|
|
5060
|
+
toSubscriberPaymentRecord,
|
|
5061
|
+
toSubscriberReconciliationCaseRecord,
|
|
3211
5062
|
toSubscriberRecord,
|
|
5063
|
+
toSubscriberRefundRecord,
|
|
5064
|
+
toSubscriberReversalRecord,
|
|
3212
5065
|
toSubscriberTaxOriginChangeRecord,
|
|
3213
5066
|
toSubscriberVatIdCheckRecord,
|
|
3214
5067
|
toSubscriptionBundleRecord,
|
|
3215
5068
|
toSubscriptionContractRecord,
|
|
5069
|
+
toSubscriptionInvoiceDocumentRecord,
|
|
5070
|
+
toSubscriptionInvoiceRecord,
|
|
3216
5071
|
toSubscriptionNoticeRecord,
|
|
3217
5072
|
toTaxOriginValues,
|
|
3218
5073
|
toTaxTreatment,
|
|
3219
5074
|
toVersionRetirementRecord,
|
|
5075
|
+
utcMidnightOf,
|
|
5076
|
+
vatCategoryOf,
|
|
3220
5077
|
vatIdCheckFromStore,
|
|
3221
5078
|
versionOnSale,
|
|
3222
5079
|
versionOnSaleOrNext,
|
|
3223
|
-
versionSale
|
|
5080
|
+
versionSale,
|
|
5081
|
+
withdrawalParts,
|
|
5082
|
+
withdrawnFeaturesOf,
|
|
5083
|
+
yearOfDay
|
|
3224
5084
|
};
|