@stamhoofd/backend 2.120.5 → 2.121.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 (61) hide show
  1. package/package.json +12 -12
  2. package/src/audit-logs/RegistrationInvitationLogger.ts +46 -0
  3. package/src/audit-logs/init.ts +2 -0
  4. package/src/crons/index.ts +2 -0
  5. package/src/crons/invoices.ts +166 -0
  6. package/src/crons/mollie-chargebacks.ts +87 -0
  7. package/src/crons.ts +47 -10
  8. package/src/email-recipient-loaders/payments.ts +84 -41
  9. package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +51 -0
  10. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +22 -3
  11. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +4 -0
  12. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
  13. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +495 -0
  14. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +216 -0
  15. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +405 -0
  16. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +168 -0
  17. package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +15 -0
  18. package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +3 -4
  19. package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
  20. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
  21. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +42 -19
  22. package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
  23. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +11 -3
  24. package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +308 -0
  25. package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
  26. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +7 -0
  27. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +5 -4
  28. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +7 -2
  29. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +17 -8
  30. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -3
  31. package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +127 -0
  32. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +13 -4
  33. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +7 -1
  34. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
  35. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -11
  36. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +14 -19
  37. package/src/helpers/AdminPermissionChecker.ts +11 -3
  38. package/src/helpers/AuthenticatedStructures.ts +94 -6
  39. package/src/helpers/FinancialSupportHelper.ts +21 -0
  40. package/src/helpers/RecordAnswerHelper.test.ts +746 -0
  41. package/src/helpers/RecordAnswerHelper.ts +116 -0
  42. package/src/helpers/StripeHelper.ts +2 -3
  43. package/src/helpers/ViesHelper.ts +7 -3
  44. package/src/seeds/1750090030-records-configuration.ts +68 -3
  45. package/src/seeds/1752848561-groups-registration-periods.ts +26 -2
  46. package/src/seeds/1779121239-default-invoice-email-template.sql +3 -0
  47. package/src/services/BalanceItemService.ts +12 -16
  48. package/src/services/InvoiceService.ts +372 -72
  49. package/src/services/MollieService.ts +537 -0
  50. package/src/services/PaymentMandateService.ts +214 -0
  51. package/src/services/PaymentService.ts +578 -222
  52. package/src/services/PlatformMembershipService.ts +1 -1
  53. package/src/services/RegistrationService.ts +66 -5
  54. package/src/services/STPackageService.ts +0 -7
  55. package/src/services/data/invoice.hbs.html +686 -0
  56. package/src/sql-filters/groups.ts +11 -1
  57. package/src/sql-filters/payments.ts +5 -0
  58. package/src/sql-filters/registration-invitations.ts +90 -0
  59. package/src/sql-sorters/registration-invitations.ts +36 -0
  60. package/vitest.config.js +1 -0
  61. package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +0 -216
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.120.5",
3
+ "version": "2.121.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -31,7 +31,7 @@
31
31
  "dev": "wait-on ../../shared/middleware/dist/index.js && concurrently -r 'yarn -s build --watch --preserveWatchOutput' \"yarn -s dev:watch\"",
32
32
  "dev:build": "yarn -s build",
33
33
  "dev:full": "yarn -s dev",
34
- "dev:watch": "wait-on ./dist/index.js && nodemon --quiet --inspect=5858 --watch dist --watch '../../../shared/*/dist/' --watch '../../shared/*/dist/' --ext .ts,.json,.sql,.js --delay 2000ms --exec 'node --enable-source-maps ./dist/index.js' --signal SIGTERM",
34
+ "dev:watch": "wait-on ./dist/index.js && nodemon --quiet --inspect=5858 --watch dist --watch '../../../shared/*/dist/' --watch '../../../shared/*/package.json' --watch '../../shared/*/dist/' --watch '../../shared/*/package.json' --ext .ts,.json,.sql,.js,.html --delay 2000ms --exec 'echo \"Restarting API...\" && node --enable-source-maps ./dist/index.js' --signal SIGTERM",
35
35
  "dev:backend": "yarn -s dev",
