@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.143.1",
3
+ "version": "2.145.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -58,27 +58,27 @@
58
58
  "@aws-sdk/client-sqs": "3.1090.0",
59
59
  "@aws-sdk/s3-request-presigner": "3.1090.0",
60
60
  "@bwip-js/node": "4.11.2",
61
- "@mollie/api-client": "4.6.0",
62
61
  "@simonbackx/simple-database": "1.37.1",
63
62
  "@simonbackx/simple-encoding": "2.26.11",
64
63
  "@simonbackx/simple-endpoints": "1.22.0",
65
64
  "@simonbackx/simple-errors": "1.5.0",
66
65
  "@simonbackx/simple-logging": "1.0.1",
67
66
  "@simplewebauthn/server": "13.3.2",
68
- "@stamhoofd/backend-env": "2.143.1",
69
- "@stamhoofd/backend-i18n": "2.143.1",
70
- "@stamhoofd/backend-middleware": "2.143.1",
71
- "@stamhoofd/crons": "2.143.1",
72
- "@stamhoofd/email": "2.143.1",
73
- "@stamhoofd/excel-writer": "2.143.1",
74
- "@stamhoofd/logging": "2.143.1",
75
- "@stamhoofd/models": "2.143.1",
76
- "@stamhoofd/object-differ": "2.143.1",
77
- "@stamhoofd/queues": "2.143.1",
78
- "@stamhoofd/sql": "2.143.1",
79
- "@stamhoofd/structures": "2.143.1",
80
- "@stamhoofd/types": "2.143.1",
81
- "@stamhoofd/utility": "2.143.1",
67
+ "@stamhoofd/backend-env": "2.145.0",
68
+ "@stamhoofd/backend-i18n": "2.145.0",
69
+ "@stamhoofd/backend-middleware": "2.145.0",
70
+ "@stamhoofd/crons": "2.145.0",
71
+ "@stamhoofd/email": "2.145.0",
72
+ "@stamhoofd/excel-writer": "2.145.0",
73
+ "@stamhoofd/logging": "2.145.0",
74
+ "@stamhoofd/models": "2.145.0",
75
+ "@stamhoofd/object-differ": "2.145.0",
76
+ "@stamhoofd/queues": "2.145.0",
77
+ "@stamhoofd/sql": "2.145.0",
78
+ "@stamhoofd/structures": "2.145.0",
79
+ "@stamhoofd/types": "2.145.0",
80
+ "@stamhoofd/utility": "2.145.0",
81
+ "@stamhoofd/vies": "2.145.0",
82
82
  "archiver": "7.0.1",
83
83
  "argon2": "0.44.0",
84
84
  "axios": "1.18.1",
@@ -89,11 +89,11 @@
89
89
  "handlebars": "4.7.9",
90
90
  "jose": "5.10.0",
91
91
  "jsonwebtoken": "9.0.3",
92
- "jsvat-next": "3.0.4",
93
92
  "libphonenumber-js": "1.13.9",
94
93
  "luxon": "3.7.2",
95
94
  "mailparser": "3.9.14",
96
95
  "mockdate": "3.0.5",
96
+ "mollie-api-typescript": "1.12.23",
97
97
  "mysql2": "3.23.1",
98
98
  "node-rsa": "2.0.0",
99
99
  "openid-client": "5.7.1",
@@ -102,7 +102,7 @@
102
102
  "uuid": "14.0.1"
103
103
  },
104
104
  "devDependencies": {
105
- "@stamhoofd/test-utils": "2.143.1",
105
+ "@stamhoofd/test-utils": "2.145.0",
106
106
  "@types/cookie": "0.6.0",
107
107
  "@types/luxon": "3.7.2",
108
108
  "@types/mailparser": "3.4.6",
@@ -117,5 +117,5 @@
117
117
  "publishConfig": {
118
118
  "access": "public"
119
119
  },
120
- "gitHead": "2102409bc039c3ee4dc77cdd6c1338f6cf390c9c"
120
+ "gitHead": "ee58cd6383c03df39218d8589679638019241e15"
121
121
  }
