@saasicat/core 1.0.0-rc.25 → 1.0.0-rc.26

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,10 @@ 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
411
430
  };
412
431
  function toSubscriptionNoticeRecord(row) {
413
432
  return {
@@ -538,6 +557,106 @@ function toBundleVersionRetirementRecord(row) {
538
557
  };
539
558
  }
540
559
 
560
+ // src/feature-withdrawal.types.ts
561
+ var FEATURE_WITHDRAWAL_REASON_MAX_LENGTH = 1e3;
562
+ function isFeatureWithdrawnAt(withdrawal, at) {
563
+ return withdrawal.effectiveFrom <= at && (withdrawal.liftedFrom === null || at < withdrawal.liftedFrom);
564
+ }
565
+ function featuresWithdrawnAt(withdrawals, at) {
566
+ return new Set(
567
+ withdrawals.filter((withdrawal) => isFeatureWithdrawnAt(withdrawal, at)).map((withdrawal) => withdrawal.featureKey)
568
+ );
569
+ }
570
+ function nextWithdrawalChange(withdrawals, at) {
571
+ let next = null;
572
+ for (const { effectiveFrom, liftedFrom } of withdrawals) {
573
+ for (const moment of [effectiveFrom, liftedFrom]) {
574
+ if (moment === null || moment <= at) continue;
575
+ if (next === null || moment < next) next = moment;
576
+ }
577
+ }
578
+ return next;
579
+ }
580
+ var FEATURE_WITHDRAWAL_CAPABILITY = "features.withdraw";
581
+ function featureWithdrawalRowOf(record) {
582
+ return {
583
+ id: record.id,
584
+ featureKey: record.featureKey,
585
+ reason: record.reason,
586
+ effectiveFrom: record.effectiveFrom.toISOString(),
587
+ liftedFrom: record.liftedFrom?.toISOString() ?? null,
588
+ reductions: record.reductions,
589
+ announcedAt: record.announcedAt.toISOString(),
590
+ announcedBy: record.announcedBy,
591
+ liftedAt: record.liftedAt?.toISOString() ?? null,
592
+ liftedBy: record.liftedBy,
593
+ toldAt: record.toldAt?.toISOString() ?? null
594
+ };
595
+ }
596
+ function withdrawnFeaturesOf(withdrawals, now) {
597
+ return withdrawals.filter(
598
+ ({ effectiveFrom, liftedFrom }) => liftedFrom === null || liftedFrom > now && effectiveFrom < liftedFrom
599
+ ).sort((a, b) => a.effectiveFrom.getTime() - b.effectiveFrom.getTime()).map((withdrawal) => ({
600
+ featureKey: withdrawal.featureKey,
601
+ reason: withdrawal.reason,
602
+ effectiveFrom: withdrawal.effectiveFrom.toISOString(),
603
+ liftedFrom: withdrawal.liftedFrom?.toISOString() ?? null
604
+ }));
605
+ }
606
+ function endedAtOnceSubjectOf(subscriptionOrBookingId, at) {
607
+ return `${subscriptionOrBookingId}@${at.toISOString()}`;
608
+ }
609
+
610
+ // src/feature-withdrawal-mapping.ts
611
+ var TABLE2 = "feature_withdrawals";
612
+ var KINDS2 = { plan: true, bundle: true };
613
+ var CYCLES = { MONTHLY: true, YEARLY: true };
614
+ function toFeatureWithdrawalRecord(row) {
615
+ return {
616
+ id: row.id,
617
+ featureKey: row.featureKey,
618
+ reason: row.reason,
619
+ effectiveFrom: row.effectiveFrom,
620
+ liftedFrom: row.liftedFrom,
621
+ reductions: reductionsOf(row),
622
+ announcedAt: row.announcedAt,
623
+ announcedBy: row.announcedBy,
624
+ liftedAt: row.liftedAt,
625
+ liftedBy: row.liftedBy,
626
+ toldAt: row.toldAt
627
+ };
628
+ }
629
+ function reductionsOf(row) {
630
+ if (!Array.isArray(row.reductions)) {
631
+ throw new Error(`${TABLE2} row '${row.id}' holds reductions that are not a list.`);
632
+ }
633
+ return row.reductions.map((entry, index) => {
634
+ const column = `reductions[${index}]`;
635
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
636
+ throw new Error(`${TABLE2} row '${row.id}' holds ${column} that is not an object.`);
637
+ }
638
+ const { kind, key, billingCycle, amountNet } = entry;
639
+ if (typeof kind !== "string" || typeof key !== "string" || typeof billingCycle !== "string" || typeof amountNet !== "number" || !Number.isFinite(amountNet)) {
640
+ throw new Error(
641
+ `${TABLE2} row '${row.id}' holds ${column} without a kind, a key, a billing cycle and a finite amount.`
642
+ );
643
+ }
644
+ const source = { table: TABLE2, id: row.id };
645
+ return {
646
+ kind: oneOf(Object.keys(KINDS2), kind, {
647
+ ...source,
648
+ column: `${column}.kind`
649
+ }),
650
+ key,
651
+ billingCycle: oneOf(Object.keys(CYCLES), billingCycle, {
652
+ ...source,
653
+ column: `${column}.billingCycle`
654
+ }),
655
+ amountNet
656
+ };
657
+ });
658
+ }
659
+
541
660
  // src/scheduled-change-after-write.ts