36
36
  "build": "tsc --build tsconfig.build.json --clean && yarn -s copy-assets && tsc --build tsconfig.build.json",
37
37
  "copy-assets": "rsync --delete --mkpath --exclude='*.ts' --exclude='*.js' --exclude='*.map' -r --checksum ./src/ ./dist/src/",
@@ -60,19 +60,19 @@
60
60
  "@aws-sdk/client-sqs": "^3.839.0",
61
61
  "@aws-sdk/s3-request-presigner": "^3.839.0",
62
62
  "@bwip-js/node": "^4.5.1",
63
- "@mollie/api-client": "4.3.3",
63
+ "@mollie/api-client": "4.5.0",
64
64
  "@simonbackx/simple-database": "1.36.12",
65
65
  "@simonbackx/simple-encoding": "2.26.6",
66
66
  "@simonbackx/simple-endpoints": "1.21.1",
67
67
  "@simonbackx/simple-logging": "^1.0.1",
68
- "@stamhoofd/backend-i18n": "2.120.5",
69
- "@stamhoofd/backend-middleware": "2.120.5",
70
- "@stamhoofd/email": "2.120.5",
71
- "@stamhoofd/models": "2.120.5",
72
- "@stamhoofd/queues": "2.120.5",
73
- "@stamhoofd/sql": "2.120.5",
74
- "@stamhoofd/structures": "2.120.5",
75
- "@stamhoofd/utility": "2.120.5",
68
+ "@stamhoofd/backend-i18n": "2.121.0",
69
+ "@stamhoofd/backend-middleware": "2.121.0",
70
+ "@stamhoofd/email": "2.121.0",
71
+ "@stamhoofd/models": "2.121.0",
72
+ "@stamhoofd/queues": "2.121.0",
73
+ "@stamhoofd/sql": "2.121.0",
74
+ "@stamhoofd/structures": "2.121.0",
75
+ "@stamhoofd/utility": "2.121.0",
76
76
  "archiver": "^7.0.1",
77
77
  "axios": "^1.13.2",
78
78
  "cookie": "^0.7.0",
@@ -90,5 +90,5 @@
90
90
  "publishConfig": {
91
91
  "access": "public"
92
92
  },
93
- "gitHead": "5c4a1e159033bd2bfb2f6f3c60c9b03bd89c9291"
93
+ "gitHead": "11a280f3924b9eaab5af78f265216c774df69266"
94
94
  }
@@ -0,0 +1,46 @@
1
+ import { Group, Member, RegistrationInvitation } from '@stamhoofd/models';
2
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
3
+ import { getDefaultGenerator, ModelLogger } from './ModelLogger.js';
4
+
5
+ const defaultGenerator = getDefaultGenerator<RegistrationInvitation>({
6
+ created: AuditLogType.RegistrationInvitationAdded,
7
+ deleted: AuditLogType.RegistrationInvitationDeleted,
8
+ });
9
+
10
+ export const RegistrationInvitationLogger = new ModelLogger(RegistrationInvitation, {
11
+ async optionsGenerator(event) {
12
+ const result = await defaultGenerator(event);
13
+ if (!result) {
14
+ return;
15
+ }
16
+
17
+ const member = await Member.getByID(event.model.memberId);
18
+ const group = await Group.getByID(event.model.groupId);
19
+
20
+ if (!member || !group) {
21
+ return;
22
+ }
23
+
24
+ return {
25
+ ...result,
26
+ data: {
27
+ member,
28
+ group,
29
+ },
30
+ };
31
+ },
32
+ createReplacements(_, { data }) {
33
+ return new Map([
34
+ ['m', AuditLogReplacement.create({
35
+ id: data.member.id,
36
+ value: data.member.details.name,
37
+ type: AuditLogReplacementType.Member,
38
+ })],
39
+ ['g', AuditLogReplacement.create({
40
+ id: data.group.id,
41
+ value: data.group.settings.name.toString(),
42
+ type: AuditLogReplacementType.Group,
43
+ })],
44
+ ]);
45
+ },
46
+ });
@@ -18,6 +18,7 @@ import { StripeAccountLogger } from './StripeAccountLogger.js';
18
18
  import { UserLogger } from './UserLogger.js';