@@ -90,8 +90,8 @@ describe('Cron.fake-settlements', () => {
90
90
  settledAt: Formatter.luxon(inWeek(2)).startOf('week').plus({ weeks: 1, days: 2 }).toJSDate(),
91
91
 
92
92
  // Refunds are taken out of the payout, just like the provider does, and so are the
93
- // fake Mollie costs: 3 payments x 0.30 + 21% VAT
94
- amount: 65_0000 - 90_00 - 18_90,
93
+ // fake Mollie fees: 3 payments x 0.363
94
+ amount: 65_0000 - 3 * 36_30,
95
95
  });
96
96
 
97
97
  // All payments of the week point to the same payout
@@ -135,8 +135,7 @@ describe('Cron.fake-settlements', () => {
135
135
  ];
136
136
 
137
137
  // Same week, but four accounts: four payouts, each holding only its own money. The Mollie
138
- // payouts are reduced by the fake cost of one payment (0.30 + 21% VAT); the Stripe payments
139
- // carry no fees here
138
+ // payouts are reduced by the fake fee of one payment; the Stripe payments carry no fees here
140
139
  expect(settlements.map(s => s!.amount)).toEqual([50_0000 - 36_30, 30_0000 - 36_30, 20_0000, 15_0000]);
141
140
  expect(new Set(settlements.map(s => s!.id)).size).toBe(4);
142
141
  expect(new Set(settlements.map(s => s!.reference)).size).toBe(4);
@@ -192,7 +191,7 @@ describe('Cron.fake-settlements', () => {
192
191
  return await Settlement.select().where('externalId', reference!.id).first(true);
193
192
  };
194
193
 
195
- test('A Mollie payout stores payment lines and cost rows, and reconciles to zero', async () => {
194
+ test('A Mollie payout stores payment lines and fee rows, and reconciles to zero', async () => {
196
195
  const first = await createPayment({ price: 50_0000 });
197
196
  const second = await createPayment({ price: -10_0000, type: PaymentType.Refund });
198
197
 
@@ -201,7 +200,7 @@ describe('Cron.fake-settlements', () => {
201
200
  const settlement = await getSettlementRow(first);
202
201
  expect(settlement.provider).toBe(PaymentProvider.Mollie);
203
202
  expect(settlement.organizationId).toBe(organization.id);
204
- expect(settlement.amount).toBe(40_0000 - 60_00 - 12_60);
203
+ expect(settlement.amount).toBe(40_0000 - 2 * 36_30);
205
204
  expect(settlement.unexplainedAmount).toBe(0);
206
205
  expect(settlement.syncedAt).not.toBeNull();
207
206
 
@@ -211,12 +210,15 @@ describe('Cron.fake-settlements', () => {
211
210
  [second.id, -10_0000],
212
211
  ].sort());
213
212
 
214
- const settledMonth = settlement.settledAt.getFullYear() + '-' + (settlement.settledAt.getMonth() + 1).toString().padStart(2, '0');
215
213
  const charges = await SettlementCharge.select().where('settlementId', settlement.id).fetch();
216
- expect(charges.map(c => ({ type: c.type, amount: c.amount, providerInvoiceId: c.providerInvoiceId })).sort((a, b) => a.amount - b.amount)).toEqual([
217
- { type: SettlementChargeType.ProviderTransactionFee, amount: -60_00, providerInvoiceId: 'fake-invoice-mollie-' + settledMonth },
218
- { type: SettlementChargeType.Tax, amount: -12_60, providerInvoiceId: 'fake-invoice-mollie-' + settledMonth },
219
- ]);
214
+ expect(charges.map(c => [c.paymentId, c.type, c.amount]).sort()).toEqual([
215
+ [first.id, SettlementChargeType.ProviderTransactionFee, -36_30],
216
+ [second.id, SettlementChargeType.ProviderTransactionFee, -36_30],
217
+ ].sort());
218
+
219
+ // Each settled payment holds its own fee
220
+ expect((await Payment.getByID(first.id))!.transferFee).toBe(36_30);
221
+ expect((await Payment.getByID(second.id))!.transferFee).toBe(36_30);
220
222
  });
221
223
 
222
224
  test('A Stripe application fee is mirrored on the payout and a monthly platform payout', async () => {
@@ -34,10 +34,9 @@ const PAYOUT_DELAY_DAYS = 2;
34
34
  const BATCH_SIZE = 1000;
35
35
 
36
36
  /**
37
- * Fake Mollie transaction fee per settled payment, excluding VAT.
37
+ * Fake Mollie transaction fee per settled payment, including VAT.
38
38
  */
39
- const MOLLIE_FEE_PER_PAYMENT = 30_00;
40
- const MOLLIE_FEE_VAT_PERCENTAGE = 21;
39
+ const MOLLIE_FEE_PER_PAYMENT = 36_30;
41
40
 
42
41
  export async function createFakeSettlements() {
43
42
  if (STAMHOOFD.environment !== 'development') {
@@ -183,8 +182,7 @@ async function settleWeek(payments: Payment[], weekStart: DateTime, settledAt: D
183
182
  return total + serviceFee + transferFee;
184
183
  }, 0);
185
184
  } else if (provider === PaymentProvider.Mollie) {
186
- const net = MOLLIE_FEE_PER_PAYMENT * group.length;
187
- fees = net + Math.round(net * MOLLIE_FEE_VAT_PERCENTAGE / 100);
185
+ fees = MOLLIE_FEE_PER_PAYMENT * group.length;
188
186
  }
189
187
 
190
188
  const grossAmount = group.reduce((total, payment) => total + payment.price, 0);
@@ -223,7 +221,7 @@ async function settleWeek(payments: Payment[], weekStart: DateTime, settledAt: D
223
221
  if (provider === PaymentProvider.Stripe) {
224
222
  await createStripeFeeRows(settlement, group, settledAt, platformOrganizationId);
225
223
  } else if (provider === PaymentProvider.Mollie) {
226
- await createMollieCostRows(settlement, group, settledAt);
224
+ await createMollieFeeRows(settlement, group);
227
225
  }
228
226
 
229
227
  await finishFakeSync(settlement);
@@ -322,43 +320,29 @@ async function createStripeFeeRows(settlement: Settlement, group: Payment[], set
322
320
 
323
321
  // A dev database can contain fee payments (e.g. copied history): keep their derived lines
324
322
  // in sync like a real payout walk does
325
- await SettlementService.updatePaymentSettlementsForAccountDeductionBalanceItems([...invoicedFeeBalanceItemIds]);
323
+ await SettlementService.updatePaymentSettlementsForApplicationFeeBalanceItems([...invoicedFeeBalanceItemIds]);
326
324
 
327
325
  await finishFakeSync(platformSettlement);
328
326
  }
329
327
 
330
328
  /**
331
- * Mollie deducts its own costs from the settlement: one fake transaction fee line plus its VAT, so
332
- * the settlement reconciles to 0 like a real one.
329
+ * Mollie deducts its fee per transaction: one fake fee charge per settled payment, linked to it
330
+ * like the real balance transaction walk stores, so the settlement reconciles to 0.
333
331
  */
334
- async function createMollieCostRows(settlement: Settlement, group: Payment[], settledAt: Date) {
335
- const externalId = settlement.externalId + ':cost:0';
336
- const existing = await SettlementCharge.select().where('externalId', externalId).first(false);
337
-
338
- const addedNet = MOLLIE_FEE_PER_PAYMENT * group.length;
339
- const net = (existing ? -existing.amount : 0) + addedNet;
340
- const vat = Math.round(net * MOLLIE_FEE_VAT_PERCENTAGE / 100);
341
- const providerInvoiceId = 'fake-invoice-mollie-' + SettlementService.getPeriodKey(settledAt);
342
-
343
- await SettlementService.upsertCharge({
344
- type: SettlementChargeType.ProviderTransactionFee,
345
- externalId,
346
- amount: -net,
347
- settlementId: settlement.id,
348
- organizationId: settlement.organizationId,
349
- providerInvoiceId,
350
- description: 'DEV Mollie transactiekosten',
351
- occurredAt: settledAt,
352
- });
332
+ async function createMollieFeeRows(settlement: Settlement, group: Payment[]) {
333
+ for (const payment of group) {
334
+ await SettlementService.upsertCharge({
335
+ type: SettlementChargeType.ProviderTransactionFee,
336
+ externalId: 'fake-baltr-' + payment.id,
337
+ amount: -MOLLIE_FEE_PER_PAYMENT,
338
+ settlementId: settlement.id,
339
+ paymentId: payment.id,
340
+ organizationId: settlement.organizationId,
341
+ description: 'DEV Mollie transactiekosten',
342
+ occurredAt: payment.paidAt!,
343
+ });
353
344
 
354
- await SettlementService.upsertCharge({
355
- type: SettlementChargeType.Tax,
356
- externalId: externalId + ':tax',
357
- amount: -vat,
358
- settlementId: settlement.id,
359
- organizationId: settlement.organizationId,
360
- providerInvoiceId,
361
- description: 'DEV BTW op Mollie transactiekosten',
362
- occurredAt: settledAt,
363
- });
345
+ payment.transferFee = MOLLIE_FEE_PER_PAYMENT;
346
+ await payment.save();
347
+ }
364
348
  }
@@ -42,40 +42,42 @@ export async function checkMollieChargebacksFor(service: MollieService, checkAll
42
42
  // Check last 3 days
43
43
  const offset = new Date(Date.now() - 1000 * 60 * 60 * 24 * 4);
44
44
 
45
- // due to a bug in mollie client code, testmode paramter is missing in the typescript definitions
46
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
47
- for await (const chargeback of service.client.chargebacks.iterate({ sort: 'desc', testmode: service.testMode } as any)) {
48
- if (!checkAll && new Date(chargeback.createdAt) < offset) {
49
- break;
50
- }
45
+ const pages = await service.client.chargebacks.all({ sort: 'desc', limit: 250 });
51
46
 
52
- // Check if this chargeback has been handled
53
- const existingChargeback = await MolliePayment.select().where('mollieId', chargeback.id).first(false);
54
- if (existingChargeback) {
55
- // We can break because all older chargebacks will also be handled
56
- break;
57
- }
47
+ pagesLoop: for await (const page of pages) {
48
+ for (const chargeback of page.result.embedded.chargebacks) {
49
+ if (!checkAll && new Date(chargeback.createdAt) < offset) {
50
+ break pagesLoop;
51
+ }
52
+
53
+ // Check if this chargeback has been handled
54
+ const existingChargeback = await MolliePayment.select().where('mollieId', chargeback.id).first(false);
55
+ if (existingChargeback) {
56
+ // We can break because all older chargebacks will also be handled
57
+ break pagesLoop;
58
+ }
58
59
 
59
- if (chargeback.paymentId) {
60
- const molliePayment = await MolliePayment.select().where('mollieId', chargeback.paymentId).first(false);
61
- if (molliePayment) {
62
- const payment = await Payment.getByID(molliePayment.paymentId);
63
- if (payment) {
64
- try {
65
- const amount = Math.round(parseFloat(chargeback.amount.value) * 100) * 100;
66
- const createdPayment = await PaymentService.registerChargeback(payment, amount, new Date(chargeback.createdAt));
60
+ if (chargeback.paymentId) {
61
+ const molliePayment = await MolliePayment.select().where('mollieId', chargeback.paymentId).first(false);
62
+ if (molliePayment) {
63
+ const payment = await Payment.getByID(molliePayment.paymentId);
64
+ if (payment) {
65
+ try {
66
+ const amount = Math.round(parseFloat(chargeback.amount.value) * 100) * 100;
67
+ const createdPayment = await PaymentService.registerChargeback(payment, amount, new Date(chargeback.createdAt));
67
68
 
68
- // Link Mollie chargeback ID (so we can set settlement later in the settlements cron)
69
- const molliePayment = new MolliePayment();
70
- molliePayment.paymentId = createdPayment.id;
71
- molliePayment.mollieId = chargeback.id;
72
- await molliePayment.save();
73
- } catch (e) {
74
- console.error('Failed to register chargeback ' + chargeback.id, e);
69
+ // Link Mollie chargeback ID (so we can set settlement later in the settlements cron)
70
+ const molliePayment = new MolliePayment();
71
+ molliePayment.paymentId = createdPayment.id;
72
+ molliePayment.mollieId = chargeback.id;
73
+ await molliePayment.save();
74
+ } catch (e) {
75
+ console.error('Failed to register chargeback ' + chargeback.id, e);
76
+ }
75
77
  }
78
+ } else {
79
+ console.error('Invalid chargeback payment id ' + chargeback.paymentId + ', not found');
76
80
  }
77
- } else {
78
- console.error('Invalid chargeback payment id ' + chargeback.paymentId + ', not found');
79
81
  }
80
82
  }
81
83
  }
@@ -1,7 +1,7 @@
1
- import { RefundStatus } from '@mollie/api-client';
2
1
  import { registerCron } from '@stamhoofd/crons';
3
2
  import { MolliePayment, MollieToken, Organization, Payment } from '@stamhoofd/models';
4
3
  import { PaymentStatus, PaymentType } from '@stamhoofd/structures';
4
+ import type { ListEntityRefundStatus } from 'mollie-api-typescript/models';
5
5
  import { MollieService } from '../services/MollieService.js';
6
6
  import { PaymentService } from '../services/PaymentService.js';
7
7
 
@@ -49,73 +49,75 @@ export async function checkMollieRefundsFor(service: MollieService, checkAll = f
49
49
  // Check last 7 days
50
50
  const offset = new Date(Date.now() - 1000 * 60 * 60 * 24 * 7);
51
51
 
52
- // due to a bug in mollie client code, testmode paramter is missing in the typescript definitions
53
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
54
- for await (const refund of service.client.refunds.iterate({ sort: 'desc', testmode: service.testMode } as any)) {
55
- const createdAt = new Date(refund.createdAt);
52
+ const pages = await service.client.refunds.all({ sort: 'desc', limit: 250 });
56
53
 
57
- if (createdAt < MOLLIE_REFUNDS_MINIMUM_DATE) {
58
- break;
59
- }
54
+ pagesLoop: for await (const page of pages) {
55
+ for (const refund of page.result.embedded.refunds) {
56
+ const createdAt = new Date(refund.createdAt);
60
57
 
61
- if (!checkAll && createdAt < offset) {
62
- break;
63
- }
58
+ if (createdAt < MOLLIE_REFUNDS_MINIMUM_DATE) {
59
+ break pagesLoop;
60
+ }
64
61
 
65
- // Check if this refund is already registered (refunds created via the API are linked on
66
- // creation, so we cannot break here: an older refund created in the Mollie UI could still
67
- // be unhandled). Registered refunds that are still pending locally are reconciled: Mollie
68
- // refunds can still fail or be canceled until they are actually executed.
69
- const existingRefund = await MolliePayment.select().where('mollieId', refund.id).first(false);
70
- if (existingRefund) {
71
- await reconcileRefundStatus(existingRefund.paymentId, refund.status, createdAt);
72
- continue;
73
- }
62
+ if (!checkAll && createdAt < offset) {
63
+ break pagesLoop;
64
+ }
74
65
 
75
- // Refunds that were canceled or failed before we ever registered them can be ignored
76
- if (refund.status === RefundStatus.canceled || refund.status === RefundStatus.failed) {
77
- continue;
78
- }
66
+ // Check if this refund is already registered (refunds created via the API are linked on
67
+ // creation, so we cannot break here: an older refund created in the Mollie UI could still
68
+ // be unhandled). Registered refunds that are still pending locally are reconciled: Mollie
69
+ // refunds can still fail or be canceled until they are actually executed.
70
+ const existingRefund = await MolliePayment.select().where('mollieId', refund.id).first(false);
71
+ if (existingRefund) {
72
+ await reconcileRefundStatus(existingRefund.paymentId, refund.status, createdAt);
73
+ continue;
74
+ }
79
75
 
80
- // Safety net: when creating a refund via the API succeeded, but saving the link afterwards
81
- // failed, the local refund payment already exists. Restore the link instead of registering
82
- // the refund twice (the metadata is set in PaymentService.createRefund).
83
- const metadata = refund.metadata as { refundPaymentId?: string } | null;
84
- if (metadata?.refundPaymentId) {
85
- const localRefund = await Payment.getByID(metadata.refundPaymentId);
86
- if (localRefund && localRefund.type === PaymentType.Refund) {
87
- console.error('Restoring missing link for Mollie refund ' + refund.id + ' to payment ' + localRefund.id);
88
- const link = new MolliePayment();
89
- link.paymentId = localRefund.id;
90
- link.mollieId = refund.id;
91
- await link.save();
92
-
93
- await reconcileRefundStatus(localRefund.id, refund.status, createdAt);
76
+ // Refunds that were canceled or failed before we ever registered them can be ignored
77
+ if (refund.status === 'canceled' || refund.status === 'failed') {
94
78
  continue;
95
79
  }
96
- }
97
80
 
98
- if (refund.paymentId) {
99
- const molliePayment = await MolliePayment.select().where('mollieId', refund.paymentId).first(false);
100
- if (molliePayment) {
101
- const payment = await Payment.getByID(molliePayment.paymentId);
102
- if (payment) {
103
- try {
104
- const amount = Math.round(parseFloat(refund.amount.value) * 100) * 100;
105
- const status = MollieService.refundStatusToPaymentStatus(refund.status) === PaymentStatus.Succeeded ? PaymentStatus.Succeeded : PaymentStatus.Pending;
106
- const createdPayment = await PaymentService.registerRefund(payment, amount, createdAt, status);
107
-
108
- // Link Mollie refund ID (so we don't register it twice and can set the settlement later)
109
- const molliePayment = new MolliePayment();
110
- molliePayment.paymentId = createdPayment.id;
111
- molliePayment.mollieId = refund.id;
112
- await molliePayment.save();
113
- } catch (e) {
114
- console.error('Failed to register refund ' + refund.id, e);
81
+ // Safety net: when creating a refund via the API succeeded, but saving the link afterwards
82
+ // failed, the local refund payment already exists. Restore the link instead of registering
83
+ // the refund twice (the metadata is set in PaymentService.createRefund).
84
+ const metadata = refund.metadata as { refundPaymentId?: string } | null;
85
+ if (metadata?.refundPaymentId) {
86
+ const localRefund = await Payment.getByID(metadata.refundPaymentId);
87
+ if (localRefund && localRefund.type === PaymentType.Refund) {
88
+ console.error('Restoring missing link for Mollie refund ' + refund.id + ' to payment ' + localRefund.id);
89
+ const link = new MolliePayment();
90
+ link.paymentId = localRefund.id;
91
+ link.mollieId = refund.id;
92
+ await link.save();
93
+
94
+ await reconcileRefundStatus(localRefund.id, refund.status, createdAt);
95
+ continue;
96
+ }
97
+ }
98
+
99
+ if (refund.paymentId) {
100
+ const molliePayment = await MolliePayment.select().where('mollieId', refund.paymentId).first(false);
101
+ if (molliePayment) {
102
+ const payment = await Payment.getByID(molliePayment.paymentId);
103
+ if (payment) {
104
+ try {
105
+ const amount = Math.round(parseFloat(refund.amount.value) * 100) * 100;
106
+ const status = MollieService.refundStatusToPaymentStatus(refund.status) === PaymentStatus.Succeeded ? PaymentStatus.Succeeded : PaymentStatus.Pending;
107
+ const createdPayment = await PaymentService.registerRefund(payment, amount, createdAt, status);
108
+
109
+ // Link Mollie refund ID (so we don't register it twice and can set the settlement later)
110
+ const molliePayment = new MolliePayment();
111
+ molliePayment.paymentId = createdPayment.id;
112
+ molliePayment.mollieId = refund.id;
113
+ await molliePayment.save();
114
+ } catch (e) {
115
+ console.error('Failed to register refund ' + refund.id, e);
116
+ }
115
117
  }
118
+ } else {
119
+ console.error('Invalid refund payment id ' + refund.paymentId + ', not found');
116
120
  }
117
- } else {
118
- console.error('Invalid refund payment id ' + refund.paymentId + ', not found');
119
121
  }
120
122
  }
121
123
  }
@@ -129,7 +131,7 @@ export async function checkMollieRefundsFor(service: MollieService, checkAll = f
129
131
  * Update a locally registered refund that is still pending when Mollie reports a final status
130
132
  * (refunded, failed or canceled).
131
133
  */
132
- async function reconcileRefundStatus(paymentId: string, mollieStatus: RefundStatus, createdAt: Date) {
134
+ async function reconcileRefundStatus(paymentId: string, mollieStatus: ListEntityRefundStatus, createdAt: Date) {
133
135
  const payment = await Payment.getByID(paymentId);
134
136
  if (!payment || !payment.organizationId) {
135
137
  return;
@@ -10,6 +10,7 @@ import { Organization as OrganizationStruct } from '@stamhoofd/structures';
10
10
  import { Formatter } from '@stamhoofd/utility';
11
11
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
12
12
  import { Context } from '../../../helpers/Context.js';
13
+ import { SQL } from '@stamhoofd/sql';
13
14
 
14
15
  type Params = Record<string, never>;
15
16
  type Query = undefined;
@@ -64,13 +65,23 @@ export class PatchOrganizationsEndpoint extends Endpoint<Params, Query, Body, Re
64
65
 
65
66
  // Financial records may not disappear together with the organization that had to pay them
66
67
  const payerRecordCounts = await Promise.all([
67
- BalanceItem.select().where('payingOrganizationId', id).count(),
68
- Payment.select().where('payingOrganizationId', id).count(),
69
- ApplicationFee.select().where('payingOrganizationId', id).count(),
70
- Invoice.select().where('payingOrganizationId', id).count(),
68
+ BalanceItem.select().where('payingOrganizationId', id).first(false),
69
+ Payment.select().where('payingOrganizationId', id).first(false),
70
+ ApplicationFee.select().where('payingOrganizationId', id).first(false),
71
+ Invoice.select().where('payingOrganizationId', id).first(false),
72
+ Payment.select()
73
+ .where('organizationId', id)
74
+ .where(
75
+ SQL.where('serviceFeePayout', '!=', 0)
76
+ .or('serviceFeeManual', '!=', 0)
77
+ .or('serviceFeeManualCharged', '!=', 0)
78
+ .or('transferFeeManual', '!=', 0)
79
+ .or('transferFeeManualCharged', '!=', 0),
80
+ )
81
+ .first(false),
71
82
  ]);
72
83
 
73
- if (payerRecordCounts.some(count => count > 0)) {
84
+ if (payerRecordCounts.some(hasSome => hasSome !== null)) {
74
85
  throw new SimpleError({
75
86
  code: 'organization_has_financial_records',
76
87
  message: 'Organization is still referenced as paying organization',