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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -23,16 +23,37 @@ __export(index_exports, {
23
23
  ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES: () => ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES,
24
24
  AUTH_ERROR_CODES: () => AUTH_ERROR_CODES,
25
25
  BILLING_ERROR_CODES: () => BILLING_ERROR_CODES,
26
+ BLOCKING_COLLECTION_STATUSES: () => BLOCKING_COLLECTION_STATUSES,
26
27
  BUNDLE_PRICE_LOOKUP_LIMIT: () => BUNDLE_PRICE_LOOKUP_LIMIT,
27
28
  BUNDLE_VERSION_RETIREMENT_CAPABILITY: () => BUNDLE_VERSION_RETIREMENT_CAPABILITY,
29
+ CASE_CLOSED_REFUND_ENDED: () => CASE_CLOSED_REFUND_ENDED,
30
+ CASE_CLOSED_REVERSAL_GIVEN_BACK: () => CASE_CLOSED_REVERSAL_GIVEN_BACK,
28
31
  CATALOGUE_KEYS: () => CATALOGUE_KEYS,
29
32
  CATALOG_ERROR_CODES: () => CATALOG_ERROR_CODES,
33
+ COLLECTION_FAILURE_REASONS: () => COLLECTION_FAILURE_REASONS,
34
+ COLLECTION_NOT_SENT_PAID: () => COLLECTION_NOT_SENT_PAID,
35
+ COLLECTION_NOT_SENT_REVERSED: () => COLLECTION_NOT_SENT_REVERSED,
36
+ COLLECTION_RETRY_AFTER_DAYS: () => COLLECTION_RETRY_AFTER_DAYS,
30
37
  CONTRACT_ERROR_CODES: () => CONTRACT_ERROR_CODES,
31
38
  CUSTOM_LIMITS_KEYS: () => CUSTOM_LIMITS_KEYS,
39
+ DIRECT_DEBIT_LEAD_DAYS: () => DIRECT_DEBIT_LEAD_DAYS,
40
+ ENDED_REFUND_STATUSES: () => ENDED_REFUND_STATUSES,
32
41
  ERROR_MESSAGES_DE: () => ERROR_MESSAGES_DE,
33
42
  ERROR_MESSAGES_EN: () => ERROR_MESSAGES_EN,
34
43
  FEATURE_NOT_LICENSED: () => FEATURE_NOT_LICENSED,
44
+ FEATURE_WITHDRAWAL_CAPABILITY: () => FEATURE_WITHDRAWAL_CAPABILITY,
45
+ FEATURE_WITHDRAWAL_REASON_MAX_LENGTH: () => FEATURE_WITHDRAWAL_REASON_MAX_LENGTH,
46
+ FOLLOWED_UP_COLLECTION_STATUSES: () => FOLLOWED_UP_COLLECTION_STATUSES,
35
47
  ForeignPaymentMethodReferenceError: () => ForeignPaymentMethodReferenceError,
48
+ GIVING_BACK_CREDIT_ENTRY_KINDS: () => GIVING_BACK_CREDIT_ENTRY_KINDS,
49
+ INVOICE_ARCHIVE_TRIGGERS: () => INVOICE_ARCHIVE_TRIGGERS,
50
+ INVOICE_COLLECTION_ASK_EVERY_MS: () => INVOICE_COLLECTION_ASK_EVERY_MS,
51
+ INVOICE_COLLECTION_LEASE_MS: () => INVOICE_COLLECTION_LEASE_MS,
52
+ INVOICE_COLLECTION_STATUSES: () => INVOICE_COLLECTION_STATUSES,
53
+ INVOICE_DOCUMENT_CAPABILITY: () => INVOICE_DOCUMENT_CAPABILITY,
54
+ INVOICE_ERROR_CODES: () => INVOICE_ERROR_CODES,
55
+ INVOICE_SEQUENCE_DIGITS: () => INVOICE_SEQUENCE_DIGITS,
56
+ INVOICE_SETTLEMENT_KINDS: () => INVOICE_SETTLEMENT_KINDS,
36
57
  LEGAL_IDENTITY_FIELDS: () => LEGAL_IDENTITY_FIELDS,
37
58
  MAINTENANCE_CAPABILITY: () => MAINTENANCE_CAPABILITY,
38
59
  MAINTENANCE_ERROR_CODES: () => MAINTENANCE_ERROR_CODES,
@@ -58,24 +79,41 @@ __export(index_exports, {
58
79
  PersistenceCapabilityError: () => PersistenceCapabilityError,
59
80
  PersistenceRefusal: () => PersistenceRefusal,
60
81
  PlatformUserExistsError: () => PlatformUserExistsError,
82
+ RECONCILIATION_CASE_KINDS: () => RECONCILIATION_CASE_KINDS,
83
+ RECONCILIATION_CLOSE_CAPABILITY: () => RECONCILIATION_CLOSE_CAPABILITY,
84
+ RECONCILIATION_NOTE_MAX_LENGTH: () => RECONCILIATION_NOTE_MAX_LENGTH,
85
+ RECONCILIATION_READ_CAPABILITY: () => RECONCILIATION_READ_CAPABILITY,
61
86
  REGISTRATION_ERROR_CODES: () => REGISTRATION_ERROR_CODES,
62
87
  REGISTRATION_RESUME_TTL_MINUTES: () => REGISTRATION_RESUME_TTL_MINUTES,
63
88
  REGISTRATION_STEP_BY_STATUS: () => REGISTRATION_STEP_BY_STATUS,
89
+ REOPENING_SETTLEMENT_KINDS: () => REOPENING_SETTLEMENT_KINDS,
64
90
  RETIREMENT_REMINDER_LEAD_DAYS: () => RETIREMENT_REMINDER_LEAD_DAYS,
91
+ REVERSIBLE_COLLECTION_STATUSES: () => REVERSIBLE_COLLECTION_STATUSES,
65
92
  SETTINGS_ERROR_CODES: () => SETTINGS_ERROR_CODES,
66
93
  SETUP_ERROR_CODES: () => SETUP_ERROR_CODES,
67
94
  SUBSCRIBER_ACCOUNT_CAPABILITY: () => SUBSCRIBER_ACCOUNT_CAPABILITY,
68
95
  SUBSCRIBER_ATTENTION_CAPABILITY: () => SUBSCRIBER_ATTENTION_CAPABILITY,
69
96
  SUBSCRIBER_ATTENTION_PAGE_SIZE: () => SUBSCRIBER_ATTENTION_PAGE_SIZE,
97
+ SUBSCRIBER_CHARGE_ORIGINS: () => SUBSCRIBER_CHARGE_ORIGINS,
98
+ SUBSCRIBER_CHARGE_SOURCES: () => SUBSCRIBER_CHARGE_SOURCES,
70
99
  SUBSCRIBER_CORRECTION_CAPABILITY: () => SUBSCRIBER_CORRECTION_CAPABILITY,
100
+ SUBSCRIBER_CREDIT_CAPABILITY: () => SUBSCRIBER_CREDIT_CAPABILITY,
101
+ SUBSCRIBER_CREDIT_ENTRY_KINDS: () => SUBSCRIBER_CREDIT_ENTRY_KINDS,
71
102
  SUBSCRIBER_ERROR_CODES: () => SUBSCRIBER_ERROR_CODES,
72
103
  SUBSCRIBER_INVOICE_ADDRESS_FIELDS: () => SUBSCRIBER_INVOICE_ADDRESS_FIELDS,
104
+ SUBSCRIBER_PAYMENTS_CAPABILITY: () => SUBSCRIBER_PAYMENTS_CAPABILITY,
105
+ SUBSCRIBER_PAYOUT_CAPABILITY: () => SUBSCRIBER_PAYOUT_CAPABILITY,
106
+ SUBSCRIBER_REFUND_STATUSES: () => SUBSCRIBER_REFUND_STATUSES,
73
107
  SUBSCRIBER_STANDING_CAPABILITY: () => SUBSCRIBER_STANDING_CAPABILITY,
74
108
  SUBSCRIBER_TAX_ORIGIN_FIELDS: () => SUBSCRIBER_TAX_ORIGIN_FIELDS,
75
109
  TAX_ERROR_CODES: () => TAX_ERROR_CODES,
76
110
  TAX_TREATMENT_KINDS: () => TAX_TREATMENT_KINDS,
111
+ UNSETTLED_COLLECTION_STATUSES: () => UNSETTLED_COLLECTION_STATUSES,
77
112
  VERSION_RETIREMENT_CAPABILITY: () => VERSION_RETIREMENT_CAPABILITY,
113
+ addDaysToDay: () => addDaysToDay,
114
+ announcedCollectionDay: () => announcedCollectionDay,
78
115
  applyPromo: () => applyPromo,
116
+ asXmlReadsIt: () => asXmlReadsIt,
79
117
  assertPersistenceCapabilities: () => assertPersistenceCapabilities,
80
118
  buildActivePlanVersionWhere: () => buildActivePlanVersionWhere,
81
119
  buildActiveVersionWhere: () => buildActiveVersionWhere,
@@ -85,38 +123,69 @@ __export(index_exports, {
85
123
  bundleStemDefaults: () => bundleStemDefaults,
86
124
  canonicalJson: () => canonicalJson,
87
125
  canonicalVatId: () => canonicalVatId,
126
+ casesCovered: () => casesCovered,
88
127
  catalogDraftExists: () => catalogDraftExists,
89
128
  catalogVersionAlreadyPublished: () => catalogVersionAlreadyPublished,
90
129
  catalogVersionGone: () => catalogVersionGone,
130
+ chargedAgainOn: () => chargedAgainOn,
91
131
  classifyBundleVersionDiff: () => classifyBundleVersionDiff,
92
132
  classifyIssuerChange: () => classifyIssuerChange,
93
133
  classifyPlanDiff: () => classifyPlanDiff,
94
134
  classifyVersionOffer: () => classifyVersionOffer,
95
135
  collectUnsatisfiedRequires: () => collectUnsatisfiedRequires,
136
+ collectionAttemptKey: () => collectionAttemptKey,
137
+ collectionOutcomeColumns: () => collectionOutcomeColumns,
138
+ collectionStatusOf: () => collectionStatusOf,
139
+ columnAmountOf: () => columnAmountOf,
96
140
  computeIncludedVat: () => computeIncludedVat,
97
141
  concurrencyGate: () => concurrencyGate,
98
142
  concurrencyGateOf: () => concurrencyGateOf,
99
143
  contractPartiesOf: () => contractPartiesOf,
100
144
  coverageExcludingSelf: () => coverageExcludingSelf,
145
+ coversLifted: () => coversLifted,
146
+ coversOf: () => coversOf,
101
147
  customLimitsReader: () => customLimitsReader,
148
+ dayInZone: () => dayInZone,
102
149
  definedFields: () => definedFields,
103
150
  diffSettings: () => diffSettings,
151
+ directDebitLeadDaysOf: () => directDebitLeadDaysOf,
152
+ draftSubscriptionInvoice: () => draftSubscriptionInvoice,
153
+ endedAtOnceSubjectOf: () => endedAtOnceSubjectOf,
154
+ featureWithdrawalRowOf: () => featureWithdrawalRowOf,
155
+ featuresWithdrawnAt: () => featuresWithdrawnAt,
104
156
  formatCustomerNumber: () => formatCustomerNumber,
105
157
  formatErrorMessage: () => formatErrorMessage,
158
+ formatInvoiceNumber: () => formatInvoiceNumber,
106
159
  grossFromNet: () => grossFromNet,
160
+ holdsOnlyXmlCharacters: () => holdsOnlyXmlCharacters,
107
161
  identityCorrectionDelta: () => identityCorrectionDelta,
162
+ inWithdrawalOrder: () => inWithdrawalOrder,
163
+ invoiceArchiveProtectionProblem: () => invoiceArchiveProtectionProblem,
164
+ invoiceCollectionColumns: () => invoiceCollectionColumns,
165
+ invoiceContentDraftOf: () => invoiceContentDraftOf,
166
+ invoiceFooterOf: () => invoiceFooterOf,
167
+ invoiceGroupsOf: () => invoiceGroupsOf,
168
+ invoiceIssuerOf: () => invoiceIssuerOf,
169
+ invoicePaymentOf: () => invoicePaymentOf,
108
170
  isBundleRedundant: () => isBundleRedundant,
171
+ isChargedAgain: () => isChargedAgain,
172
+ isEndedRefund: () => isEndedRefund,
173
+ isFeatureWithdrawnAt: () => isFeatureWithdrawnAt,
109
174
  isForeignPaymentMethodReferenceError: () => isForeignPaymentMethodReferenceError,
175
+ isInvoiceDocumentChecksum: () => isInvoiceDocumentChecksum,
110
176
  isMaintenanceAnnouncementLapsed: () => isMaintenanceAnnouncementLapsed,
111
177
  isMaintenanceOverrun: () => isMaintenanceOverrun,
112
178
  isPaymentCallbackRejectedError: () => isPaymentCallbackRejectedError,
113
179
  isPersistenceRefusal: () => isPersistenceRefusal,
114
180
  isPlatformUserExistsError: () => isPlatformUserExistsError,
181
+ isRetryableCollectionFailure: () => isRetryableCollectionFailure,
115
182
  isVersionActiveAt: () => isVersionActiveAt,
116
183
  isVersionEditable: () => isVersionEditable,
117
184
  issuerIdentityOf: () => issuerIdentityOf,
118
185
  keepOneRecommended: () => keepOneRecommended,
119
186
  keepsVatIdCheck: () => keepsVatIdCheck,
187
+ lastCollectionOf: () => lastCollectionOf,
188
+ lastDayInZone: () => lastDayInZone,
120
189
  maintenanceRetryAfterSeconds: () => maintenanceRetryAfterSeconds,
121
190
  maintenanceStatusOf: () => maintenanceStatusOf,
122
191
  maintenanceWindowStatusOf: () => maintenanceWindowStatusOf,
@@ -125,14 +194,22 @@ __export(index_exports, {
125
194
  membersLeftOut: () => membersLeftOut,
126
195
  missingRequiresFor: () => missingRequiresFor,
127
196
  movedIdentityFields: () => movedIdentityFields,
197
+ netByInvoice: () => netByInvoice,
128
198
  netFromGross: () => netFromGross,
199
+ nextWithdrawalChange: () => nextWithdrawalChange,
129
200
  noActivePlanVersion: () => noActivePlanVersion,
201
+ openOfCredit: () => openOfCredit,
202
+ owedOn: () => owedOn,
203
+ paidOutByCredit: () => paidOutByCredit,
130
204
  parseZonedInstant: () => parseZonedInstant,
205
+ paymentSettlement: () => paymentSettlement,
206
+ payoutEntryColumns: () => payoutEntryColumns,
131
207
  percentOf: () => percentOf,
132
208
  pickActivePromo: () => pickActivePromo,
133
209
  planCatalogSettingsOf: () => planCatalogSettingsOf,
134
210
  planKeyTaken: () => planKeyTaken,
135
211
  planNotInCatalog: () => planNotInCatalog,
212
+ plannedCollectionColumns: () => plannedCollectionColumns,
136
213
  previousUtcDay: () => previousUtcDay,
137
214
  promoCodeTaken: () => promoCodeTaken,
138
215
  promoStatus: () => promoStatus,
@@ -141,56 +218,92 @@ __export(index_exports, {
141
218
  readCustomLimits: () => readCustomLimits,
142
219
  readQuotaRecord: () => readQuotaRecord,
143
220
  readQuotaValue: () => readQuotaValue,
221
+ reconciliationCaseColumns: () => reconciliationCaseColumns,
144
222
  recordedIssuerIdentity: () => recordedIssuerIdentity,
223
+ refundEntryColumns: () => refundEntryColumns,
224
+ refundParts: () => refundParts,
145
225
  refuseForeignPaymentMethodReference: () => refuseForeignPaymentMethodReference,
226
+ refusesFirstAttempt: () => refusesFirstAttempt,
227
+ refusesPlannedAttempt: () => refusesPlannedAttempt,
228
+ reinstatementParts: () => reinstatementParts,
146
229
  resolveBundleAvailability: () => resolveBundleAvailability,
147
230
  resolveErrorMessage: () => resolveErrorMessage,
148
231
  retirementCostsTheSubscription: () => retirementCostsTheSubscription,
149
232
  retirementReminderDueAt: () => retirementReminderDueAt,
150
233
  retirementReminderIsDue: () => retirementReminderIsDue,
151
234
  retirementSwitchTerms: () => retirementSwitchTerms,
235
+ reversalEntryColumns: () => reversalEntryColumns,
152
236
  roundToCents: () => roundToCents,
153
237
  sameLegalIdentity: () => sameLegalIdentity,
154
238
  scheduledChangeAfterWrite: () => scheduledChangeAfterWrite,
155
239
  selectChargeableBundles: () => selectChargeableBundles,
240
+ sentCollectionOf: () => sentCollectionOf,
156
241
  settingsSubtreeOf: () => settingsSubtreeOf,
242
+ settledAmountOf: () => settledAmountOf,
157
243
  startOfUtcDay: () => startOfUtcDay,
158
244
  subscriberChargeColumns: () => subscriberChargeColumns,
159
245
  subscriberFromRegistration: () => subscriberFromRegistration,
246
+ subscriberPaymentColumns: () => subscriberPaymentColumns,
160
247
  subscriberPaymentMethodColumns: () => subscriberPaymentMethodColumns,
248
+ subscriberRefundColumns: () => subscriberRefundColumns,
249
+ subscriberReversalColumns: () => subscriberReversalColumns,
161
250
  subscriptionBundleAlreadyCancelled: () => subscriptionBundleAlreadyCancelled,
162
251
  subscriptionBundleGone: () => subscriptionBundleGone,
163
252
  subscriptionContractGone: () => subscriptionContractGone,
164
253
  subscriptionGone: () => subscriptionGone,
254
+ subscriptionInvoiceChargeInvoiced: () => subscriptionInvoiceChargeInvoiced,
255
+ subscriptionInvoiceColumns: () => subscriptionInvoiceColumns,
256
+ subscriptionInvoiceDocumentChanged: () => subscriptionInvoiceDocumentChanged,
257
+ subscriptionInvoiceLineColumns: () => subscriptionInvoiceLineColumns,
165
258
  sumToCents: () => sumToCents,
166
259
  taxOriginOf: () => taxOriginOf,
167
260
  taxOriginWrite: () => taxOriginWrite,
261
+ taxPerRate: () => taxPerRate,
168
262
  taxTreatmentToJson: () => taxTreatmentToJson,
263
+ toArchivedInvoiceDocument: () => toArchivedInvoiceDocument,
169
264
  toBundleStemRow: () => toBundleStemRow,
170
265
  toBundleVersionRetirementRecord: () => toBundleVersionRetirementRecord,
171
266
  toCents: () => toCents,
267
+ toCollectionAnnouncedNotice: () => toCollectionAnnouncedNotice,
268
+ toCollectionFailedNotice: () => toCollectionFailedNotice,
269
+ toCollectionReversedNotice: () => toCollectionReversedNotice,
172
270
  toContractLineItemRecord: () => toContractLineItemRecord,
173
271
  toDecimalString: () => toDecimalString,
272
+ toFeatureWithdrawalRecord: () => toFeatureWithdrawalRecord,
273
+ toInvoiceCollectionRecord: () => toInvoiceCollectionRecord,
174
274
  toMaintenanceWindowRecord: () => toMaintenanceWindowRecord,
175
275
  toPlanRow: () => toPlanRow,
176
276
  toPlanVersionRow: () => toPlanVersionRow,
177
277
  toRunningContractIssuer: () => toRunningContractIssuer,
178
278
  toSubscriberChargeRecord: () => toSubscriberChargeRecord,
179
279
  toSubscriberCorrectionRecord: () => toSubscriberCorrectionRecord,
280
+ toSubscriberCreditEntryRecord: () => toSubscriberCreditEntryRecord,
281
+ toSubscriberCreditRecord: () => toSubscriberCreditRecord,
180
282
  toSubscriberPaymentMethodRecord: () => toSubscriberPaymentMethodRecord,
283
+ toSubscriberPaymentRecord: () => toSubscriberPaymentRecord,
284
+ toSubscriberReconciliationCaseRecord: () => toSubscriberReconciliationCaseRecord,
181
285
  toSubscriberRecord: () => toSubscriberRecord,
286
+ toSubscriberRefundRecord: () => toSubscriberRefundRecord,
287
+ toSubscriberReversalRecord: () => toSubscriberReversalRecord,
182
288
  toSubscriberTaxOriginChangeRecord: () => toSubscriberTaxOriginChangeRecord,
183
289
  toSubscriberVatIdCheckRecord: () => toSubscriberVatIdCheckRecord,
184
290
  toSubscriptionBundleRecord: () => toSubscriptionBundleRecord,
185
291
  toSubscriptionContractRecord: () => toSubscriptionContractRecord,
292
+ toSubscriptionInvoiceDocumentRecord: () => toSubscriptionInvoiceDocumentRecord,
293
+ toSubscriptionInvoiceRecord: () => toSubscriptionInvoiceRecord,
186
294
  toSubscriptionNoticeRecord: () => toSubscriptionNoticeRecord,
187
295
  toTaxOriginValues: () => toTaxOriginValues,
188
296
  toTaxTreatment: () => toTaxTreatment,
189
297
  toVersionRetirementRecord: () => toVersionRetirementRecord,
298
+ utcMidnightOf: () => utcMidnightOf,
299
+ vatCategoryOf: () => vatCategoryOf,
190
300
  vatIdCheckFromStore: () => vatIdCheckFromStore,
191
301
  versionOnSale: () => versionOnSale,
192
302
  versionOnSaleOrNext: () => versionOnSaleOrNext,
193
- versionSale: () => versionSale
303
+ versionSale: () => versionSale,
304
+ withdrawalParts: () => withdrawalParts,
305
+ withdrawnFeaturesOf: () => withdrawnFeaturesOf,
306
+ yearOfDay: () => yearOfDay
194
307
  });
195
308
  module.exports = __toCommonJS(index_exports);
196
309
 
@@ -353,6 +466,22 @@ function computeIncludedVat(gross, vatRate) {
353
466
  const rate = exactDecimal(vatRate);
354
467
  return amountOf(cents(exactDecimal(gross), rate, sum(HUNDRED, rate)));
355
468
  }
469
+ function taxPerRate(lines) {
470
+ const nets = /* @__PURE__ */ new Map();
471
+ for (const line of lines) {
472
+ const key = String(line.rate);
473
+ const entry = nets.get(key) ?? { rate: line.rate, terms: [] };
474
+ entry.terms.push(line.net);
475
+ nets.set(key, entry);
476
+ }
477
+ const rates = [...nets.values()].sort((a, b) => a.rate - b.rate).map(({ rate, terms }) => {
478
+ const net2 = sumToCents(...terms);
479
+ return { rate, net: net2, tax: percentOf(net2, rate) };
480
+ });
481
+ const net = sumToCents(...rates.map((entry) => entry.net));
482
+ const tax = sumToCents(...rates.map((entry) => entry.tax));
483
+ return { rates, net, tax, gross: sumToCents(net, tax) };
484
+ }
356
485
 
357
486
  // src/promotion.types.ts
358
487
  function promoStatus(promo, today = /* @__PURE__ */ new Date()) {
@@ -603,7 +732,13 @@ var KINDS = {
603
732
  "version-retirement-reminder": true,
604
733
  "bundle-version-retired": true,
605
734
  "bundle-version-retirement-reminder": true,
606
- "bundle-version-offered": true
735
+ "bundle-version-offered": true,
736
+ "feature-withdrawn": true,
737
+ "feature-withdrawal-lifted": true,
738
+ "ended-at-once": true,
739
+ "collection-failed": true,
740
+ "collection-announced": true,
741
+ "collection-reversed": true
607
742
  };
608
743
  function toSubscriptionNoticeRecord(row) {
609
744
  return {
@@ -734,6 +869,106 @@ function toBundleVersionRetirementRecord(row) {
734
869
  };
735
870
  }
736
871
 
872
+ // src/feature-withdrawal.types.ts
873
+ var FEATURE_WITHDRAWAL_REASON_MAX_LENGTH = 1e3;
874
+ function isFeatureWithdrawnAt(withdrawal, at) {
875
+ return withdrawal.effectiveFrom <= at && (withdrawal.liftedFrom === null || at < withdrawal.liftedFrom);
876
+ }
877
+ function featuresWithdrawnAt(withdrawals, at) {
878
+ return new Set(
879
+ withdrawals.filter((withdrawal) => isFeatureWithdrawnAt(withdrawal, at)).map((withdrawal) => withdrawal.featureKey)
880
+ );
881
+ }
882
+ function nextWithdrawalChange(withdrawals, at) {
883
+ let next = null;
884
+ for (const { effectiveFrom, liftedFrom } of withdrawals) {
885
+ for (const moment of [effectiveFrom, liftedFrom]) {
886
+ if (moment === null || moment <= at) continue;
887
+ if (next === null || moment < next) next = moment;
888
+ }
889
+ }
890
+ return next;
891
+ }
892
+ var FEATURE_WITHDRAWAL_CAPABILITY = "features.withdraw";
893
+ function featureWithdrawalRowOf(record) {
894
+ return {
895
+ id: record.id,
896
+ featureKey: record.featureKey,
897
+ reason: record.reason,
898
+ effectiveFrom: record.effectiveFrom.toISOString(),
899
+ liftedFrom: record.liftedFrom?.toISOString() ?? null,
900
+ reductions: record.reductions,
901
+ announcedAt: record.announcedAt.toISOString(),
902
+ announcedBy: record.announcedBy,
903
+ liftedAt: record.liftedAt?.toISOString() ?? null,
904
+ liftedBy: record.liftedBy,
905
+ toldAt: record.toldAt?.toISOString() ?? null
906
+ };
907
+ }
908
+ function withdrawnFeaturesOf(withdrawals, now) {
909
+ return withdrawals.filter(
910
+ ({ effectiveFrom, liftedFrom }) => liftedFrom === null || liftedFrom > now && effectiveFrom < liftedFrom
911
+ ).sort((a, b) => a.effectiveFrom.getTime() - b.effectiveFrom.getTime()).map((withdrawal) => ({
912
+ featureKey: withdrawal.featureKey,
913
+ reason: withdrawal.reason,
914
+ effectiveFrom: withdrawal.effectiveFrom.toISOString(),
915
+ liftedFrom: withdrawal.liftedFrom?.toISOString() ?? null
916
+ }));
917
+ }
918
+ function endedAtOnceSubjectOf(subscriptionOrBookingId, at) {
919
+ return `${subscriptionOrBookingId}@${at.toISOString()}`;
920
+ }
921
+
922
+ // src/feature-withdrawal-mapping.ts
923
+ var TABLE2 = "feature_withdrawals";
924
+ var KINDS2 = { plan: true, bundle: true };
925
+ var CYCLES = { MONTHLY: true, YEARLY: true };
926
+ function toFeatureWithdrawalRecord(row) {
927
+ return {
928
+ id: row.id,
929
+ featureKey: row.featureKey,
930
+ reason: row.reason,
931
+ effectiveFrom: row.effectiveFrom,
932
+ liftedFrom: row.liftedFrom,
933
+ reductions: reductionsOf(row),
934
+ announcedAt: row.announcedAt,
935
+ announcedBy: row.announcedBy,
936
+ liftedAt: row.liftedAt,
937
+ liftedBy: row.liftedBy,
938
+ toldAt: row.toldAt
939
+ };
940
+ }
941
+ function reductionsOf(row) {
942
+ if (!Array.isArray(row.reductions)) {
943
+ throw new Error(`${TABLE2} row '${row.id}' holds reductions that are not a list.`);
944
+ }
945
+ return row.reductions.map((entry, index) => {
946
+ const column = `reductions[${index}]`;
947
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
948
+ throw new Error(`${TABLE2} row '${row.id}' holds ${column} that is not an object.`);
949
+ }
950
+ const { kind, key, billingCycle, amountNet } = entry;
951
+ if (typeof kind !== "string" || typeof key !== "string" || typeof billingCycle !== "string" || typeof amountNet !== "number" || !Number.isFinite(amountNet)) {
952
+ throw new Error(
953
+ `${TABLE2} row '${row.id}' holds ${column} without a kind, a key, a billing cycle and a finite amount.`
954
+ );
955
+ }
956
+ const source = { table: TABLE2, id: row.id };
957
+ return {
958
+ kind: oneOf(Object.keys(KINDS2), kind, {
959
+ ...source,
960
+ column: `${column}.kind`
961
+ }),
962
+ key,
963
+ billingCycle: oneOf(Object.keys(CYCLES), billingCycle, {
964
+ ...source,
965
+ column: `${column}.billingCycle`
966
+ }),
967
+ amountNet
968
+ };
969
+ });
970
+ }
971
+
737
972
  // src/scheduled-change-after-write.ts
738
973
  var NO_SCHEDULED_CHANGE = {
739
974
  pendingPlan: null,
@@ -870,6 +1105,37 @@ function parseZonedInstant(text) {
870
1105
  return Number.isNaN(moment.getTime()) ? null : moment;
871
1106
  }
872
1107
 
1108
+ // src/zoned-day.ts
1109
+ function dayInZone(at, timeZone) {
1110
+ if (typeof timeZone !== "string" || timeZone === "") {
1111
+ throw new RangeError("A day is read in a named time zone.");
1112
+ }
1113
+ const parts = new Intl.DateTimeFormat("en-US", {
1114
+ timeZone,
1115
+ year: "numeric",
1116
+ month: "2-digit",
1117
+ day: "2-digit"
1118
+ }).formatToParts(at);
1119
+ const part = (type) => parts.find((candidate) => candidate.type === type)?.value;
1120
+ return `${part("year")}-${part("month")}-${part("day")}`;
1121
+ }
1122
+ function lastDayInZone(period, timeZone) {
1123
+ if (!(period.until.getTime() > period.from.getTime())) {
1124
+ throw new RangeError("A period ends after it begins.");
1125
+ }
1126
+ return dayInZone(new Date(period.until.getTime() - 1), timeZone);
1127
+ }
1128
+ function addDaysToDay(day, days) {
1129
+ if (!Number.isInteger(days)) throw new RangeError("Days are counted in whole days.");
1130
+ const [year, month, date] = day.split("-").map(Number);
1131
+ if (!year || !month || !date) throw new RangeError(`${day} is not a day.`);
1132
+ const moved = new Date(Date.UTC(year, month - 1, date + days));
1133
+ return moved.toISOString().slice(0, 10);
1134
+ }
1135
+ function yearOfDay(day) {
1136
+ return Number(day.slice(0, 4));
1137
+ }
1138
+
873
1139
  // src/settings-subtree.ts
874
1140
  var CATALOGUE_KEYS = /* @__PURE__ */ new Set([
875
1141
  "schemaVersion",
@@ -1327,6 +1593,80 @@ var BILLING_ERROR_CODES = {
1327
1593
  * show it instead of guessing.
1328
1594
  */
1329
1595
  BUNDLE_VERSION_OFFER_CHANGED: "BUNDLE_VERSION_OFFER_CHANGED",
1596
+ // ── withdrawing a feature for an external reason ──
1597
+ /**
1598
+ * Withdrawing a feature is not offered: the installation's stores cannot
1599
+ * list everybody a withdrawal would reach.
1600
+ */
1601
+ FEATURE_WITHDRAWAL_UNAVAILABLE: "FEATURE_WITHDRAWAL_UNAVAILABLE",
1602
+ /** The catalogue knows no feature of that key, so there is nothing to withdraw. Carries `featureKey`. */
1603
+ FEATURE_WITHDRAWAL_FEATURE_UNKNOWN: "FEATURE_WITHDRAWAL_FEATURE_UNKNOWN",
1604
+ /**
1605
+ * The date names a moment before now. A feature is withdrawn from the
1606
+ * moment it is announced or from a later date: the days before were
1607
+ * delivered.
1608
+ */
1609
+ FEATURE_WITHDRAWAL_DATE_IN_PAST: "FEATURE_WITHDRAWAL_DATE_IN_PAST",
1610
+ /** A withdrawal of the feature is not lifted yet; one at a time. Carries `featureKey`. */
1611
+ FEATURE_WITHDRAWAL_OPEN: "FEATURE_WITHDRAWAL_OPEN",
1612
+ /**
1613
+ * A withdrawal of the feature is lifted only from a date after the one
1614
+ * named, so the two would overlap. Carries `featureKey` and the `date` a
1615
+ * new one may begin on.
1616
+ */
1617
+ FEATURE_WITHDRAWAL_OVERLAPS: "FEATURE_WITHDRAWAL_OVERLAPS",
1618
+ /**
1619
+ * A reduction names a plan or an add-on, in a rhythm, that no line the
1620
+ * withdrawal reaches is. Carries `kind`, `key` and `billingCycle`.
1621
+ */
1622
+ FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED: "FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED",
1623
+ /** A reduction is named twice. Carries `kind`, `key` and `billingCycle`. */
1624
+ FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE: "FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE",
1625
+ /**
1626
+ * A reduction is more than the lowest price among the lines it reduces:
1627
+ * a price is reduced to nothing at most. Carries `key`, `billingCycle` and
1628
+ * that `price`.
1629
+ */
1630
+ FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE: "FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE",
1631
+ /**
1632
+ * The subscriptions the withdrawal reaches changed since the preview the
1633
+ * operator looked at. Nothing was announced; carries the current `preview`.
1634
+ */
1635
+ FEATURE_WITHDRAWAL_PREVIEW_CHANGED: "FEATURE_WITHDRAWAL_PREVIEW_CHANGED",
1636
+ /** No withdrawal has that id. Carries `withdrawalId`. */
1637
+ FEATURE_WITHDRAWAL_NOT_FOUND: "FEATURE_WITHDRAWAL_NOT_FOUND",
1638
+ /** The withdrawal was lifted already. Carries the `date` it is lifted from. */
1639
+ FEATURE_WITHDRAWAL_ALREADY_LIFTED: "FEATURE_WITHDRAWAL_ALREADY_LIFTED",
1640
+ /**
1641
+ * Nobody the withdrawal reaches has been told of it yet, and telling them
1642
+ * failed just now: a withdrawal is lifted only once they were. Nothing was
1643
+ * lifted; the next try, or the quarter-hourly run, tells them first.
1644
+ */
1645
+ FEATURE_WITHDRAWAL_NOT_TOLD_YET: "FEATURE_WITHDRAWAL_NOT_TOLD_YET",
1646
+ /**
1647
+ * The date a withdrawal would be lifted from lies before now: what was
1648
+ * withdrawn until then was withdrawn.
1649
+ */
1650
+ FEATURE_WITHDRAWAL_LIFT_IN_PAST: "FEATURE_WITHDRAWAL_LIFT_IN_PAST",
1651
+ /**
1652
+ * The withdrawal does not take its feature away now — its date is ahead,
1653
+ * or it was lifted — so nothing ends at once because of it. Carries
1654
+ * `featureKey`.
1655
+ */
1656
+ FEATURE_WITHDRAWAL_NOT_IN_EFFECT: "FEATURE_WITHDRAWAL_NOT_IN_EFFECT",
1657
+ /**
1658
+ * The withdrawal did not reach the subscription, or the line to end: it
1659
+ * was not running at the announcement, or no longer holds the feature.
1660
+ */
1661
+ FEATURE_WITHDRAWAL_DOES_NOT_REACH: "FEATURE_WITHDRAWAL_DOES_NOT_REACH",
1662
+ /** What was to be ended at once has ended already. */
1663
+ FEATURE_WITHDRAWAL_ALREADY_ENDED: "FEATURE_WITHDRAWAL_ALREADY_ENDED",
1664
+ /**
1665
+ * A cancellation is declared for a later date, and the installation's
1666
+ * subscription write or booking store cannot bring it forward (`endNow`).
1667
+ * It ends as declared. Carries that `date`.
1668
+ */
1669
+ FEATURE_WITHDRAWAL_END_NOW_UNSUPPORTED: "FEATURE_WITHDRAWAL_END_NOW_UNSUPPORTED",
1330
1670
  // ── the preview routes' own blockers and warnings ──
1331
1671
  //
1332
1672
  // These travel inside a 200 response, in `blockers[]` and `warnings[]`, not
@@ -1484,6 +1824,92 @@ var CONTRACT_ERROR_CODES = {
1484
1824
  */
1485
1825
  SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED: "SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED"
1486
1826
  };
1827
+ var INVOICE_ERROR_CODES = {
1828
+ /**
1829
+ * A charge already stands on an invoice: a second run issuing the same
1830
+ * charges lost the race. Nothing is written and no number is drawn.
1831
+ */
1832
+ SUBSCRIPTION_INVOICE_CHARGE_INVOICED: "SUBSCRIPTION_INVOICE_CHARGE_INVOICED",
1833
+ /**
1834
+ * The invoice lacks content the tax adapter requires of it — `missing`
1835
+ * names the fields, `adapter` the adapter. It draws no number and waits
1836
+ * until what is missing is supplied (`SC-PRIC-027`).
1837
+ */
1838
+ SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE: "SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE",
1839
+ /**
1840
+ * The contract's parties were copied by a migration and the operator has
1841
+ * not confirmed them yet; its charges are not invoiced until then
1842
+ * (`SC-AUD-012`).
1843
+ */
1844
+ SUBSCRIPTION_INVOICE_PARTIES_UNCONFIRMED: "SUBSCRIPTION_INVOICE_PARTIES_UNCONFIRMED",
1845
+ /**
1846
+ * The subscriber's direct debit names no mandate reference
1847
+ * (`paymentMethodId`), which has to be announced before it is debited
1848
+ * (`SC-PRIC-028`). An invoice collected by it draws no number, and one
1849
+ * planned with it is not announced, until the payment method is replaced.
1850
+ */
1851
+ SUBSCRIPTION_INVOICE_MANDATE_MISSING: "SUBSCRIPTION_INVOICE_MANDATE_MISSING",
1852
+ /**
1853
+ * The subscriber (`subscriberId`) has no payment method in use, so the
1854
+ * invoice is not collected until one is (`SC-PRIC-082`).
1855
+ */
1856
+ SUBSCRIPTION_INVOICE_NO_PAYMENT_METHOD: "SUBSCRIPTION_INVOICE_NO_PAYMENT_METHOD",
1857
+ /**
1858
+ * A direct debit announced again after a failed collection (`attemptKey`)
1859
+ * waits until the subscriber has been told of that failure
1860
+ * (`SC-PRIC-080`).
1861
+ */
1862
+ SUBSCRIPTION_INVOICE_FAILURE_NOT_TOLD: "SUBSCRIPTION_INVOICE_FAILURE_NOT_TOLD",
1863
+ /**
1864
+ * A file of the invoice's document has another checksum than the one
1865
+ * recorded or archived for it (`invoiceId`, `recorded`, `rendered`), and
1866
+ * the first stays. The run takes the archived file as the one issued, or —
1867
+ * where nothing is archived — records the new rendering in place of the
1868
+ * old (`SC-AUD-019`); the invoice waits only where another run changed the
1869
+ * record or the archive in between, and the next run settles it.
1870
+ */
1871
+ SUBSCRIPTION_INVOICE_DOCUMENT_CHANGED: "SUBSCRIPTION_INVOICE_DOCUMENT_CHANGED",
1872
+ /**
1873
+ * The tax adapter refuses the rendered document — `missing` names what it
1874
+ * lacks or disagrees with, `adapter` the adapter. The invoice keeps its
1875
+ * number and waits for a document that passes (`SC-PRIC-042`).
1876
+ */
1877
+ SUBSCRIPTION_INVOICE_DOCUMENT_REFUSED: "SUBSCRIPTION_INVOICE_DOCUMENT_REFUSED",
1878
+ /**
1879
+ * The archived document does not match the checksum recorded when it was
1880
+ * rendered (`invoiceId`). It is not handed out (`SC-AUD-019`).
1881
+ */
1882
+ SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH: "SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH",
1883
+ /** The invoice's document is not archived yet (`invoiceId`); nothing is offered until it is (`SC-AUD-014`). */
1884
+ SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED: "SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED",
1885
+ /** No invoice with this id (`invoiceId`). */
1886
+ SUBSCRIPTION_INVOICE_NOT_FOUND: "SUBSCRIPTION_INVOICE_NOT_FOUND",
1887
+ /**
1888
+ * The subscriber's record holds no invoice email (`subscriberId`), so the
1889
+ * archived invoice cannot be sent (`SC-PRIC-047`); it is sent once one is
1890
+ * entered.
1891
+ */
1892
+ SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL: "SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL",
1893
+ /** No credit under this id for the tenant's subscriber (`creditId`). */
1894
+ SUBSCRIBER_CREDIT_NOT_FOUND: "SUBSCRIBER_CREDIT_NOT_FOUND",
1895
+ /**
1896
+ * A payout of more than the credit still owes (`creditId`, `open`): nothing
1897
+ * is paid out beyond it (`SC-PRIC-083`), and nothing is recorded.
1898
+ */
1899
+ SUBSCRIBER_PAYOUT_EXCEEDS_CREDIT: "SUBSCRIBER_PAYOUT_EXCEEDS_CREDIT",
1900
+ /**
1901
+ * A payout on a day still to come (`paidOn`, `today` in the installation's
1902
+ * zone): a payout is recorded once it left.
1903
+ */
1904
+ SUBSCRIBER_PAYOUT_NOT_MADE_YET: "SUBSCRIBER_PAYOUT_NOT_MADE_YET",
1905
+ /** No case to reconcile under this id (`caseId`). */
1906
+ SUBSCRIBER_RECONCILIATION_CASE_NOT_FOUND: "SUBSCRIBER_RECONCILIATION_CASE_NOT_FOUND",
1907
+ /**
1908
+ * A case closed before (`caseId`, `closedAt`), by the operator or by
1909
+ * SaaSiCat; its note stands, and nothing is written (`SC-ADM-033`).
1910
+ */
1911
+ SUBSCRIBER_RECONCILIATION_CASE_CLOSED: "SUBSCRIBER_RECONCILIATION_CASE_CLOSED"
1912
+ };
1487
1913
  var SUBSCRIBER_ERROR_CODES = {
1488
1914
  /**
1489
1915
  * A contract, a plan change or a booking for a tenant that has no
@@ -1664,6 +2090,7 @@ var PLATFORM_ERROR_CODES = {
1664
2090
  ...CATALOG_ERROR_CODES,
1665
2091
  ...BILLING_ERROR_CODES,
1666
2092
  ...CONTRACT_ERROR_CODES,
2093
+ ...INVOICE_ERROR_CODES,
1667
2094
  ...SUBSCRIBER_ERROR_CODES,
1668
2095
  ...REGISTRATION_ERROR_CODES,
1669
2096
  ...PAYMENT_ERROR_CODES,
@@ -1717,7 +2144,45 @@ var OPTIONAL_CANONICAL_MODELS = {
1717
2144
  SubscriptionNotice: ["tenantBilling.subscriptionNotices"],
1718
2145
  VersionRetirement: ["tenantBilling.versionRetirements"],
1719
2146
  BundleVersionRetirement: ["tenantBilling.bundleVersionRetirements"],
2147
+ FeatureWithdrawal: ["tenantBilling.featureWithdrawals"],
1720
2148
  SubscriberLedgerEntry: ["entitlement.subscriberLedgerRepository"],
2149
+ SubscriptionInvoice: ["entitlement.subscriptionInvoiceRepository"],
2150
+ SubscriptionInvoiceLine: ["entitlement.subscriptionInvoiceRepository"],
2151
+ SubscriptionInvoiceNumber: ["entitlement.subscriptionInvoiceRepository"],
2152
+ SubscriptionInvoiceDocument: ["entitlement.subscriptionInvoiceRepository"],
2153
+ SubscriptionInvoiceArchive: ["entitlement.subscriptionInvoiceArchive"],
2154
+ SubscriptionInvoiceCollection: [
2155
+ "entitlement.invoiceCollectionRepository",
2156
+ "entitlement.subscriberPaymentRepository"
2157
+ ],
2158
+ SubscriberPayment: [
2159
+ "entitlement.subscriberPaymentRepository",
2160
+ "entitlement.invoiceCollectionRepository"
2161
+ ],
2162
+ SubscriptionInvoiceSettlement: [
2163
+ "entitlement.subscriberPaymentRepository",
2164
+ "entitlement.invoiceCollectionRepository"
2165
+ ],
2166
+ SubscriberCredit: [
2167
+ "entitlement.subscriberPaymentRepository",
2168
+ "entitlement.invoiceCollectionRepository"
2169
+ ],
2170
+ SubscriberCreditEntry: [
2171
+ "entitlement.subscriberPaymentRepository",
2172
+ "entitlement.invoiceCollectionRepository"
2173
+ ],
2174
+ SubscriberRefund: [
2175
+ "entitlement.subscriberPaymentRepository",
2176
+ "entitlement.invoiceCollectionRepository"
2177
+ ],
2178
+ SubscriberReversal: [
2179
+ "entitlement.subscriberPaymentRepository",
2180
+ "entitlement.invoiceCollectionRepository"
2181
+ ],
2182
+ SubscriberReconciliationCase: [
2183
+ "entitlement.subscriberPaymentRepository",
2184
+ "entitlement.invoiceCollectionRepository"
2185
+ ],
1721
2186
  PromoCodeHold: ["promo.holdRepository"]
1722
2187
  };
1723
2188
  function membersLeftOut(notAdopted = []) {
@@ -1900,6 +2365,23 @@ var ERROR_MESSAGES_EN = {
1900
2365
  BUNDLE_RETIREMENT_REINSTATE_SUBSCRIPTION_ENDS: "Version {version} of {bundleKey} is being retired, and this booking ends before it would move. Your subscription ends by then as well.",
1901
2366
  BUNDLE_RETIREMENT_SWITCH_PLAN_CHANGES: "{bundleName} moves to its new version on {date}, and your plan changes before then, so the price a switch would keep is not known yet. Switch once your plan has changed, or let it move on that date.",
1902
2367
  BUNDLE_VERSION_OFFER_CHANGED: "The offer changed since it was shown. Look at the current one before switching.",
2368
+ FEATURE_WITHDRAWAL_UNAVAILABLE: "Withdrawing a feature is not offered here: the stores cannot list everybody a withdrawal reaches.",
2369
+ FEATURE_WITHDRAWAL_FEATURE_UNKNOWN: "The catalogue knows no feature {featureKey}.",
2370
+ FEATURE_WITHDRAWAL_DATE_IN_PAST: "A feature is withdrawn from now on or from a later date, never from a date already past.",
2371
+ FEATURE_WITHDRAWAL_OPEN: "{featureKey} is withdrawn already, and that withdrawal is not lifted. Lift it before announcing another.",
2372
+ FEATURE_WITHDRAWAL_OVERLAPS: "{featureKey} is withdrawn until {date}. Another withdrawal can begin on that date at the earliest.",
2373
+ FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED: "The reduction for {key} billed {billingCycle} names nothing this withdrawal reaches.",
2374
+ FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE: "The reduction for {key} billed {billingCycle} is named twice.",
2375
+ FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE: "The reduction for {key} billed {billingCycle} is more than the lowest price it reduces, {price}.",
2376
+ FEATURE_WITHDRAWAL_PREVIEW_CHANGED: "The subscriptions this withdrawal reaches changed since they were shown. Look at them again before announcing it.",
2377
+ FEATURE_WITHDRAWAL_NOT_FOUND: "Feature withdrawal {withdrawalId} not found.",
2378
+ FEATURE_WITHDRAWAL_ALREADY_LIFTED: "This withdrawal is lifted already, from {date}.",
2379
+ FEATURE_WITHDRAWAL_NOT_TOLD_YET: "Nobody has been told of this withdrawal yet, and it is not lifted before they are. Try again in a moment.",
2380
+ FEATURE_WITHDRAWAL_LIFT_IN_PAST: "A withdrawal is lifted from now on or from a later date, never from a date already past.",
2381
+ FEATURE_WITHDRAWAL_NOT_IN_EFFECT: "{featureKey} is not withdrawn now, so nothing can be ended at once because of it.",
2382
+ FEATURE_WITHDRAWAL_DOES_NOT_REACH: "This withdrawal does not reach your subscription, or no longer this booking.",
2383
+ FEATURE_WITHDRAWAL_ALREADY_ENDED: "This has ended already.",
2384
+ FEATURE_WITHDRAWAL_END_NOW_UNSUPPORTED: "Your cancellation for {date} is recorded, and it cannot be brought forward here. It ends on that date, and until then the reduction applies.",
1903
2385
  RETIREMENT_SWITCH_NOT_PENDING: "No retirement of your version is waiting for its date, so there is nothing to switch to.",
1904
2386
  RETIREMENT_SWITCH_IN_TRIAL: "The switch opens when your trial ends.",
1905
2387
  RETIREMENT_SWITCH_BUNDLE_CANNOT_FOLLOW: "{bundleName} cannot run on {planName} and runs until {until} at the earliest. Once it is cancelled, the switch can be made from that day.",
@@ -1942,6 +2424,23 @@ var ERROR_MESSAGES_EN = {
1942
2424
  PROMO_MAX_REDEMPTIONS_LOWERED: "maxRedemptions cannot be lowered.",
1943
2425
  // ── contract ──
1944
2426
  SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED: "The contract states {stated} % in {field}, and the tax adapter decides {decided} %.",
2427
+ SUBSCRIPTION_INVOICE_CHARGE_INVOICED: "Charge '{chargeId}' already stands on an invoice.",
2428
+ SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE: "The invoice lacks what the tax adapter {adapter} requires of it: {missing}. It is issued once that is supplied.",
2429
+ 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.",
2430
+ 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.",
2431
+ SUBSCRIPTION_INVOICE_NO_PAYMENT_METHOD: "The subscriber {subscriberId} has no payment method in use. The invoice is collected once one is.",
2432
+ SUBSCRIPTION_INVOICE_FAILURE_NOT_TOLD: "The direct debit is announced once the subscriber has been told that collection {attemptKey} failed.",
2433
+ 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.",
2434
+ 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.",
2435
+ 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.",
2436
+ SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED: "The document of invoice {invoiceId} is not archived yet.",
2437
+ SUBSCRIPTION_INVOICE_NOT_FOUND: "No invoice '{invoiceId}'.",
2438
+ SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL: "Subscriber {subscriberId} has no invoice email, so its invoice cannot be sent; it is sent once one is entered.",
2439
+ SUBSCRIBER_CREDIT_NOT_FOUND: "No credit '{creditId}' of this tenant's subscriber.",
2440
+ SUBSCRIBER_PAYOUT_EXCEEDS_CREDIT: "The credit {creditId} still owes {open}; nothing beyond that is paid out, and nothing was recorded.",
2441
+ SUBSCRIBER_PAYOUT_NOT_MADE_YET: "A payout is recorded once it left: {paidOn} is after today, {today}.",
2442
+ SUBSCRIBER_RECONCILIATION_CASE_NOT_FOUND: "No case to reconcile '{caseId}'.",
2443
+ SUBSCRIBER_RECONCILIATION_CASE_CLOSED: "The case {caseId} was closed on {closedAt}; its note stands, and nothing was recorded.",
1945
2444
  CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "A checkout offer can yield only one contract, and only once its line items are frozen.",
1946
2445
  CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "A checkout offer requires a frozen plan line item.",
1947
2446
  CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Every selected bundle version requires a frozen bundle line item.",
@@ -2027,11 +2526,21 @@ var ERROR_MESSAGES_EN = {
2027
2526
  TAX_VAT_ID_CHECK_NOT_COMPLETED: "The VAT identification number could not be checked just now ({reason}). Please try again later.",
2028
2527
  TAX_VAT_ID_CHECK_NOT_AVAILABLE: "This installation names no tax adapter, so no VAT identification number is checked."
2029
2528
  };
2529
+ function parameterText(value) {
2530
+ if (typeof value === "string") return value;
2531
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
2532
+ return String(value);
2533
+ }
2534
+ if (value instanceof Date) return String(value);
2535
+ if (!Array.isArray(value)) return null;
2536
+ const items = value.map(parameterText);
2537
+ return items.every((item) => item !== null) ? items.join(",") : null;
2538
+ }
2030
2539
  function formatErrorMessage(template, params = {}) {
2031
- return template.replace(/\{(\w+)\}/g, (match, name) => {
2032
- const value = params[name];
2033
- return value === void 0 || value === null ? match : String(value);
2034
- });
2540
+ return template.replace(
2541
+ /\{(\w+)\}/g,
2542
+ (match, name) => parameterText(params[name]) ?? match
2543
+ );
2035
2544
  }
2036
2545
  function resolveErrorMessage(body, overrides = {}, defaults = ERROR_MESSAGES_EN) {
2037
2546
  const code = typeof body.code === "string" ? body.code : void 0;
@@ -2209,6 +2718,13 @@ function selectChargeableBundles(planFeatures, selectedBundles) {
2209
2718
  }
2210
2719
 
2211
2720
  // src/payment-gateway.types.ts
2721
+ var RETRYABLE_FAILURES = /* @__PURE__ */ new Set([
2722
+ "insufficient-funds",
2723
+ "declined"
2724
+ ]);
2725
+ function isRetryableCollectionFailure(reason) {
2726
+ return RETRYABLE_FAILURES.has(reason);
2727
+ }
2212
2728
  var PAYMENT_CALLBACK_REJECTED = "PAYMENT_CALLBACK_REJECTED";
2213
2729
  var PaymentCallbackRejectedError = class extends Error {
2214
2730
  code = PAYMENT_CALLBACK_REJECTED;
@@ -2384,17 +2900,20 @@ var ORIGINS = {
2384
2900
  bundleBooking: true,
2385
2901
  bundleChange: true,
2386
2902
  credit: true,
2387
- correction: true
2903
+ correction: true,
2904
+ reductionTakenBack: true
2388
2905
  };
2389
2906
  var SOURCES = {
2390
2907
  plan: true,
2391
2908
  bundle: true,
2392
2909
  discount: true
2393
2910
  };
2394
- var TABLE2 = "subscriber_ledger_entries";
2911
+ var SUBSCRIBER_CHARGE_ORIGINS = keysOf(ORIGINS);
2912
+ var SUBSCRIBER_CHARGE_SOURCES = keysOf(SOURCES);
2913
+ var TABLE3 = "subscriber_ledger_entries";
2395
2914
  var CENT_TOLERANCE = 1e-6;
2396
2915
  function toSubscriberChargeRecord(row) {
2397
- const where = (column) => ({ table: TABLE2, column, id: row.id });
2916
+ const where = (column) => ({ table: TABLE3, column, id: row.id });
2398
2917
  return {
2399
2918
  id: row.id,
2400
2919
  subscriberId: row.subscriberId,
@@ -2402,8 +2921,8 @@ function toSubscriberChargeRecord(row) {
2402
2921
  subscriptionId: row.subscriptionId,
2403
2922
  contractId: row.contractId,
2404
2923
  contractLineItemId: row.contractLineItemId,
2405
- origin: oneOf(keysOf(ORIGINS), row.origin, where("origin")),
2406
- source: oneOf(keysOf(SOURCES), row.source, where("source")),
2924
+ origin: oneOf(SUBSCRIBER_CHARGE_ORIGINS, row.origin, where("origin")),
2925
+ source: oneOf(SUBSCRIBER_CHARGE_SOURCES, row.source, where("source")),
2407
2926
  sourceRef: row.sourceRef,
2408
2927
  periodStart: row.periodStart,
2409
2928
  periodEnd: row.periodEnd,
@@ -2443,6 +2962,96 @@ function keysOf(record) {
2443
2962
  // src/subscriber-ledger.types.ts
2444
2963
  var SUBSCRIBER_ACCOUNT_CAPABILITY = "charges.read";
2445
2964
 
2965
+ // src/subscription-invoice.types.ts
2966
+ var INVOICE_SEQUENCE_DIGITS = 6;
2967
+ function formatInvoiceNumber(prefix, year, sequence) {
2968
+ if (!Number.isInteger(sequence) || sequence < 1) {
2969
+ throw new RangeError(`An invoice number's sequence starts at 1; ${sequence} is not one.`);
2970
+ }
2971
+ return `${prefix}-${year}-${String(sequence).padStart(INVOICE_SEQUENCE_DIGITS, "0")}`;
2972
+ }
2973
+
2974
+ // src/column-day.ts
2975
+ var DAY_LENGTH = "YYYY-MM-DD".length;
2976
+ function dayOf2(value, where) {
2977
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
2978
+ return value.toISOString().slice(0, DAY_LENGTH);
2979
+ }
2980
+ if (typeof value === "string" && value.length === DAY_LENGTH && !Number.isNaN(Date.parse(value))) {
2981
+ return value;
2982
+ }
2983
+ throw new Error(`${where.table}.${where.column} of ${where.id} is not a day.`);
2984
+ }
2985
+
2986
+ // src/subscriber-payment-method-mapping.ts
2987
+ var PAYMENT_METHOD_TYPES = ["card", "sepa_debit"];
2988
+ var STATUSES = ["ACTIVE", "REPLACED"];
2989
+ var TABLE4 = "subscriber_payment_methods";
2990
+ function toSubscriberPaymentMethodRecord(row) {
2991
+ return {
2992
+ id: row.id,
2993
+ subscriberId: row.subscriberId,
2994
+ gatewayAccount: row.gatewayAccount,
2995
+ provider: row.provider,
2996
+ customerRef: row.customerRef,
2997
+ paymentMethodRef: row.paymentMethodRef,
2998
+ type: oneOf(PAYMENT_METHOD_TYPES, row.type, { table: TABLE4, column: "type", id: row.id }),
2999
+ brand: row.brand,
3000
+ last4: row.last4,
3001
+ expiryMonth: row.expiryMonth,
3002
+ expiryYear: row.expiryYear,
3003
+ country: row.country,
3004
+ bankCode: row.bankCode,
3005
+ mandateReference: row.mandateReference,
3006
+ status: oneOf(STATUSES, row.status, { table: TABLE4, column: "status", id: row.id }),
3007
+ confirmedAt: row.confirmedAt,
3008
+ replacedAt: row.replacedAt,
3009
+ createdAt: row.createdAt
3010
+ };
3011
+ }
3012
+ function subscriberPaymentMethodColumns(data) {
3013
+ return {
3014
+ subscriberId: data.subscriberId,
3015
+ gatewayAccount: data.gatewayAccount,
3016
+ provider: data.provider,
3017
+ customerRef: data.customerRef,
3018
+ paymentMethodRef: data.paymentMethodRef,
3019
+ type: data.type,
3020
+ brand: data.brand,
3021
+ last4: data.last4,
3022
+ expiryMonth: data.expiryMonth,
3023
+ expiryYear: data.expiryYear,
3024
+ country: data.country,
3025
+ bankCode: data.bankCode,
3026
+ mandateReference: data.mandateReference,
3027
+ confirmedAt: data.confirmedAt
3028
+ };
3029
+ }
3030
+ var FOREIGN_PAYMENT_METHOD_REFERENCE = "FOREIGN_PAYMENT_METHOD_REFERENCE";
3031
+ var ForeignPaymentMethodReferenceError = class extends Error {
3032
+ constructor(gatewayAccount, paymentMethodRef) {
3033
+ super(
3034
+ `Payment method '${paymentMethodRef}' of account '${gatewayAccount}' belongs to another subscriber. A reference belongs to exactly one, so it is not handed out.`
3035
+ );
3036
+ this.gatewayAccount = gatewayAccount;
3037
+ this.paymentMethodRef = paymentMethodRef;
3038
+ this.name = "ForeignPaymentMethodReferenceError";
3039
+ }
3040
+ gatewayAccount;
3041
+ paymentMethodRef;
3042
+ code = FOREIGN_PAYMENT_METHOD_REFERENCE;
3043
+ };
3044
+ function isForeignPaymentMethodReferenceError(error) {
3045
+ return error instanceof Error && error.code === FOREIGN_PAYMENT_METHOD_REFERENCE;
3046
+ }
3047
+ function refuseForeignPaymentMethodReference(recorded, subscriberId) {
3048
+ if (recorded.subscriberId === subscriberId) return;
3049
+ throw new ForeignPaymentMethodReferenceError(
3050
+ recorded.gatewayAccount,
3051
+ recorded.paymentMethodRef
3052
+ );
3053
+ }
3054
+
2446
3055
  // src/subscriber.types.ts
2447
3056
  var SUBSCRIBER_INVOICE_ADDRESS_FIELDS = [
2448
3057
  "addressLine1",
@@ -2574,89 +3183,1375 @@ function textEntries(value) {
2574
3183
  return entries;
2575
3184
  }
2576
3185
 
2577
- // src/subscriber-mapping.ts
2578
- function formatCustomerNumber(prefix, sequence) {
2579
- return `${prefix}${sequence}`;
2580
- }
2581
- function toSubscriberRecord(row, tenantId) {
3186
+ // src/subscription-invoice-mapping.ts
3187
+ var INVOICES = "subscription_invoices";
3188
+ var LINES = "subscription_invoice_lines";
3189
+ var CENT_TOLERANCE2 = 1e-6;
3190
+ function toSubscriptionInvoiceRecord(row, lines) {
3191
+ const where = (column) => ({ table: INVOICES, column, id: row.id });
3192
+ const treatment = toTaxTreatment(row.taxTreatment);
3193
+ if (!treatment) {
3194
+ throw new Error(`${INVOICES}.taxTreatment of ${row.id} is not a tax treatment.`);
3195
+ }
3196
+ const dueDate = dayOf2(row.dueDate, where("dueDate"));
2582
3197
  return {
2583
3198
  id: row.id,
2584
- customerNumber: formatCustomerNumber(row.customerNumberPrefix, row.customerSequence),
2585
- legalName: row.legalName,
2586
- addressLine1: row.addressLine1,
2587
- addressLine2: row.addressLine2,
2588
- postalCode: row.postalCode,
2589
- city: row.city,
2590
- country: row.country,
2591
- vatId: row.vatId,
2592
- taxNumber: row.taxNumber,
2593
- invoiceEmail: row.invoiceEmail,
2594
- business: row.business,
2595
- tenantId,
2596
- migrated: row.migrated,
2597
- createdAt: row.createdAt,
2598
- updatedAt: row.updatedAt
3199
+ number: row.number,
3200
+ numberPrefix: row.numberPrefix,
3201
+ numberYear: row.numberYear,
3202
+ numberSequence: row.numberSequence,
3203
+ tenantId: row.tenantId,
3204
+ subscriberId: row.subscriberId,
3205
+ subscriptionId: row.subscriptionId,
3206
+ contractId: row.contractId,
3207
+ issuedAt: row.issuedAt,
3208
+ issueDate: dayOf2(row.issueDate, where("issueDate")),
3209
+ dueDate,
3210
+ servicePeriodFrom: dayOf2(row.servicePeriodFrom, where("servicePeriodFrom")),
3211
+ servicePeriodUntil: dayOf2(row.servicePeriodUntil, where("servicePeriodUntil")),
3212
+ ...collectionOf(row.payment, row.collectionDate, dueDate, where),
3213
+ currency: row.currency,
3214
+ issuer: issuerOf(row.issuerParty, where("issuerParty")),
3215
+ subscriber: subscriberOf(row.subscriberParty, where("subscriberParty")),
3216
+ footer: footerOf(row.footer, where("footer")),
3217
+ taxTreatment: treatment,
3218
+ taxes: taxesOf(row.taxes, where("taxes")),
3219
+ totalNet: amountOf2(row.totalNet, where("totalNet")),
3220
+ totalTax: amountOf2(row.totalTax, where("totalTax")),
3221
+ totalGross: amountOf2(row.totalGross, where("totalGross")),
3222
+ lines: [...lines].sort((a, b) => a.position - b.position).map((line) => toSubscriptionInvoiceLineRecord(line)),
3223
+ createdAt: row.createdAt
2599
3224
  };
2600
3225
  }
2601
- function toSubscriberCorrectionRecord(row) {
3226
+ function toSubscriptionInvoiceLineRecord(row) {
3227
+ const where = (column) => ({ table: LINES, column, id: row.id });
2602
3228
  return {
2603
3229
  id: row.id,
2604
- subscriberId: row.subscriberId,
2605
- previous: toIdentityValues(row.previous),
2606
- corrected: toIdentityValues(row.corrected),
2607
- reason: row.reason,
2608
- correctedBy: row.correctedBy,
2609
- correctedAt: row.correctedAt
3230
+ invoiceId: row.invoiceId,
3231
+ position: row.position,
3232
+ chargeId: row.chargeId,
3233
+ contractLineItemId: row.contractLineItemId,
3234
+ title: row.title,
3235
+ origin: oneOf(SUBSCRIBER_CHARGE_ORIGINS, row.origin, where("origin")),
3236
+ source: oneOf(SUBSCRIBER_CHARGE_SOURCES, row.source, where("source")),
3237
+ periodFrom: dayOf2(row.periodFrom, where("periodFrom")),
3238
+ periodUntil: dayOf2(row.periodUntil, where("periodUntil")),
3239
+ amountNet: amountOf2(row.amountNet, where("amountNet")),
3240
+ taxRate: amountOf2(row.taxRate, where("taxRate"))
2610
3241
  };
2611
3242
  }
2612
- function toSubscriberTaxOriginChangeRecord(row) {
3243
+ function subscriptionInvoiceColumns(invoice, drawn) {
2613
3244
  return {
2614
- id: row.id,
2615
- subscriberId: row.subscriberId,
2616
- previous: toTaxOriginValues(row.previous),
2617
- changed: toTaxOriginValues(row.changed),
2618
- changedBy: row.changedBy,
2619
- changedAt: row.changedAt,
2620
- reason: row.reason
3245
+ number: drawn.number,
3246
+ numberPrefix: invoice.numberPrefix,
3247
+ numberYear: invoice.numberYear,
3248
+ numberSequence: drawn.numberSequence,
3249
+ tenantId: invoice.tenantId,
3250
+ subscriberId: invoice.subscriberId,
3251
+ subscriptionId: invoice.subscriptionId,
3252
+ contractId: invoice.contractId,
3253
+ issuedAt: invoice.issuedAt,
3254
+ issueDate: invoice.issueDate,
3255
+ dueDate: invoice.dueDate,
3256
+ servicePeriodFrom: invoice.servicePeriodFrom,
3257
+ servicePeriodUntil: invoice.servicePeriodUntil,
3258
+ payment: invoice.payment ? { ...invoice.payment } : null,
3259
+ collectionDate: invoice.collectionDate,
3260
+ currency: invoice.currency,
3261
+ issuerParty: { ...invoice.issuer },
3262
+ subscriberParty: { ...invoice.subscriber },
3263
+ footer: [...invoice.footer],
3264
+ taxTreatment: taxTreatmentToJson(invoice.taxTreatment),
3265
+ taxes: invoice.taxes.map((entry) => ({ rate: entry.rate, net: entry.net, tax: entry.tax })),
3266
+ totalNet: centsText(invoice.totalNet, "totalNet"),
3267
+ totalTax: centsText(invoice.totalTax, "totalTax"),
3268
+ totalGross: centsText(invoice.totalGross, "totalGross")
2621
3269
  };
2622
3270
  }
2623
- function identityCorrectionDelta(current, corrected) {
2624
- const previous = {};
2625
- const next = {};
2626
- for (const field of LEGAL_IDENTITY_FIELDS) {
2627
- const value = corrected[field];
2628
- if (value === void 0 || value === current[field]) continue;
2629
- previous[field] = current[field];
2630
- next[field] = value;
2631
- }
3271
+ function subscriptionInvoiceLineColumns(line, invoiceId) {
2632
3272
  return {
2633
- previous,
2634
- corrected: next
3273
+ invoiceId,
3274
+ position: line.position,
3275
+ chargeId: line.chargeId,
3276
+ contractLineItemId: line.contractLineItemId,
3277
+ title: line.title,
3278
+ origin: line.origin,
3279
+ source: line.source,
3280
+ periodFrom: line.periodFrom,
3281
+ periodUntil: line.periodUntil,
3282
+ amountNet: centsText(line.amountNet, "amountNet"),
3283
+ taxRate: centsText(line.taxRate, "taxRate")
2635
3284
  };
2636
3285
  }
2637
- function contractPartiesOf(subscriber, issuer) {
2638
- const party = {
2639
- customerNumber: subscriber.customerNumber,
2640
- legalName: subscriber.legalName,
2641
- vatId: subscriber.vatId,
2642
- taxNumber: subscriber.taxNumber,
2643
- addressLine1: subscriber.addressLine1,
2644
- addressLine2: subscriber.addressLine2,
2645
- postalCode: subscriber.postalCode,
2646
- city: subscriber.city,
2647
- country: subscriber.country
3286
+ function utcMidnightOf(day) {
3287
+ return /* @__PURE__ */ new Date(`${day}T00:00:00.000Z`);
3288
+ }
3289
+ function collectionOf(payment, collectionDate, dueDate, where) {
3290
+ const paymentAt = where("payment");
3291
+ if (payment === void 0 || collectionDate === void 0) {
3292
+ throw new Error(
3293
+ `${paymentAt.table} row ${paymentAt.id} was read without its payment or its collection date.`
3294
+ );
3295
+ }
3296
+ if (payment === null) {
3297
+ if (collectionDate !== null) {
3298
+ throw new Error(
3299
+ `${paymentAt.table}.collectionDate of ${paymentAt.id} is set on an invoice that is not collected.`
3300
+ );
3301
+ }
3302
+ return { payment: null, collectionDate: null };
3303
+ }
3304
+ if (typeof payment !== "object" || Array.isArray(payment)) {
3305
+ throw new Error(`${paymentAt.table}.payment of ${paymentAt.id} is not a payment method.`);
3306
+ }
3307
+ const required = (key) => {
3308
+ const value = textOrNull2(payment, key, paymentAt);
3309
+ if (value === null) {
3310
+ throw new Error(`${paymentAt.table}.payment of ${paymentAt.id} names no ${key}.`);
3311
+ }
3312
+ return value;
2648
3313
  };
2649
- return {
2650
- subscriberId: subscriber.id,
2651
- subscriber: party,
2652
- issuer: issuer ? issuerPartyOf(issuer) : null
3314
+ const type = oneOf(PAYMENT_METHOD_TYPES, required("type"), {
3315
+ ...paymentAt,
3316
+ column: "payment.type"
3317
+ });
3318
+ const recorded = {
3319
+ paymentMethodId: required("paymentMethodId"),
3320
+ gatewayAccount: required("gatewayAccount"),
3321
+ type,
3322
+ brand: textOrNull2(payment, "brand", paymentAt),
3323
+ last4: required("last4"),
3324
+ mandateReference: type === "sepa_debit" ? required("mandateReference") : null
2653
3325
  };
3326
+ if (type !== "sepa_debit") {
3327
+ if (collectionDate !== null) {
3328
+ throw new Error(
3329
+ `${paymentAt.table}.collectionDate of ${paymentAt.id} is set on an invoice paid by ${type}.`
3330
+ );
3331
+ }
3332
+ return { payment: recorded, collectionDate: null };
3333
+ }
3334
+ const collected = dayOf2(collectionDate, where("collectionDate"));
3335
+ if (collected !== dueDate) {
3336
+ throw new Error(
3337
+ `${paymentAt.table}.dueDate of ${paymentAt.id} is ${dueDate}, and a direct debit falls due on the day it is collected, ${collected}.`
3338
+ );
3339
+ }
3340
+ return { payment: recorded, collectionDate: collected };
2654
3341
  }
2655
- function issuerPartyOf(issuer) {
2656
- const identity = issuerIdentityOf(issuer);
2657
- return {
2658
- legalName: identity?.legalName ?? "",
2659
- vatId: identity?.vatId ?? null,
3342
+ function footerOf(value, where) {
3343
+ if (!Array.isArray(value) || !value.every((line) => typeof line === "string")) {
3344
+ throw new Error(`${where.table}.${where.column} of ${where.id} is not a list of lines.`);
3345
+ }
3346
+ return [...value];
3347
+ }
3348
+ function amountOf2(value, where) {
3349
+ const amount = Number(value === null || value === void 0 ? NaN : String(value));
3350
+ if (!Number.isFinite(amount)) {
3351
+ throw new Error(`${where.table}.${where.column} of ${where.id} is not an amount.`);
3352
+ }
3353
+ return amount;
3354
+ }
3355
+ function centsText(amount, field) {
3356
+ const cents2 = amount * 100;
3357
+ if (!Number.isFinite(cents2) || Math.abs(cents2 - Math.round(cents2)) > CENT_TOLERANCE2) {
3358
+ throw new Error(
3359
+ `An invoice's ${field} of ${amount} is not a whole number of cents; it is rounded once, where the invoice is drafted, before it is written.`
3360
+ );
3361
+ }
3362
+ return amount.toFixed(2);
3363
+ }
3364
+ function fieldOf(source, key) {
3365
+ const field = Object.getOwnPropertyDescriptor(source, key)?.value;
3366
+ return field;
3367
+ }
3368
+ function textOrNull2(source, key, where) {
3369
+ const value = fieldOf(source, key);
3370
+ if (value === null || value === void 0) return null;
3371
+ if (typeof value !== "string") {
3372
+ throw new Error(`${where.table}.${where.column}.${key} of ${where.id} is not text.`);
3373
+ }
3374
+ return value;
3375
+ }
3376
+ function partyOf(value, where) {
3377
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3378
+ throw new Error(`${where.table}.${where.column} of ${where.id} is not a party.`);
3379
+ }
3380
+ const source = value;
3381
+ const legalName = textOrNull2(source, "legalName", where);
3382
+ if (legalName === null) {
3383
+ throw new Error(`${where.table}.${where.column} of ${where.id} names no legal name.`);
3384
+ }
3385
+ return {
3386
+ legalName,
3387
+ vatId: textOrNull2(source, "vatId", where),
3388
+ taxNumber: textOrNull2(source, "taxNumber", where),
3389
+ addressLine1: textOrNull2(source, "addressLine1", where),
3390
+ addressLine2: textOrNull2(source, "addressLine2", where),
3391
+ postalCode: textOrNull2(source, "postalCode", where),
3392
+ city: textOrNull2(source, "city", where),
3393
+ country: textOrNull2(source, "country", where)
3394
+ };
3395
+ }
3396
+ function issuerOf(value, where) {
3397
+ return partyOf(value, where);
3398
+ }
3399
+ function subscriberOf(value, where) {
3400
+ const party = partyOf(value, where);
3401
+ const customerNumber = textOrNull2(value, "customerNumber", where);
3402
+ if (customerNumber === null) {
3403
+ throw new Error(`${where.table}.${where.column} of ${where.id} names no customer number.`);
3404
+ }
3405
+ return { ...party, customerNumber };
3406
+ }
3407
+ function taxesOf(value, where) {
3408
+ if (!Array.isArray(value)) {
3409
+ throw new Error(`${where.table}.${where.column} of ${where.id} is not a list of rates.`);
3410
+ }
3411
+ return value.map((entry) => {
3412
+ const item = entry ?? {};
3413
+ return {
3414
+ rate: amountOf2(item.rate, where),
3415
+ net: amountOf2(item.net, where),
3416
+ tax: amountOf2(item.tax, where)
3417
+ };
3418
+ });
3419
+ }
3420
+
3421
+ // src/subscription-invoice-refusals.ts
3422
+ function subscriptionInvoiceChargeInvoiced(chargeId) {
3423
+ return new PersistenceRefusal(
3424
+ INVOICE_ERROR_CODES.SUBSCRIPTION_INVOICE_CHARGE_INVOICED,
3425
+ "moved",
3426
+ `Charge '${chargeId}' already stands on an invoice.`,
3427
+ { chargeId }
3428
+ );
3429
+ }
3430
+ function subscriptionInvoiceDocumentChanged(invoiceId, recorded, rendered) {
3431
+ return new PersistenceRefusal(
3432
+ INVOICE_ERROR_CODES.SUBSCRIPTION_INVOICE_DOCUMENT_CHANGED,
3433
+ "moved",
3434
+ `The document rendered for invoice '${invoiceId}' differs from the one recorded for it.`,
3435
+ { invoiceId, recorded, rendered }
3436
+ );
3437
+ }
3438
+
3439
+ // src/subscription-invoice-draft.ts
3440
+ function invoiceGroupsOf(charges) {
3441
+ const groups = /* @__PURE__ */ new Map();
3442
+ for (const charge of charges) {
3443
+ const key = `${charge.contractId}|${charge.bookedAt.toISOString()}`;
3444
+ const group = groups.get(key);
3445
+ if (group) group.charges.push(charge);
3446
+ else
3447
+ groups.set(key, {
3448
+ contractId: charge.contractId,
3449
+ bookedAt: charge.bookedAt,
3450
+ charges: [charge]
3451
+ });
3452
+ }
3453
+ return [...groups.values()].filter((group) => group.charges.some((charge) => charge.amountNet !== 0)).sort(
3454
+ (a, b) => a.bookedAt.getTime() - b.bookedAt.getTime() || a.contractId.localeCompare(b.contractId)
3455
+ );
3456
+ }
3457
+ var DIRECT_DEBIT_LEAD_DAYS = 14;
3458
+ function directDebitLeadDaysOf(invoicing) {
3459
+ return invoicing.directDebitLeadDays ?? DIRECT_DEBIT_LEAD_DAYS;
3460
+ }
3461
+ function invoicePaymentOf(method) {
3462
+ if (!method) return null;
3463
+ if (method.type === "sepa_debit" && method.mandateReference === null) return null;
3464
+ return {
3465
+ paymentMethodId: method.id,
3466
+ gatewayAccount: method.gatewayAccount,
3467
+ type: method.type,
3468
+ brand: method.brand,
3469
+ last4: method.last4,
3470
+ mandateReference: method.type === "sepa_debit" ? method.mandateReference : null
3471
+ };
3472
+ }
3473
+ function draftSubscriptionInvoice(input) {
3474
+ const { group, treatment, timeZone } = input;
3475
+ const lines = linesOf(input);
3476
+ const issueDate = dayInZone(input.issuedAt, timeZone);
3477
+ const tax = input.tax(lines.map((line) => ({ net: line.amountNet, rate: line.taxRate })));
3478
+ const payment = tax.gross > 0 ? input.payment : null;
3479
+ if (payment?.type === "sepa_debit" && payment.mandateReference === null) {
3480
+ throw new Error(
3481
+ `Payment method ${payment.paymentMethodId} is a direct debit without a mandate reference, which an invoice collected by it has to announce.`
3482
+ );
3483
+ }
3484
+ const collectionDate = payment?.type === "sepa_debit" ? addDaysToDay(issueDate, input.directDebitLeadDays) : null;
3485
+ return {
3486
+ numberPrefix: input.numberPrefix,
3487
+ numberYear: yearOfDay(issueDate),
3488
+ tenantId: input.tenantId,
3489
+ subscriberId: input.subscriberId,
3490
+ subscriptionId: group.charges[0].subscriptionId,
3491
+ contractId: group.contractId,
3492
+ issuedAt: input.issuedAt,
3493
+ issueDate,
3494
+ dueDate: collectionDate ?? addDaysToDay(issueDate, input.paymentTermDays),
3495
+ payment: payment ? { ...payment } : null,
3496
+ collectionDate,
3497
+ servicePeriodFrom: lines.map((line) => line.periodFrom).sort()[0],
3498
+ servicePeriodUntil: lines.map((line) => line.periodUntil).sort().at(-1),
3499
+ currency: currencyOf(group),
3500
+ issuer: input.issuer,
3501
+ subscriber: input.subscriber,
3502
+ footer: [...input.footer],
3503
+ taxTreatment: treatment,
3504
+ taxes: tax.rates,
3505
+ totalNet: tax.net,
3506
+ totalTax: tax.tax,
3507
+ totalGross: tax.gross,
3508
+ lines
3509
+ };
3510
+ }
3511
+ function invoiceContentDraftOf(invoice) {
3512
+ return {
3513
+ issuer: invoice.issuer,
3514
+ subscriber: invoice.subscriber,
3515
+ treatment: invoice.taxTreatment,
3516
+ issueDate: invoice.issueDate,
3517
+ dueDate: invoice.dueDate,
3518
+ servicePeriod: { from: invoice.servicePeriodFrom, until: invoice.servicePeriodUntil },
3519
+ lines: invoice.lines.map((line) => ({
3520
+ title: line.title,
3521
+ net: line.amountNet,
3522
+ rate: line.taxRate
3523
+ }))
3524
+ };
3525
+ }
3526
+ function linesOf(input) {
3527
+ const placeOf = new Map(input.contractLines.map((line, index) => [line.id, index]));
3528
+ const place = (charge) => {
3529
+ const index = placeOf.get(charge.contractLineItemId);
3530
+ if (index === void 0) {
3531
+ throw new Error(
3532
+ `Charge ${charge.id} names contract line ${charge.contractLineItemId}, which contract ${input.group.contractId} does not have.`
3533
+ );
3534
+ }
3535
+ return index;
3536
+ };
3537
+ return [...input.group.charges].sort(
3538
+ (a, b) => place(a) - place(b) || a.periodStart.getTime() - b.periodStart.getTime() || a.origin.localeCompare(b.origin)
3539
+ ).map((charge, index) => ({
3540
+ position: index + 1,
3541
+ chargeId: charge.id,
3542
+ contractLineItemId: charge.contractLineItemId,
3543
+ title: input.contractLines[place(charge)].titleSnapshot,
3544
+ origin: charge.origin,
3545
+ source: charge.source,
3546
+ periodFrom: dayInZone(charge.periodStart, input.timeZone),
3547
+ periodUntil: lastDayInZone(
3548
+ { from: charge.periodStart, until: charge.periodEnd },
3549
+ input.timeZone
3550
+ ),
3551
+ amountNet: charge.amountNet,
3552
+ taxRate: input.treatment.rate
3553
+ }));
3554
+ }
3555
+ function currencyOf(group) {
3556
+ const currencies = new Set(group.charges.map((charge) => charge.currency));
3557
+ if (currencies.size !== 1) {
3558
+ throw new Error(
3559
+ `The charges of contract ${group.contractId} booked at ${group.bookedAt.toISOString()} are in ${[...currencies].join(" and ")}; an invoice is in one currency.`
3560
+ );
3561
+ }
3562
+ return [...currencies][0];
3563
+ }
3564
+
3565
+ // src/invoice-issuer.ts
3566
+ function invoiceIssuerOf(copy, configured, history) {
3567
+ const target = configured && identityOf2(configured);
3568
+ let reached = identityOf2(copy);
3569
+ if (!target || !reached) return { ...copy };
3570
+ for (const step of history) {
3571
+ if (sameLegalIdentity(reached, target)) break;
3572
+ const change = classifyIssuerChange(
3573
+ recordedIssuerIdentity(step.previous),
3574
+ issuerBlockOf(step.current)
3575
+ );
3576
+ if (change.kind === "corrected" && sameLegalIdentity(change.recorded, reached)) {
3577
+ reached = change.current;
3578
+ }
3579
+ }
3580
+ return sameLegalIdentity(reached, target) ? { ...configured } : { ...copy };
3581
+ }
3582
+ function invoiceFooterOf(issuer, configured, footer) {
3583
+ const named = identityOf2(issuer);
3584
+ const target = configured && identityOf2(configured);
3585
+ return named && target && sameLegalIdentity(named, target) ? [...footer] : [];
3586
+ }
3587
+ function identityOf2(party) {
3588
+ return recordedIssuerIdentity({ issuer: party });
3589
+ }
3590
+ function issuerBlockOf(values) {
3591
+ const issuer = values.issuer;
3592
+ if (issuer === null || typeof issuer !== "object" || Array.isArray(issuer)) return void 0;
3593
+ return issuer;
3594
+ }
3595
+
3596
+ // src/invoice-document.types.ts
3597
+ var INVOICE_DOCUMENT_CAPABILITY = "invoices.read";
3598
+
3599
+ // src/invoice-document-mapping.ts
3600
+ var DOCUMENTS = "subscription_invoice_documents";
3601
+ var ARCHIVE = "subscription_invoice_archive";
3602
+ var CHECKSUM = /^[0-9a-f]{64}$/;
3603
+ function isInvoiceDocumentChecksum(value) {
3604
+ return typeof value === "string" && CHECKSUM.test(value);
3605
+ }
3606
+ function toSubscriptionInvoiceDocumentRecord(row) {
3607
+ if (!isInvoiceDocumentChecksum(row.checksum)) {
3608
+ throw new Error(`${DOCUMENTS}.checksum of ${row.invoiceId} is not a SHA-256 digest.`);
3609
+ }
3610
+ const size = Number(row.size);
3611
+ if (!Number.isSafeInteger(size) || size < 0) {
3612
+ throw new Error(`${DOCUMENTS}.size of ${row.invoiceId} is not a size in bytes.`);
3613
+ }
3614
+ return {
3615
+ invoiceId: row.invoiceId,
3616
+ checksum: row.checksum,
3617
+ mediaType: row.mediaType,
3618
+ fileName: row.fileName,
3619
+ size,
3620
+ renderedAt: row.renderedAt,
3621
+ archivedAt: row.archivedAt,
3622
+ deliveryClaimedAt: row.deliveryClaimedAt,
3623
+ deliveredAt: row.deliveredAt,
3624
+ deliveredTo: row.deliveredTo
3625
+ };
3626
+ }
3627
+ function toArchivedInvoiceDocument(row) {
3628
+ if (!isInvoiceDocumentChecksum(row.checksum)) {
3629
+ throw new Error(`${ARCHIVE}.checksum of ${row.invoiceId} is not a SHA-256 digest.`);
3630
+ }
3631
+ if (!(row.content instanceof Uint8Array)) {
3632
+ throw new Error(`${ARCHIVE}.content of ${row.invoiceId} is not a file.`);
3633
+ }
3634
+ return {
3635
+ invoiceId: row.invoiceId,
3636
+ checksum: row.checksum,
3637
+ mediaType: row.mediaType,
3638
+ fileName: row.fileName,
3639
+ // A copy that owns its bytes: a driver's `Buffer` may be a view into a
3640
+ // pooled allocation, and a caller handing it on must not see that pool.
3641
+ content: new Uint8Array(row.content),
3642
+ archivedAt: row.archivedAt
3643
+ };
3644
+ }
3645
+ var INVOICE_ARCHIVE_TRIGGERS = [
3646
+ "subscription_invoice_archive_is_kept",
3647
+ "subscription_invoice_archive_is_not_emptied"
3648
+ ];
3649
+ function invoiceArchiveProtectionProblem(enabledTriggers) {
3650
+ const missing = INVOICE_ARCHIVE_TRIGGERS.filter((name) => !enabledTriggers.includes(name));
3651
+ if (missing.length === 0) return null;
3652
+ 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.`;
3653
+ }
3654
+
3655
+ // src/invoice-collection.types.ts
3656
+ function sentCollectionOf(attempt) {
3657
+ const { submittedAt, heldAt } = attempt;
3658
+ if (submittedAt === null || heldAt === null) {
3659
+ throw new Error(`Collection attempt ${attempt.attemptKey} has not been sent.`);
3660
+ }
3661
+ return { ...attempt, submittedAt, heldAt };
3662
+ }
3663
+ var INVOICE_COLLECTION_LEASE_MS = 15 * 6e4;
3664
+ var INVOICE_COLLECTION_ASK_EVERY_MS = 60 * 6e4;
3665
+ function collectionAttemptKey(invoiceId, attempt) {
3666
+ if (!Number.isInteger(attempt) || attempt < 1) {
3667
+ throw new RangeError(`A collection attempt counts from 1; ${attempt} is not one.`);
3668
+ }
3669
+ return `saasicat:collect:${invoiceId}:${attempt}`;
3670
+ }
3671
+ function collectionStatusOf(outcome) {
3672
+ if (outcome.status === "succeeded") return "SUCCEEDED";
3673
+ if (outcome.status === "pending") return "PENDING";
3674
+ return "FAILED";
3675
+ }
3676
+ var COLLECTION_NOT_SENT_PAID = {
3677
+ status: "failed",
3678
+ paymentRef: null,
3679
+ reason: "other",
3680
+ gatewayCode: "saasicat_invoice_paid"
3681
+ };
3682
+ var COLLECTION_NOT_SENT_REVERSED = {
3683
+ status: "failed",
3684
+ paymentRef: null,
3685
+ reason: "other",
3686
+ gatewayCode: "saasicat_payment_method_reversed"
3687
+ };
3688
+
3689
+ // src/invoice-collection-mapping.ts
3690
+ var TABLE5 = "subscription_invoice_collections";
3691
+ var INVOICE_COLLECTION_STATUSES = [
3692
+ "SCHEDULED",
3693
+ "SUBMITTED",
3694
+ "PENDING",
3695
+ "SUCCEEDED",
3696
+ "FAILED",
3697
+ "WITHDRAWN",
3698
+ "REVERSED"
3699
+ ];
3700
+ var UNSETTLED_COLLECTION_STATUSES = [
3701
+ "SUBMITTED",
3702
+ "PENDING"
3703
+ ];
3704
+ var BLOCKING_COLLECTION_STATUSES = [
3705
+ "SCHEDULED",
3706
+ ...UNSETTLED_COLLECTION_STATUSES,
3707
+ "SUCCEEDED"
3708
+ ];
3709
+ var FOLLOWED_UP_COLLECTION_STATUSES = [
3710
+ "FAILED",
3711
+ "REVERSED"
3712
+ ];
3713
+ var REVERSIBLE_COLLECTION_STATUSES = [
3714
+ "SUCCEEDED",
3715
+ "FAILED"
3716
+ ];
3717
+ function lastCollectionOf(earlier) {
3718
+ return earlier.reduce((last, row) => Math.max(last, row.attempt), 0);
3719
+ }
3720
+ function refusesFirstAttempt(earlier) {
3721
+ return earlier.length > 0;
3722
+ }
3723
+ function refusesPlannedAttempt(earlier, after) {
3724
+ return earlier.some((row) => BLOCKING.has(row.status)) || lastCollectionOf(earlier) !== after;
3725
+ }
3726
+ var BLOCKING = new Set(BLOCKING_COLLECTION_STATUSES);
3727
+ var NOT_SENT = /* @__PURE__ */ new Set(["SCHEDULED", "WITHDRAWN"]);
3728
+ var COLLECTION_FAILURE_REASONS = [
3729
+ "insufficient-funds",
3730
+ "declined",
3731
+ "authentication-required",
3732
+ "payment-method-unusable",
3733
+ "mandate-invalid",
3734
+ "other"
3735
+ ];
3736
+ function toInvoiceCollectionRecord(row) {
3737
+ const where = (column) => ({ table: TABLE5, column, id: row.id });
3738
+ const amount = Number(
3739
+ // 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
3740
+ row.amount === null || row.amount === void 0 ? NaN : String(row.amount)
3741
+ );
3742
+ if (!Number.isFinite(amount)) {
3743
+ throw new Error(`${TABLE5}.amount of ${row.id} is not an amount.`);
3744
+ }
3745
+ const status = oneOf(INVOICE_COLLECTION_STATUSES, row.status, where("status"));
3746
+ const sent = !NOT_SENT.has(status);
3747
+ if (sent !== (row.submittedAt !== null) || sent !== (row.heldAt !== null)) {
3748
+ throw new Error(
3749
+ `${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.`
3750
+ );
3751
+ }
3752
+ const collectOn = row.collectOn === null || row.collectOn === void 0 ? null : dayOf2(row.collectOn, where("collectOn"));
3753
+ if (status === "SCHEDULED" && collectOn === null) {
3754
+ throw new Error(`${TABLE5}.collectOn of ${row.id} is missing from a planned attempt.`);
3755
+ }
3756
+ return {
3757
+ id: row.id,
3758
+ invoiceId: row.invoiceId,
3759
+ attempt: row.attempt,
3760
+ attemptKey: row.attemptKey,
3761
+ gatewayAccount: row.gatewayAccount,
3762
+ paymentMethodId: row.paymentMethodId,
3763
+ paymentMethodType: oneOf(
3764
+ PAYMENT_METHOD_TYPES,
3765
+ row.paymentMethodType,
3766
+ where("paymentMethodType")
3767
+ ),
3768
+ customerRef: row.customerRef,
3769
+ paymentMethodRef: row.paymentMethodRef,
3770
+ invoiceNumber: row.invoiceNumber,
3771
+ amount,
3772
+ currency: row.currency,
3773
+ status,
3774
+ paymentRef: row.paymentRef,
3775
+ failureReason: row.failureReason === null ? null : oneOf(COLLECTION_FAILURE_REASONS, row.failureReason, where("failureReason")),
3776
+ failureCode: row.failureCode,
3777
+ submittedAt: row.submittedAt,
3778
+ heldAt: row.heldAt,
3779
+ resolvedAt: row.resolvedAt,
3780
+ collectOn,
3781
+ announcedAt: row.announcedAt,
3782
+ followedUpAt: row.followedUpAt
3783
+ };
3784
+ }
3785
+ function invoiceCollectionColumns(attempt, number) {
3786
+ return {
3787
+ invoiceId: attempt.invoiceId,
3788
+ attempt: number,
3789
+ attemptKey: collectionAttemptKey(attempt.invoiceId, number),
3790
+ gatewayAccount: attempt.gatewayAccount,
3791
+ paymentMethodId: attempt.paymentMethodId,
3792
+ paymentMethodType: attempt.paymentMethodType,
3793
+ customerRef: attempt.customerRef,
3794
+ paymentMethodRef: attempt.paymentMethodRef,
3795
+ invoiceNumber: attempt.invoiceNumber,
3796
+ amount: attempt.amount.toFixed(2),
3797
+ currency: attempt.currency,
3798
+ status: "SUBMITTED",
3799
+ submittedAt: attempt.at,
3800
+ heldAt: attempt.at,
3801
+ collectOn: null
3802
+ };
3803
+ }
3804
+ function plannedCollectionColumns(plan, number) {
3805
+ return {
3806
+ invoiceId: plan.invoiceId,
3807
+ attempt: number,
3808
+ attemptKey: collectionAttemptKey(plan.invoiceId, number),
3809
+ gatewayAccount: plan.gatewayAccount,
3810
+ paymentMethodId: plan.paymentMethodId,
3811
+ paymentMethodType: plan.paymentMethodType,
3812
+ customerRef: plan.customerRef,
3813
+ paymentMethodRef: plan.paymentMethodRef,
3814
+ invoiceNumber: plan.invoiceNumber,
3815
+ amount: plan.amount.toFixed(2),
3816
+ currency: plan.currency,
3817
+ status: "SCHEDULED",
3818
+ submittedAt: null,
3819
+ heldAt: null,
3820
+ collectOn: plan.collectOn
3821
+ };
3822
+ }
3823
+ function collectionOutcomeColumns(outcome, at) {
3824
+ const failed = outcome.status === "failed" ? outcome : null;
3825
+ return {
3826
+ status: collectionStatusOf(outcome),
3827
+ paymentRef: outcome.paymentRef,
3828
+ failureReason: failed?.reason ?? null,
3829
+ failureCode: failed?.gatewayCode ?? null,
3830
+ resolvedAt: outcome.status === "pending" ? null : at
3831
+ };
3832
+ }
3833
+
3834
+ // src/invoice-collection-plan.ts
3835
+ var COLLECTION_RETRY_AFTER_DAYS = 7;
3836
+ function isChargedAgain(failed, attempts) {
3837
+ if (failed.status !== "FAILED" || failed.failureReason === null) return false;
3838
+ if (!isRetryableCollectionFailure(failed.failureReason)) return false;
3839
+ return !attempts.some(
3840
+ (other) => other.id !== failed.id && other.status === "FAILED" && other.paymentMethodId === failed.paymentMethodId
3841
+ );
3842
+ }
3843
+ function chargedAgainOn(failedOn) {
3844
+ return addDaysToDay(failedOn, COLLECTION_RETRY_AFTER_DAYS);
3845
+ }
3846
+ function announcedCollectionDay(earliest2, announcedOn, leadDays) {
3847
+ const afterLead = addDaysToDay(announcedOn, leadDays);
3848
+ return afterLead > earliest2 ? afterLead : earliest2;
3849
+ }
3850
+
3851
+ // src/invoice-collection-notice-mapping.ts
3852
+ var TABLE6 = "subscription_notices";
3853
+ var NEXT_ACTIONS = [
3854
+ "charged-again",
3855
+ "announced-again",
3856
+ "replace-payment-method"
3857
+ ];
3858
+ var AFTER_REVERSAL_ACTIONS = [
3859
+ "collected-with-payment-method-in-use",
3860
+ "replace-payment-method"
3861
+ ];
3862
+ var NoticeFields = class _NoticeFields {
3863
+ constructor(content, id) {
3864
+ this.content = content;
3865
+ this.id = id;
3866
+ }
3867
+ content;
3868
+ id;
3869
+ where(column) {
3870
+ return { table: TABLE6, column: `content.${column}`, id: this.id };
3871
+ }
3872
+ value(name) {
3873
+ if (this.content === null || typeof this.content !== "object") {
3874
+ throw new Error(`${TABLE6}.content of ${this.id} is not a notice.`);
3875
+ }
3876
+ return Object.getOwnPropertyDescriptor(this.content, name)?.value;
3877
+ }
3878
+ text(name) {
3879
+ const value = this.value(name);
3880
+ if (typeof value !== "string" || value === "") {
3881
+ throw new Error(`${TABLE6}.content.${name} of ${this.id} is not a text.`);
3882
+ }
3883
+ return value;
3884
+ }
3885
+ amount(name) {
3886
+ const value = this.value(name);
3887
+ if (typeof value !== "number" || !Number.isFinite(value)) {
3888
+ throw new Error(`${TABLE6}.content.${name} of ${this.id} is not an amount.`);
3889
+ }
3890
+ return value;
3891
+ }
3892
+ day(name) {
3893
+ return dayOf2(this.value(name), this.where(name));
3894
+ }
3895
+ oneOf(allowed, name) {
3896
+ return oneOf(allowed, this.text(name), this.where(name));
3897
+ }
3898
+ nested(name) {
3899
+ return new _NoticeFields(this.value(name), this.id);
3900
+ }
3901
+ invoice() {
3902
+ return {
3903
+ tenantId: this.text("tenantId"),
3904
+ subscriptionId: this.text("subscriptionId"),
3905
+ subscriberId: this.text("subscriberId"),
3906
+ invoiceId: this.text("invoiceId"),
3907
+ invoiceNumber: this.text("invoiceNumber"),
3908
+ amount: this.amount("amount"),
3909
+ currency: this.text("currency")
3910
+ };
3911
+ }
3912
+ };
3913
+ function toCollectionFailedNotice(content, id) {
3914
+ const fields = new NoticeFields(content, id);
3915
+ const next = fields.nested("next");
3916
+ const action = next.oneOf(NEXT_ACTIONS, "action");
3917
+ return {
3918
+ kind: fields.oneOf(["collection-failed"], "kind"),
3919
+ ...fields.invoice(),
3920
+ paymentMethodType: fields.oneOf(PAYMENT_METHOD_TYPES, "paymentMethodType"),
3921
+ reason: fields.oneOf(COLLECTION_FAILURE_REASONS, "reason"),
3922
+ next: action === "charged-again" ? { action, on: next.day("on") } : { action }
3923
+ };
3924
+ }
3925
+ function toCollectionAnnouncedNotice(content, id) {
3926
+ const fields = new NoticeFields(content, id);
3927
+ return {
3928
+ kind: fields.oneOf(["collection-announced"], "kind"),
3929
+ ...fields.invoice(),
3930
+ collectOn: fields.day("collectOn"),
3931
+ mandateReference: fields.text("mandateReference"),
3932
+ last4: fields.text("last4")
3933
+ };
3934
+ }
3935
+ function toCollectionReversedNotice(content, id) {
3936
+ const fields = new NoticeFields(content, id);
3937
+ return {
3938
+ kind: fields.oneOf(["collection-reversed"], "kind"),
3939
+ ...fields.invoice(),
3940
+ paymentMethodType: fields.oneOf(PAYMENT_METHOD_TYPES, "paymentMethodType"),
3941
+ owed: fields.amount("owed"),
3942
+ next: { action: fields.nested("next").oneOf(AFTER_REVERSAL_ACTIONS, "action") }
3943
+ };
3944
+ }
3945
+
3946
+ // src/subscriber-payment.types.ts
3947
+ var SUBSCRIBER_CREDIT_CAPABILITY = "credits.read";
3948
+ var SUBSCRIBER_PAYOUT_CAPABILITY = "credits.payout";
3949
+
3950
+ // src/column-amount.ts
3951
+ function columnAmountOf(value, where) {
3952
+ const amount = Number(value === null || value === void 0 ? NaN : String(value));
3953
+ if (!Number.isFinite(amount)) {
3954
+ throw new Error(`${where.table}.${where.column} of ${where.id} is not an amount.`);
3955
+ }
3956
+ return amount;
3957
+ }
3958
+
3959
+ // src/subscriber-payment-mapping.ts
3960
+ var PAYMENTS = "subscriber_payments";
3961
+ var CREDITS = "subscriber_credits";
3962
+ var CREDIT_ENTRIES = "subscriber_credit_entries";
3963
+ var REFUNDS = "subscriber_refunds";
3964
+ var SETTLEMENTS = "subscription_invoice_settlements";
3965
+ var REVERSALS = "subscriber_reversals";
3966
+ var INVOICE_SETTLEMENT_KINDS = [
3967
+ "PAYMENT",
3968
+ "REVERSAL",
3969
+ "REINSTATEMENT"
3970
+ ];
3971
+ var REOPENING_SETTLEMENT_KINDS = ["REVERSAL"];
3972
+ var SUBSCRIBER_CREDIT_ENTRY_KINDS = [
3973
+ "REFUND",
3974
+ "REFUND_FAILED",
3975
+ "PAYOUT",
3976
+ "REVERSAL",
3977
+ "REVERSAL_TAKEN_BACK"
3978
+ ];
3979
+ var REFUND_ENTRY_KINDS = /* @__PURE__ */ new Set([
3980
+ "REFUND",
3981
+ "REFUND_FAILED"
3982
+ ]);
3983
+ var REVERSAL_ENTRY_KINDS = /* @__PURE__ */ new Set([
3984
+ "REVERSAL",
3985
+ "REVERSAL_TAKEN_BACK"
3986
+ ]);
3987
+ var SUBSCRIBER_REFUND_STATUSES = [
3988
+ "PENDING",
3989
+ "SUCCEEDED",
3990
+ "FAILED",
3991
+ "CANCELED"
3992
+ ];
3993
+ var ENDED_REFUND_STATUSES = ["FAILED", "CANCELED"];
3994
+ function isEndedRefund(status) {
3995
+ return ENDED_REFUND_STATUSES.includes(status);
3996
+ }
3997
+ function toSubscriberPaymentRecord(row) {
3998
+ return {
3999
+ id: row.id,
4000
+ subscriberId: row.subscriberId,
4001
+ tenantId: row.tenantId,
4002
+ invoiceId: row.invoiceId,
4003
+ collectionId: row.collectionId,
4004
+ gatewayAccount: row.gatewayAccount,
4005
+ provider: row.provider,
4006
+ gatewayRef: row.gatewayRef,
4007
+ amount: columnAmountOf(row.amount, { table: PAYMENTS, column: "amount", id: row.id }),
4008
+ currency: row.currency,
4009
+ recordedAt: row.recordedAt
4010
+ };
4011
+ }
4012
+ function subscriberPaymentColumns(payment, invoice) {
4013
+ return {
4014
+ subscriberId: invoice.subscriberId,
4015
+ tenantId: invoice.tenantId,
4016
+ invoiceId: payment.invoiceId,
4017
+ collectionId: payment.collectionId,
4018
+ gatewayAccount: payment.gatewayAccount,
4019
+ provider: payment.provider,
4020
+ gatewayRef: payment.gatewayRef,
4021
+ amount: toDecimalString(payment.amount),
4022
+ currency: payment.currency,
4023
+ recordedAt: payment.at
4024
+ };
4025
+ }
4026
+ function settledAmountOf(row) {
4027
+ const kind = oneOf(INVOICE_SETTLEMENT_KINDS, row.kind, {
4028
+ table: SETTLEMENTS,
4029
+ column: "kind",
4030
+ id: row.id
4031
+ });
4032
+ const amount = columnAmountOf(row.amount, { table: SETTLEMENTS, column: "amount", id: row.id });
4033
+ return REOPENING_SETTLEMENT_KINDS.includes(kind) ? -amount : amount;
4034
+ }
4035
+ function toSubscriberCreditRecord(row) {
4036
+ return {
4037
+ id: row.id,
4038
+ subscriberId: row.subscriberId,
4039
+ tenantId: row.tenantId,
4040
+ amount: columnAmountOf(row.amount, { table: CREDITS, column: "amount", id: row.id }),
4041
+ currency: row.currency,
4042
+ origin: creditOriginOf(row),
4043
+ createdAt: row.createdAt
4044
+ };
4045
+ }
4046
+ function creditOriginOf(row) {
4047
+ const origins = [];
4048
+ if (row.originPaymentId !== null) {
4049
+ origins.push({ kind: "payment", paymentId: row.originPaymentId });
4050
+ }
4051
+ if (row.originInvoiceId !== null) {
4052
+ origins.push({ kind: "invoice", invoiceId: row.originInvoiceId });
4053
+ }
4054
+ if (row.originReversalId !== null) {
4055
+ origins.push({ kind: "reversal", reversalId: row.originReversalId });
4056
+ }
4057
+ const [origin] = origins;
4058
+ if (origin && origins.length === 1) return origin;
4059
+ throw new Error(
4060
+ `${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.`
4061
+ );
4062
+ }
4063
+ function toSubscriberCreditEntryRecord(row) {
4064
+ const where = (column) => ({ table: CREDIT_ENTRIES, column, id: row.id });
4065
+ const kind = oneOf(SUBSCRIBER_CREDIT_ENTRY_KINDS, row.kind, where("kind"));
4066
+ const paidOn = row.paidOn === null || row.paidOn === void 0 ? null : dayOf2(row.paidOn, where("paidOn"));
4067
+ if (REFUND_ENTRY_KINDS.has(kind) !== (row.refundId !== null)) {
4068
+ throw new Error(
4069
+ `${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.`
4070
+ );
4071
+ }
4072
+ if (REVERSAL_ENTRY_KINDS.has(kind) !== (row.reversalId !== null)) {
4073
+ throw new Error(
4074
+ `${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.`
4075
+ );
4076
+ }
4077
+ return {
4078
+ id: row.id,
4079
+ creditId: row.creditId,
4080
+ kind,
4081
+ amount: columnAmountOf(row.amount, where("amount")),
4082
+ refundId: row.refundId,
4083
+ reversalId: row.reversalId,
4084
+ reference: row.reference,
4085
+ paidOn,
4086
+ recordedBy: row.recordedBy,
4087
+ note: row.note,
4088
+ recordedAt: row.recordedAt
4089
+ };
4090
+ }
4091
+ function refundEntryColumns(creditId, refund, kind, amount, at) {
4092
+ return {
4093
+ creditId,
4094
+ kind,
4095
+ amount: toDecimalString(amount),
4096
+ refundId: refund.id,
4097
+ reversalId: null,
4098
+ reference: refund.gatewayRef,
4099
+ paidOn: null,
4100
+ recordedBy: null,
4101
+ note: null,
4102
+ recordedAt: at
4103
+ };
4104
+ }
4105
+ function payoutEntryColumns(payout) {
4106
+ return {
4107
+ creditId: payout.creditId,
4108
+ kind: "PAYOUT",
4109
+ amount: toDecimalString(payout.amount),
4110
+ refundId: null,
4111
+ reversalId: null,
4112
+ reference: payout.reference,
4113
+ paidOn: payout.paidOn,
4114
+ recordedBy: payout.recordedBy,
4115
+ note: payout.note,
4116
+ recordedAt: payout.at
4117
+ };
4118
+ }
4119
+ function toSubscriberRefundRecord(row) {
4120
+ const where = (column) => ({ table: REFUNDS, column, id: row.id });
4121
+ const status = oneOf(SUBSCRIBER_REFUND_STATUSES, row.status, where("status"));
4122
+ if (isEndedRefund(status) !== (row.endedAt !== null)) {
4123
+ throw new Error(
4124
+ `${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.`
4125
+ );
4126
+ }
4127
+ return {
4128
+ id: row.id,
4129
+ subscriberId: row.subscriberId,
4130
+ paymentId: row.paymentId,
4131
+ gatewayAccount: row.gatewayAccount,
4132
+ gatewayRef: row.gatewayRef,
4133
+ amount: columnAmountOf(row.amount, where("amount")),
4134
+ currency: row.currency,
4135
+ status,
4136
+ recordedAt: row.recordedAt,
4137
+ endedAt: row.endedAt
4138
+ };
4139
+ }
4140
+ function subscriberRefundColumns(refund, payment) {
4141
+ return {
4142
+ subscriberId: payment.subscriberId,
4143
+ paymentId: payment.id,
4144
+ gatewayAccount: refund.gatewayAccount,
4145
+ gatewayRef: refund.gatewayRef,
4146
+ amount: toDecimalString(refund.amount),
4147
+ currency: refund.currency,
4148
+ status: refund.status,
4149
+ recordedAt: refund.at,
4150
+ endedAt: isEndedRefund(refund.status) ? refund.at : null
4151
+ };
4152
+ }
4153
+ function reversalEntryColumns(creditId, reversal, kind, amount, at) {
4154
+ return {
4155
+ creditId,
4156
+ kind,
4157
+ amount: toDecimalString(amount),
4158
+ refundId: null,
4159
+ reversalId: reversal.id,
4160
+ reference: reversal.gatewayRef,
4161
+ paidOn: null,
4162
+ recordedBy: null,
4163
+ note: null,
4164
+ recordedAt: at
4165
+ };
4166
+ }
4167
+ function toSubscriberReversalRecord(row) {
4168
+ const where = (column) => ({ table: REVERSALS, column, id: row.id });
4169
+ const amount = columnAmountOf(row.amount, where("amount"));
4170
+ const fromCredit = columnAmountOf(row.fromCredit, where("fromCredit"));
4171
+ const reinstated = columnAmountOf(row.reinstated, where("reinstated"));
4172
+ const within = (part) => part >= 0 && toCents(part) <= toCents(amount);
4173
+ if (!within(fromCredit) || !within(reinstated)) {
4174
+ throw new Error(
4175
+ `${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.`
4176
+ );
4177
+ }
4178
+ if (reinstated > 0 !== (row.reinstatedAt !== null)) {
4179
+ throw new Error(
4180
+ `${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.`
4181
+ );
4182
+ }
4183
+ return {
4184
+ id: row.id,
4185
+ subscriberId: row.subscriberId,
4186
+ tenantId: row.tenantId,
4187
+ paymentId: row.paymentId,
4188
+ gatewayAccount: row.gatewayAccount,
4189
+ gatewayRef: row.gatewayRef,
4190
+ amount,
4191
+ currency: row.currency,
4192
+ fromCredit,
4193
+ reinstated,
4194
+ recordedAt: row.recordedAt,
4195
+ reinstatedAt: row.reinstatedAt
4196
+ };
4197
+ }
4198
+ function subscriberReversalColumns(reversal, payment) {
4199
+ return {
4200
+ subscriberId: payment.subscriberId,
4201
+ tenantId: payment.tenantId,
4202
+ paymentId: payment.id,
4203
+ gatewayAccount: reversal.gatewayAccount,
4204
+ gatewayRef: reversal.gatewayRef,
4205
+ amount: toDecimalString(0),
4206
+ currency: reversal.currency,
4207
+ fromCredit: toDecimalString(0),
4208
+ reinstated: toDecimalString(0),
4209
+ recordedAt: reversal.at,
4210
+ reinstatedAt: null
4211
+ };
4212
+ }
4213
+
4214
+ // src/subscriber-payment-rules.ts
4215
+ function owedOn(gross, settled) {
4216
+ const paid = settled.reduce((sum2, amount) => sum2 + toCents(amount), 0);
4217
+ return (toCents(gross) - paid) / 100;
4218
+ }
4219
+ function paymentSettlement(amount, owed) {
4220
+ const brought = toCents(amount);
4221
+ const settles = Math.min(brought, Math.max(0, toCents(owed)));
4222
+ return { settles: settles / 100, credits: (brought - settles) / 100 };
4223
+ }
4224
+ var GIVING_BACK_CREDIT_ENTRY_KINDS = [
4225
+ "REFUND_FAILED",
4226
+ "REVERSAL_TAKEN_BACK"
4227
+ ];
4228
+ var GIVING_BACK = new Set(GIVING_BACK_CREDIT_ENTRY_KINDS);
4229
+ function paidOutBy(entry) {
4230
+ const cents2 = toCents(entry.amount);
4231
+ return GIVING_BACK.has(entry.kind) ? -cents2 : cents2;
4232
+ }
4233
+ function openOfCredit(amount, entries) {
4234
+ const paidOut = entries.reduce((sum2, entry) => sum2 + paidOutBy(entry), 0);
4235
+ return Math.max(0, toCents(amount) - paidOut) / 100;
4236
+ }
4237
+ function refundParts(amount, paymentId, credits) {
4238
+ const ownFirst = [...credits].sort(
4239
+ (a, b) => rankFor(a, paymentId) - rankFor(b, paymentId) || a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id, "en")
4240
+ );
4241
+ let left = toCents(amount);
4242
+ const parts = [];
4243
+ for (const credit of ownFirst) {
4244
+ if (left === 0) break;
4245
+ const taken = Math.min(left, toCents(credit.open));
4246
+ if (taken <= 0) continue;
4247
+ parts.push({ creditId: credit.id, amount: taken / 100 });
4248
+ left -= taken;
4249
+ }
4250
+ return { parts, unmatched: left / 100 };
4251
+ }
4252
+ function rankFor(credit, paymentId) {
4253
+ return credit.origin.kind === "payment" && credit.origin.paymentId === paymentId ? 0 : 1;
4254
+ }
4255
+ function withdrawalParts(amount, credit, invoices, covering = []) {
4256
+ let left = toCents(amount);
4257
+ const share = credit ? Math.min(left, Math.max(0, toCents(credit.left))) : 0;
4258
+ const open = credit ? Math.min(share, Math.max(0, toCents(credit.open))) : 0;
4259
+ left -= share;
4260
+ const opening = [];
4261
+ for (const invoice of invoices) {
4262
+ if (left === 0) break;
4263
+ const taken = Math.min(left, Math.max(0, toCents(invoice.left)));
4264
+ if (taken === 0) continue;
4265
+ opening.push({ invoiceId: invoice.invoiceId, cents: taken });
4266
+ left -= taken;
4267
+ }
4268
+ let toCover = opening.reduce((sum2, each) => sum2 + each.cents, 0);
4269
+ const covered = [];
4270
+ for (const other of covering) {
4271
+ if (toCover === 0) break;
4272
+ const taken = Math.min(toCover, Math.max(0, toCents(other.open)));
4273
+ if (taken === 0) continue;
4274
+ covered.push({ creditId: other.creditId, amount: taken / 100 });
4275
+ toCover -= taken;
4276
+ }
4277
+ let coveredCents = opening.reduce((sum2, each) => sum2 + each.cents, 0) - toCover;
4278
+ const opened = [];
4279
+ for (const each of opening) {
4280
+ const kept = Math.min(each.cents, coveredCents);
4281
+ coveredCents -= kept;
4282
+ if (each.cents > kept) {
4283
+ opened.push({ invoiceId: each.invoiceId, amount: (each.cents - kept) / 100 });
4284
+ }
4285
+ }
4286
+ return {
4287
+ fromCredit: share / 100,
4288
+ credit: credit && open > 0 ? { creditId: credit.creditId, amount: open / 100 } : null,
4289
+ covering: covered,
4290
+ invoices: opened,
4291
+ unmatched: (share - open + left) / 100
4292
+ };
4293
+ }
4294
+ function reinstatementParts(amount, took) {
4295
+ let left = toCents(amount);
4296
+ const covered = Math.min(left, Math.max(0, toCents(took.unmatched)));
4297
+ left -= covered;
4298
+ const credits = [];
4299
+ for (const credit of took.credits) {
4300
+ const given = Math.min(left, Math.max(0, toCents(credit.left)));
4301
+ if (given === 0) continue;
4302
+ credits.push({ creditId: credit.creditId, amount: given / 100 });
4303
+ left -= given;
4304
+ }
4305
+ const invoices = [];
4306
+ let credited = 0;
4307
+ for (const invoice of took.invoices) {
4308
+ const given = Math.min(left, Math.max(0, toCents(invoice.left)));
4309
+ if (given === 0) continue;
4310
+ const settles = Math.min(given, Math.max(0, toCents(invoice.owed)));
4311
+ if (settles > 0) invoices.push({ invoiceId: invoice.invoiceId, amount: settles / 100 });
4312
+ credited += given - settles;
4313
+ left -= given;
4314
+ }
4315
+ return {
4316
+ covered: covered / 100,
4317
+ credits,
4318
+ invoices,
4319
+ credited: credited / 100,
4320
+ beyond: left / 100
4321
+ };
4322
+ }
4323
+ function netByInvoice(entries) {
4324
+ const net = /* @__PURE__ */ new Map();
4325
+ for (const entry of entries) {
4326
+ net.set(entry.invoiceId, (net.get(entry.invoiceId) ?? 0) + toCents(entry.amount));
4327
+ }
4328
+ return [...net].map(([invoiceId, cents2]) => ({ invoiceId, amount: cents2 / 100 }));
4329
+ }
4330
+ function paidOutByCredit(entries) {
4331
+ const paid = /* @__PURE__ */ new Map();
4332
+ for (const entry of entries) {
4333
+ paid.set(entry.creditId, (paid.get(entry.creditId) ?? 0) + paidOutBy(entry));
4334
+ }
4335
+ return [...paid].map(([creditId, cents2]) => ({ creditId, amount: cents2 / 100 }));
4336
+ }
4337
+ function coversOf(entries, own) {
4338
+ const taken = /* @__PURE__ */ new Map();
4339
+ for (const entry of entries) {
4340
+ if (entry.reversalId === null || own.has(entry.reversalId)) continue;
4341
+ taken.set(entry.reversalId, (taken.get(entry.reversalId) ?? 0) + paidOutBy(entry));
4342
+ }
4343
+ return [...taken].filter(([, cents2]) => cents2 > 0).map(([reversalId, cents2]) => ({ reversalId, amount: cents2 / 100 }));
4344
+ }
4345
+ function coversLifted(parts, covers) {
4346
+ let reach = toCents(parts.fromCredit) - toCents(parts.credit?.amount ?? 0);
4347
+ const lifted = [];
4348
+ for (const cover of covers) {
4349
+ if (reach <= 0) break;
4350
+ const taken = Math.min(reach, toCents(cover.amount));
4351
+ if (taken <= 0) continue;
4352
+ lifted.push({ reversalId: cover.reversalId, amount: taken / 100 });
4353
+ reach -= taken;
4354
+ }
4355
+ const liftedCents = lifted.reduce((sum2, each) => sum2 + toCents(each.amount), 0);
4356
+ return { lifted, unmatched: (toCents(parts.unmatched) - liftedCents) / 100 };
4357
+ }
4358
+ function inWithdrawalOrder(parts, credits, paymentId) {
4359
+ const byId = new Map(credits.map((credit) => [credit.id, credit]));
4360
+ const rank = (part) => {
4361
+ const credit = byId.get(part.creditId);
4362
+ if (!credit) return [2, 0, part.creditId];
4363
+ const own = credit.origin.kind === "payment" && credit.origin.paymentId === paymentId;
4364
+ return [own ? 0 : 1, credit.createdAt.getTime(), credit.id];
4365
+ };
4366
+ return [...parts].sort((a, b) => {
4367
+ const [x, y] = [rank(a), rank(b)];
4368
+ return x[0] - y[0] || x[1] - y[1] || x[2].localeCompare(y[2], "en");
4369
+ });
4370
+ }
4371
+ function casesCovered(cases, covered) {
4372
+ const limit = toCents(covered);
4373
+ let running = 0;
4374
+ const ids = [];
4375
+ for (const each of cases) {
4376
+ running += toCents(each.amount);
4377
+ if (running <= limit) ids.push(each.id);
4378
+ }
4379
+ return ids;
4380
+ }
4381
+
4382
+ // src/subscriber-reconciliation.types.ts
4383
+ var RECONCILIATION_READ_CAPABILITY = "reconciliation.read";
4384
+ var RECONCILIATION_CLOSE_CAPABILITY = "reconciliation.close";
4385
+ var SUBSCRIBER_PAYMENTS_CAPABILITY = "payments.read";
4386
+ var RECONCILIATION_NOTE_MAX_LENGTH = 500;
4387
+
4388
+ // src/subscriber-reconciliation-mapping.ts
4389
+ var CASES2 = "subscriber_reconciliation_cases";
4390
+ var RECONCILIATION_CASE_KINDS = [
4391
+ "REFUND_UNMATCHED",
4392
+ "REVERSAL_UNMATCHED"
4393
+ ];
4394
+ function toSubscriberReconciliationCaseRecord(row) {
4395
+ const where = (column) => ({ table: CASES2, column, id: row.id });
4396
+ const kind = oneOf(RECONCILIATION_CASE_KINDS, row.kind, where("kind"));
4397
+ const aboutRefund = kind === "REFUND_UNMATCHED";
4398
+ if (aboutRefund !== (row.refundId !== null) || aboutRefund === (row.reversalId !== null)) {
4399
+ throw new Error(
4400
+ `${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.`
4401
+ );
4402
+ }
4403
+ if (row.closedAt === null !== (row.note === null)) {
4404
+ throw new Error(
4405
+ `${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.`
4406
+ );
4407
+ }
4408
+ if (row.closedAt === null && row.closedBy !== null) {
4409
+ throw new Error(`${CASES2} row '${row.id}' is open and names who closed it.`);
4410
+ }
4411
+ return {
4412
+ id: row.id,
4413
+ subscriberId: row.subscriberId,
4414
+ tenantId: row.tenantId,
4415
+ kind,
4416
+ amount: columnAmountOf(row.amount, where("amount")),
4417
+ currency: row.currency,
4418
+ paymentId: row.paymentId,
4419
+ refundId: row.refundId,
4420
+ reversalId: row.reversalId,
4421
+ gatewayAccount: row.gatewayAccount,
4422
+ gatewayRef: row.gatewayRef,
4423
+ recordedAt: row.recordedAt,
4424
+ closedAt: row.closedAt,
4425
+ closedBy: row.closedBy,
4426
+ note: row.note
4427
+ };
4428
+ }
4429
+ function reconciliationCaseColumns(opened) {
4430
+ const aboutRefund = opened.kind === "REFUND_UNMATCHED";
4431
+ return {
4432
+ subscriberId: opened.payment.subscriberId,
4433
+ tenantId: opened.payment.tenantId,
4434
+ kind: opened.kind,
4435
+ amount: toDecimalString(opened.amount),
4436
+ currency: opened.currency,
4437
+ paymentId: opened.payment.id,
4438
+ refundId: aboutRefund ? opened.about.id : null,
4439
+ reversalId: aboutRefund ? null : opened.about.id,
4440
+ gatewayAccount: opened.about.gatewayAccount,
4441
+ gatewayRef: opened.about.gatewayRef,
4442
+ recordedAt: opened.at,
4443
+ closedAt: null,
4444
+ closedBy: null,
4445
+ note: null
4446
+ };
4447
+ }
4448
+ var CASE_CLOSED_REFUND_ENDED = "The refund failed or was canceled, and its money came back.";
4449
+ var CASE_CLOSED_REVERSAL_GIVEN_BACK = "The gateway gave the reversal back, and with it what matched nothing.";
4450
+
4451
+ // src/e-invoice-format.ts
4452
+ var CATEGORY = {
4453
+ standard: "S",
4454
+ reduced: "S",
4455
+ exempt: "E",
4456
+ "small-business": "E",
4457
+ "reverse-charge": "AE",
4458
+ "not-taxable": "O"
4459
+ };
4460
+ function vatCategoryOf(kind) {
4461
+ return CATEGORY[kind];
4462
+ }
4463
+ var NOT_XML = /[^\t\n\r\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/gu;
4464
+ function holdsOnlyXmlCharacters(text) {
4465
+ return text.search(NOT_XML) === -1;
4466
+ }
4467
+ var CARRIAGE_RETURN = /\r\n?/g;
4468
+ function asXmlReadsIt(text) {
4469
+ return text.replace(NOT_XML, "?").replace(CARRIAGE_RETURN, "\n");
4470
+ }
4471
+
4472
+ // src/subscriber-mapping.ts
4473
+ function formatCustomerNumber(prefix, sequence) {
4474
+ return `${prefix}${sequence}`;
4475
+ }
4476
+ function toSubscriberRecord(row, tenantId) {
4477
+ return {
4478
+ id: row.id,
4479
+ customerNumber: formatCustomerNumber(row.customerNumberPrefix, row.customerSequence),
4480
+ legalName: row.legalName,
4481
+ addressLine1: row.addressLine1,
4482
+ addressLine2: row.addressLine2,
4483
+ postalCode: row.postalCode,
4484
+ city: row.city,
4485
+ country: row.country,
4486
+ vatId: row.vatId,
4487
+ taxNumber: row.taxNumber,
4488
+ invoiceEmail: row.invoiceEmail,
4489
+ business: row.business,
4490
+ tenantId,
4491
+ migrated: row.migrated,
4492
+ createdAt: row.createdAt,
4493
+ updatedAt: row.updatedAt
4494
+ };
4495
+ }
4496
+ function toSubscriberCorrectionRecord(row) {
4497
+ return {
4498
+ id: row.id,
4499
+ subscriberId: row.subscriberId,
4500
+ previous: toIdentityValues(row.previous),
4501
+ corrected: toIdentityValues(row.corrected),
4502
+ reason: row.reason,
4503
+ correctedBy: row.correctedBy,
4504
+ correctedAt: row.correctedAt
4505
+ };
4506
+ }
4507
+ function toSubscriberTaxOriginChangeRecord(row) {
4508
+ return {
4509
+ id: row.id,
4510
+ subscriberId: row.subscriberId,
4511
+ previous: toTaxOriginValues(row.previous),
4512
+ changed: toTaxOriginValues(row.changed),
4513
+ changedBy: row.changedBy,
4514
+ changedAt: row.changedAt,
4515
+ reason: row.reason
4516
+ };
4517
+ }
4518
+ function identityCorrectionDelta(current, corrected) {
4519
+ const previous = {};
4520
+ const next = {};
4521
+ for (const field of LEGAL_IDENTITY_FIELDS) {
4522
+ const value = corrected[field];
4523
+ if (value === void 0 || value === current[field]) continue;
4524
+ previous[field] = current[field];
4525
+ next[field] = value;
4526
+ }
4527
+ return {
4528
+ previous,
4529
+ corrected: next
4530
+ };
4531
+ }
4532
+ function contractPartiesOf(subscriber, issuer) {
4533
+ const party = {
4534
+ customerNumber: subscriber.customerNumber,
4535
+ legalName: subscriber.legalName,
4536
+ vatId: subscriber.vatId,
4537
+ taxNumber: subscriber.taxNumber,
4538
+ addressLine1: subscriber.addressLine1,
4539
+ addressLine2: subscriber.addressLine2,
4540
+ postalCode: subscriber.postalCode,
4541
+ city: subscriber.city,
4542
+ country: subscriber.country
4543
+ };
4544
+ return {
4545
+ subscriberId: subscriber.id,
4546
+ subscriber: party,
4547
+ issuer: issuer ? issuerPartyOf(issuer) : null
4548
+ };
4549
+ }
4550
+ function issuerPartyOf(issuer) {
4551
+ const identity = issuerIdentityOf(issuer);
4552
+ return {
4553
+ legalName: identity?.legalName ?? "",
4554
+ vatId: identity?.vatId ?? null,
2660
4555
  taxNumber: identity?.taxNumber ?? null,
2661
4556
  // Settled like the three above rather than copied verbatim: one copy
2662
4557
  // whose halves were settled differently is a row that reads two ways,
@@ -2700,75 +4595,6 @@ function toIdentityValues(value) {
2700
4595
  return values;
2701
4596
  }
2702
4597
 
2703
- // src/subscriber-payment-method-mapping.ts
2704
- var PAYMENT_METHOD_TYPES = ["card", "sepa_debit"];
2705
- var STATUSES = ["ACTIVE", "REPLACED"];
2706
- var TABLE3 = "subscriber_payment_methods";
2707
- function toSubscriberPaymentMethodRecord(row) {
2708
- return {
2709
- id: row.id,
2710
- subscriberId: row.subscriberId,
2711
- gatewayAccount: row.gatewayAccount,
2712
- provider: row.provider,
2713
- customerRef: row.customerRef,
2714
- paymentMethodRef: row.paymentMethodRef,
2715
- type: oneOf(PAYMENT_METHOD_TYPES, row.type, { table: TABLE3, column: "type", id: row.id }),
2716
- brand: row.brand,
2717
- last4: row.last4,
2718
- expiryMonth: row.expiryMonth,
2719
- expiryYear: row.expiryYear,
2720
- country: row.country,
2721
- bankCode: row.bankCode,
2722
- mandateReference: row.mandateReference,
2723
- status: oneOf(STATUSES, row.status, { table: TABLE3, column: "status", id: row.id }),
2724
- confirmedAt: row.confirmedAt,
2725
- replacedAt: row.replacedAt,
2726
- createdAt: row.createdAt
2727
- };
2728
- }
2729
- function subscriberPaymentMethodColumns(data) {
2730
- return {
2731
- subscriberId: data.subscriberId,
2732
- gatewayAccount: data.gatewayAccount,
2733
- provider: data.provider,
2734
- customerRef: data.customerRef,
2735
- paymentMethodRef: data.paymentMethodRef,
2736
- type: data.type,
2737
- brand: data.brand,
2738
- last4: data.last4,
2739
- expiryMonth: data.expiryMonth,
2740
- expiryYear: data.expiryYear,
2741
- country: data.country,
2742
- bankCode: data.bankCode,
2743
- mandateReference: data.mandateReference,
2744
- confirmedAt: data.confirmedAt
2745
- };
2746
- }
2747
- var FOREIGN_PAYMENT_METHOD_REFERENCE = "FOREIGN_PAYMENT_METHOD_REFERENCE";
2748
- var ForeignPaymentMethodReferenceError = class extends Error {
2749
- constructor(gatewayAccount, paymentMethodRef) {
2750
- super(
2751
- `Payment method '${paymentMethodRef}' of account '${gatewayAccount}' belongs to another subscriber. A reference belongs to exactly one, so it is not handed out.`
2752
- );
2753
- this.gatewayAccount = gatewayAccount;
2754
- this.paymentMethodRef = paymentMethodRef;
2755
- this.name = "ForeignPaymentMethodReferenceError";
2756
- }
2757
- gatewayAccount;
2758
- paymentMethodRef;
2759
- code = FOREIGN_PAYMENT_METHOD_REFERENCE;
2760
- };
2761
- function isForeignPaymentMethodReferenceError(error) {
2762
- return error instanceof Error && error.code === FOREIGN_PAYMENT_METHOD_REFERENCE;
2763
- }
2764
- function refuseForeignPaymentMethodReference(recorded, subscriberId) {
2765
- if (recorded.subscriberId === subscriberId) return;
2766
- throw new ForeignPaymentMethodReferenceError(
2767
- recorded.gatewayAccount,
2768
- recorded.paymentMethodRef
2769
- );
2770
- }
2771
-
2772
4598
  // src/subscription-bundle-mapping.ts
2773
4599
  var BILLING_CYCLES = ["MONTHLY", "YEARLY"];
2774
4600
  function toSubscriptionBundleRecord(row) {
@@ -3118,6 +4944,23 @@ var ERROR_MESSAGES_DE = {
3118
4944
  BUNDLE_RETIREMENT_REINSTATE_SUBSCRIPTION_ENDS: "Version {version} von {bundleKey} wird stillgelegt, und diese Buchung endet, bevor sie umziehen w\xFCrde. Ihr Abonnement endet bis dahin ebenfalls.",
3119
4945
  BUNDLE_RETIREMENT_SWITCH_PLAN_CHANGES: "{bundleName} zieht am {date} auf seine neue Version um, und Ihr Paket \xE4ndert sich vorher. Der Preis, den ein Wechsel halten w\xFCrde, steht deshalb noch nicht fest. Wechseln Sie, sobald Ihr Paket gewechselt hat, oder lassen Sie es an diesem Tag umziehen.",
3120
4946
  BUNDLE_VERSION_OFFER_CHANGED: "Das Angebot hat sich seit der Anzeige ge\xE4ndert. Bitte sieh dir das aktuelle an, bevor du wechselst.",
4947
+ FEATURE_WITHDRAWAL_UNAVAILABLE: "Ein Feature zur\xFCckzuziehen ist hier nicht m\xF6glich: Die Speicher k\xF6nnen nicht alle auflisten, die ein R\xFCckzug erreicht.",
4948
+ FEATURE_WITHDRAWAL_FEATURE_UNKNOWN: "Der Katalog kennt kein Feature {featureKey}.",
4949
+ FEATURE_WITHDRAWAL_DATE_IN_PAST: "Ein Feature wird ab jetzt oder ab einem sp\xE4teren Datum zur\xFCckgezogen, nie ab einem vergangenen.",
4950
+ FEATURE_WITHDRAWAL_OPEN: "{featureKey} ist bereits zur\xFCckgezogen, und dieser R\xFCckzug ist nicht aufgehoben. Heben Sie ihn auf, bevor Sie einen weiteren ank\xFCndigen.",
4951
+ FEATURE_WITHDRAWAL_OVERLAPS: "{featureKey} ist bis zum {date} zur\xFCckgezogen. Ein weiterer R\xFCckzug kann fr\xFChestens an diesem Tag beginnen.",
4952
+ FEATURE_WITHDRAWAL_REDUCTION_NOT_REACHED: "Die Minderung f\xFCr {key} bei Abrechnung {billingCycle} betrifft nichts, was dieser R\xFCckzug erreicht.",
4953
+ FEATURE_WITHDRAWAL_REDUCTION_NAMED_TWICE: "Die Minderung f\xFCr {key} bei Abrechnung {billingCycle} ist doppelt angegeben.",
4954
+ FEATURE_WITHDRAWAL_REDUCTION_EXCEEDS_PRICE: "Die Minderung f\xFCr {key} bei Abrechnung {billingCycle} ist h\xF6her als der niedrigste Preis, den sie mindert: {price}.",
4955
+ FEATURE_WITHDRAWAL_PREVIEW_CHANGED: "Die Abonnements, die dieser R\xFCckzug erreicht, haben sich seit der Anzeige ge\xE4ndert. Sehen Sie sie erneut an, bevor Sie ihn ank\xFCndigen.",
4956
+ FEATURE_WITHDRAWAL_NOT_FOUND: "R\xFCckzug {withdrawalId} nicht gefunden.",
4957
+ FEATURE_WITHDRAWAL_ALREADY_LIFTED: "Dieser R\xFCckzug ist bereits aufgehoben, ab dem {date}.",
4958
+ FEATURE_WITHDRAWAL_NOT_TOLD_YET: "\xDCber diesen R\xFCckzug ist noch niemand informiert, und er wird nicht aufgehoben, bevor das geschehen ist. Versuchen Sie es gleich noch einmal.",
4959
+ FEATURE_WITHDRAWAL_LIFT_IN_PAST: "Ein R\xFCckzug wird ab jetzt oder ab einem sp\xE4teren Datum aufgehoben, nie ab einem vergangenen.",
4960
+ FEATURE_WITHDRAWAL_NOT_IN_EFFECT: "{featureKey} ist gerade nicht zur\xFCckgezogen. Deshalb kann nichts sofort beendet werden.",
4961
+ FEATURE_WITHDRAWAL_DOES_NOT_REACH: "Dieser R\xFCckzug betrifft Ihr Abonnement nicht, oder diese Buchung nicht mehr.",
4962
+ FEATURE_WITHDRAWAL_ALREADY_ENDED: "Das ist bereits beendet.",
4963
+ FEATURE_WITHDRAWAL_END_NOW_UNSUPPORTED: "Ihre K\xFCndigung zum {date} ist erfasst und kann hier nicht vorgezogen werden. Sie endet an diesem Tag, und bis dahin gilt die Minderung.",
3121
4964
  RETIREMENT_SWITCH_NOT_PENDING: "F\xFCr Ihre Version steht keine Einstellung mehr bevor. Es gibt nichts, wohin Sie wechseln k\xF6nnten.",
3122
4965
  RETIREMENT_SWITCH_IN_TRIAL: "Der Wechsel ist m\xF6glich, sobald Ihre Testphase endet.",
3123
4966
  RETIREMENT_SWITCH_BUNDLE_CANNOT_FOLLOW: "{bundleName} kann im Paket {planName} nicht laufen und l\xE4uft fr\xFChestens bis {until}. Ist es gek\xFCndigt, ist der Wechsel ab diesem Tag m\xF6glich.",
@@ -3160,6 +5003,23 @@ var ERROR_MESSAGES_DE = {
3160
5003
  PROMO_MAX_REDEMPTIONS_LOWERED: "maxRedemptions l\xE4sst sich nicht senken.",
3161
5004
  // ── contract ──
3162
5005
  SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_DECIDED: "Der Vertrag nennt in {field} {stated} %, der Steueradapter entscheidet {decided} %.",
5006
+ SUBSCRIPTION_INVOICE_CHARGE_INVOICED: "Die Forderung '{chargeId}' steht bereits auf einer Rechnung.",
5007
+ SUBSCRIPTION_INVOICE_CONTENT_INCOMPLETE: "Der Rechnung fehlt, was der Steueradapter {adapter} verlangt: {missing}. Sie wird ausgestellt, sobald es erg\xE4nzt ist.",
5008
+ 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.",
5009
+ 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.",
5010
+ SUBSCRIPTION_INVOICE_NO_PAYMENT_METHOD: "Der Abonnent {subscriberId} hat keine Zahlungsart in Gebrauch. Die Rechnung wird eingezogen, sobald er eine hat.",
5011
+ SUBSCRIPTION_INVOICE_FAILURE_NOT_TOLD: "Die Lastschrift wird angek\xFCndigt, sobald dem Abonnenten mitgeteilt ist, dass der Einzug {attemptKey} gescheitert ist.",
5012
+ 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.",
5013
+ 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.",
5014
+ SUBSCRIPTION_INVOICE_DOCUMENT_MISMATCH: "Das archivierte Dokument der Rechnung {invoiceId} passt nicht zur beim Rendern festgehaltenen Pr\xFCfsumme und wird nicht herausgegeben.",
5015
+ SUBSCRIPTION_INVOICE_DOCUMENT_NOT_ARCHIVED: "Das Dokument der Rechnung {invoiceId} ist noch nicht archiviert.",
5016
+ SUBSCRIPTION_INVOICE_NOT_FOUND: "Keine Rechnung '{invoiceId}'.",
5017
+ SUBSCRIPTION_INVOICE_NO_INVOICE_EMAIL: "Der Abonnent {subscriberId} hat keine Rechnungs-E-Mail; seine Rechnung wird versendet, sobald eine eingetragen ist.",
5018
+ SUBSCRIBER_CREDIT_NOT_FOUND: "Kein Guthaben '{creditId}' des Abonnenten dieses Mandanten.",
5019
+ SUBSCRIBER_PAYOUT_EXCEEDS_CREDIT: "Vom Guthaben {creditId} sind noch {open} offen; mehr wird nicht ausgezahlt, und nichts wurde erfasst.",
5020
+ SUBSCRIBER_PAYOUT_NOT_MADE_YET: "Eine Auszahlung wird erfasst, wenn sie erfolgt ist: {paidOn} liegt nach heute, {today}.",
5021
+ SUBSCRIBER_RECONCILIATION_CASE_NOT_FOUND: "Kein abzugleichender Fall '{caseId}'.",
5022
+ SUBSCRIBER_RECONCILIATION_CASE_CLOSED: "Der Fall {caseId} wurde am {closedAt} geschlossen; seine Notiz bleibt, und nichts wurde erfasst.",
3163
5023
  CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "Aus einem Checkout-Angebot entsteht nur ein einziger Vertrag, und das erst, wenn seine Positionen eingefroren sind.",
3164
5024
  CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "Ein Checkout-Angebot braucht eine eingefrorene Plan-Position.",
3165
5025
  CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Jede gew\xE4hlte Bundle-Version braucht eine eingefrorene Bundle-Position.",
@@ -3250,16 +5110,37 @@ var ERROR_MESSAGES_DE = {
3250
5110
  ACTIVE_SUBSCRIPTION_CONTRACT_STATUSES,
3251
5111
  AUTH_ERROR_CODES,
3252
5112
  BILLING_ERROR_CODES,
5113
+ BLOCKING_COLLECTION_STATUSES,
3253
5114
  BUNDLE_PRICE_LOOKUP_LIMIT,
3254
5115
  BUNDLE_VERSION_RETIREMENT_CAPABILITY,
5116
+ CASE_CLOSED_REFUND_ENDED,
5117
+ CASE_CLOSED_REVERSAL_GIVEN_BACK,
3255
5118
  CATALOGUE_KEYS,
3256
5119
  CATALOG_ERROR_CODES,
5120
+ COLLECTION_FAILURE_REASONS,
5121
+ COLLECTION_NOT_SENT_PAID,
5122
+ COLLECTION_NOT_SENT_REVERSED,
5123
+ COLLECTION_RETRY_AFTER_DAYS,
3257
5124
  CONTRACT_ERROR_CODES,
3258
5125
  CUSTOM_LIMITS_KEYS,
5126
+ DIRECT_DEBIT_LEAD_DAYS,
5127
+ ENDED_REFUND_STATUSES,
3259
5128
  ERROR_MESSAGES_DE,
3260
5129
  ERROR_MESSAGES_EN,
3261
5130
  FEATURE_NOT_LICENSED,
5131
+ FEATURE_WITHDRAWAL_CAPABILITY,
5132
+ FEATURE_WITHDRAWAL_REASON_MAX_LENGTH,
5133
+ FOLLOWED_UP_COLLECTION_STATUSES,
3262
5134
  ForeignPaymentMethodReferenceError,
5135
+ GIVING_BACK_CREDIT_ENTRY_KINDS,
5136
+ INVOICE_ARCHIVE_TRIGGERS,
5137
+ INVOICE_COLLECTION_ASK_EVERY_MS,
5138
+ INVOICE_COLLECTION_LEASE_MS,
5139
+ INVOICE_COLLECTION_STATUSES,
5140
+ INVOICE_DOCUMENT_CAPABILITY,
5141
+ INVOICE_ERROR_CODES,
5142
+ INVOICE_SEQUENCE_DIGITS,
5143
+ INVOICE_SETTLEMENT_KINDS,
3263
5144
  LEGAL_IDENTITY_FIELDS,
3264
5145
  MAINTENANCE_CAPABILITY,
3265
5146
  MAINTENANCE_ERROR_CODES,
@@ -3285,24 +5166,41 @@ var ERROR_MESSAGES_DE = {
3285
5166
  PersistenceCapabilityError,
3286
5167
  PersistenceRefusal,
3287
5168
  PlatformUserExistsError,
5169
+ RECONCILIATION_CASE_KINDS,
5170
+ RECONCILIATION_CLOSE_CAPABILITY,
5171
+ RECONCILIATION_NOTE_MAX_LENGTH,
5172
+ RECONCILIATION_READ_CAPABILITY,
3288
5173
  REGISTRATION_ERROR_CODES,
3289
5174
  REGISTRATION_RESUME_TTL_MINUTES,
3290
5175
  REGISTRATION_STEP_BY_STATUS,
5176
+ REOPENING_SETTLEMENT_KINDS,
3291
5177
  RETIREMENT_REMINDER_LEAD_DAYS,
5178
+ REVERSIBLE_COLLECTION_STATUSES,
3292
5179
  SETTINGS_ERROR_CODES,
3293
5180
  SETUP_ERROR_CODES,
3294
5181
  SUBSCRIBER_ACCOUNT_CAPABILITY,
3295
5182
  SUBSCRIBER_ATTENTION_CAPABILITY,
3296
5183
  SUBSCRIBER_ATTENTION_PAGE_SIZE,
5184
+ SUBSCRIBER_CHARGE_ORIGINS,
5185
+ SUBSCRIBER_CHARGE_SOURCES,
3297
5186
  SUBSCRIBER_CORRECTION_CAPABILITY,
5187
+ SUBSCRIBER_CREDIT_CAPABILITY,
5188
+ SUBSCRIBER_CREDIT_ENTRY_KINDS,
3298
5189
  SUBSCRIBER_ERROR_CODES,
3299
5190
  SUBSCRIBER_INVOICE_ADDRESS_FIELDS,
5191
+ SUBSCRIBER_PAYMENTS_CAPABILITY,
5192
+ SUBSCRIBER_PAYOUT_CAPABILITY,
5193
+ SUBSCRIBER_REFUND_STATUSES,
3300
5194
  SUBSCRIBER_STANDING_CAPABILITY,
3301
5195
  SUBSCRIBER_TAX_ORIGIN_FIELDS,
3302
5196
  TAX_ERROR_CODES,
3303
5197
  TAX_TREATMENT_KINDS,
5198
+ UNSETTLED_COLLECTION_STATUSES,
3304
5199
  VERSION_RETIREMENT_CAPABILITY,
5200
+ addDaysToDay,
5201
+ announcedCollectionDay,
3305
5202
  applyPromo,
5203
+ asXmlReadsIt,
3306
5204
  assertPersistenceCapabilities,
3307
5205
  buildActivePlanVersionWhere,
3308
5206
  buildActiveVersionWhere,
@@ -3312,38 +5210,69 @@ var ERROR_MESSAGES_DE = {
3312
5210
  bundleStemDefaults,
3313
5211
  canonicalJson,
3314
5212
  canonicalVatId,
5213
+ casesCovered,
3315
5214
  catalogDraftExists,
3316
5215
  catalogVersionAlreadyPublished,
3317
5216
  catalogVersionGone,
5217
+ chargedAgainOn,
3318
5218
  classifyBundleVersionDiff,
3319
5219
  classifyIssuerChange,
3320
5220
  classifyPlanDiff,
3321
5221
  classifyVersionOffer,
3322
5222
  collectUnsatisfiedRequires,
5223
+ collectionAttemptKey,
5224
+ collectionOutcomeColumns,
5225
+ collectionStatusOf,
5226
+ columnAmountOf,
3323
5227
  computeIncludedVat,
3324
5228
  concurrencyGate,
3325
5229
  concurrencyGateOf,
3326
5230
  contractPartiesOf,
3327
5231
  coverageExcludingSelf,
5232
+ coversLifted,
5233
+ coversOf,
3328
5234
  customLimitsReader,
5235
+ dayInZone,
3329
5236
  definedFields,
3330
5237
  diffSettings,
5238
+ directDebitLeadDaysOf,
5239
+ draftSubscriptionInvoice,
5240
+ endedAtOnceSubjectOf,
5241
+ featureWithdrawalRowOf,
5242
+ featuresWithdrawnAt,
3331
5243
  formatCustomerNumber,
3332
5244
  formatErrorMessage,
5245
+ formatInvoiceNumber,
3333
5246
  grossFromNet,
5247
+ holdsOnlyXmlCharacters,
3334
5248
  identityCorrectionDelta,
5249
+ inWithdrawalOrder,
5250
+ invoiceArchiveProtectionProblem,
5251
+ invoiceCollectionColumns,
5252
+ invoiceContentDraftOf,
5253
+ invoiceFooterOf,
5254
+ invoiceGroupsOf,
5255
+ invoiceIssuerOf,
5256
+ invoicePaymentOf,
3335
5257
  isBundleRedundant,
5258
+ isChargedAgain,
5259
+ isEndedRefund,
5260
+ isFeatureWithdrawnAt,
3336
5261
  isForeignPaymentMethodReferenceError,
5262
+ isInvoiceDocumentChecksum,
3337
5263
  isMaintenanceAnnouncementLapsed,
3338
5264
  isMaintenanceOverrun,
3339
5265
  isPaymentCallbackRejectedError,
3340
5266
  isPersistenceRefusal,
3341
5267
  isPlatformUserExistsError,
5268
+ isRetryableCollectionFailure,
3342
5269
  isVersionActiveAt,
3343
5270
  isVersionEditable,
3344
5271
  issuerIdentityOf,
3345
5272
  keepOneRecommended,
3346
5273
  keepsVatIdCheck,
5274
+ lastCollectionOf,
5275
+ lastDayInZone,
3347
5276
  maintenanceRetryAfterSeconds,
3348
5277
  maintenanceStatusOf,
3349
5278
  maintenanceWindowStatusOf,
@@ -3352,14 +5281,22 @@ var ERROR_MESSAGES_DE = {
3352
5281
  membersLeftOut,
3353
5282
  missingRequiresFor,
3354
5283
  movedIdentityFields,
5284
+ netByInvoice,
3355
5285
  netFromGross,
5286
+ nextWithdrawalChange,
3356
5287
  noActivePlanVersion,
5288
+ openOfCredit,
5289
+ owedOn,
5290
+ paidOutByCredit,
3357
5291
  parseZonedInstant,
5292
+ paymentSettlement,
5293
+ payoutEntryColumns,
3358
5294
  percentOf,
3359
5295
  pickActivePromo,
3360
5296
  planCatalogSettingsOf,
3361
5297
  planKeyTaken,
3362
5298
  planNotInCatalog,
5299
+ plannedCollectionColumns,
3363
5300
  previousUtcDay,
3364
5301
  promoCodeTaken,
3365
5302
  promoStatus,
@@ -3368,54 +5305,90 @@ var ERROR_MESSAGES_DE = {
3368
5305
  readCustomLimits,
3369
5306
  readQuotaRecord,
3370
5307
  readQuotaValue,
5308
+ reconciliationCaseColumns,
3371
5309
  recordedIssuerIdentity,
5310
+ refundEntryColumns,
5311
+ refundParts,
3372
5312
  refuseForeignPaymentMethodReference,
5313
+ refusesFirstAttempt,
5314
+ refusesPlannedAttempt,
5315
+ reinstatementParts,
3373
5316
  resolveBundleAvailability,
3374
5317
  resolveErrorMessage,
3375
5318
  retirementCostsTheSubscription,
3376
5319
  retirementReminderDueAt,
3377
5320
  retirementReminderIsDue,
3378
5321
  retirementSwitchTerms,
5322
+ reversalEntryColumns,
3379
5323
  roundToCents,
3380
5324
  sameLegalIdentity,
3381
5325
  scheduledChangeAfterWrite,
3382
5326
  selectChargeableBundles,
5327
+ sentCollectionOf,
3383
5328
  settingsSubtreeOf,
5329
+ settledAmountOf,
3384
5330
  startOfUtcDay,
3385
5331
  subscriberChargeColumns,
3386
5332
  subscriberFromRegistration,
5333
+ subscriberPaymentColumns,
3387
5334
  subscriberPaymentMethodColumns,
5335
+ subscriberRefundColumns,
5336
+ subscriberReversalColumns,
3388
5337
  subscriptionBundleAlreadyCancelled,
3389
5338
  subscriptionBundleGone,
3390
5339
  subscriptionContractGone,
3391
5340
  subscriptionGone,
5341
+ subscriptionInvoiceChargeInvoiced,
5342
+ subscriptionInvoiceColumns,
5343
+ subscriptionInvoiceDocumentChanged,
5344
+ subscriptionInvoiceLineColumns,
3392
5345
  sumToCents,
3393
5346
  taxOriginOf,
3394
5347
  taxOriginWrite,
5348
+ taxPerRate,
3395
5349
  taxTreatmentToJson,
5350
+ toArchivedInvoiceDocument,
3396
5351
  toBundleStemRow,
3397
5352
  toBundleVersionRetirementRecord,
3398
5353
  toCents,
5354
+ toCollectionAnnouncedNotice,
5355
+ toCollectionFailedNotice,
5356
+ toCollectionReversedNotice,
3399
5357
  toContractLineItemRecord,
3400
5358
  toDecimalString,
5359
+ toFeatureWithdrawalRecord,
5360
+ toInvoiceCollectionRecord,
3401
5361
  toMaintenanceWindowRecord,
3402
5362
  toPlanRow,
3403
5363
  toPlanVersionRow,
3404
5364
  toRunningContractIssuer,
3405
5365
  toSubscriberChargeRecord,
3406
5366
  toSubscriberCorrectionRecord,
5367
+ toSubscriberCreditEntryRecord,
5368
+ toSubscriberCreditRecord,
3407
5369
  toSubscriberPaymentMethodRecord,
5370
+ toSubscriberPaymentRecord,
5371
+ toSubscriberReconciliationCaseRecord,
3408
5372
  toSubscriberRecord,
5373
+ toSubscriberRefundRecord,
5374
+ toSubscriberReversalRecord,
3409
5375
  toSubscriberTaxOriginChangeRecord,
3410
5376
  toSubscriberVatIdCheckRecord,
3411
5377
  toSubscriptionBundleRecord,
3412
5378
  toSubscriptionContractRecord,
5379
+ toSubscriptionInvoiceDocumentRecord,
5380
+ toSubscriptionInvoiceRecord,
3413
5381
  toSubscriptionNoticeRecord,
3414
5382
  toTaxOriginValues,
3415
5383
  toTaxTreatment,
3416
5384
  toVersionRetirementRecord,
5385
+ utcMidnightOf,
5386
+ vatCategoryOf,
3417
5387
  vatIdCheckFromStore,
3418
5388
  versionOnSale,
3419
5389
  versionOnSaleOrNext,
3420
- versionSale
5390
+ versionSale,
5391
+ withdrawalParts,
5392
+ withdrawnFeaturesOf,
5393
+ yearOfDay
3421
5394
  });