@saasicat/core 1.0.0-rc.26 → 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
@@ -426,7 +426,10 @@ var KINDS = {
426
426
  "bundle-version-offered": true,
427
427
  "feature-withdrawn": true,
428
428
  "feature-withdrawal-lifted": true,
429
- "ended-at-once": true
429
+ "ended-at-once": true,
430
+ "collection-failed": true,
431
+ "collection-announced": true,
432
+ "collection-reversed": true
430
433
  };
431
434
  function toSubscriptionNoticeRecord(row) {
432
435
  return {
@@ -1530,6 +1533,24 @@ var INVOICE_ERROR_CODES = {
1530
1533
  * (`SC-AUD-012`).
1531
1534
  */
1532
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",
1533
1554
  /**
1534
1555
  * A file of the invoice's document has another checksum than the one
1535
1556
  * recorded or archived for it (`invoiceId`, `recorded`, `rendered`), and
@@ -1559,7 +1580,26 @@ var INVOICE_ERROR_CODES = {
1559
1580
  * archived invoice cannot be sent (`SC-PRIC-047`); it is sent once one is
1560
1581
  * entered.
1561
1582
  */
1562
- SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL: "SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL"
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"
1563
1603
  };
1564
1604
  var SUBSCRIBER_ERROR_CODES = {
1565
1605
  /**
@@ -1802,6 +1842,38 @@ var OPTIONAL_CANONICAL_MODELS = {
1802
1842
  SubscriptionInvoiceNumber: ["entitlement.subscriptionInvoiceRepository"],
1803
1843
  SubscriptionInvoiceDocument: ["entitlement.subscriptionInvoiceRepository"],
1804
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
+ ],
1805
1877
  PromoCodeHold: ["promo.holdRepository"]
1806
1878
  };
1807
1879
  function membersLeftOut(notAdopted = []) {
@@ -2046,12 +2118,20 @@ var ERROR_MESSAGES_EN = {
2046
2118
  SUBSCRIPTION_INVOICE_CHARGE_INVOICED: "Charge '{chargeId}' already stands on an invoice.",
2047
2119
  SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE: "The invoice lacks what the tax adapter {adapter} requires of it: {missing}. It is issued once that is supplied.",
2048
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.",
2049
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.",
2050
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.",
2051
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.",
2052
2127
  SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED: "The document of invoice {invoiceId} is not archived yet.",
2053
2128
  SUBSCRIPTION_INVOICE_NOT_FOUND: "No invoice '{invoiceId}'.",
2054
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.",
2055
2135
  CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "A checkout offer can yield only one contract, and only once its line items are frozen.",
2056
2136
  CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "A checkout offer requires a frozen plan line item.",
2057
2137
  CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Every selected bundle version requires a frozen bundle line item.",
@@ -2137,11 +2217,21 @@ var ERROR_MESSAGES_EN = {
2137
2217
  TAX_VAT_ID_CHECK_NOT_COMPLETED: "The VAT identification number could not be checked just now ({reason}). Please try again later.",
2138
2218
  TAX_VAT_ID_CHECK_NOT_AVAILABLE: "This installation names no tax adapter, so no VAT identification number is checked."
2139
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
+ }
2140
2230
  function formatErrorMessage(template, params = {}) {
2141
- return template.replace(/\{(\w+)\}/g, (match, name) => {
2142
- const value = params[name];
2143
- return value === void 0 || value === null ? match : String(value);
2144
- });
2231
+ return template.replace(
2232
+ /\{(\w+)\}/g,
2233
+ (match, name) => parameterText(params[name]) ?? match
2234
+ );
2145
2235
  }
2146
2236
  function resolveErrorMessage(body, overrides = {}, defaults = ERROR_MESSAGES_EN) {
2147
2237
  const code = typeof body.code === "string" ? body.code : void 0;
@@ -2319,6 +2409,13 @@ function selectChargeableBundles(planFeatures, selectedBundles) {
2319
2409
  }
2320
2410
 
2321
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
+ }
2322
2419
  var PAYMENT_CALLBACK_REJECTED = "PAYMENT_CALLBACK_REJECTED";
2323
2420
  var PaymentCallbackRejectedError = class extends Error {
2324
2421
  code = PAYMENT_CALLBACK_REJECTED;
@@ -2565,6 +2662,87 @@ function formatInvoiceNumber(prefix, year, sequence) {
2565
2662
  return `${prefix}-${year}-${String(sequence).padStart(INVOICE_SEQUENCE_DIGITS, "0")}`;
2566
2663
  }
2567
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
+
2568
2746
  // src/subscriber.types.ts
2569
2747
  var SUBSCRIBER_INVOICE_ADDRESS_FIELDS = [
2570
2748
  "addressLine1",
@@ -2706,6 +2884,7 @@ function toSubscriptionInvoiceRecord(row, lines) {
2706
2884
  if (!treatment) {
2707
2885
  throw new Error(`${INVOICES}.taxTreatment of ${row.id} is not a tax treatment.`);
2708
2886
  }
2887
+ const dueDate = dayOf2(row.dueDate, where("dueDate"));
2709
2888
  return {
2710
2889
  id: row.id,
2711
2890
  number: row.number,
@@ -2718,9 +2897,10 @@ function toSubscriptionInvoiceRecord(row, lines) {
2718
2897
  contractId: row.contractId,
2719
2898
  issuedAt: row.issuedAt,
2720
2899
  issueDate: dayOf2(row.issueDate, where("issueDate")),
2721
- dueDate: dayOf2(row.dueDate, where("dueDate")),
2900
+ dueDate,
2722
2901
  servicePeriodFrom: dayOf2(row.servicePeriodFrom, where("servicePeriodFrom")),
2723
2902
  servicePeriodUntil: dayOf2(row.servicePeriodUntil, where("servicePeriodUntil")),
2903
+ ...collectionOf(row.payment, row.collectionDate, dueDate, where),
2724
2904
  currency: row.currency,
2725
2905
  issuer: issuerOf(row.issuerParty, where("issuerParty")),
2726
2906
  subscriber: subscriberOf(row.subscriberParty, where("subscriberParty")),
@@ -2766,6 +2946,8 @@ function subscriptionInvoiceColumns(invoice, drawn) {
2766
2946
  dueDate: invoice.dueDate,
2767
2947
  servicePeriodFrom: invoice.servicePeriodFrom,
2768
2948
  servicePeriodUntil: invoice.servicePeriodUntil,
2949
+ payment: invoice.payment ? { ...invoice.payment } : null,
2950
+ collectionDate: invoice.collectionDate,
2769
2951
  currency: invoice.currency,
2770
2952
  issuerParty: { ...invoice.issuer },
2771
2953
  subscriberParty: { ...invoice.subscriber },
@@ -2795,15 +2977,58 @@ function subscriptionInvoiceLineColumns(line, invoiceId) {
2795
2977
  function utcMidnightOf(day) {
2796
2978
  return /* @__PURE__ */ new Date(`${day}T00:00:00.000Z`);
2797
2979
  }
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);
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
+ );
2802
2986
  }
