@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/.build-stamp +1 -1
- package/dist/index.cjs +1221 -84
- package/dist/index.d.cts +1994 -14
- package/dist/index.d.ts +1994 -14
- package/dist/index.js +1147 -84
- package/package.json +1 -1
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(
|
|
2142
|
-
|
|
2143
|
-
|
|
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
|
|
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
|
-
|
|
2799
|
-
|
|
2800
|
-
if (
|
|
2801
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
};
|