@stamhoofd/backend 2.143.1 → 2.145.0

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.
Files changed (111) hide show
  1. package/package.json +19 -19
  2. package/src/crons/fake-settlements.test.ts +13 -11
  3. package/src/crons/fake-settlements.ts +22 -38
  4. package/src/crons/mollie-chargebacks.ts +31 -29
  5. package/src/crons/mollie-refunds.ts +61 -59
  6. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +16 -5
  7. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +210 -4
  8. package/src/endpoints/auth/CreateTokenEndpoint.ts +9 -20
  9. package/src/endpoints/auth/DeleteTokenEndpoint.test.ts +29 -0
  10. package/src/endpoints/auth/DeleteTokenEndpoint.ts +2 -1
  11. package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -11
  12. package/src/endpoints/auth/MFA.security.test.ts +9 -7
  13. package/src/endpoints/auth/MFA.test.ts +42 -15
  14. package/src/endpoints/auth/PatchUserEndpoint.ts +3 -2
  15. package/src/endpoints/auth/VerifyEmailEndpoint.ts +5 -4
  16. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +4 -3
  17. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +6 -5
  18. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +5 -4
  19. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +5 -4
  20. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -1
  21. package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -1
  22. package/src/endpoints/global/files/UploadFile.test.ts +2 -1
  23. package/src/endpoints/global/files/UploadFile.ts +53 -25
  24. package/src/endpoints/global/files/UploadImage.test.ts +2 -1
  25. package/src/endpoints/global/files/UploadImage.ts +32 -6
  26. package/src/endpoints/global/files/upload-security.test.ts +3 -2
  27. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +4 -3
  28. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +26 -25
  29. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +57 -56
  30. package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.test.ts +4 -3
  31. package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +6 -4
  32. package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +6 -6
  33. package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +9 -8
  34. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +14 -13
  35. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +24 -23
  36. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +4 -3
  37. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +2 -1
  38. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +2 -1
  39. package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +20 -19
  40. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +2 -1
  41. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.test.ts +2 -1
  42. package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +3 -2
  43. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +4 -3
  44. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +5 -4
  45. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +5 -4
  46. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.test.ts +2 -1
  47. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.test.ts +2 -1
  48. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +0 -1
  49. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +23 -22
  50. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +4 -4
  51. package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +2 -1
  52. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +4 -3
  53. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +2 -2
  54. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.test.ts +2 -1
  55. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +5 -4
  56. package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +7 -6
  57. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +9 -8
  58. package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +3 -2
  59. package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +2 -1
  60. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +5 -4
  61. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +2 -1
  62. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +3 -2
  63. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +6 -5
  64. package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +16 -15
  65. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +2 -1
  66. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.test.ts +2 -1
  67. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.test.ts +2 -1
  68. package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +2 -1
  69. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +4 -3
  70. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -5
  71. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +94 -8
  72. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +18 -18
  73. package/src/helpers/ApplicationFeeInvoicer.ts +1 -1
  74. package/src/helpers/Context.ts +14 -21
  75. package/src/helpers/ContextStorage.ts +5 -0
  76. package/src/helpers/MollieSettlementSync.test.ts +94 -41
  77. package/src/helpers/MollieSettlementSync.ts +174 -78
  78. package/src/helpers/StripeSettlementSync.test.ts +0 -85
  79. package/src/helpers/StripeSettlementSync.ts +47 -70
  80. package/src/helpers/TwoFactorHelper.ts +21 -22
  81. package/src/helpers/streamForBreakdown.ts +1 -1
  82. package/src/services/AdminSessionService.ts +5 -3
  83. package/src/services/InvoiceService.ts +2 -2
  84. package/src/services/MollieService.ts +168 -126
  85. package/src/services/PaymentService.ts +2 -2
  86. package/src/services/SSOService.test.ts +25 -1
  87. package/src/services/SSOService.ts +17 -6
  88. package/src/services/SessionService.test.ts +319 -0
  89. package/src/services/SessionService.ts +357 -0
  90. package/src/services/SettlementService.test.ts +13 -85
  91. package/src/services/SettlementService.ts +5 -113
  92. package/src/services/WebshopCrowdfundingService.test.ts +4 -3
  93. package/src/sql-filters/balance-items.ts +10 -0
  94. package/src/sql-filters/registrations.ts +59 -1
  95. package/tests/actions/patchUserMember.ts +2 -1
  96. package/tests/e2e/api-rate-limits.test.ts +2 -1
  97. package/tests/e2e/bundle-discounts.test.ts +4 -3
  98. package/tests/e2e/charge-members.test.ts +6 -5
  99. package/tests/e2e/documents.test.ts +4 -3
  100. package/tests/e2e/private-files.test.ts +9 -8
  101. package/tests/e2e/register.test.ts +4 -3
  102. package/tests/e2e/stock.test.ts +4 -3
  103. package/tests/e2e/tickets.test.ts +4 -3
  104. package/tests/helpers/ExportSlice.ts +2 -1
  105. package/tests/helpers/MollieMocker.ts +173 -38
  106. package/tests/init/initAdmin.ts +2 -1
  107. package/tests/init/initPlatformAdmin.ts +2 -1
  108. package/src/helpers/ViesHelper.test.ts +0 -401
  109. package/src/helpers/ViesHelper.ts +0 -215
  110. package/src/services/PeppolDirectoryService.test.ts +0 -139
  111. package/src/services/PeppolDirectoryService.ts +0 -111
