@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.
- package/package.json +12 -12
- package/src/audit-logs/RegistrationInvitationLogger.ts +46 -0
- package/src/audit-logs/init.ts +2 -0
- package/src/crons/index.ts +2 -0
- package/src/crons/invoices.ts +166 -0
- package/src/crons/mollie-chargebacks.ts +87 -0
- package/src/crons.ts +47 -10
- package/src/email-recipient-loaders/payments.ts +84 -41
- package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +51 -0
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +22 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +4 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +495 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +216 -0
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +405 -0
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +168 -0
- package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +15 -0
- package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +3 -4
- package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +42 -19
- package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
- package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +11 -3
- package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +308 -0
- package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +7 -0
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +5 -4
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +7 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +17 -8
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +127 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +13 -4
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +7 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -11
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +14 -19
- package/src/helpers/AdminPermissionChecker.ts +11 -3
- package/src/helpers/AuthenticatedStructures.ts +94 -6
- package/src/helpers/FinancialSupportHelper.ts +21 -0
- package/src/helpers/RecordAnswerHelper.test.ts +746 -0
- package/src/helpers/RecordAnswerHelper.ts +116 -0
- package/src/helpers/StripeHelper.ts +2 -3
- package/src/helpers/ViesHelper.ts +7 -3
- package/src/seeds/1750090030-records-configuration.ts +68 -3
- package/src/seeds/1752848561-groups-registration-periods.ts +26 -2
- package/src/seeds/1779121239-default-invoice-email-template.sql +3 -0
- package/src/services/BalanceItemService.ts +12 -16
- package/src/services/InvoiceService.ts +372 -72
- package/src/services/MollieService.ts +537 -0
- package/src/services/PaymentMandateService.ts +214 -0
- package/src/services/PaymentService.ts +578 -222
- package/src/services/PlatformMembershipService.ts +1 -1
- package/src/services/RegistrationService.ts +66 -5
- package/src/services/STPackageService.ts +0 -7
- package/src/services/data/invoice.hbs.html +686 -0
- package/src/sql-filters/groups.ts +11 -1
- package/src/sql-filters/payments.ts +5 -0
- package/src/sql-filters/registration-invitations.ts +90 -0
- package/src/sql-sorters/registration-invitations.ts +36 -0
- package/vitest.config.js +1 -0
- 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.
|
|
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.
|
|
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.
|
|
69
|
-
"@stamhoofd/backend-middleware": "2.
|
|
70
|
-
"@stamhoofd/email": "2.
|
|
71
|
-
"@stamhoofd/models": "2.
|
|
72
|
-
"@stamhoofd/queues": "2.
|
|
73
|
-
"@stamhoofd/sql": "2.
|
|
74
|
-
"@stamhoofd/structures": "2.
|
|
75
|
-
"@stamhoofd/utility": "2.
|
|
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": "
|
|
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
|
+
});
|
package/src/audit-logs/init.ts
CHANGED
|
@@ -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);
|
package/src/crons/index.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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 <
|
|
33
|
+
if (base < 100) {
|
|
43
34
|
// Do full scan
|
|
44
|
-
query.limit =
|
|
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
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
}
|