542
661
  var NO_SCHEDULED_CHANGE = {
543
662
  pendingPlan: null,
@@ -674,6 +793,37 @@ function parseZonedInstant(text) {
674
793
  return Number.isNaN(moment.getTime()) ? null : moment;
675
794
  }
676
795
 
796
+ // src/zoned-day.ts
797
+ function dayInZone(at, timeZone) {
798
+ if (typeof timeZone !== "string" || timeZone === "") {
799
+ throw new RangeError("A day is read in a named time zone.");
800
+ }
801
+ const parts = new Intl.DateTimeFormat("en-US", {
802
+ timeZone,
803
+ year: "numeric",
804
+ month: "2-digit",
805
+ day: "2-digit"
806
+ }).formatToParts(at);
807
+ const part = (type) => parts.find((candidate) => candidate.type === type)?.value;
808
+ return `${part("year")}-${part("month")}-${part("day")}`;
809
+ }
810
+ function lastDayInZone(period, timeZone) {
811
+ if (!(period.until.getTime() > period.from.getTime())) {
812
+ throw new RangeError("A period ends after it begins.");
813
+ }
814
+ return dayInZone(new Date(period.until.getTime() - 1), timeZone);
815
+ }
816
+ function addDaysToDay(day, days) {
817
+ if (!Number.isInteger(days)) throw new RangeError("Days are counted in whole days.");
818
+ const [year, month, date] = day.split("-").map(Number);
819
+ if (!year || !month || !date) throw new RangeError(`${day} is not a day.`);
820
+ const moved = new Date(Date.UTC(year, month - 1, date + days));
821
+ return moved.toISOString().slice(0, 10);
822
+ }
823
+ function yearOfDay(day) {
824
+ return Number(day.slice(0, 4));
825
+ }
826
+
677
827
  // src/settings-subtree.ts
678
828
  var CATALOGUE_KEYS = /* @__PURE__ */ new Set([
679
829
  "schemaVersion",
@@ -1131,6 +1281,80 @@ var BILLING_ERROR_CODES = {
1131
1281
  * show it instead of guessing.
1132
1282
  */
1133
1283
  BUNDLE_VERSION_OFFER_CHANGED: "BUNDLE_VERSION_OFFER_CHANGED",
1284
+ // ── withdrawing a feature for an external reason ──
1285
+ /**
1286
+ * Withdrawing a feature is not offered: the installation's stores cannot
1287
+ * list everybody a withdrawal would reach.
1288
+ */
1289
+ FEATURE_WITHDRAWAL_UNAVAILABLE: "FEATURE_WITHDRAWAL_UNAVAILABLE",
1290
+ /** The catalogue knows no feature of that key, so there is nothing to withdraw. Carries `featureKey`. */
1291
+ FEATURE_WITHDRAWAL_FEATURE_UNKNOWN: "FEATURE_WITHDRAWAL_FEATURE_UNKNOWN",
1292
+ /**
1293
+ * The date names a moment before now. A feature is withdrawn from the
1294
+ * moment it is announced or from a later date: the days before were
1295
+ * delivered.
1296
+ */
1297
+ FEATURE_WITHDRAWAL_DATE_IN_PAST: "FEATURE_WITHDRAWAL_DATE_IN_PAST",
1298
+ /** A withdrawal of the feature is not lifted yet; one at a time. Carries `featureKey`. */
1299
+ FEATURE_WITHDRAWAL_OPEN: "FEATURE_WITHDRAWAL_OPEN",
1300
+ /**
1301
+ * A withdrawal of the feature is lifted only from a date after the one
1302
+ * named, so the two would overlap. Carries `featureKey` and the `date` a
1303
+ * new one may begin on.
1304
+ */
1305
+ FEATURE_WITHDRAWAL_OVERLAPS: "FEATURE_WITHDRAWAL_OVERLAPS",
1306
+ /**
1307
+ * A reduction names a plan or an add-on, in a rhythm, that no line the
1308
+ * withdrawal reaches is. Carries `kind`, `key` and `billingCycle`.
1309
+ */
1310
+ FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED: "FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED",
1311
+ /** A reduction is named twice. Carries `kind`, `key` and `billingCycle`. */
1312
+ FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE: "FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE",
1313
+ /**
1314
+ * A reduction is more than the lowest price among the lines it reduces:
1315
+ * a price is reduced to nothing at most. Carries `key`, `billingCycle` and
1316
+ * that `price`.
1317
+ */
1318
+ FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE: "FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE",
1319
+ /**
1320
+ * The subscriptions the withdrawal reaches changed since the preview the
1321
+ * operator looked at. Nothing was announced; carries the current `preview`.
1322
+ */
1323
+ FEATURE_WITHDRAWAL_PREVIEW_CHANGED: "FEATURE_WITHDRAWAL_PREVIEW_CHANGED",
1324
+ /** No withdrawal has that id. Carries `withdrawalId`. */
1325
+ FEATURE_WITHDRAWAL_NOT_FOUND: "FEATURE_WITHDRAWAL_NOT_FOUND",
1326
+ /** The withdrawal was lifted already. Carries the `date` it is lifted from. */
1327
+ FEATURE_WITHDRAWAL_ALREADY_LIFTED: "FEATURE_WITHDRAWAL_ALREADY_LIFTED",
1328
+ /**
1329
+ * Nobody the withdrawal reaches has been told of it yet, and telling them
1330
+ * failed just now: a withdrawal is lifted only once they were. Nothing was
1331
+ * lifted; the next try, or the quarter-hourly run, tells them first.
1332
+ */
1333
+ FEATURE_WITHDRAWAL_NOT_TOLD_YET: "FEATURE_WITHDRAWAL_NOT_TOLD_YET",
1334
+ /**
1335
+ * The date a withdrawal would be lifted from lies before now: what was
1336
+ * withdrawn until then was withdrawn.
1337
+ */
1338
+ FEATURE_WITHDRAWAL_LIFT_IN_PAST: "FEATURE_WITHDRAWAL_LIFT_IN_PAST",
1339
+ /**
1340
+ * The withdrawal does not take its feature away now — its date is ahead,
1341
+ * or it was lifted — so nothing ends at once because of it. Carries
1342
+ * `featureKey`.
1343
+ */
1344
+ FEATURE_WITHDRAWAL_NOT_IN_EFFECT: "FEATURE_WITHDRAWAL_NOT_IN_EFFECT",
1345
+ /**
1346
+ * The withdrawal did not reach the subscription, or the line to end: it
1347
+ * was not running at the announcement, or no longer holds the feature.
1348
+ */
1349
+ FEATURE_WITHDRAWAL_DOES_NOT_REACH: "FEATURE_WITHDRAWAL_DOES_NOT_REACH",
1350
+ /** What was to be ended at once has ended already. */
1351
+ FEATURE_WITHDRAWAL_ALREADY_ENDED: "FEATURE_WITHDRAWAL_ALREADY_ENDED",
1352
+ /**
1353
+ * A cancellation is declared for a later date, and the installation's
1354
+ * subscription write or booking store cannot bring it forward (`endNow`).
1355
+ * It ends as declared. Carries that `date`.
1356
+ */
1357
+ FEATURE_WITHDRAWAL_END_NOW_UNSUPPORTED: "FEATURE_WITHDRAWAL_END_NOW_UNSUPPORTED",
1134
1358
  // ── the preview routes' own blockers and warnings ──
1135
1359
  //
1136
1360
  // These travel inside a 200 response, in `blockers[]` and `warnings[]`, not
@@ -1288,6 +1512,55 @@ var CONTRACT_ERROR_CODES = {
1288
1512
  */
1289
1513
  SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED: "SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED"
1290
1514
  };
1515
+ var INVOICE_ERROR_CODES = {
1516
+ /**
1517
+ * A charge already stands on an invoice: a second run issuing the same
1518
+ * charges lost the race. Nothing is written and no number is drawn.
1519
+ */
1520
+ SUBSCRIPTION_INVOICE_CHARGE_INVOICED: "SUBSCRIPTION_INVOICE_CHARGE_INVOICED",
1521
+ /**
1522
+ * The invoice lacks content the tax adapter requires of it — `missing`
1523
+ * names the fields, `adapter` the adapter. It draws no number and waits
1524
+ * until what is missing is supplied (`SC-PRIC-027`).
1525
+ */
1526
+ SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE: "SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE",
1527
+ /**
1528
+ * The contract's parties were copied by a migration and the operator has
1529
+ * not confirmed them yet; its charges are not invoiced until then
1530
+ * (`SC-AUD-012`).
1531
+ */
1532
+ SUBSCRIPTION_INVOICE_PARTIES_UNCONFIRMED: "SUBSCRIPTION_INVOICE_PARTIES_UNCONFIRMED",
1533
+ /**
1534
+ * A file of the invoice's document has another checksum than the one
1535
+ * recorded or archived for it (`invoiceId`, `recorded`, `rendered`), and
1536
+ * the first stays. The run takes the archived file as the one issued, or —
1537
+ * where nothing is archived — records the new rendering in place of the
1538
+ * old (`SC-AUD-019`); the invoice waits only where another run changed the
1539
+ * record or the archive in between, and the next run settles it.
1540
+ */
1541
+ SUBSCRIPTION_INVOICE_DOCUMENT_CHANGED: "SUBSCRIPTION_INVOICE_DOCUMENT_CHANGED",
1542
+ /**
1543
+ * The tax adapter refuses the rendered document — `missing` names what it
1544
+ * lacks or disagrees with, `adapter` the adapter. The invoice keeps its
1545
+ * number and waits for a document that passes (`SC-PRIC-042`).
1546
+ */
1547
+ SUBSCRIPTION_INVOICE_DOCUMENT_REFUSED: "SUBSCRIPTION_INVOICE_DOCUMENT_REFUSED",
1548
+ /**
1549
+ * The archived document does not match the checksum recorded when it was
1550
+ * rendered (`invoiceId`). It is not handed out (`SC-AUD-019`).
1551
+ */
1552
+ SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH: "SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH",
1553
+ /** The invoice's document is not archived yet (`invoiceId`); nothing is offered until it is (`SC-AUD-014`). */
1554
+ SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED: "SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED",
1555
+ /** No invoice with this id (`invoiceId`). */
1556
+ SUBSCRIPTION_INVOICE_NOT_FOUND: "SUBSCRIPTION_INVOICE_NOT_FOUND",
1557
+ /**
1558
+ * The subscriber's record holds no invoice email (`subscriberId`), so the
1559
+ * archived invoice cannot be sent (`SC-PRIC-047`); it is sent once one is
1560
+ * entered.
1561
+ */
1562
+ SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL: "SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL"
1563
+ };
1291
1564
  var SUBSCRIBER_ERROR_CODES = {
1292
1565
  /**
1293
1566
  * A contract, a plan change or a booking for a tenant that has no
@@ -1468,6 +1741,7 @@ var PLATFORM_ERROR_CODES = {
1468
1741
  ...CATALOG_ERROR_CODES,
1469
1742
  ...BILLING_ERROR_CODES,
1470
1743
  ...CONTRACT_ERROR_CODES,
1744
+ ...INVOICE_ERROR_CODES,
1471
1745
  ...SUBSCRIBER_ERROR_CODES,
1472
1746
  ...REGISTRATION_ERROR_CODES,
1473
1747
  ...PAYMENT_ERROR_CODES,
@@ -1521,7 +1795,13 @@ var OPTIONAL_CANONICAL_MODELS = {
1521
1795
  SubscriptionNotice: ["tenantBilling.subscriptionNotices"],
1522
1796
  VersionRetirement: ["tenantBilling.versionRetirements"],
1523
1797
  BundleVersionRetirement: ["tenantBilling.bundleVersionRetirements"],
1798
+ FeatureWithdrawal: ["tenantBilling.featureWithdrawals"],
1524
1799
  SubscriberLedgerEntry: ["entitlement.subscriberLedgerRepository"],
1800
+ SubscriptionInvoice: ["entitlement.subscriptionInvoiceRepository"],
1801
+ SubscriptionInvoiceLine: ["entitlement.subscriptionInvoiceRepository"],
1802
+ SubscriptionInvoiceNumber: ["entitlement.subscriptionInvoiceRepository"],
1803
+ SubscriptionInvoiceDocument: ["entitlement.subscriptionInvoiceRepository"],
1804
+ SubscriptionInvoiceArchive: ["entitlement.subscriptionInvoiceArchive"],
1525
1805
  PromoCodeHold: ["promo.holdRepository"]
1526
1806
  };
1527
1807
  function membersLeftOut(notAdopted = []) {
@@ -1704,6 +1984,23 @@ var ERROR_MESSAGES_EN = {
1704
1984
  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
1985
  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
1986
  BUNDLE_VERSION_OFFER_CHANGED: "The offer changed since it was shown. Look at the current one before switching.",
1987
+ FEATURE_WITHDRAWAL_UNAVAILABLE: "Withdrawing a feature is not offered here: the stores cannot list everybody a withdrawal reaches.",
1988
+ FEATURE_WITHDRAWAL_FEATURE_UNKNOWN: "The catalogue knows no feature {featureKey}.",
1989
+ FEATURE_WITHDRAWAL_DATE_IN_PAST: "A feature is withdrawn from now on or from a later date, never from a date already past.",
1990
+ FEATURE_WITHDRAWAL_OPEN: "{featureKey} is withdrawn already, and that withdrawal is not lifted. Lift it before announcing another.",
1991
+ FEATURE_WITHDRAWAL_OVERLAPS: "{featureKey} is withdrawn until {date}. Another withdrawal can begin on that date at the earliest.",
1992
+ FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED: "The reduction for {key} billed {billingCycle} names nothing this withdrawal reaches.",
1993
+ FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE: "The reduction for {key} billed {billingCycle} is named twice.",
1994
+ FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE: "The reduction for {key} billed {billingCycle} is more than the lowest price it reduces, {price}.",
1995
+ FEATURE_WITHDRAWAL_PREVIEW_CHANGED: "The subscriptions this withdrawal reaches changed since they were shown. Look at them again before announcing it.",
1996
+ FEATURE_WITHDRAWAL_NOT_FOUND: "Feature withdrawal {withdrawalId} not found.",
1997
+ FEATURE_WITHDRAWAL_ALREADY_LIFTED: "This withdrawal is lifted already, from {date}.",
1998
+ 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.",
1999
+ FEATURE_WITHDRAWAL_LIFT_IN_PAST: "A withdrawal is lifted from now on or from a later date, never from a date already past.",
2000
+ FEATURE_WITHDRAWAL_NOT_IN_EFFECT: "{featureKey} is not withdrawn now, so nothing can be ended at once because of it.",
2001
+ FEATURE_WITHDRAWAL_DOES_NOT_REACH: "This withdrawal does not reach your subscription, or no longer this booking.",
2002
+ FEATURE_WITHDRAWAL_ALREADY_ENDED: "This has ended already.",
2003
+ 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
2004
  RETIREMENT_SWITCH_NOT_PENDING: "No retirement of your version is waiting for its date, so there is nothing to switch to.",
1708
2005
  RETIREMENT_SWITCH_IN_TRIAL: "The switch opens when your trial ends.",
1709
2006
  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 +2043,15 @@ var ERROR_MESSAGES_EN = {
1746
2043
  PROMO_MAX_REDEMPTIONS_LOWERED: "maxRedemptions cannot be lowered.",
1747
2044
  // ── contract ──
1748
2045
  SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED: "The contract states {stated} % in {field}, and the tax adapter decides {decided} %.",
2046
+ SUBSCRIPTION_INVOICE_CHARGE_INVOICED: "Charge '{chargeId}' already stands on an invoice.",
2047
+ SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE: "The invoice lacks what the tax adapter {adapter} requires of it: {missing}. It is issued once that is supplied.",
2048
+ 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.",
2049
+ 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.",
2050
+ 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.",
2051
+ 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.",
2052
+ SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED: "The document of invoice {invoiceId} is not archived yet.",
2053
+ SUBSCRIPTION_INVOICE_NOT_FOUND: "No invoice '{invoiceId}'.",
2054
+ SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL: "Subscriber {subscriberId} has no invoice email, so its invoice cannot be sent; it is sent once one is entered.",
1749
2055
  CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "A checkout offer can yield only one contract, and only once its line items are frozen.",
1750
2056
  CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "A checkout offer requires a frozen plan line item.",
1751
2057
  CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Every selected bundle version requires a frozen bundle line item.",
@@ -2188,17 +2494,20 @@ var ORIGINS = {
2188
2494
  bundleBooking: true,
2189
2495
  bundleChange: true,
2190
2496
  credit: true,
2191
- correction: true
2497
+ correction: true,
2498
+ reductionTakenBack: true
2192
2499
  };
2193
2500
  var SOURCES = {
2194
2501
  plan: true,
2195
2502
  bundle: true,
2196
2503
  discount: true
2197
2504
  };
2198
- var TABLE2 = "subscriber_ledger_entries";
2505
+ var SUBSCRIBER_CHARGE_ORIGINS = keysOf(ORIGINS);
2506
+ var SUBSCRIBER_CHARGE_SOURCES = keysOf(SOURCES);
2507
+ var TABLE3 = "subscriber_ledger_entries";
2199
2508
  var CENT_TOLERANCE = 1e-6;
2200
2509
  function toSubscriberChargeRecord(row) {
2201
- const where = (column) => ({ table: TABLE2, column, id: row.id });
2510
+ const where = (column) => ({ table: TABLE3, column, id: row.id });
2202
2511
  return {
2203
2512
  id: row.id,
2204
2513
  subscriberId: row.subscriberId,
@@ -2206,8 +2515,8 @@ function toSubscriberChargeRecord(row) {
2206
2515
  subscriptionId: row.subscriptionId,
2207
2516
  contractId: row.contractId,
2208
2517
  contractLineItemId: row.contractLineItemId,
2209
- origin: oneOf(keysOf(ORIGINS), row.origin, where("origin")),
2210
- source: oneOf(keysOf(SOURCES), row.source, where("source")),
2518
+ origin: oneOf(SUBSCRIBER_CHARGE_ORIGINS, row.origin, where("origin")),
2519
+ source: oneOf(SUBSCRIBER_CHARGE_SOURCES, row.source, where("source")),
2211
2520
  sourceRef: row.sourceRef,
2212
2521
  periodStart: row.periodStart,
2213
2522
  periodEnd: row.periodEnd,
@@ -2247,6 +2556,15 @@ function keysOf(record) {
2247
2556
  // src/subscriber-ledger.types.ts
2248
2557
  var SUBSCRIBER_ACCOUNT_CAPABILITY = "charges.read";
2249
2558
 
2559
+ // src/subscription-invoice.types.ts
2560
+ var INVOICE_SEQUENCE_DIGITS = 6;
2561
+ function formatInvoiceNumber(prefix, year, sequence) {
2562
+ if (!Number.isInteger(sequence) || sequence < 1) {
2563
+ throw new RangeError(`An invoice number's sequence starts at 1; ${sequence} is not one.`);
2564
+ }
2565
+ return `${prefix}-${year}-${String(sequence).padStart(INVOICE_SEQUENCE_DIGITS, "0")}`;
2566
+ }
2567
+
2250
2568
  // src/subscriber.types.ts
2251
2569
  var SUBSCRIBER_INVOICE_ADDRESS_FIELDS = [
2252
2570
  "addressLine1",
@@ -2378,6 +2696,420 @@ function textEntries(value) {
2378
2696
  return entries;
2379
2697
  }
2380
2698
 
2699
+ // src/subscription-invoice-mapping.ts
2700
+ var INVOICES = "subscription_invoices";
2701
+ var LINES = "subscription_invoice_lines";
2702
+ var CENT_TOLERANCE2 = 1e-6;
2703
+ function toSubscriptionInvoiceRecord(row, lines) {
2704
+ const where = (column) => ({ table: INVOICES, column, id: row.id });
2705
+ const treatment = toTaxTreatment(row.taxTreatment);
2706
+ if (!treatment) {
2707
+ throw new Error(`${INVOICES}.taxTreatment of ${row.id} is not a tax treatment.`);
2708
+ }
2709
+ return {
2710
+ id: row.id,
2711
+ number: row.number,
2712
+ numberPrefix: row.numberPrefix,
2713
+ numberYear: row.numberYear,
2714
+ numberSequence: row.numberSequence,
2715
+ tenantId: row.tenantId,
2716
+ subscriberId: row.subscriberId,
2717
+ subscriptionId: row.subscriptionId,
2718
+ contractId: row.contractId,
2719
+ issuedAt: row.issuedAt,
2720
+ issueDate: dayOf2(row.issueDate, where("issueDate")),
2721
+ dueDate: dayOf2(row.dueDate, where("dueDate")),
2722
+ servicePeriodFrom: dayOf2(row.servicePeriodFrom, where("servicePeriodFrom")),
2723
+ servicePeriodUntil: dayOf2(row.servicePeriodUntil, where("servicePeriodUntil")),
2724
+ currency: row.currency,
2725
+ issuer: issuerOf(row.issuerParty, where("issuerParty")),
2726
+ subscriber: subscriberOf(row.subscriberParty, where("subscriberParty")),
2727
+ footer: footerOf(row.footer, where("footer")),
2728
+ taxTreatment: treatment,
2729
+ taxes: taxesOf(row.taxes, where("taxes")),
2730
+ totalNet: amountOf2(row.totalNet, where("totalNet")),
2731
+ totalTax: amountOf2(row.totalTax, where("totalTax")),
2732
+ totalGross: amountOf2(row.totalGross, where("totalGross")),
2733
+ lines: [...lines].sort((a, b) => a.position - b.position).map((line) => toSubscriptionInvoiceLineRecord(line)),
2734
+ createdAt: row.createdAt
2735
+ };
2736
+ }
2737
+ function toSubscriptionInvoiceLineRecord(row) {
2738
+ const where = (column) => ({ table: LINES, column, id: row.id });
2739
+ return {
2740
+ id: row.id,
2741
+ invoiceId: row.invoiceId,
2742
+ position: row.position,
2743
+ chargeId: row.chargeId,
2744
+ contractLineItemId: row.contractLineItemId,
2745
+ title: row.title,
2746
+ origin: oneOf(SUBSCRIBER_CHARGE_ORIGINS, row.origin, where("origin")),
2747
+ source: oneOf(SUBSCRIBER_CHARGE_SOURCES, row.source, where("source")),
2748
+ periodFrom: dayOf2(row.periodFrom, where("periodFrom")),
2749
+ periodUntil: dayOf2(row.periodUntil, where("periodUntil")),
2750
+ amountNet: amountOf2(row.amountNet, where("amountNet")),
2751
+ taxRate: amountOf2(row.taxRate, where("taxRate"))
2752
+ };
2753
+ }
2754
+ function subscriptionInvoiceColumns(invoice, drawn) {
2755
+ return {
2756
+ number: drawn.number,
2757
+ numberPrefix: invoice.numberPrefix,
2758
+ numberYear: invoice.numberYear,
2759
+ numberSequence: drawn.numberSequence,
2760
+ tenantId: invoice.tenantId,
2761
+ subscriberId: invoice.subscriberId,
2762
+ subscriptionId: invoice.subscriptionId,
2763
+ contractId: invoice.contractId,
2764
+ issuedAt: invoice.issuedAt,
2765
+ issueDate: invoice.issueDate,
2766
+ dueDate: invoice.dueDate,
2767
+ servicePeriodFrom: invoice.servicePeriodFrom,
2768
+ servicePeriodUntil: invoice.servicePeriodUntil,
2769
+ currency: invoice.currency,
2770
+ issuerParty: { ...invoice.issuer },
2771
+ subscriberParty: { ...invoice.subscriber },
2772
+ footer: [...invoice.footer],
2773
+ taxTreatment: taxTreatmentToJson(invoice.taxTreatment),
2774
+ taxes: invoice.taxes.map((entry) => ({ rate: entry.rate, net: entry.net, tax: entry.tax })),
2775
+ totalNet: centsText(invoice.totalNet, "totalNet"),
2776
+ totalTax: centsText(invoice.totalTax, "totalTax"),
2777
+ totalGross: centsText(invoice.totalGross, "totalGross")
2778
+ };
2779
+ }
2780
+ function subscriptionInvoiceLineColumns(line, invoiceId) {
2781
+ return {
2782
+ invoiceId,
2783
+ position: line.position,
2784
+ chargeId: line.chargeId,
2785
+ contractLineItemId: line.contractLineItemId,
2786
+ title: line.title,
2787
+ origin: line.origin,
2788
+ source: line.source,
2789
+ periodFrom: line.periodFrom,
2790
+ periodUntil: line.periodUntil,
2791
+ amountNet: centsText(line.amountNet, "amountNet"),
2792
+ taxRate: centsText(line.taxRate, "taxRate")
2793
+ };
2794
+ }
2795
+ function utcMidnightOf(day) {
2796
+ return /* @__PURE__ */ new Date(`${day}T00:00:00.000Z`);
2797
+ }
2798
+ var DAY_LENGTH = "YYYY-MM-DD".length;
2799
+ function dayOf2(value, where) {
2800
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
2801
+ return value.toISOString().slice(0, DAY_LENGTH);
2802
+ }
2803
+ if (typeof value === "string" && value.length === DAY_LENGTH && !Number.isNaN(Date.parse(value))) {
2804
+ return value;
2805
+ }
2806
+ throw new Error(`${where.table}.${where.column} of ${where.id} is not a day.`);
2807
+ }
2808
+ function footerOf(value, where) {
2809
+ if (!Array.isArray(value) || !value.every((line) => typeof line === "string")) {
2810
+ throw new Error(`${where.table}.${where.column} of ${where.id} is not a list of lines.`);
2811
+ }
2812
+ return [...value];
2813
+ }
2814
+ function amountOf2(value, where) {
2815
+ const amount = Number(value === null || value === void 0 ? NaN : String(value));
2816
+ if (!Number.isFinite(amount)) {
2817
+ throw new Error(`${where.table}.${where.column} of ${where.id} is not an amount.`);
2818
+ }
2819
+ return amount;
2820
+ }
2821
+ function centsText(amount, field) {
2822
+ const cents2 = amount * 100;
2823
+ if (!Number.isFinite(cents2) || Math.abs(cents2 - Math.round(cents2)) > CENT_TOLERANCE2) {
2824
+ throw new Error(
2825
+ `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.`
2826
+ );
2827
+ }
2828
+ return amount.toFixed(2);
2829
+ }
2830
+ function textOrNull2(source, key, where) {
2831
+ const value = source[key];
2832
+ if (value === null || value === void 0) return null;
2833
+ if (typeof value !== "string") {
2834
+ throw new Error(`${where.table}.${where.column}.${key} of ${where.id} is not text.`);
2835
+ }
2836
+ return value;
2837
+ }
2838
+ function partyOf(value, where) {
2839
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
2840
+ throw new Error(`${where.table}.${where.column} of ${where.id} is not a party.`);
2841
+ }
2842
+ const source = value;
2843
+ const legalName = textOrNull2(source, "legalName", where);
2844
+ if (legalName === null) {
2845
+ throw new Error(`${where.table}.${where.column} of ${where.id} names no legal name.`);
2846
+ }
2847
+ return {
2848
+ legalName,
2849
+ vatId: textOrNull2(source, "vatId", where),
2850
+ taxNumber: textOrNull2(source, "taxNumber", where),
2851
+ addressLine1: textOrNull2(source, "addressLine1", where),
2852
+ addressLine2: textOrNull2(source, "addressLine2", where),
2853
+ postalCode: textOrNull2(source, "postalCode", where),
2854
+ city: textOrNull2(source, "city", where),
2855
+ country: textOrNull2(source, "country", where)
2856
+ };
2857
+ }
2858
+ function issuerOf(value, where) {
2859
+ return partyOf(value, where);
2860
+ }
2861
+ function subscriberOf(value, where) {
2862
+ const party = partyOf(value, where);
2863
+ const customerNumber = textOrNull2(value, "customerNumber", where);
2864
+ if (customerNumber === null) {
2865
+ throw new Error(`${where.table}.${where.column} of ${where.id} names no customer number.`);
2866
+ }
2867
+ return { ...party, customerNumber };
2868
+ }
2869
+ function taxesOf(value, where) {
2870
+ if (!Array.isArray(value)) {
2871
+ throw new Error(`${where.table}.${where.column} of ${where.id} is not a list of rates.`);
2872
+ }
2873
+ return value.map((entry) => {
2874
+ const item = entry ?? {};
2875
+ return {
2876
+ rate: amountOf2(item.rate, where),
2877
+ net: amountOf2(item.net, where),
2878
+ tax: amountOf2(item.tax, where)
2879
+ };
2880
+ });
2881
+ }
2882
+
2883
+ // src/subscription-invoice-refusals.ts
2884
+ function subscriptionInvoiceChargeInvoiced(chargeId) {
2885
+ return new PersistenceRefusal(
2886
+ INVOICE_ERROR_CODES.SUBSCRIPTION_INVOICE_CHARGE_INVOICED,
2887
+ "moved",
2888
+ `Charge '${chargeId}' already stands on an invoice.`,
2889
+ { chargeId }
2890
+ );
2891
+ }
2892
+ function subscriptionInvoiceDocumentChanged(invoiceId, recorded, rendered) {
2893
+ return new PersistenceRefusal(
2894
+ INVOICE_ERROR_CODES.SUBSCRIPTION_INVOICE_DOCUMENT_CHANGED,
2895
+ "moved",
2896
+ `The document rendered for invoice '${invoiceId}' differs from the one recorded for it.`,
2897
+ { invoiceId, recorded, rendered }
2898
+ );
2899
+ }
2900
+
2901
+ // src/subscription-invoice-draft.ts
2902
+ function invoiceGroupsOf(charges) {
2903
+ const groups = /* @__PURE__ */ new Map();
2904
+ for (const charge of charges) {
2905
+ const key = `${charge.contractId}|${charge.bookedAt.toISOString()}`;
2906
+ const group = groups.get(key);
2907
+ if (group) group.charges.push(charge);
2908
+ else
2909
+ groups.set(key, {
2910
+ contractId: charge.contractId,
2911
+ bookedAt: charge.bookedAt,
2912
+ charges: [charge]
2913
+ });
2914
+ }
2915
+ return [...groups.values()].filter((group) => group.charges.some((charge) => charge.amountNet !== 0)).sort(
2916
+ (a, b) => a.bookedAt.getTime() - b.bookedAt.getTime() || a.contractId.localeCompare(b.contractId)
2917
+ );
2918
+ }
2919
+ function draftSubscriptionInvoice(input) {
2920
+ const { group, treatment, timeZone } = input;
2921
+ const lines = linesOf(input);
2922
+ const issueDate = dayInZone(input.issuedAt, timeZone);
2923
+ const tax = input.tax(lines.map((line) => ({ net: line.amountNet, rate: line.taxRate })));
2924
+ return {
2925
+ numberPrefix: input.numberPrefix,
2926
+ numberYear: yearOfDay(issueDate),
2927
+ tenantId: input.tenantId,
2928
+ subscriberId: input.subscriberId,
2929
+ subscriptionId: group.charges[0].subscriptionId,
2930
+ contractId: group.contractId,
2931
+ issuedAt: input.issuedAt,
2932
+ issueDate,
2933
+ dueDate: addDaysToDay(issueDate, input.paymentTermDays),
2934
+ servicePeriodFrom: lines.map((line) => line.periodFrom).sort()[0],
2935
+ servicePeriodUntil: lines.map((line) => line.periodUntil).sort().at(-1),
2936
+ currency: currencyOf(group),
2937
+ issuer: input.issuer,
2938
+ subscriber: input.subscriber,
2939
+ footer: [...input.footer],
2940
+ taxTreatment: treatment,
2941
+ taxes: tax.rates,
2942
+ totalNet: tax.net,
2943
+ totalTax: tax.tax,
2944
+ totalGross: tax.gross,
2945
+ lines
2946
+ };
2947
+ }
2948
+ function invoiceContentDraftOf(invoice) {
2949
+ return {
2950
+ issuer: invoice.issuer,
2951
+ subscriber: invoice.subscriber,
2952
+ treatment: invoice.taxTreatment,
2953
+ issueDate: invoice.issueDate,
2954
+ dueDate: invoice.dueDate,
2955
+ servicePeriod: { from: invoice.servicePeriodFrom, until: invoice.servicePeriodUntil },
2956
+ lines: invoice.lines.map((line) => ({
2957
+ title: line.title,
2958
+ net: line.amountNet,
2959
+ rate: line.taxRate
2960
+ }))
2961
+ };
2962
+ }
2963
+ function linesOf(input) {
2964
+ const placeOf = new Map(input.contractLines.map((line, index) => [line.id, index]));
2965
+ const place = (charge) => {
2966
+ const index = placeOf.get(charge.contractLineItemId);
2967
+ if (index === void 0) {
2968
+ throw new Error(
2969
+ `Charge ${charge.id} names contract line ${charge.contractLineItemId}, which contract ${input.group.contractId} does not have.`
2970
+ );
2971
+ }
2972
+ return index;
2973
+ };
2974
+ return [...input.group.charges].sort(
2975
+ (a, b) => place(a) - place(b) || a.periodStart.getTime() - b.periodStart.getTime() || a.origin.localeCompare(b.origin)
2976
+ ).map((charge, index) => ({
2977
+ position: index + 1,
2978
+ chargeId: charge.id,
2979
+ contractLineItemId: charge.contractLineItemId,
2980
+ title: input.contractLines[place(charge)].titleSnapshot,
2981
+ origin: charge.origin,
2982
+ source: charge.source,
2983
+ periodFrom: dayInZone(charge.periodStart, input.timeZone),
2984
+ periodUntil: lastDayInZone(
2985
+ { from: charge.periodStart, until: charge.periodEnd },
2986
+ input.timeZone
2987
+ ),
2988
+ amountNet: charge.amountNet,
2989
+ taxRate: input.treatment.rate
2990
+ }));
2991
+ }
2992
+ function currencyOf(group) {
2993
+ const currencies = new Set(group.charges.map((charge) => charge.currency));
2994
+ if (currencies.size !== 1) {
2995
+ throw new Error(
2996
+ `The charges of contract ${group.contractId} booked at ${group.bookedAt.toISOString()} are in ${[...currencies].join(" and ")}; an invoice is in one currency.`
2997
+ );
2998
+ }
2999
+ return [...currencies][0];
3000
+ }
3001
+
3002
+ // src/invoice-issuer.ts
3003
+ function invoiceIssuerOf(copy, configured, history) {
3004
+ const target = configured && identityOf2(configured);
3005
+ let reached = identityOf2(copy);
3006
+ if (!target || !reached) return { ...copy };
3007
+ for (const step of history) {
3008
+ if (sameLegalIdentity(reached, target)) break;
3009
+ const change = classifyIssuerChange(
3010
+ recordedIssuerIdentity(step.previous),
3011
+ issuerBlockOf(step.current)
3012
+ );
3013
+ if (change.kind === "corrected" && sameLegalIdentity(change.recorded, reached)) {
3014
+ reached = change.current;
3015
+ }
3016
+ }
3017
+ return sameLegalIdentity(reached, target) ? { ...configured } : { ...copy };
3018
+ }
3019
+ function invoiceFooterOf(issuer, configured, footer) {
3020
+ const named = identityOf2(issuer);
3021
+ const target = configured && identityOf2(configured);
3022
+ return named && target && sameLegalIdentity(named, target) ? [...footer] : [];
3023
+ }
3024
+ function identityOf2(party) {
3025
+ return recordedIssuerIdentity({ issuer: party });
3026
+ }
3027
+ function issuerBlockOf(values) {
3028
+ const issuer = values.issuer;
3029
+ if (issuer === null || typeof issuer !== "object" || Array.isArray(issuer)) return void 0;
3030
+ return issuer;
3031
+ }
3032
+
3033
+ // src/invoice-document.types.ts
3034
+ var INVOICE_DOCUMENT_CAPABILITY = "invoices.read";
3035
+
3036
+ // src/invoice-document-mapping.ts
3037
+ var DOCUMENTS = "subscription_invoice_documents";
3038
+ var ARCHIVE = "subscription_invoice_archive";
3039
+ var CHECKSUM = /^[0-9a-f]{64}$/;
3040
+ function isInvoiceDocumentChecksum(value) {
3041
+ return typeof value === "string" && CHECKSUM.test(value);
3042
+ }
3043
+ function toSubscriptionInvoiceDocumentRecord(row) {
3044
+ if (!isInvoiceDocumentChecksum(row.checksum)) {
3045
+ throw new Error(`${DOCUMENTS}.checksum of ${row.invoiceId} is not a SHA-256 digest.`);
3046
+ }
3047
+ const size = Number(row.size);
3048
+ if (!Number.isSafeInteger(size) || size < 0) {
3049
+ throw new Error(`${DOCUMENTS}.size of ${row.invoiceId} is not a size in bytes.`);
3050
+ }
3051
+ return {
3052
+ invoiceId: row.invoiceId,
3053
+ checksum: row.checksum,
3054
+ mediaType: row.mediaType,
3055
+ fileName: row.fileName,
3056
+ size,
3057
+ renderedAt: row.renderedAt,
3058
+ archivedAt: row.archivedAt,
3059
+ deliveryClaimedAt: row.deliveryClaimedAt,
3060
+ deliveredAt: row.deliveredAt,
3061
+ deliveredTo: row.deliveredTo
3062
+ };
3063
+ }
3064
+ function toArchivedInvoiceDocument(row) {
3065
+ if (!isInvoiceDocumentChecksum(row.checksum)) {
3066
+ throw new Error(`${ARCHIVE}.checksum of ${row.invoiceId} is not a SHA-256 digest.`);
3067
+ }
3068
+ if (!(row.content instanceof Uint8Array)) {
3069
+ throw new Error(`${ARCHIVE}.content of ${row.invoiceId} is not a file.`);
3070
+ }
3071
+ return {
3072
+ invoiceId: row.invoiceId,
3073
+ checksum: row.checksum,
3074
+ mediaType: row.mediaType,
3075
+ fileName: row.fileName,
3076
+ // A copy that owns its bytes: a driver's `Buffer` may be a view into a
3077
+ // pooled allocation, and a caller handing it on must not see that pool.
3078
+ content: new Uint8Array(row.content),
3079
+ archivedAt: row.archivedAt
3080
+ };
3081
+ }
3082
+ var INVOICE_ARCHIVE_TRIGGERS = [
3083
+ "subscription_invoice_archive_is_kept",
3084
+ "subscription_invoice_archive_is_not_emptied"
3085
+ ];
3086
+ function invoiceArchiveProtectionProblem(enabledTriggers) {
3087
+ const missing = INVOICE_ARCHIVE_TRIGGERS.filter((name) => !enabledTriggers.includes(name));
3088
+ if (missing.length === 0) return null;
3089
+ 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.`;
3090
+ }
3091
+
3092
+ // src/e-invoice-format.ts
3093
+ var CATEGORY = {
3094
+ standard: "S",
3095
+ reduced: "S",
3096
+ exempt: "E",
3097
+ "small-business": "E",
3098
+ "reverse-charge": "AE",
3099
+ "not-taxable": "O"
3100
+ };
3101
+ function vatCategoryOf(kind) {
3102
+ return CATEGORY[kind];
3103
+ }
3104
+ var NOT_XML = /[^\t\n\r\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/gu;
3105
+ function holdsOnlyXmlCharacters(text) {
3106
+ return text.search(NOT_XML) === -1;
3107
+ }
3108
+ var CARRIAGE_RETURN = /\r\n?/g;
3109
+ function asXmlReadsIt(text) {
3110
+ return text.replace(NOT_XML, "?").replace(CARRIAGE_RETURN, "\n");
3111
+ }
3112
+
2381
3113
  // src/subscriber-mapping.ts
2382
3114
  function formatCustomerNumber(prefix, sequence) {
2383
3115
  return `${prefix}${sequence}`;
@@ -2507,7 +3239,7 @@ function toIdentityValues(value) {
2507
3239
  // src/subscriber-payment-method-mapping.ts
2508
3240
  var PAYMENT_METHOD_TYPES = ["card", "sepa_debit"];
2509
3241
  var STATUSES = ["ACTIVE", "REPLACED"];
2510
- var TABLE3 = "subscriber_payment_methods";
3242
+ var TABLE4 = "subscriber_payment_methods";
2511
3243
  function toSubscriberPaymentMethodRecord(row) {
2512
3244
  return {
2513
3245
  id: row.id,
@@ -2516,7 +3248,7 @@ function toSubscriberPaymentMethodRecord(row) {
2516
3248
  provider: row.provider,
2517
3249
  customerRef: row.customerRef,
2518
3250
  paymentMethodRef: row.paymentMethodRef,
2519
- type: oneOf(PAYMENT_METHOD_TYPES, row.type, { table: TABLE3, column: "type", id: row.id }),
3251
+ type: oneOf(PAYMENT_METHOD_TYPES, row.type, { table: TABLE4, column: "type", id: row.id }),
2520
3252
  brand: row.brand,
2521
3253
  last4: row.last4,
2522
3254
  expiryMonth: row.expiryMonth,
@@ -2524,7 +3256,7 @@ function toSubscriberPaymentMethodRecord(row) {
2524
3256
  country: row.country,
2525
3257
  bankCode: row.bankCode,
2526
3258
  mandateReference: row.mandateReference,
2527
- status: oneOf(STATUSES, row.status, { table: TABLE3, column: "status", id: row.id }),
3259
+ status: oneOf(STATUSES, row.status, { table: TABLE4, column: "status", id: row.id }),
2528
3260
  confirmedAt: row.confirmedAt,
2529
3261
  replacedAt: row.replacedAt,
2530
3262
  createdAt: row.createdAt
@@ -2922,6 +3654,23 @@ var ERROR_MESSAGES_DE = {
2922
3654
  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
3655
  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
3656
  BUNDLE_VERSION_OFFER_CHANGED: "Das Angebot hat sich seit der Anzeige ge\xE4ndert. Bitte sieh dir das aktuelle an, bevor du wechselst.",
3657
+ FEATURE_WITHDRAWAL_UNAVAILABLE: "Ein Feature zur\xFCckzuziehen ist hier nicht m\xF6glich: Die Speicher k\xF6nnen nicht alle auflisten, die ein R\xFCckzug erreicht.",
3658
+ FEATURE_WITHDRAWAL_FEATURE_UNKNOWN: "Der Katalog kennt kein Feature {featureKey}.",
3659
+ FEATURE_WITHDRAWAL_DATE_IN_PAST: "Ein Feature wird ab jetzt oder ab einem sp\xE4teren Datum zur\xFCckgezogen, nie ab einem vergangenen.",
3660
+ 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.",
3661
+ FEATURE_WITHDRAWAL_OVERLAPS: "{featureKey} ist bis zum {date} zur\xFCckgezogen. Ein weiterer R\xFCckzug kann fr\xFChestens an diesem Tag beginnen.",
3662
+ FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED: "Die Minderung f\xFCr {key} bei Abrechnung {billingCycle} betrifft nichts, was dieser R\xFCckzug erreicht.",
3663
+ FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE: "Die Minderung f\xFCr {key} bei Abrechnung {billingCycle} ist doppelt angegeben.",
3664
+ FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE: "Die Minderung f\xFCr {key} bei Abrechnung {billingCycle} ist h\xF6her als der niedrigste Preis, den sie mindert: {price}.",
3665
+ 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.",
3666
+ FEATURE_WITHDRAWAL_NOT_FOUND: "R\xFCckzug {withdrawalId} nicht gefunden.",
3667
+ FEATURE_WITHDRAWAL_ALREADY_LIFTED: "Dieser R\xFCckzug ist bereits aufgehoben, ab dem {date}.",
3668
+ 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.",
3669
+ FEATURE_WITHDRAWAL_LIFT_IN_PAST: "Ein R\xFCckzug wird ab jetzt oder ab einem sp\xE4teren Datum aufgehoben, nie ab einem vergangenen.",
3670
+ FEATURE_WITHDRAWAL_NOT_IN_EFFECT: "{featureKey} ist gerade nicht zur\xFCckgezogen. Deshalb kann nichts sofort beendet werden.",
3671
+ FEATURE_WITHDRAWAL_DOES_NOT_REACH: "Dieser R\xFCckzug betrifft Ihr Abonnement nicht, oder diese Buchung nicht mehr.",
3672
+ FEATURE_WITHDRAWAL_ALREADY_ENDED: "Das ist bereits beendet.",
3673
+ 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
3674
  RETIREMENT_SWITCH_NOT_PENDING: "F\xFCr Ihre Version steht keine Einstellung mehr bevor. Es gibt nichts, wohin Sie wechseln k\xF6nnten.",
2926
3675
  RETIREMENT_SWITCH_IN_TRIAL: "Der Wechsel ist m\xF6glich, sobald Ihre Testphase endet.",
2927
3676
  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 +3713,15 @@ var ERROR_MESSAGES_DE = {
2964
3713
  PROMO_MAX_REDEMPTIONS_LOWERED: "maxRedemptions l\xE4sst sich nicht senken.",
2965
3714
  // ── contract ──
2966
3715
  SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED: "Der Vertrag nennt in {field} {stated} %, der Steueradapter entscheidet {decided} %.",
3716
+ SUBSCRIPTION_INVOICE_CHARGE_INVOICED: "Die Forderung '{chargeId}' steht bereits auf einer Rechnung.",
3717
+ SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE: "Der Rechnung fehlt, was der Steueradapter {adapter} verlangt: {missing}. Sie wird ausgestellt, sobald es erg\xE4nzt ist.",
3718
+ 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.",
3719
+ 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.",
3720
+ 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.",
3721
+ SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH: "Das archivierte Dokument der Rechnung {invoiceId} passt nicht zur beim Rendern festgehaltenen Pr\xFCfsumme und wird nicht herausgegeben.",
3722
+ SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED: "Das Dokument der Rechnung {invoiceId} ist noch nicht archiviert.",
3723
+ SUBSCRIPTION_INVOICE_NOT_FOUND: "Keine Rechnung '{invoiceId}'.",
3724
+ SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL: "Der Abonnent {subscriberId} hat keine Rechnungs-E-Mail; seine Rechnung wird versendet, sobald eine eingetragen ist.",
2967
3725
  CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "Aus einem Checkout-Angebot entsteht nur ein einziger Vertrag, und das erst, wenn seine Positionen eingefroren sind.",
2968
3726
  CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "Ein Checkout-Angebot braucht eine eingefrorene Plan-Position.",
2969
3727
  CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Jede gew\xE4hlte Bundle-Version braucht eine eingefrorene Bundle-Position.",
@@ -3062,7 +3820,13 @@ export {
3062
3820
  ERROR_MESSAGES_DE,
3063
3821
  ERROR_MESSAGES_EN,
3064
3822
  FEATURE_NOT_LICENSED,
3823
+ FEATURE_WITHDRAWAL_CAPABILITY,
3824
+ FEATURE_WITHDRAWAL_REASON_MAX_LENGTH,
3065
3825
  ForeignPaymentMethodReferenceError,
3826
+ INVOICE_ARCHIVE_TRIGGERS,
3827
+ INVOICE_DOCUMENT_CAPABILITY,
3828
+ INVOICE_ERROR_CODES,
3829
+ INVOICE_SEQUENCE_DIGITS,
3066
3830
  LEGAL_IDENTITY_FIELDS,
3067
3831
  MAINTENANCE_CAPABILITY,
3068
3832
  MAINTENANCE_ERROR_CODES,
@@ -3097,6 +3861,8 @@ export {
3097
3861
  SUBSCRIBER_ACCOUNT_CAPABILITY,
3098
3862
  SUBSCRIBER_ATTENTION_CAPABILITY,
3099
3863
  SUBSCRIBER_ATTENTION_PAGE_SIZE,
3864
+ SUBSCRIBER_CHARGE_ORIGINS,
3865
+ SUBSCRIBER_CHARGE_SOURCES,
3100
3866
  SUBSCRIBER_CORRECTION_CAPABILITY,
3101
3867
  SUBSCRIBER_ERROR_CODES,
3102
3868
  SUBSCRIBER_INVOICE_ADDRESS_FIELDS,
@@ -3105,7 +3871,9 @@ export {
3105
3871
  TAX_ERROR_CODES,
3106
3872
  TAX_TREATMENT_KINDS,
3107
3873
  VERSION_RETIREMENT_CAPABILITY,
3874
+ addDaysToDay,
3108
3875
  applyPromo,
3876
+ asXmlReadsIt,
3109
3877
  assertPersistenceCapabilities,
3110
3878
  buildActivePlanVersionWhere,
3111
3879
  buildActiveVersionWhere,
@@ -3129,14 +3897,28 @@ export {
3129
3897
  contractPartiesOf,
3130
3898
  coverageExcludingSelf,
3131
3899
  customLimitsReader,
3900
+ dayInZone,
3132
3901
  definedFields,
3133
3902
  diffSettings,
3903
+ draftSubscriptionInvoice,
3904
+ endedAtOnceSubjectOf,
3905
+ featureWithdrawalRowOf,
3906
+ featuresWithdrawnAt,
3134
3907
  formatCustomerNumber,
3135
3908
  formatErrorMessage,
3909
+ formatInvoiceNumber,
3136
3910
  grossFromNet,
3911
+ holdsOnlyXmlCharacters,
3137
3912
  identityCorrectionDelta,
3913
+ invoiceArchiveProtectionProblem,
3914
+ invoiceContentDraftOf,
3915
+ invoiceFooterOf,
3916
+ invoiceGroupsOf,
3917
+ invoiceIssuerOf,
3138
3918
  isBundleRedundant,
3919
+ isFeatureWithdrawnAt,
3139
3920
  isForeignPaymentMethodReferenceError,
3921
+ isInvoiceDocumentChecksum,
3140
3922
  isMaintenanceAnnouncementLapsed,
3141
3923
  isMaintenanceOverrun,
3142
3924
  isPaymentCallbackRejectedError,
@@ -3147,6 +3929,7 @@ export {
3147
3929
  issuerIdentityOf,
3148
3930
  keepOneRecommended,
3149
3931
  keepsVatIdCheck,
3932
+ lastDayInZone,
3150
3933
  maintenanceRetryAfterSeconds,
3151
3934
  maintenanceStatusOf,
3152
3935
  maintenanceWindowStatusOf,
@@ -3156,6 +3939,7 @@ export {
3156
3939
  missingRequiresFor,
3157
3940
  movedIdentityFields,
3158
3941
  netFromGross,
3942
+ nextWithdrawalChange,
3159
3943
  noActivePlanVersion,
3160
3944
  parseZonedInstant,
3161
3945
  percentOf,
@@ -3192,15 +3976,22 @@ export {
3192
3976
  subscriptionBundleGone,
3193
3977
  subscriptionContractGone,
3194
3978
  subscriptionGone,
3979
+ subscriptionInvoiceChargeInvoiced,
3980
+ subscriptionInvoiceColumns,
3981
+ subscriptionInvoiceDocumentChanged,
3982
+ subscriptionInvoiceLineColumns,
3195
3983
  sumToCents,
3196
3984
  taxOriginOf,
3197
3985
  taxOriginWrite,
3986
+ taxPerRate,
3198
3987
  taxTreatmentToJson,
3988
+ toArchivedInvoiceDocument,
3199
3989
  toBundleStemRow,
3200
3990
  toBundleVersionRetirementRecord,
3201
3991
  toCents,
3202
3992
  toContractLineItemRecord,
3203
3993
  toDecimalString,
3994
+ toFeatureWithdrawalRecord,
3204
3995
  toMaintenanceWindowRecord,
3205
3996
  toPlanRow,
3206
3997
  toPlanVersionRow,
@@ -3213,12 +4004,18 @@ export {
3213
4004
  toSubscriberVatIdCheckRecord,
3214
4005
  toSubscriptionBundleRecord,
3215
4006
  toSubscriptionContractRecord,
4007
+ toSubscriptionInvoiceDocumentRecord,
4008
+ toSubscriptionInvoiceRecord,
3216
4009
  toSubscriptionNoticeRecord,
3217
4010
  toTaxOriginValues,
3218
4011
  toTaxTreatment,
3219
4012
  toVersionRetirementRecord,
4013
+ utcMidnightOf,
4014
+ vatCategoryOf,
3220
4015
  vatIdCheckFromStore,
3221
4016
  versionOnSale,
3222
4017
  versionOnSaleOrNext,
3223
- versionSale
4018
+ versionSale,
4019
+ withdrawnFeaturesOf,
4020
+ yearOfDay
3224
4021
  };