@@ -662,91 +662,6 @@ describe('StripeSettlementSync', () => {
662
662
  expect(after).toEqual(rowIds);
663
663
  });
664
664
 
665
- test('a payment that moved to another payout is swept', async () => {
666
- const payment = await createPayment();
667
- const otherPayment = await createPayment({ price: 50_00_00 });
668
- const payout = stripeMocker.createPayout({ amount: 15000, arrivalDate, stripeAccount: stripeAccount.accountId });
669
-
670
- stripeMocker.createBalanceTransaction({
671
- type: 'payment',
672
- amount: 10000,
673
- created,
674
- payout: payout.id,
675
- stripeAccount: stripeAccount.accountId,
676
- source: stripeMocker.createChargeObject({ metadata: { payment: payment.id } }),
677
- });
678
- const moved = stripeMocker.createBalanceTransaction({
679
- type: 'payment',
680
- amount: 5000,
681
- created,
682
- payout: payout.id,
683
- stripeAccount: stripeAccount.accountId,
684
- source: stripeMocker.createChargeObject({ metadata: { payment: otherPayment.id } }),
685
- });
686
-
687
- const sync = createConnectedSync();
688
- expect(await sync.syncPayouts({ start })).toEqual({ synced: 1, skipped: 0, failed: 0 });
689
-
690
- const settlement = await getSettlement(payout);
691
- expect(await PaymentSettlement.select().where('settlementId', settlement.id).count()).toBe(2);
692
-
693
- // Stripe moved the second payment to a later payout
694
- moved.payout = stripeMocker.createId('po');
695
- payout.amount = 10000;
696
-
697
- expect(await sync.syncPayouts({ start, force: true })).toEqual({ synced: 1, skipped: 0, failed: 0 });
698
-
699
- const lines = await PaymentSettlement.select().where('settlementId', settlement.id).fetch();
700
- expect(lines).toHaveLength(1);
701
- expect(lines[0].paymentId).toBe(payment.id);
702
- expect((await getSettlement(payout)).unexplainedAmount).toBe(0);
703
- });
704
-
705
- test('an application fee that moved to another payout is only unlinked', async () => {
706
- const payment = await createPayment();
707
- const payout = stripeMocker.createPayout({ amount: 250, arrivalDate });
708
- const movedFee = stripeMocker.createBalanceTransaction({
709
- type: 'application_fee',
710
- amount: 250,
711
- created,
712
- payout: payout.id,
713
- source: stripeMocker.createApplicationFee({
714
- amount: 250,
715
- account: stripeAccount.accountId,
716
- originatingTransaction: stripeMocker.createChargeObject({ metadata: { payment: payment.id, serviceFee: '30' } }),
717
- }),
718
- });
719
-
720
- const sync = createSync();
721
- expect(await sync.syncPayouts({ start })).toEqual({ synced: 1, skipped: 0, failed: 0 });
722
-
723
- const settlement = await getSettlement(payout);
724
- const applicationFeeId = ((movedFee.source as StripeObject)).id;
725
- expect(await ApplicationFee.select().where('settlementId', settlement.id).count()).toBe(2);
726
-
727
- movedFee.payout = stripeMocker.createId('po');
728
- payout.amount = 0;
729
-
730
- expect(await sync.syncPayouts({ start, force: true })).toEqual({ synced: 1, skipped: 0, failed: 0 });
731
-
732
- // The fee rows outlive the payout: they are only unlinked, and so are the deduction
733
- // charges they point at (a foreign key forbids deleting those)
734
- const fees = await ApplicationFee.select().where('externalId', applicationFeeId).fetch();
735
- expect(fees).toHaveLength(2);
736
- for (const fee of fees) {
737
- expect(fee.settlementId).toBeNull();
738
- }
739
- const deductions = await SettlementCharge.select().where('applicationFeeId', applicationFeeId).fetch();
740
- expect(deductions).toHaveLength(2);
741
- for (const row of deductions) {
742
- expect(row.settlementId).toBeNull();
743
- }
744
-
745
- const after = await getSettlement(payout);
746
- expect(after.unexplainedAmount).toBe(0);
747
- expect(after.pendingFees).toBe(0);
748
- });
749
-
750
665
  describe('Payers we can no longer reach', () => {
751
666
  /**
752
667
  * A platform payout that only receives one application fee, plus the payout transaction.
@@ -12,10 +12,10 @@ import { SettlementStatus } from '@stamhoofd/structures/settlements/SettlementSt
12
12
  import Stripe from 'stripe';
13
13
 
14
14
  import { ApplicationFeeService } from '../services/ApplicationFeeService.js';
15
- import { ReportedRows, SettlementService } from '../services/SettlementService.js';
15
+ import { SettlementService } from '../services/SettlementService.js';
16
16
  import { ApplicationFeeDetails } from './ApplicationFeeDetails.js';
17
- import { passthroughFetch } from './passthroughFetch.js';
18
17
  import { getPaymentIdForStripeCharge } from './getPaymentIdForStripeCharge.js';
18
+ import { passthroughFetch } from './passthroughFetch.js';
19
19
  import { WebmasterReport } from './WebmasterReport.js';
20
20
 
21
21
  /**
@@ -109,8 +109,7 @@ export class StripeSettlementSync {
109
109
  }
110
110
 
111
111
  /**
112
- * Sync all paid payouts that arrived in the window. A failing payout is marked, reported and
113
- * skipped so the other payouts still sync; the summary tells the caller how bad it was.
112
+ * Sync all paid payouts that arrived in the window.
114
113
  */
115
114
  async syncPayouts({ start, end, force = false, abort }: { start: Date; end?: Date; force?: boolean; abort?: AbortSignal }): Promise<{ synced: number; skipped: number; failed: number }> {
116
115
  const result = { synced: 0, skipped: 0, failed: 0 };
@@ -180,7 +179,7 @@ export class StripeSettlementSync {
180
179
 
181
180
  // Storing a fee can link it to a balance item right away (a month the legacy invoicer
182
181
  // billed). When it is already paid out, its payout needs the derived line for it
183
- const invoicedFeeBalanceItemIds = new Set<string>();
182
+ const settledApplicationFeeBalanceItems = new Set<string>();
184
183
 
185
184
  try {
186
185
  for await (const transaction of this.stripe.balanceTransactions.list({
@@ -195,7 +194,7 @@ export class StripeSettlementSync {
195
194
  abort?.throwIfAborted();
196
195
 
197
196
  try {
198
- await this.#handleApplicationFee(transaction, { invoicedFeeBalanceItemIds });
197
+ await this.#handleApplicationFee(transaction, { settledApplicationFeeBalanceItems });
199
198
  } catch (e) {
200
199
  // The month is only invoiced after a walk without errors, so an interrupted
201
200
  // walk has to stop the walk itself instead of joining the errors of its
@@ -209,11 +208,11 @@ export class StripeSettlementSync {
209
208
  } catch (e) {
210
209
  // The payouts of the fees stored so far still need their derived lines, but failing to
211
210
  // update them may not hide what broke the walk
212
- await SettlementService.updatePaymentSettlementsForAccountDeductionBalanceItems([...invoicedFeeBalanceItemIds]).catch(console.error);
211
+ await SettlementService.updatePaymentSettlementsForApplicationFeeBalanceItems([...settledApplicationFeeBalanceItems]).catch(console.error);
213
212
  throw e;
214
213
  }
215
214
 
216
- await SettlementService.updatePaymentSettlementsForAccountDeductionBalanceItems([...invoicedFeeBalanceItemIds]);
215
+ await SettlementService.updatePaymentSettlementsForApplicationFeeBalanceItems([...settledApplicationFeeBalanceItems]);
217
216
 
218
217
  if (errors.length > 0) {
219
218
  throw new SimpleError({
@@ -234,7 +233,7 @@ export class StripeSettlementSync {
234
233
  * be updated afterwards. Filled while storing, not from the return value: a fee that is
235
234
  * stored before a later one throws still needs its derived line.
236
235
  */
237
- invoicedFeeBalanceItemIds?: Set<string>;
236
+ settledApplicationFeeBalanceItems?: Set<string>;
238
237
  } = {}): Promise<{ fees: ApplicationFee[]; charges: SettlementCharge[] }> {
239
238
  const fee = transaction.source as Stripe.ApplicationFee;
240
239
  const payingAccountId = typeof fee.account === 'string' ? fee.account : fee.account.id;
@@ -317,7 +316,7 @@ export class StripeSettlementSync {
317
316
  // An invoiced fee is explained by the derived line of its fee payment instead of by
318
317
  // itself, so the payouts it sits in have to rebuild those lines
319
318
  if (storedFee.balanceItemId && storedFee.settlementId) {
320
- options.invoicedFeeBalanceItemIds?.add(storedFee.balanceItemId);
319
+ options.settledApplicationFeeBalanceItems?.add(storedFee.balanceItemId);
321
320
  }
322
321
  }
323
322
 
@@ -467,12 +466,11 @@ export class StripeSettlementSync {
467
466
  }
468
467
 
469
468
  async #walkPayout(payout: Stripe.Payout, settlement: Settlement, abort: AbortSignal) {
470
- const reported = new ReportedRows();
471
469
  let transactionCount = 0;
472
470
 
473
- // Invoiced fees linked to or unlinked from this settlement during the walk: their fee
474
- // payments' derived lines must follow
475
- const invoicedFeeBalanceItemIds = new Set<string>();
471
+ // Keep track of all balance items linked to (settled) application fees that are created or updated.
472
+ // So we can update the settlement status of the associated payments
473
+ const settledApplicationFeeBalanceItems = new Set<string>();
476
474
 
477
475
  try {
478
476
  for await (const transaction of this.stripe.balanceTransactions.list({
@@ -482,67 +480,51 @@ export class StripeSettlementSync {
482
480
  ? ['data.source', 'data.source.application_fee', 'data.source.application_fee.originating_transaction', 'data.source.charge']
483
481
  : ['data.source', 'data.source.originating_transaction', 'data.source.charge'],
484
482
  })) {
485
- // Between two transactions is a safe point to stop: only a complete walk sweeps
486
- // and marks the settlement synced, so an interrupted one is re-walked from scratch
487
483
  abort.throwIfAborted();
488
484
 
489
485
  transactionCount += 1;
490
- await this.#handleTransaction(transaction, settlement, reported, invoicedFeeBalanceItemIds);
486
+ await this.#handleTransaction(transaction, settlement, settledApplicationFeeBalanceItems);
491
487
  }
492
488
 
493
- // Stripe reported nothing for money that did move: storing that as a complete sync
494
- // would silently hide the whole payout
495
489
  if (transactionCount === 0 && settlement.amount !== 0) {
496
490
  throw new SimpleError({
497
491
  code: 'empty_payout',
498
492
  message: 'Payout ' + payout.id + ' of ' + settlement.amount + ' has no balance transactions',
499
493
  });
500
494
  }
501
-
502
- // Only a complete walk may sweep: it can't tell a row that moved away from one this
503
- // walk never reached
504
- const { unlinkedFees } = await SettlementService.sweepSettlement(settlement, reported);
505
- for (const fee of unlinkedFees) {
506
- if (fee.balanceItemId) {
507
- invoicedFeeBalanceItemIds.add(fee.balanceItemId);
508
- }
509
- }
510
495
  } catch (e) {
511
496
  // The fee payments still follow the fees this walk linked before it broke (a fee may
512
497
  // never sit in a payout that has no line for it), but failing to update them may not
513
498
  // hide what broke the walk
514
- await SettlementService.updatePaymentSettlementsForAccountDeductionBalanceItems([...invoicedFeeBalanceItemIds]).catch(console.error);
499
+ await SettlementService.updatePaymentSettlementsForApplicationFeeBalanceItems([...settledApplicationFeeBalanceItems]).catch(console.error);
515
500
  throw e;
516
501
  }
517
502
 
518
- await SettlementService.updatePaymentSettlementsForAccountDeductionBalanceItems([...invoicedFeeBalanceItemIds]);
503
+ await SettlementService.updatePaymentSettlementsForApplicationFeeBalanceItems([...settledApplicationFeeBalanceItems]);
519
504
  await SettlementService.finishSync(settlement, { transactionCount });
520
505
  }
521
506
 
522
- async #handleTransaction(transaction: Stripe.BalanceTransaction, settlement: Settlement, reported: ReportedRows, invoicedFeeBalanceItemIds: Set<string>) {
507
+ async #handleTransaction(transaction: Stripe.BalanceTransaction, settlement: Settlement, settledApplicationFeeBalanceItems: Set<string>) {
523
508
  const occurredAt = new Date(transaction.created * 1000);
524
509
 
525
- // A plain string switch: the pinned SDK's type union misses some real-world types
526
- // (e.g. network_cost)
527
510
  switch (transaction.type as string) {
528
511
  case 'charge':
529
512
  case 'payment': {
530
513
  const payment = await this.#resolvePayment(transaction);
531
514
 
532
- // A destination charge only passes through our balance on its way to the
533
- // organization: its own payout settles it, ours stays out of it
534
- if (payment.organizationId !== settlement.organizationId) {
535
- await this.#storePaidFeesForTransaction(transaction, settlement, reported, { paymentId: null });
515
+ if (!payment || payment.organizationId !== settlement.organizationId) {
516
+ // Destination charge via the platform
517
+ await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: null });
536
518
  return;
537
519
  }
538
520
 
539
- reported.paymentLine(await SettlementService.upsertPaymentLine(settlement, {
521
+ await SettlementService.upsertPaymentLine(settlement, {
540
522
  paymentId: payment.id,
541
523
  amount: transaction.amount * 100,
542
524
  externalId: transaction.id,
543
525
  occurredAt,
544
- }));
545
- await this.#storePaidFeesForTransaction(transaction, settlement, reported, { paymentId: payment.id });
526
+ });
527
+ await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: payment.id });
546
528
  await this.#updateTransferFee(transaction, settlement, payment);
547
529
  await SettlementService.updateLegacySettlementReference(payment);
548
530
  return;
@@ -559,30 +541,28 @@ export class StripeSettlementSync {
559
541
  const isReturned = transaction.type === 'refund_failure';
560
542
  const refunded = await this.#resolveRefundedPayment(transaction);
561
543
 
562
- // The reverse of the pass-through above: refunding another organization's payment
563
- // only moves the money back through our balance
564
544
  if (refunded.organizationId !== settlement.organizationId) {
565
- await this.#storePaidFeesForTransaction(transaction, settlement, reported, { paymentId: null });
545
+ // Destination charge via the platform
546
+ await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: null });
566
547
  return;
567
548
  }
568
549
 
569
550
  const payment = await this.#resolveReversingPayment(transaction, refunded, { negated: isReturned });
570
- reported.paymentLine(await SettlementService.upsertPaymentLine(settlement, {
551
+ await SettlementService.upsertPaymentLine(settlement, {
571
552
  paymentId: payment.id,
572
553
  amount: transaction.amount * 100,
573
554
  externalId: transaction.id,
574
555
  occurredAt,
575
- }));
576
- await this.#storePaidFeesForTransaction(transaction, settlement, reported, { paymentId: payment.id });
556
+ });
557
+ await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: payment.id });
577
558
  await SettlementService.updateLegacySettlementReference(payment);
578
559
  return;
579
560
  }
580
561
 
581
562
  case 'application_fee': {
582
563
  // The fee rows, now linked to the platform payout that contains them
583
- const { fees } = await this.#handleApplicationFee(transaction, { settlementId: settlement.id, invoicedFeeBalanceItemIds });
584
- reported.applicationFees(fees);
585
- await this.#storePaidFeesForTransaction(transaction, settlement, reported, { paymentId: null });
564
+ await this.#handleApplicationFee(transaction, { settlementId: settlement.id, settledApplicationFeeBalanceItems });
565
+ await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: null });
586
566
  return;
587
567
  }
588
568
 
@@ -612,7 +592,7 @@ export class StripeSettlementSync {
612
592
  case 'stripe_fee':
613
593
  case 'network_cost':
614
594
  case 'tax_fee': {
615
- const charge = await SettlementService.upsertCharge({
595
+ await SettlementService.upsertCharge({
616
596
  type: transaction.type === 'tax_fee' ? SettlementChargeType.Tax : SettlementChargeType.ProviderAccountFee,
617
597
  externalId: transaction.id,
618
598
  amount: transaction.amount * 100,
@@ -622,7 +602,6 @@ export class StripeSettlementSync {
622
602
  description: transaction.description ?? '',
623
603
  occurredAt,
624
604
  });
625
- reported.charge(charge);
626
605
  return;
627
606
  }
628
607
 
@@ -630,7 +609,7 @@ export class StripeSettlementSync {
630
609
  case 'reserved_funds':
631
610
  case 'reserve_hold':
632
611
  case 'reserve_release': {
633
- const charge = await SettlementService.upsertCharge({
612
+ await SettlementService.upsertCharge({
634
613
  type: SettlementChargeType.Reserve,
635
614
  externalId: transaction.id,
636
615
  amount: transaction.amount * 100,
@@ -639,14 +618,13 @@ export class StripeSettlementSync {
639
618
  description: transaction.description ?? '',
640
619
  occurredAt,
641
620
  });
642
- reported.charge(charge);
643
621
  return;
644
622
  }
645
623
 
646
624
  case 'adjustment':
647
625
  case 'payment_reversal': {
648
626
  const paymentId = await this.#tryResolveAdjustmentPayment(transaction, settlement);
649
- const charge = await SettlementService.upsertCharge({
627
+ await SettlementService.upsertCharge({
650
628
  type: SettlementChargeType.Adjustment,
651
629
  externalId: transaction.id,
652
630
  amount: transaction.amount * 100,
@@ -657,8 +635,7 @@ export class StripeSettlementSync {
657
635
  description: transaction.description ?? '',
658
636
  occurredAt,
659
637
  });
660
- reported.charge(charge);
661
- await this.#storePaidFeesForTransaction(transaction, settlement, reported, { paymentId });
638
+ await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId });
662
639
  return;
663
640
  }
664
641
 
@@ -672,7 +649,7 @@ export class StripeSettlementSync {
672
649
  case 'connect_collection_transfer':
673
650
  case 'stripe_balance_payment_debit':
674
651
  case 'stripe_balance_payment_debit_reversal': {
675
- const charge = await SettlementService.upsertCharge({
652
+ await SettlementService.upsertCharge({
676
653
  type: SettlementChargeType.BalanceMovement,
677
654
  externalId: transaction.id,
678
655
  amount: transaction.amount * 100,
@@ -681,8 +658,7 @@ export class StripeSettlementSync {
681
658
  description: transaction.description ?? transaction.type,
682
659
  occurredAt,
683
660
  });
684
- reported.charge(charge);
685
- await this.#storePaidFeesForTransaction(transaction, settlement, reported, { paymentId: null });
661
+ await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: null });
686
662
  return;
687
663
  }
688
664
 
@@ -703,7 +679,7 @@ export class StripeSettlementSync {
703
679
  * connected account also the application fee we charged. The amounts always come from the
704
680
  * balance transaction itself.
705
681
  */
706
- async #storePaidFeesForTransaction(transaction: Stripe.BalanceTransaction, settlement: Settlement, reported: ReportedRows, { paymentId }: { paymentId: string | null }) {
682
+ async #storePaidFeesForTransaction(transaction: Stripe.BalanceTransaction, settlement: Settlement, { paymentId }: { paymentId: string | null }) {
707
683
  const occurredAt = new Date(transaction.created * 1000);
708
684
 
709
685
  for (const [index, detail] of (transaction.fee_details ?? []).entries()) {
@@ -712,11 +688,11 @@ export class StripeSettlementSync {
712
688
  }
713
689
 
714
690
  if (detail.type === 'application_fee') {
715
- await this.#storePaidApplicationFeeForTransaction(transaction, detail, settlement, reported, { paymentId });
691
+ await this.#storePaidApplicationFeeForTransaction(transaction, detail, settlement, { paymentId });
716
692
  continue;
717
693
  }
718
694
 
719
- const charge = await SettlementService.upsertCharge({
695
+ await SettlementService.upsertCharge({
720
696
  type: getFeeDetailType(detail, transaction),
721
697
  externalId: transaction.id + ':fee:' + index,
722
698
  amount: -detail.amount * 100,
@@ -729,7 +705,6 @@ export class StripeSettlementSync {
729
705
  description: detail.description ?? '',
730
706
  occurredAt,
731
707
  });
732
- reported.charge(charge);
733
708
  }
734
709
  }
735
710
 
@@ -740,7 +715,7 @@ export class StripeSettlementSync {
740
715
  * applicationFeeId both sides of one kind sum to zero. This walk fills their settlementId; the
741
716
  * rows themselves usually already exist (created by the fee walk).
742
717
  */
743
- async #storePaidApplicationFeeForTransaction(transaction: Stripe.BalanceTransaction, detail: Stripe.BalanceTransaction.FeeDetail, settlement: Settlement, reported: ReportedRows, { paymentId }: { paymentId: string | null }) {
718
+ async #storePaidApplicationFeeForTransaction(transaction: Stripe.BalanceTransaction, detail: Stripe.BalanceTransaction.FeeDetail, settlement: Settlement, { paymentId }: { paymentId: string | null }) {
744
719
  if (!this.stripeAccount) {
745
720
  throw new SimpleError({
746
721
  code: 'unexpected_application_fee_detail',
@@ -803,7 +778,6 @@ export class StripeSettlementSync {
803
778
  stripeAccountId: this.stripeAccount.id,
804
779
  occurredAt,
805
780
  });
806
- reported.charge(charge);
807
781
 
808
782
  // What the organization pays here is what we receive on the other side. The two are
809
783
  // written from different Stripe transactions, so a divergence would silently bill the
@@ -852,7 +826,7 @@ export class StripeSettlementSync {
852
826
  await payment.save();
853
827
  }
854
828
 
855
- async #resolvePayment(transaction: Stripe.BalanceTransaction): Promise<Payment> {
829
+ async #resolvePayment(transaction: Stripe.BalanceTransaction): Promise<Payment | null> {
856
830
  const source = transaction.source;
857
831
  if (!source || typeof source === 'string' || source.object !== 'charge') {
858
832
  throw new SimpleError({
@@ -874,10 +848,13 @@ export class StripeSettlementSync {
874
848
 
875
849
  const payment = await Payment.getByID(paymentId);
876
850
  if (!payment) {
877
- throw new SimpleError({
878
- code: 'payment_not_found',
879
- message: 'Payment ' + paymentId + ' referenced by charge ' + source.id + ' does not exist',
880
- });
851
+ // Probably deleted
852
+
853
+ return null;
854
+ // throw new SimpleError({
855
+ // code: 'payment_not_found',
856
+ // message: 'Payment ' + paymentId + ' referenced by charge ' + source.id + ' does not exist',
857
+ // });
881
858
  }
882
859
 
883
860
  this.#assertPaymentScope(payment, source.id);
@@ -1,13 +1,14 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
2
  import type { I18n } from '@stamhoofd/backend-i18n/I18n';
3
- import type { User } from '@stamhoofd/models';
4
- import { MFARecoveryCode, MFATOTP, MFAToken, Organization, Platform, RateLimiter, Token, WebauthnCredential } from '@stamhoofd/models';
3
+ import type { User, Token } from '@stamhoofd/models';
4
+ import { MFARecoveryCode, MFATOTP, MFAToken, Organization, Platform, RateLimiter, WebauthnCredential } from '@stamhoofd/models';
5
5
  import type { User as UserStruct } from '@stamhoofd/structures';
6
- import { MFAChallengeResponse, MFAEnrollmentResult, MFAMethodType, MFASetupResponse, MFAStatus, PasskeyCredential, RecoveryCodes, TOTPCredential, Token as TokenStruct } from '@stamhoofd/structures';
6
+ import { MFAChallengeResponse, MFAEnrollmentResult, MFAMethodType, MFASetupResponse, MFAStatus, PasskeyCredential, RecoveryCodes, SessionLoginMethod, TOTPCredential, Token as TokenStruct } from '@stamhoofd/structures';
7
7
 
8
8
  import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
9
9
 
10
10
  import { PasswordForgotService } from '../services/PasswordForgotService.js';
11
+ import { SessionService } from '../services/SessionService.js';
11
12
  import { RecoveryCodeHelper } from './RecoveryCodeHelper.js';
12
13
  import { WebauthnHelper } from './WebauthnHelper.js';
13
14
  import { Formatter, Sorter } from '@stamhoofd/utility';
@@ -133,33 +134,33 @@ export class TwoFactorHelper {
133
134
  * credential was accepted.
134
135
  *
135
136
  * `loginMethod` describes the credential that was just verified:
136
- * - 'password': the account password. A single credential that says nothing about
137
+ * - Password: the account password. A single credential that says nothing about
137
138
  * whether the user still reads the email address on the account, so a required
138
139
  * second factor must be set up here if the user does not have one yet — and a
139
140
  * long-inactive admin has to confirm their email address before they may.
140
- * - 'email': a password token or email verification code. Also a single credential,
141
+ * - Email: a password token or email verification code. Also a single credential,
141
142
  * but one that only reaches someone who reads the account's email, so it is the way
142
143
  * out of that email confirmation.
143
- * - 'sso': an external identity provider already authenticated the user, and is
144
+ * - SSO: an external identity provider already authenticated the user, and is
144
145
  * trusted to apply its own second factor. An enrolled factor is still verified
145
146
  * (the user asked us to protect their account), but we do not force enrollment —
146
147
  * unless the account ALSO has a password, because then the password remains a way
147
148
  * in that bypasses whatever the provider enforces.
148
149
  */
149
- static async getSecondFactorRequirement(user: User, organization: Organization | null, { loginMethod }: { loginMethod: 'password' | 'email' | 'sso' }): Promise<SecondFactorRequirement> {
150
+ static async getSecondFactorRequirement(user: User, organization: Organization | null, { loginMethod }: { loginMethod: SessionLoginMethod }): Promise<SecondFactorRequirement> {
150
151
  if (await TwoFactorHelper.userHasFactors(user.id)) {
151
- return { type: 'challenge', challenge: await TwoFactorHelper.createLoginChallenge(user) };
152
+ return { type: 'challenge', challenge: await TwoFactorHelper.createLoginChallenge(user, { loginMethod }) };
152
153
  }
153
154
 
154
- if (loginMethod === 'sso' && !user.hasPasswordBasedAccount()) {
155
+ if (loginMethod === SessionLoginMethod.SSO && !user.hasPasswordBasedAccount()) {
155
156
  return { type: 'none' };
156
157
  }
157
158
 
158
159
  if (await TwoFactorHelper.isTwoFactorRequired(user, organization)) {
159
- if (loginMethod === 'password' && TwoFactorHelper.isInactiveForEnrollment(user)) {
160
+ if (loginMethod === SessionLoginMethod.Password && TwoFactorHelper.isInactiveForEnrollment(user)) {
160
161
  return { type: 'confirm-email' };
161
162
  }
162
- return { type: 'setup', setupToken: await MFAToken.createFor(user.id, 'setup') };
163
+ return { type: 'setup', setupToken: await MFAToken.createFor(user.id, 'setup', { loginMethod }) };
163
164
  }
164
165
 
165
166
  return { type: 'none' };
@@ -173,16 +174,14 @@ export class TwoFactorHelper {
173
174
  static async sendEnrollmentConfirmationEmail(user: User, organization: Organization | null, i18n: I18n): Promise<void> {
174
175
  try {
175
176
  enrollmentConfirmationRateLimiter.track(user.id);
176
- }
177
- catch {
177
+ } catch {
178
178
  // Sent often enough already.
179
179
  return;
180
180
  }
181
181
 
182
182
  try {
183
183
  await PasswordForgotService.sendPasswordRecoveryEmail(user, organization, i18n);
184
- }
185
- catch (e) {
184
+ } catch (e) {
186
185
  console.error('Could not send the two-factor enrollment confirmation email', e);
187
186
  }
188
187
  }
@@ -203,7 +202,7 @@ export class TwoFactorHelper {
203
202
  * whoever holds the link could enroll one and get a session anyway, and the client
204
203
  * needs a session to let the user choose a password before enrolling.
205
204
  */
206
- static async assertSecondFactorOrThrow(user: User, organization: Organization | null, version: number, { loginMethod, i18n, allowTemporarySession = false }: { loginMethod: 'password' | 'email'; i18n: I18n; allowTemporarySession?: boolean }): Promise<void> {
205
+ static async assertSecondFactorOrThrow(user: User, organization: Organization | null, version: number, { loginMethod, i18n, allowTemporarySession = false }: { loginMethod: SessionLoginMethod.Password | SessionLoginMethod.Email; i18n: I18n; allowTemporarySession?: boolean }): Promise<void> {
207
206
  const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod });
208
207
 
209
208
  if (requirement.type === 'confirm-email') {
@@ -228,7 +227,7 @@ export class TwoFactorHelper {
228
227
  }
229
228
 
230
229
  if (requirement.type === 'setup') {
231
- const temporaryToken = allowTemporarySession ? new TokenStruct(await Token.createToken(user, new Date())) : null;
230
+ const temporaryToken = allowTemporarySession ? new TokenStruct(await SessionService.createSession(user, { loginMethod, authenticatedAt: new Date() })) : null;
232
231
 
233
232
  throw new SimpleError({
234
233
  code: 'require_mfa_setup',
@@ -313,7 +312,7 @@ export class TwoFactorHelper {
313
312
  /**
314
313
  * Create a login MFA session token and the challenge payload returned to the client.
315
314
  */
316
- static async createLoginChallenge(user: User): Promise<MFAChallengeResponse> {
315
+ static async createLoginChallenge(user: User, { loginMethod }: { loginMethod: SessionLoginMethod }): Promise<MFAChallengeResponse> {
317
316
  const { methods, passkeys } = await TwoFactorHelper.getEnrolledMethods(user.id);
318
317
 
319
318
  let webauthnOptions: PublicKeyCredentialRequestOptionsJSON | null = null;
@@ -323,7 +322,7 @@ export class TwoFactorHelper {
323
322
  challenge = webauthnOptions.challenge;
324
323
  }
325
324
 
326
- const mfaToken = await MFAToken.createFor(user.id, 'login', challenge);
325
+ const mfaToken = await MFAToken.createFor(user.id, 'login', { webauthnChallenge: challenge, loginMethod });
327
326
 
328
327
  return MFAChallengeResponse.create({
329
328
  token: mfaToken.token,
@@ -376,12 +375,12 @@ export class TwoFactorHelper {
376
375
  }
377
376
 
378
377
  // Before minting the new session, so it is never signed out by its own enrollment.
379
- await Token.deleteOtherSessions(user.id, currentToken?.accessToken ?? null);
378
+ await SessionService.deleteOtherSessions({ userId: user.id, keepAccessToken: currentToken?.accessToken ?? null });
380
379
 
381
380
  let token: TokenStruct | null = null;
382
381
  if (setupToken) {
383
382
  await setupToken.consume();
384
- const t = await Token.createToken(user, new Date());
383
+ const t = await SessionService.createSession(user, { loginMethod: setupToken.loginMethod, authenticatedAt: new Date() });
385
384
  await user.markActive();
386
385
  token = new TokenStruct(t);
387
386
  }
@@ -403,7 +402,7 @@ export class TwoFactorHelper {
403
402
  await MFARecoveryCode.deleteForUser(user.id);
404
403
  }
405
404
 
406
- await Token.deleteOtherSessions(user.id, currentToken?.accessToken ?? null);
405
+ await SessionService.deleteOtherSessions({ userId: user.id, keepAccessToken: currentToken?.accessToken ?? null });
407
406
 
408
407
  return await TwoFactorHelper.buildStatus(user);
409
408
  }
@@ -14,7 +14,7 @@ import { fetchToAsyncIterator } from './fetchToAsyncIterator.js';
14
14
  * They are read page by page and added up right away, so they are never all in memory at once. Above
15
15
  * this many objects a breakdown takes too long to wait for, so we ask the user to narrow down instead.
16
16
  */
17
- export const MAX_BREAKDOWN_OBJECTS = 10000;
17
+ export const MAX_BREAKDOWN_OBJECTS = 50_000;
18
18
 
19
19
  const PAGE_SIZE = 100;
20
20
 
@@ -1,4 +1,6 @@
1
- import { Token, User } from '@stamhoofd/models';
1
+ import { User } from '@stamhoofd/models';
2
+
3
+ import { SessionService } from './SessionService.js';
2
4
 
3
5
  /**
4
6
  * Signs out administrators in bulk.
@@ -25,7 +27,7 @@ export class AdminSessionService {
25
27
  */
26
28
  static async signOutOrganizationAdmins(organizationId: string, keepAccessToken: string | null): Promise<number> {
27
29
  const admins = await User.getAdmins(organizationId);
28
- return await Token.deleteForUsers(admins.map(a => a.id), keepAccessToken);
30
+ return await SessionService.deleteForUsers({ userIds: admins.map(a => a.id), keepAccessToken });
29
31
  }
30
32
 
31
33
  /**
@@ -36,6 +38,6 @@ export class AdminSessionService {
36
38
  */
37
39
  static async signOutPlatformAdmins(keepAccessToken: string | null): Promise<number> {
38
40
  const admins = await User.getPlatformAdmins();
39
- return await Token.deleteForUsers(admins.map(a => a.id), keepAccessToken);
41
+ return await SessionService.deleteForUsers({ userIds: admins.map(a => a.id), keepAccessToken });
40
42
  }
41
43
  }
@@ -6,7 +6,7 @@ import { InvoiceCounter } from '@stamhoofd/models/helpers/InvoiceCounter.js';
6
6
  import type { Invoice as InvoiceStruct } from '@stamhoofd/structures';
7
7
  import { EmailTemplateType, PaymentStatus, Recipient, Replacement } from '@stamhoofd/structures';
8
8
  import { Formatter } from '@stamhoofd/utility';
9
- import { ViesHelper } from '../helpers/ViesHelper.js';
9
+ import { ViesService } from '@stamhoofd/vies';
10
10
  import { WebmasterReport } from '../helpers/WebmasterReport.js';
11
11
  import { ApplicationFeeService } from './ApplicationFeeService.js';
12
12
  import { BalanceItemService } from './BalanceItemService.js';
@@ -35,7 +35,7 @@ export class InvoiceService {
35
35
  model.customer = struct.customer;
36
36
 
37
37
  if (model.customer.company) {
38
- await ViesHelper.checkCompany(model.customer.company, model.customer.company);
38
+ await ViesService.checkCompany(model.customer.company, null, { forceValidation: true });
39
39
  }
40
40
 
41
41
  struct.updatePrices();