2803
- if (typeof value === "string" && value.length === DAY_LENGTH && !Number.isNaN(Date.parse(value))) {
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
+ }
2804
3003
  return value;
3004
+ };
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
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 };
2805
3024
  }
2806
- throw new Error(`${where.table}.${where.column} of ${where.id} is not a day.`);
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 };
2807
3032
  }
2808
3033
  function footerOf(value, where) {
2809
3034
  if (!Array.isArray(value) || !value.every((line) => typeof line === "string")) {
@@ -2827,8 +3052,12 @@ function centsText(amount, field) {
2827
3052
  }
2828
3053
  return amount.toFixed(2);
2829
3054
  }
3055
+ function fieldOf(source, key) {
3056
+ const field = Object.getOwnPropertyDescriptor(source, key)?.value;
3057
+ return field;
3058
+ }
2830
3059
  function textOrNull2(source, key, where) {
2831
- const value = source[key];
3060
+ const value = fieldOf(source, key);
2832
3061
  if (value === null || value === void 0) return null;
2833
3062
  if (typeof value !== "string") {
2834
3063
  throw new Error(`${where.table}.${where.column}.${key} of ${where.id} is not text.`);
@@ -2916,11 +3145,34 @@ function invoiceGroupsOf(charges) {
2916
3145
  (a, b) => a.bookedAt.getTime() - b.bookedAt.getTime() || a.contractId.localeCompare(b.contractId)
2917
3146
  );
2918
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
+ }
2919
3164
  function draftSubscriptionInvoice(input) {
2920
3165
  const { group, treatment, timeZone } = input;
2921
3166
  const lines = linesOf(input);
2922
3167
  const issueDate = dayInZone(input.issuedAt, timeZone);
2923
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;
2924
3176
  return {
2925
3177
  numberPrefix: input.numberPrefix,
2926
3178
  numberYear: yearOfDay(issueDate),
@@ -2930,7 +3182,9 @@ function draftSubscriptionInvoice(input) {
2930
3182
  contractId: group.contractId,
2931
3183
  issuedAt: input.issuedAt,
2932
3184
  issueDate,
2933
- dueDate: addDaysToDay(issueDate, input.paymentTermDays),
3185
+ dueDate: collectionDate ?? addDaysToDay(issueDate, input.paymentTermDays),
3186
+ payment: payment ? { ...payment } : null,
3187
+ collectionDate,
2934
3188
  servicePeriodFrom: lines.map((line) => line.periodFrom).sort()[0],
2935
3189
  servicePeriodUntil: lines.map((line) => line.periodUntil).sort().at(-1),
2936
3190
  currency: currencyOf(group),
@@ -3089,6 +3343,802 @@ function invoiceArchiveProtectionProblem(enabledTriggers) {
3089
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.`;
3090
3344
  }
3091
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
+
3092
4142
  // src/e-invoice-format.ts
3093
4143
  var CATEGORY = {
3094
4144
  standard: "S",
@@ -3236,75 +4286,6 @@ function toIdentityValues(value) {
3236
4286
  return values;
3237
4287
  }
3238
4288
 
3239
- // src/subscriber-payment-method-mapping.ts
3240
- var PAYMENT_METHOD_TYPES = ["card", "sepa_debit"];
3241
- var STATUSES = ["ACTIVE", "REPLACED"];
3242
- var TABLE4 = "subscriber_payment_methods";
3243
- function toSubscriberPaymentMethodRecord(row) {
3244
- return {
3245
- id: row.id,
3246
- subscriberId: row.subscriberId,
3247
- gatewayAccount: row.gatewayAccount,
3248
- provider: row.provider,
3249
- customerRef: row.customerRef,
3250
- paymentMethodRef: row.paymentMethodRef,
3251
- type: oneOf(PAYMENT_METHOD_TYPES, row.type, { table: TABLE4, column: "type", id: row.id }),
3252
- brand: row.brand,
3253
- last4: row.last4,
3254
- expiryMonth: row.expiryMonth,
3255
- expiryYear: row.expiryYear,
3256
- country: row.country,
3257
- bankCode: row.bankCode,
3258
- mandateReference: row.mandateReference,
3259
- status: oneOf(STATUSES, row.status, { table: TABLE4, column: "status", id: row.id }),
3260
- confirmedAt: row.confirmedAt,
3261
- replacedAt: row.replacedAt,
3262
- createdAt: row.createdAt
3263
- };
3264
- }
3265
- function subscriberPaymentMethodColumns(data) {
3266
- return {
3267
- subscriberId: data.subscriberId,
3268
- gatewayAccount: data.gatewayAccount,
3269
- provider: data.provider,
3270
- customerRef: data.customerRef,
3271
- paymentMethodRef: data.paymentMethodRef,
3272
- type: data.type,
3273
- brand: data.brand,
3274
- last4: data.last4,
3275
- expiryMonth: data.expiryMonth,
3276
- expiryYear: data.expiryYear,
3277
- country: data.country,
3278
- bankCode: data.bankCode,
3279
- mandateReference: data.mandateReference,
3280
- confirmedAt: data.confirmedAt
3281
- };
3282
- }
3283
- var FOREIGN_PAYMENT_METHOD_REFERENCE = "FOREIGN_PAYMENT_METHOD_REFERENCE";
3284
- var ForeignPaymentMethodReferenceError = class extends Error {
3285
- constructor(gatewayAccount, paymentMethodRef) {
3286
- super(
3287
- `Payment method '${paymentMethodRef}' of account '${gatewayAccount}' belongs to another subscriber. A reference belongs to exactly one, so it is not handed out.`
3288
- );
3289
- this.gatewayAccount = gatewayAccount;
3290
- this.paymentMethodRef = paymentMethodRef;
3291
- this.name = "ForeignPaymentMethodReferenceError";
3292
- }
3293
- gatewayAccount;
3294
- paymentMethodRef;
3295
- code = FOREIGN_PAYMENT_METHOD_REFERENCE;
3296
- };
3297
- function isForeignPaymentMethodReferenceError(error) {
3298
- return error instanceof Error && error.code === FOREIGN_PAYMENT_METHOD_REFERENCE;
3299
- }
3300
- function refuseForeignPaymentMethodReference(recorded, subscriberId) {
3301
- if (recorded.subscriberId === subscriberId) return;
3302
- throw new ForeignPaymentMethodReferenceError(
3303
- recorded.gatewayAccount,
3304
- recorded.paymentMethodRef
3305
- );
3306
- }
3307
-
3308
4289
  // src/subscription-bundle-mapping.ts
3309
4290
  var BILLING_CYCLES = ["MONTHLY", "YEARLY"];
3310
4291
  function toSubscriptionBundleRecord(row) {
@@ -3716,12 +4697,20 @@ var ERROR_MESSAGES_DE = {
3716
4697
  SUBSCRIPTION_INVOICE_CHARGE_INVOICED: "Die Forderung '{chargeId}' steht bereits auf einer Rechnung.",
3717
4698
  SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE: "Der Rechnung fehlt, was der Steueradapter {adapter} verlangt: {missing}. Sie wird ausgestellt, sobald es erg\xE4nzt ist.",
3718
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.",
3719
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.",
3720
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.",
3721
4705
  SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH: "Das archivierte Dokument der Rechnung {invoiceId} passt nicht zur beim Rendern festgehaltenen Pr\xFCfsumme und wird nicht herausgegeben.",
3722
4706
  SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED: "Das Dokument der Rechnung {invoiceId} ist noch nicht archiviert.",
3723
4707
  SUBSCRIPTION_INVOICE_NOT_FOUND: "Keine Rechnung '{invoiceId}'.",
3724
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.",
3725
4714
  CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "Aus einem Checkout-Angebot entsteht nur ein einziger Vertrag, und das erst, wenn seine Positionen eingefroren sind.",
3726
4715
  CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "Ein Checkout-Angebot braucht eine eingefrorene Plan-Position.",
3727
4716
  CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Jede gew\xE4hlte Bundle-Version braucht eine eingefrorene Bundle-Position.",
@@ -3811,22 +4800,37 @@ export {
3811
4800
  ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES,
3812
4801
  AUTH_ERROR_CODES,
3813
4802
  BILLING_ERROR_CODES,
4803
+ BLOCKING_COLLECTION_STATUSES,
3814
4804
  BUNDLE_PRICE_LOOKUP_LIMIT,
3815
4805
  BUNDLE_VERSION_RETIREMENT_CAPABILITY,
4806
+ CASE_CLOSED_REFUND_ENDED,
4807
+ CASE_CLOSED_REVERSAL_GIVEN_BACK,
3816
4808
  CATALOGUE_KEYS,
3817
4809
  CATALOG_ERROR_CODES,
4810
+ COLLECTION_FAILURE_REASONS,
4811
+ COLLECTION_NOT_SENT_PAID,
4812
+ COLLECTION_NOT_SENT_REVERSED,
4813
+ COLLECTION_RETRY_AFTER_DAYS,
3818
4814
  CONTRACT_ERROR_CODES,
3819
4815
  CUSTOM_LIMITS_KEYS,
4816
+ DIRECT_DEBIT_LEAD_DAYS,
4817
+ ENDED_REFUND_STATUSES,
3820
4818
  ERROR_MESSAGES_DE,
3821
4819
  ERROR_MESSAGES_EN,
3822
4820
  FEATURE_NOT_LICENSED,
3823
4821
  FEATURE_WITHDRAWAL_CAPABILITY,
3824
4822
  FEATURE_WITHDRAWAL_REASON_MAX_LENGTH,
4823
+ FOLLOWED_UP_COLLECTION_STATUSES,
3825
4824
  ForeignPaymentMethodReferenceError,
4825
+ GIVING_BACK_CREDIT_ENTRY_KINDS,
3826
4826
  INVOICE_ARCHIVE_TRIGGERS,
4827
+ INVOICE_COLLECTION_ASK_EVERY_MS,
4828
+ INVOICE_COLLECTION_LEASE_MS,
4829
+ INVOICE_COLLECTION_STATUSES,
3827
4830
  INVOICE_DOCUMENT_CAPABILITY,
3828
4831
  INVOICE_ERROR_CODES,
3829
4832
  INVOICE_SEQUENCE_DIGITS,
4833
+ INVOICE_SETTLEMENT_KINDS,
3830
4834
  LEGAL_IDENTITY_FIELDS,
3831
4835
  MAINTENANCE_CAPABILITY,
3832
4836
  MAINTENANCE_ERROR_CODES,
@@ -3852,10 +4856,16 @@ export {
3852
4856
  PersistenceCapabilityError,
3853
4857
  PersistenceRefusal,
3854
4858
  PlatformUserExistsError,
4859
+ RECONCILIATION_CASE_KINDS,
4860
+ RECONCILIATION_CLOSE_CAPABILITY,
4861
+ RECONCILIATION_NOTE_MAX_LENGTH,
4862
+ RECONCILIATION_READ_CAPABILITY,
3855
4863
  REGISTRATION_ERROR_CODES,
3856
4864
  REGISTRATION_RESUME_TTL_MINUTES,
3857
4865
  REGISTRATION_STEP_BY_STATUS,
4866
+ REOPENING_SETTLEMENT_KINDS,
3858
4867
  RETIREMENT_REMINDER_LEAD_DAYS,
4868
+ REVERSIBLE_COLLECTION_STATUSES,
3859
4869
  SETTINGS_ERROR_CODES,
3860
4870
  SETUP_ERROR_CODES,
3861
4871
  SUBSCRIBER_ACCOUNT_CAPABILITY,
@@ -3864,14 +4874,21 @@ export {
3864
4874
  SUBSCRIBER_CHARGE_ORIGINS,
3865
4875
  SUBSCRIBER_CHARGE_SOURCES,
3866
4876
  SUBSCRIBER_CORRECTION_CAPABILITY,
4877
+ SUBSCRIBER_CREDIT_CAPABILITY,
4878
+ SUBSCRIBER_CREDIT_ENTRY_KINDS,
3867
4879
  SUBSCRIBER_ERROR_CODES,
3868
4880
  SUBSCRIBER_INVOICE_ADDRESS_FIELDS,
4881
+ SUBSCRIBER_PAYMENTS_CAPABILITY,
4882
+ SUBSCRIBER_PAYOUT_CAPABILITY,
4883
+ SUBSCRIBER_REFUND_STATUSES,
3869
4884
  SUBSCRIBER_STANDING_CAPABILITY,
3870
4885
  SUBSCRIBER_TAX_ORIGIN_FIELDS,
3871
4886
  TAX_ERROR_CODES,
3872
4887
  TAX_TREATMENT_KINDS,
4888
+ UNSETTLED_COLLECTION_STATUSES,
3873
4889
  VERSION_RETIREMENT_CAPABILITY,
3874
4890
  addDaysToDay,
4891
+ announcedCollectionDay,
3875
4892
  applyPromo,
3876
4893
  asXmlReadsIt,
3877
4894
  assertPersistenceCapabilities,
@@ -3883,23 +4900,32 @@ export {
3883
4900
  bundleStemDefaults,
3884
4901
  canonicalJson,
3885
4902
  canonicalVatId,
4903
+ casesCovered,
3886
4904
  catalogDraftExists,
3887
4905
  catalogVersionAlreadyPublished,
3888
4906
  catalogVersionGone,
4907
+ chargedAgainOn,
3889
4908
  classifyBundleVersionDiff,
3890
4909
  classifyIssuerChange,
3891
4910
  classifyPlanDiff,
3892
4911
  classifyVersionOffer,
3893
4912
  collectUnsatisfiedRequires,
4913
+ collectionAttemptKey,
4914
+ collectionOutcomeColumns,
4915
+ collectionStatusOf,
4916
+ columnAmountOf,
3894
4917
  computeIncludedVat,
3895
4918
  concurrencyGate,
3896
4919
  concurrencyGateOf,
3897
4920
  contractPartiesOf,
3898
4921
  coverageExcludingSelf,
4922
+ coversLifted,
4923
+ coversOf,
3899
4924
  customLimitsReader,
3900
4925
  dayInZone,
3901
4926
  definedFields,
3902
4927
  diffSettings,
4928
+ directDebitLeadDaysOf,
3903
4929
  draftSubscriptionInvoice,
3904
4930
  endedAtOnceSubjectOf,
3905
4931
  featureWithdrawalRowOf,
@@ -3910,12 +4936,17 @@ export {
3910
4936
  grossFromNet,
3911
4937
  holdsOnlyXmlCharacters,
3912
4938
  identityCorrectionDelta,
4939
+ inWithdrawalOrder,
3913
4940
  invoiceArchiveProtectionProblem,
4941
+ invoiceCollectionColumns,
3914
4942
  invoiceContentDraftOf,
3915
4943
  invoiceFooterOf,
3916
4944
  invoiceGroupsOf,
3917
4945
  invoiceIssuerOf,
4946
+ invoicePaymentOf,
3918
4947
  isBundleRedundant,
4948
+ isChargedAgain,
4949
+ isEndedRefund,
3919
4950
  isFeatureWithdrawnAt,
3920
4951
  isForeignPaymentMethodReferenceError,
3921
4952
  isInvoiceDocumentChecksum,
@@ -3924,11 +4955,13 @@ export {
3924
4955
  isPaymentCallbackRejectedError,
3925
4956
  isPersistenceRefusal,
3926
4957
  isPlatformUserExistsError,
4958
+ isRetryableCollectionFailure,
3927
4959
  isVersionActiveAt,
3928
4960
  isVersionEditable,
3929
4961
  issuerIdentityOf,
3930
4962
  keepOneRecommended,
3931
4963
  keepsVatIdCheck,
4964
+ lastCollectionOf,
3932
4965
  lastDayInZone,
3933
4966
  maintenanceRetryAfterSeconds,
3934
4967
  maintenanceStatusOf,
@@ -3938,15 +4971,22 @@ export {
3938
4971
  membersLeftOut,
3939
4972
  missingRequiresFor,
3940
4973
  movedIdentityFields,
4974
+ netByInvoice,
3941
4975
  netFromGross,
3942
4976
  nextWithdrawalChange,
3943
4977
  noActivePlanVersion,
4978
+ openOfCredit,
4979
+ owedOn,
4980
+ paidOutByCredit,
3944
4981
  parseZonedInstant,
4982
+ paymentSettlement,
4983
+ payoutEntryColumns,
3945
4984
  percentOf,
3946
4985
  pickActivePromo,
3947
4986
  planCatalogSettingsOf,
3948
4987
  planKeyTaken,
3949
4988
  planNotInCatalog,
4989
+ plannedCollectionColumns,
3950
4990
  previousUtcDay,
3951
4991
  promoCodeTaken,
3952
4992
  promoStatus,
@@ -3955,23 +4995,35 @@ export {
3955
4995
  readCustomLimits,
3956
4996
  readQuotaRecord,
3957
4997
  readQuotaValue,
4998
+ reconciliationCaseColumns,
3958
4999
  recordedIssuerIdentity,
5000
+ refundEntryColumns,
5001
+ refundParts,
3959
5002
  refuseForeignPaymentMethodReference,
5003
+ refusesFirstAttempt,
5004
+ refusesPlannedAttempt,
5005
+ reinstatementParts,
3960
5006
  resolveBundleAvailability,
3961
5007
  resolveErrorMessage,
3962
5008
  retirementCostsTheSubscription,
3963
5009
  retirementReminderDueAt,
3964
5010
  retirementReminderIsDue,
3965
5011
  retirementSwitchTerms,
5012
+ reversalEntryColumns,
3966
5013
  roundToCents,
3967
5014
  sameLegalIdentity,
3968
5015
  scheduledChangeAfterWrite,
3969
5016
  selectChargeableBundles,
5017
+ sentCollectionOf,
3970
5018
  settingsSubtreeOf,
5019
+ settledAmountOf,
3971
5020
  startOfUtcDay,
3972
5021
  subscriberChargeColumns,
3973
5022
  subscriberFromRegistration,
5023
+ subscriberPaymentColumns,
3974
5024
  subscriberPaymentMethodColumns,
5025
+ subscriberRefundColumns,
5026
+ subscriberReversalColumns,
3975
5027
  subscriptionBundleAlreadyCancelled,
3976
5028
  subscriptionBundleGone,
3977
5029
  subscriptionContractGone,
@@ -3989,17 +5041,27 @@ export {
3989
5041
  toBundleStemRow,
3990
5042
  toBundleVersionRetirementRecord,
3991
5043
  toCents,
5044
+ toCollectionAnnouncedNotice,
5045
+ toCollectionFailedNotice,
5046
+ toCollectionReversedNotice,
3992
5047
  toContractLineItemRecord,
3993
5048
  toDecimalString,
3994
5049
  toFeatureWithdrawalRecord,
5050
+ toInvoiceCollectionRecord,
3995
5051
  toMaintenanceWindowRecord,
3996
5052
  toPlanRow,
3997
5053
  toPlanVersionRow,
3998
5054
  toRunningContractIssuer,
3999
5055
  toSubscriberChargeRecord,
4000
5056
  toSubscriberCorrectionRecord,
5057
+ toSubscriberCreditEntryRecord,
5058
+ toSubscriberCreditRecord,
4001
5059
  toSubscriberPaymentMethodRecord,
5060
+ toSubscriberPaymentRecord,
5061
+ toSubscriberReconciliationCaseRecord,
4002
5062
  toSubscriberRecord,
5063
+ toSubscriberRefundRecord,
5064
+ toSubscriberReversalRecord,
4003
5065
  toSubscriberTaxOriginChangeRecord,
4004
5066
  toSubscriberVatIdCheckRecord,
4005
5067
  toSubscriptionBundleRecord,
@@ -4016,6 +5078,7 @@ export {
4016
5078
  versionOnSale,
4017
5079
  versionOnSaleOrNext,
4018
5080
  versionSale,
5081
+ withdrawalParts,
4019
5082
  withdrawnFeaturesOf,
4020
5083
  yearOfDay
4021
5084
  };