@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/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(/\{(\w+)\}/g, (match, name) => {
1836
- const value = params[name];
1837
- return value === void 0 || value === null ? match : String(value);
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 TABLE2 = "subscriber_ledger_entries";
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: TABLE2, column, id: row.id });
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(keysOf(ORIGINS), row.origin, where("origin")),
2210
- source: oneOf(keysOf(SOURCES), row.source, where("source")),
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/subscriber-mapping.ts
2382
- function formatCustomerNumber(prefix, sequence) {
2383
- return `${prefix}${sequence}`;
2384
- }
2385
- function toSubscriberRecord(row, tenantId) {
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
- customerNumber: formatCustomerNumber(row.customerNumberPrefix, row.customerSequence),
2389
- legalName: row.legalName,
2390
- addressLine1: row.addressLine1,
2391
- addressLine2: row.addressLine2,
2392
- postalCode: row.postalCode,
2393
- city: row.city,
2394
- country: row.country,
2395
- vatId: row.vatId,
2396
- taxNumber: row.taxNumber,
2397
- invoiceEmail: row.invoiceEmail,
2398
- business: row.business,
2399
- tenantId,
2400
- migrated: row.migrated,
2401
- createdAt: row.createdAt,
2402
- updatedAt: row.updatedAt
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 toSubscriberCorrectionRecord(row) {
2917
+ function toSubscriptionInvoiceLineRecord(row) {
2918
+ const where = (column) => ({ table: LINES, column, id: row.id });
2406
2919
  return {
2407
2920
  id: row.id,
2408
- subscriberId: row.subscriberId,
2409
- previous: toIdentityValues(row.previous),
2410
- corrected: toIdentityValues(row.corrected),
2411
- reason: row.reason,
2412
- correctedBy: row.correctedBy,
2413
- correctedAt: row.correctedAt
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 toSubscriberTaxOriginChangeRecord(row) {
2934
+ function subscriptionInvoiceColumns(invoice, drawn) {
2417
2935
  return {
2418
- id: row.id,
2419
- subscriberId: row.subscriberId,
2420
- previous: toTaxOriginValues(row.previous),
2421
- changed: toTaxOriginValues(row.changed),
2422
- changedBy: row.changedBy,
2423
- changedAt: row.changedAt,
2424
- reason: row.reason
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 identityCorrectionDelta(current, corrected) {
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
- previous,
2438
- corrected: next
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 contractPartiesOf(subscriber, issuer) {
2442
- const party = {
2443
- customerNumber: subscriber.customerNumber,
2444
- legalName: subscriber.legalName,
2445
- vatId: subscriber.vatId,
2446
- taxNumber: subscriber.taxNumber,
2447
- addressLine1: subscriber.addressLine1,
2448
- addressLine2: subscriber.addressLine2,
2449
- postalCode: subscriber.postalCode,
2450
- city: subscriber.city,
2451
- country: subscriber.country
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
- return {
2454
- subscriberId: subscriber.id,
2455
- subscriber: party,
2456
- issuer: issuer ? issuerPartyOf(issuer) : null
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 issuerPartyOf(issuer) {
2460
- const identity = issuerIdentityOf(issuer);
2461
- return {
2462
- legalName: identity?.legalName ?? "",
2463
- vatId: identity?.vatId ?? null,
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
  };