19
19
  import { WebshopLogger } from './WebshopLogger.js';
20
20
  import { modelLogDefinitions } from '../services/AuditLogService.js';
21
+ import { RegistrationInvitationLogger } from './RegistrationInvitationLogger.js';
21
22
 
22
23
  modelLogDefinitions.set(RegistrationLogger.model, RegistrationLogger);
23
24
  modelLogDefinitions.set(GroupLogger.model, GroupLogger);
@@ -38,3 +39,4 @@ modelLogDefinitions.set(EmailLogger.model, EmailLogger);
38
39
  modelLogDefinitions.set(EmailTemplateLogger.model, EmailTemplateLogger);
39
40
  modelLogDefinitions.set(EmailAddressLogger.model, EmailAddressLogger);
40
41
  modelLogDefinitions.set(UserLogger.model, UserLogger);
42
+ modelLogDefinitions.set(RegistrationInvitationLogger.model, RegistrationInvitationLogger);
@@ -5,3 +5,5 @@ import './update-cached-balances.js';
5
5
  import './balance-emails.js';
6
6
  import './delete-old-email-drafts.js';
7
7
  import './delete-archived-data.js';
8
+ import './mollie-chargebacks.js';
9
+ import './invoices.js';
@@ -0,0 +1,166 @@
1
+ import { isSimpleError, isSimpleErrors } from '@simonbackx/simple-errors';
2
+ import { registerCron } from '@stamhoofd/crons';
3
+ import type { Invoice } from '@stamhoofd/models';
4
+ import { Organization, Payment, sendEmailTemplate } from '@stamhoofd/models';
5
+ import type { IterableSQLSelect } from '@stamhoofd/sql';
6
+ import { EmailTemplateType, InvoiceStruct, PaymentStatus, Replacement } from '@stamhoofd/structures';
7
+ import { Formatter, Sorter } from '@stamhoofd/utility';
8
+ import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures.js';
9
+ import { InvoiceService } from '../services/InvoiceService.js';
10
+
11
+ registerCron('invoices', invoices);
12
+
13
+ let lastFullRun = new Date(0);
14
+ let savedIterator: IterableSQLSelect<Organization> | null = null;
15
+
16
+ const bootAt = new Date();
17
+
18
+ async function invoices() {
19
+ // Do not run within 30 minutes after boot to avoid creating multiple email models for emails that failed to send
20
+ if (bootAt.getTime() > new Date().getTime() - 1000 * 60 * 30 && STAMHOOFD.environment !== 'development') {
21
+ return;
22
+ }
23
+
24
+ if (lastFullRun.getTime() > new Date().getTime() - 1000 * 60 * 60 * 12) {
25
+ return;
26
+ }
27
+
28
+ if ((new Date().getHours() > 10 || new Date().getHours() < 3) && STAMHOOFD.environment !== 'development') {
29
+ return;
30
+ }
31
+
32
+ // Get the next x organization to send e-mails for
33
+ if (savedIterator === null) {
34
+ savedIterator = Organization.select().limit(10).all();
35
+ }
36
+
37
+ for await (const organization of savedIterator.maxQueries(5)) {
38
+ if (!organization.meta.invoicesEnabled) {
39
+ continue;
40
+ }
41
+
42
+ // Create all invoices for this organization
43
+ await createInvoicesFor(organization)
44
+ }
45
+
46
+ if (savedIterator.isDone) {
47
+ savedIterator = null;
48
+ lastFullRun = new Date();
49
+ }
50
+ }
51
+
52
+ async function createInvoicesFor(organization: Organization) {
53
+ const seller = organization.meta.companies[0];
54
+ if (!seller) {
55
+ return;
56
+ }
57
+
58
+ // Belgian rules: allowed to invoice up to the 15th day of the next month. We extend it with one month to fix mistakes.
59
+ const today = Formatter.luxon();
60
+ const startDate = today.day <= 15 ? today.minus({month: 2}).startOf('month') : today.minus({month: 1}).startOf('month');
61
+
62
+ // Don't invoice below 1 euro - unless we reached the timeout date for invoices (end of month + 15 days - 3 days margin)
63
+ const invoiceLimit = 1_0000
64
+ function getPaymentTimeoutDate(p: Payment) {
65
+ return Formatter.luxon(p.paidAt ?? p.createdAt).plus({month: 1}).set({day: 15 - 3}).startOf('day').toJSDate()
66
+ }
67
+
68
+ console.log('Fetching all payments between ' + Formatter.dateTime(startDate.toJSDate()) + ' and now for ' + organization.name);
69
+
70
+ const payments = await Payment.select()
71
+ .where('organizationId', organization.id)
72
+ .where('status', PaymentStatus.Succeeded)
73
+ .where('paidAt', '>=', startDate.toJSDate())
74
+ .where('invoiceId', null)
75
+ .where('customer', '!=', null)
76
+ .where('payingOrganizationId', '!=', null)
77
+ .limit(1_000)
78
+ .orderBy('payingOrganizationId')
79
+ .fetch();
80
+
81
+ console.log('Invoicing ' + payments.length + ' payments');
82
+
83
+ // Group by VATNumber, company number or company name
84
+ const groups = new Map<string, Payment[]>();
85
+ for (const payment of payments) {
86
+ const blob = {
87
+ // Grouping by payingOrganizationId avoid privacy issues and data leaks
88
+ payingOrganizationId: payment.payingOrganizationId ?? null,
89
+ vatNumber: payment.customer?.company?.VATNumber,
90
+ companyNumber: payment.customer?.company?.companyNumber,
91
+ // Name and adress is ignored, because subject to changes
92
+ };
93
+ const id = JSON.stringify(blob);
94
+ const existing = groups.get(id);
95
+ if (existing) {
96
+ existing.push(payment)
97
+ } else {
98
+ groups.set(id, [payment])
99
+ }
100
+ }
101
+
102
+ console.log('Invoicing ' + groups.size + ' customers');
103
+
104
+ const errors: string[] = []
105
+ const invoices: Invoice[] = []
106
+ let skipped = 0;
107
+
108
+ for (const [_, payments] of groups) {
109
+ // Group from last to newest (so we use the last customer details if the address changed during the month)
110
+ payments.sort((a, b) => Sorter.byDateValue(a.createdAt, b.createdAt))
111
+ const customer = payments[0].customer!.dynamicName;
112
+ try {
113
+ const generalStructs = await AuthenticatedStructures.paymentsGeneral(payments, false);
114
+
115
+ const invoice = InvoiceStruct.create({
116
+ seller,
117
+ customer: payments[0].customer!,
118
+ payments: generalStructs,
119
+ });
120
+ invoice.buildFromPayments();
121
+
122
+ if (invoice.totalWithVAT < invoiceLimit) {
123
+ const first = new Date(Math.min(...payments.map(p => getPaymentTimeoutDate(p).getTime())));
124
+ if (first > new Date()) {
125
+ console.log('Delaying invoicing ' + customer + ' at ' + organization.id + ' until ' + Formatter.dateIso(first))
126
+ skipped += 1;
127
+ continue;
128
+ } else {
129
+ console.log('Invoiced low priced invoice, because of date ' + Formatter.dateIso(first) + ' being in the past')
130
+ }
131
+ }
132
+
133
+ const model = await InvoiceService.createFrom(organization, invoice);
134
+ invoices.push(model)
135
+ } catch (e) {
136
+ console.error(payments.map(p => p.id), e);
137
+
138
+ const prefix = customer + ' ('+payments.map(p => '<a href="'+Formatter.escapeHtml('https://'+organization.getDashboardHost() + '/boekhouding/betalingen/' + p.id)+'">'+ Formatter.escapeHtml($t('%14a') + ' ' + p.id.substring(0, 8))+'</a>').join(', ') + '): ';
139
+
140
+ if (isSimpleError(e) || isSimpleErrors(e)) {
141
+ errors.push(prefix + Formatter.escapeHtml(e.getHuman()))
142
+ } else {
143
+ errors.push(prefix + Formatter.escapeHtml($t('%1ED')))
144
+ }
145
+ }
146
+ }
147
+
148
+ console.log('Created ' + invoices.length + ' invoices with ' + errors.length + ' errors and skipped ' + skipped);
149
+
150
+ if (errors.length) {
151
+ await sendEmailTemplate(organization, {
152
+ template: {
153
+ type: EmailTemplateType.InvoiceGenerationErrors
154
+ },
155
+ recipients: await organization.getAdminRecipients(),
156
+ type: 'transactional',
157
+ fromStamhoofd: true,
158
+ defaultReplacements: [
159
+ Replacement.create({
160
+ token: 'errors',
161
+ html: '<ul><li>'+errors.join('</li><li>')+'</li></ul>' + (skipped > 0 ? '<p>'+Formatter.escapeHtml(skipped === 1 ? $t('%1U4') : $t('%1Sp', {count: skipped}))+'</p>' : '')
162
+ })
163
+ ]
164
+ })
165
+ }
166
+ }
@@ -0,0 +1,87 @@
1
+ import { registerCron } from '@stamhoofd/crons';
2
+ import { MolliePayment, MollieToken, Organization, Payment } from '@stamhoofd/models';
3
+ import { MollieService } from '../services/MollieService.js';
4
+ import { PaymentService } from '../services/PaymentService.js';
5
+
6
+ registerCron('mollie-chargebacks', checkMollieChargebacks);
7
+
8
+ let lastRun: Date | null = null
9
+ export async function checkMollieChargebacks() {
10
+ if (STAMHOOFD.environment !== 'development') {
11
+ if (lastRun && new Date().getTime() - lastRun.getTime() < 1000 * 60 * 60 * 24) {
12
+ return
13
+ }
14
+ lastRun = new Date()
15
+ }
16
+ await doCheckMollieChargebacks(false)
17
+ }
18
+
19
+ export async function doCheckMollieChargebacks(checkAll = false) {
20
+ if (STAMHOOFD.environment !== 'development') {
21
+ console.log('Checking Mollie chargebacks');
22
+ }
23
+
24
+ // Loop all mollie tokens
25
+ for await (const token of MollieToken.select().limit(1).all({}, 'organizationId')) {
26
+ // todo
27
+ const sellingOrganization = await Organization.getByID(token.id)
28
+ if (sellingOrganization) {
29
+ const service = await MollieService.create({ sellingOrganization })
30
+ if (service) {
31
+ await checkMollieChargebacksFor(service, checkAll)
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ export async function checkMollieChargebacksFor(service: MollieService, checkAll = false) {
38
+ if (STAMHOOFD.environment !== 'development') {
39
+ console.log('Checking Mollie chargebacks for ' + service.sellingOrganization.name);
40
+ }
41
+
42
+ // Check last 3 days
43
+ const offset = new Date(Date.now() - 1000 * 60 * 60 * 24 * 4)
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
+ }
51
+
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
+ }
58
+
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)
67
+
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)
75
+ }
76
+ }
77
+ } else {
78
+ console.error('Invalid chargeback payment id '+chargeback.paymentId + ', not found')
79
+ }
80
+ }
81
+
82
+ }
83
+
84
+ if (STAMHOOFD.environment !== 'development') {
85
+ console.log('Done checking Mollie chargebacks for ' + service.sellingOrganization.name);
86
+ }
87
+ }
package/src/crons.ts CHANGED
@@ -123,19 +123,22 @@ async function checkWebshopDNS() {
123
123
 
124
124
  // 11 min - 2 hours
125
125
  async function checkPayments() {
126
+ let timeout = 60 * 1000 * 11;
127
+ const methods = [
128
+ PaymentMethod.Bancontact, PaymentMethod.iDEAL, PaymentMethod.Payconiq, PaymentMethod.CreditCard,
129
+ ];
130
+
126
131
  if (STAMHOOFD.environment === 'development') {
127
- // return;
132
+ // For Mollie, webhooks won't work, so we poll in the backend
133
+ timeout = 0;
128
134
  }
129
135
 
130
- const timeout = 60 * 1000 * 11;
131
136
  const timeout2 = 60 * 1000 * 60 * 2;
132
137
 
133
138
  // TODO: only select the ID + organizationId
134
139
  const payments = await Payment.select()
135
140
  .where(
136
- SQL.where('method', [
137
- PaymentMethod.Bancontact, PaymentMethod.iDEAL, PaymentMethod.Payconiq, PaymentMethod.CreditCard,
138
- ])
141
+ SQL.where('method', methods)
139
142
  .and('status', [PaymentStatus.Created, PaymentStatus.Pending])
140
143
  .and('createdAt', '<', new Date(new Date().getTime() - timeout))
141
144
  .and('createdAt', '>', new Date(new Date().getTime() - timeout2)),
@@ -152,9 +155,10 @@ async function checkPayments() {
152
155
  .orderBy('createdAt', 'ASC')
153
156
  .limit(500)
154
157
  .fetch();
155
-
156
- console.log('[DELAYED PAYMENTS] Checking pending payments: ' + payments.length);
157
- await doCheckPayments(payments);
158
+ if (payments.length) {
159
+ console.log('[DELAYED PAYMENTS] Checking pending payments: ' + payments.length);
160
+ await doCheckPayments(payments);
161
+ }
158
162
  }
159
163
 
160
164
  // 2 hours - 3 days
@@ -180,8 +184,40 @@ async function checkOldPayments() {
180
184
  .limit(500)
181
185
  .fetch();
182
186
 
183
- console.log('[DELAYED PAYMENTS] Checking old pending payments: ' + payments.length);
184
- await doCheckPayments(payments);
187
+ if (payments.length) {
188
+ console.log('[DELAYED PAYMENTS] Checking old pending payments: ' + payments.length);
189
+ await doCheckPayments(payments);
190
+ }
191
+ }
192
+
193
+ // 5 days - 10 days
194
+ async function checkOldDirectDebitPayments() {
195
+ let timeout = 60 * 1000 * 60 * 24 * 5;
196
+ const timeout2 = 60 * 1000 * 60 * 24 * 10;
197
+
198
+ if (STAMHOOFD.environment === 'development') {
199
+ // For Mollie, webhooks won't work, so we poll in the backend
200
+ timeout = 0;
201
+ }
202
+
203
+ // TODO: only select the ID + organizationId
204
+ const payments = await Payment.select()
205
+ .where(
206
+ SQL.where('method', [
207
+ PaymentMethod.DirectDebit,
208
+ ])
209
+ .and('status', [PaymentStatus.Created, PaymentStatus.Pending])
210
+ .and('createdAt', '<', new Date(new Date().getTime() - timeout))
211
+ .and('createdAt', '>', new Date(new Date().getTime() - timeout2)),
212
+ )
213
+ .orderBy('createdAt', 'ASC')
214
+ .limit(500)
215
+ .fetch();
216
+
217
+ if (payments.length) {
218
+ console.log('[DELAYED PAYMENTS] Checking old direct debit pending payments: ' + payments.length);
219
+ await doCheckPayments(payments);
220
+ }
185
221
  }
186
222
 
187
223
  async function doCheckPayments(payments: Payment[]) {
@@ -382,6 +418,7 @@ registerCron('checkDNS', checkDNS);
382
418
  registerCron('checkWebshopDNS', checkWebshopDNS);
383
419
  registerCron('checkPayments', checkPayments);
384
420
  registerCron('checkOldPayments', checkOldPayments);
421
+ registerCron('checkOldDirectDebitPayments', checkOldDirectDebitPayments);
385
422
  registerCron('checkDrips', checkDrips);
386
423
 
387
424
  // Register other crons
@@ -14,15 +14,6 @@ type BeforeFetchAllResult = {
14
14
  areAllPaymentsTransfers: boolean;
15
15
  };
16
16
 
17
- async function fetchPaymentRecipients(query: LimitedFilteredRequest, subfilter: StamhoofdFilter, beforeFetchAllResult?: BeforeFetchAllResult) {
18
- const result = await GetPaymentsEndpoint.buildData(query);
19
-
20
- return new PaginatedResponse({
21
- results: await getRecipients(result, EmailRecipientFilterType.Payment, subfilter, beforeFetchAllResult),
22
- next: result.next,
23
- });
24
- }
25
-
26
17
  const paymentRecipientLoader: RecipientLoader<BeforeFetchAllResult> = {
27
18
  beforeFetchAll: async (query: LimitedFilteredRequest) => {
28
19
  const doesIncludePaymentWithoutOrders = await doesQueryIncludePaymentsWithoutOrder(query);
@@ -39,9 +30,9 @@ const paymentRecipientLoader: RecipientLoader<BeforeFetchAllResult> = {
39
30
  const q = await GetPaymentsEndpoint.buildQuery(query);
40
31
  const base = await q.count();
41
32
 
42
- if (base < 1000) {
33
+ if (base < 100) {
43
34
  // Do full scan
44
- query.limit = 1000;
35
+ query.limit = 100;
45
36
  const result = await fetchPaymentRecipients(query, _subfilter);
46
37
  return result.results.length;
47
38
  }
@@ -52,6 +43,36 @@ const paymentRecipientLoader: RecipientLoader<BeforeFetchAllResult> = {
52
43
 
53
44
  Email.recipientLoaders.set(EmailRecipientFilterType.Payment, paymentRecipientLoader);
54
45
 
46
+ const paymentOrganizationRecipientLoader: RecipientLoader<BeforeFetchAllResult> = {
47
+ fetch: async (query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null, beforeFetchAllResult) => fetchPaymentOrganizationRecipients(query, subfilter, beforeFetchAllResult),
48
+
49
+ // For now: only count the number of payments - not the amount of emails
50
+ count: async (query: LimitedFilteredRequest, _subfilter) => {
51
+ const q = await GetPaymentsEndpoint.buildQuery(query);
52
+ const base = await q.count();
53
+
54
+ if (base < 100) {
55
+ // Do full scan
56
+ query.limit = 100;
57
+ const result = await fetchPaymentOrganizationRecipients(query, _subfilter);
58
+ return result.results.length;
59
+ }
60
+
61
+ return base;
62
+ },
63
+ };
64
+
65
+ Email.recipientLoaders.set(EmailRecipientFilterType.PaymentOrganization, paymentOrganizationRecipientLoader);
66
+
67
+ async function fetchPaymentRecipients(query: LimitedFilteredRequest, subfilter: StamhoofdFilter, beforeFetchAllResult?: BeforeFetchAllResult) {
68
+ const result = await GetPaymentsEndpoint.buildData(query);
69
+
70
+ return new PaginatedResponse({
71
+ results: await getRecipients(result, EmailRecipientFilterType.Payment, subfilter, beforeFetchAllResult),
72
+ next: result.next,
73
+ });
74
+ }
75
+
55
76
  async function doesQueryIncludePaymentsWithoutOrder(filterRequest: LimitedFilteredRequest) {
56
77
  // create count request (without limit and page filter)
57
78
  const countRequest = new CountFilteredRequest({
@@ -113,17 +134,6 @@ async function fetchPaymentOrganizationRecipients(query: LimitedFilteredRequest,
113
134
  });
114
135
  }
115
136
 
116
- const paymentOrganizationRecipientLoader: RecipientLoader<BeforeFetchAllResult> = {
117
- fetch: async (query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null, beforeFetchAllResult) => fetchPaymentOrganizationRecipients(query, subfilter, beforeFetchAllResult),
118
- // For now: only count the number of payments - not the amount of emails
119
- count: async (query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) => {
120
- const q = await GetPaymentsEndpoint.buildQuery(query);
121
- return await q.count();
122
- },
123
- };
124
-
125
- Email.recipientLoaders.set(EmailRecipientFilterType.PaymentOrganization, paymentOrganizationRecipientLoader);
126
-
127
137
  async function getRecipients(result: PaginatedResponse<PaymentGeneral[], LimitedFilteredRequest>, type: EmailRecipientFilterType.Payment | EmailRecipientFilterType.PaymentOrganization, subFilter: StamhoofdFilter | null, beforeFetchAllResult: BeforeFetchAllResult | undefined) {
128
138
  const recipients: EmailRecipient[] = [];
129
139
  const userIds: { userId: string; payment: PaymentGeneral }[] = [];
@@ -273,7 +283,7 @@ async function getMembersForOrganizations(organizationIds: string[], filter: Sta
273
283
  }
274
284
 
275
285
  async function getOrganizationRecipients(ids: { organizationId: string; payment: PaymentGeneral }[], replacementOptions: ReplacementsOptions, subFilter: StamhoofdFilter | null): Promise<EmailRecipient[]> {
276
- if (ids.length === 0 || subFilter === null) {
286
+ if (ids.length === 0) {
277
287
  return [];
278
288
  }
279
289
 
@@ -287,32 +297,65 @@ async function getOrganizationRecipients(ids: { organizationId: string; payment:
287
297
  organizationMap.set(organization.id, organization);
288
298
  }
289
299
 
290
- const membersForOrganizations = await getMembersForOrganizations(allOrganizationIds, subFilter);
291
-
292
300
  const results: EmailRecipient[] = [];
293
301
 
294
- for (const { organizationId, payment } of ids) {
295
- const organization = organizationMap.get(organizationId);
302
+ if (subFilter === null) {
303
+ // Use full admins instead
304
+ const admins = await User.getAdmins(allOrganizationIds, {verified: true});
305
+ for (const { organizationId, payment } of ids) {
306
+ const organization = organizationMap.get(organizationId);
307
+ if (!organization) {
308
+ continue;
309
+ }
310
+ const users = admins.filter(a => a.permissions?.forOrganization(organization)?.hasFullAccess());
296
311
 
297
- if (organization) {
298
- const members = membersForOrganizations.get(organizationId);
299
- if (!members) {
312
+ if (users.length === 0) {
313
+ console.warn('No admins found for organization with id ', organizationId, ' while fetching email recipients for payment with id ', payment.id);
300
314
  continue;
301
315
  }
302
316
 
303
317
  const replacements = getEmailReplacementsForPayment(payment, replacementOptions);
304
318
 
305
- for (const member of members) {
306
- for (const email of member.details.getNotificationEmails()) {
307
- results.push(EmailRecipient.create({
308
- objectId: payment.id,
309
- name: organization.name,
310
- memberId: member.id,
311
- firstName: member.details.firstName,
312
- lastName: member.details.lastName,
313
- email,
314
- replacements,
315
- }));
319
+ for (const user of users) {
320
+ results.push(EmailRecipient.create({
321
+ objectId: payment.id,
322
+ name: organization.name,
323
+ userId: user.id,
324
+ firstName: user.firstName,
325
+ lastName: user.lastName,
326
+ email: user.email,
327
+ replacements,
328
+ }));
329
+ }
330
+ }
331
+ } else {
332
+ const membersForOrganizations = await getMembersForOrganizations(allOrganizationIds, subFilter);
333
+
334
+ for (const { organizationId, payment } of ids) {
335
+ const organization = organizationMap.get(organizationId);
336
+
337
+ if (organization) {
338
+ const members = membersForOrganizations.get(organizationId);
339
+ if (!members || members.length === 0) {
340
+ // Use admins by default
341
+
342
+ continue;
343
+ }
344
+
345
+ const replacements = getEmailReplacementsForPayment(payment, replacementOptions);
346
+
347
+ for (const member of members) {
348
+ for (const email of member.details.getNotificationEmails()) {
349
+ results.push(EmailRecipient.create({
350
+ objectId: payment.id,
351
+ name: organization.name,
352
+ memberId: member.id,
353
+ firstName: member.details.firstName,
354
+ lastName: member.details.lastName,
355
+ email,
356
+ replacements,
357
+ }));
358
+ }
316
359
  }
317
360
  }
318
361
  }