@stamhoofd/backend 2.114.1 → 2.115.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/index.ts +1 -0
- package/package.json +11 -10
- package/src/boot.ts +30 -20
- package/src/email-recipient-loaders/documents.ts +1 -14
- package/src/email-recipient-loaders/payments.ts +344 -49
- package/src/email-recipient-loaders/receivable-balances.ts +2 -11
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +1 -1
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +6 -5
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +15 -0
- package/src/endpoints/system/HealthEndpoint.ts +77 -0
- package/src/helpers/email-html-helpers.ts +144 -0
- package/src/seeds/data/default-email-templates.sql +2 -1
- package/src/services/CpuService.ts +123 -0
- package/src/sql-sorters/document-templates.ts +1 -1
- package/src/sql-sorters/documents.ts +1 -1
- package/src/sql-sorters/orders.ts +0 -84
- package/src/sql-sorters/organizations.ts +3 -3
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.115.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"license": "UNLICENCED",
|
|
20
20
|
"scripts": {
|
|
21
21
|
"dev": "wait-on ../../shared/middleware/dist/index.js && concurrently -r 'yarn -s build --watch --preserveWatchOutput' \"yarn -s dev:watch\"",
|
|
22
|
+
"dev:full": "yarn -s dev",
|
|
22
23
|
"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",
|
|
23
24
|
"dev:backend": "yarn -s dev",
|
|
24
25
|
"build": "rm -rf ./dist/src/migrations && rm -rf ./dist/src/seeds && yarn -s copy-assets && tsc -b",
|
|
@@ -54,14 +55,14 @@
|
|
|
54
55
|
"@simonbackx/simple-encoding": "2.23.1",
|
|
55
56
|
"@simonbackx/simple-endpoints": "1.20.1",
|
|
56
57
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
57
|
-
"@stamhoofd/backend-i18n": "2.
|
|
58
|
-
"@stamhoofd/backend-middleware": "2.
|
|
59
|
-
"@stamhoofd/email": "2.
|
|
60
|
-
"@stamhoofd/models": "2.
|
|
61
|
-
"@stamhoofd/queues": "2.
|
|
62
|
-
"@stamhoofd/sql": "2.
|
|
63
|
-
"@stamhoofd/structures": "2.
|
|
64
|
-
"@stamhoofd/utility": "2.
|
|
58
|
+
"@stamhoofd/backend-i18n": "2.115.0",
|
|
59
|
+
"@stamhoofd/backend-middleware": "2.115.0",
|
|
60
|
+
"@stamhoofd/email": "2.115.0",
|
|
61
|
+
"@stamhoofd/models": "2.115.0",
|
|
62
|
+
"@stamhoofd/queues": "2.115.0",
|
|
63
|
+
"@stamhoofd/sql": "2.115.0",
|
|
64
|
+
"@stamhoofd/structures": "2.115.0",
|
|
65
|
+
"@stamhoofd/utility": "2.115.0",
|
|
65
66
|
"archiver": "^7.0.1",
|
|
66
67
|
"axios": "^1.13.2",
|
|
67
68
|
"cookie": "^0.7.0",
|
|
@@ -79,5 +80,5 @@
|
|
|
79
80
|
"publishConfig": {
|
|
80
81
|
"access": "public"
|
|
81
82
|
},
|
|
82
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "b68983abee843a6d4f4460b7818d66a8780587cc"
|
|
83
84
|
}
|
package/src/boot.ts
CHANGED
|
@@ -10,17 +10,19 @@ import { SimpleError } from '@simonbackx/simple-errors';
|
|
|
10
10
|
import { startCrons, stopCrons, waitForCrons } from '@stamhoofd/crons';
|
|
11
11
|
import { Platform } from '@stamhoofd/models';
|
|
12
12
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
13
|
-
import { resumeEmails } from './helpers/EmailResumer';
|
|
14
|
-
import { GlobalHelper } from './helpers/GlobalHelper';
|
|
15
|
-
import { SetupStepUpdater } from './helpers/SetupStepUpdater';
|
|
16
|
-
import { ContextMiddleware } from './middleware/ContextMiddleware';
|
|
17
|
-
import { AuditLogService } from './services/AuditLogService';
|
|
18
|
-
import { BalanceItemService } from './services/BalanceItemService';
|
|
19
|
-
import { DocumentService } from './services/DocumentService';
|
|
20
|
-
import { FileSignService } from './services/FileSignService';
|
|
21
|
-
import { PlatformMembershipService } from './services/PlatformMembershipService';
|
|
22
|
-
import { UitpasService } from './services/uitpas/UitpasService';
|
|
23
|
-
import { UniqueUserService } from './services/UniqueUserService';
|
|
13
|
+
import { resumeEmails } from './helpers/EmailResumer.js';
|
|
14
|
+
import { GlobalHelper } from './helpers/GlobalHelper.js';
|
|
15
|
+
import { SetupStepUpdater } from './helpers/SetupStepUpdater.js';
|
|
16
|
+
import { ContextMiddleware } from './middleware/ContextMiddleware.js';
|
|
17
|
+
import { AuditLogService } from './services/AuditLogService.js';
|
|
18
|
+
import { BalanceItemService } from './services/BalanceItemService.js';
|
|
19
|
+
import { DocumentService } from './services/DocumentService.js';
|
|
20
|
+
import { FileSignService } from './services/FileSignService.js';
|
|
21
|
+
import { PlatformMembershipService } from './services/PlatformMembershipService.js';
|
|
22
|
+
import { UitpasService } from './services/uitpas/UitpasService.js';
|
|
23
|
+
import { UniqueUserService } from './services/UniqueUserService.js';
|
|
24
|
+
import { CpuService } from './services/CpuService.js';
|
|
25
|
+
import { SQLLogger } from '@stamhoofd/sql';
|
|
24
26
|
|
|
25
27
|
process.on('unhandledRejection', (error: Error) => {
|
|
26
28
|
console.error('unhandledRejection');
|
|
@@ -123,16 +125,16 @@ export const boot = async (options: { killProcess: boolean }) => {
|
|
|
123
125
|
productionLog('Loading loaders...');
|
|
124
126
|
|
|
125
127
|
// Register Excel loaders
|
|
126
|
-
await import('./excel-loaders');
|
|
128
|
+
await import('./excel-loaders/index.js');
|
|
127
129
|
|
|
128
130
|
// Register Email Recipient loaders
|
|
129
|
-
await import('./email-recipient-loaders/members');
|
|
130
|
-
await import('./email-recipient-loaders/registrations');
|
|
131
|
-
await import('./email-recipient-loaders/orders');
|
|
132
|
-
await import('./email-recipient-loaders/receivable-balances');
|
|
133
|
-
await import('./excel-loaders/registrations');
|
|
134
|
-
await import('./email-recipient-loaders/documents');
|
|
135
|
-
await import ('./email-recipient-loaders/payments');
|
|
131
|
+
await import('./email-recipient-loaders/members.js');
|
|
132
|
+
await import('./email-recipient-loaders/registrations.js');
|
|
133
|
+
await import('./email-recipient-loaders/orders.js');
|
|
134
|
+
await import('./email-recipient-loaders/receivable-balances.js');
|
|
135
|
+
await import('./excel-loaders/registrations.js');
|
|
136
|
+
await import('./email-recipient-loaders/documents.js');
|
|
137
|
+
await import ('./email-recipient-loaders/payments.js');
|
|
136
138
|
|
|
137
139
|
productionLog('Opening port...');
|
|
138
140
|
routerServer.listen(STAMHOOFD.PORT ?? 9090);
|
|
@@ -142,6 +144,14 @@ export const boot = async (options: { killProcess: boolean }) => {
|
|
|
142
144
|
|
|
143
145
|
resumeEmails().catch(console.error);
|
|
144
146
|
|
|
147
|
+
if (STAMHOOFD.environment !== 'development' && STAMHOOFD.environment !== 'test') {
|
|
148
|
+
CpuService.startMonitoring();
|
|
149
|
+
}
|
|
150
|
+
else if (STAMHOOFD.environment === 'development') {
|
|
151
|
+
SQLLogger.slowQueryThresholdMs = 200;
|
|
152
|
+
SQLLogger.explainAllAndLogInefficient = true;
|
|
153
|
+
}
|
|
154
|
+
|
|
145
155
|
if (routerServer.server) {
|
|
146
156
|
// Default timeout is a bit too short
|
|
147
157
|
routerServer.server.timeout = 61000;
|
|
@@ -235,7 +245,7 @@ export const boot = async (options: { killProcess: boolean }) => {
|
|
|
235
245
|
}
|
|
236
246
|
|
|
237
247
|
// Register crons
|
|
238
|
-
await import('./crons');
|
|
248
|
+
await import('./crons.js');
|
|
239
249
|
|
|
240
250
|
AuditLogService.listen();
|
|
241
251
|
PlatformMembershipService.listen();
|
|
@@ -47,20 +47,7 @@ async function fetch(query: LimitedFilteredRequest) {
|
|
|
47
47
|
async function count(request: LimitedFilteredRequest) {
|
|
48
48
|
const query = await GetDocumentsEndpoint.buildQuery(request);
|
|
49
49
|
const uniqueMemberIds = await query.count(SQL.distinct(SQL.column('memberId')));
|
|
50
|
-
|
|
51
|
-
if (uniqueMemberIds > 100 || uniqueMemberIds === 0) {
|
|
52
|
-
return uniqueMemberIds; // rough estimate
|
|
53
|
-
}
|
|
54
|
-
// do full count
|
|
55
|
-
request.limit = 100;
|
|
56
|
-
let count = 0;
|
|
57
|
-
let req: LimitedFilteredRequest | null = request;
|
|
58
|
-
while (req) {
|
|
59
|
-
const result = await fetch(request);
|
|
60
|
-
count += result.results.length;
|
|
61
|
-
req = result.next ?? null;
|
|
62
|
-
}
|
|
63
|
-
return count;
|
|
50
|
+
return uniqueMemberIds;
|
|
64
51
|
};
|
|
65
52
|
|
|
66
53
|
Email.recipientLoaders.set(EmailRecipientFilterType.Documents, { fetch, count });
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import { Email, Member, MemberResponsibilityRecord, Order, Organization, User } from '@stamhoofd/models';
|
|
2
|
-
import { compileToSQLFilter } from '@stamhoofd/sql';
|
|
3
|
-
import { EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, PaymentGeneral, Replacement, StamhoofdFilter } from '@stamhoofd/structures';
|
|
1
|
+
import { BalanceItem, BalanceItemPayment, Email, Member, MemberResponsibilityRecord, Order, Organization, Payment, RecipientLoader, User, Webshop } from '@stamhoofd/models';
|
|
2
|
+
import { compileToSQLFilter, SQL } from '@stamhoofd/sql';
|
|
3
|
+
import { BalanceItemRelationType, BalanceItemType, CountFilteredRequest, EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, PaymentGeneral, PaymentMethod, PaymentMethodHelper, Replacement, StamhoofdFilter, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
4
4
|
import { Formatter } from '@stamhoofd/utility';
|
|
5
5
|
import { GetPaymentsEndpoint } from '../endpoints/organization/dashboard/payments/GetPaymentsEndpoint.js';
|
|
6
|
+
import { createOrderDataHTMLTable, createPaymentDataHTMLTable } from '../helpers/email-html-helpers.js';
|
|
6
7
|
import { memberResponsibilityRecordFilterCompilers } from '../sql-filters/member-responsibility-records.js';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
type ReplacementsOptions = {
|
|
10
|
+
shouldAddReplacementsForOrder: boolean;
|
|
11
|
+
shouldAddReplacementsForTransfers: boolean;
|
|
12
|
+
orderMap: Map<string, Order>;
|
|
13
|
+
webshopMap: Map<string, Webshop>;
|
|
14
|
+
organizationMap: Map<string, Organization>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function getRecipients(result: PaginatedResponse<PaymentGeneral[], LimitedFilteredRequest>, type: EmailRecipientFilterType.Payment | EmailRecipientFilterType.PaymentOrganization, subFilter: StamhoofdFilter | null, beforeFetchAllResult: BeforeFetchAllResult | undefined) {
|
|
9
18
|
const recipients: EmailRecipient[] = [];
|
|
10
19
|
const userIds: { userId: string; payment: PaymentGeneral }[] = [];
|
|
11
20
|
const organizationIds: { organizationId: string; payment: PaymentGeneral }[] = [];
|
|
@@ -71,20 +80,58 @@ async function getRecipients(result: PaginatedResponse<PaymentGeneral[], Limited
|
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
82
|
|
|
83
|
+
// get all orders linked to the payments
|
|
84
|
+
const allOrderIdsSet = new Set<string>();
|
|
85
|
+
const allWebshopIdsSet = new Set<string>();
|
|
86
|
+
const organizationIdsForOrdersSet = new Set<string>();
|
|
87
|
+
|
|
88
|
+
for (const payment of result.results) {
|
|
89
|
+
payment.webshopIds.forEach(id => allWebshopIdsSet.add(id));
|
|
90
|
+
|
|
91
|
+
for (const balanceItemPayment of payment.balanceItemPayments) {
|
|
92
|
+
const balanceItem = balanceItemPayment.balanceItem;
|
|
93
|
+
if (balanceItem.orderId) {
|
|
94
|
+
allOrderIdsSet.add(balanceItem.orderId);
|
|
95
|
+
|
|
96
|
+
// only important if balance item has order
|
|
97
|
+
organizationIdsForOrdersSet.add(balanceItem.organizationId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// get all orders (for replacements later)
|
|
103
|
+
const orders = await Order.getByIDs(...allOrderIdsSet);
|
|
104
|
+
const orderMap = new Map<string, Order>(orders.map(o => [o.id, o] as [string, Order]));
|
|
105
|
+
|
|
106
|
+
// get all webshops (for replacements later)
|
|
107
|
+
const webshops = await Webshop.getByIDs(...allWebshopIdsSet);
|
|
108
|
+
const webshopMap = new Map<string, Webshop>(webshops.map(w => [w.id, w] as [string, Webshop]));
|
|
109
|
+
|
|
110
|
+
// get all organizations (for replacements later)
|
|
111
|
+
const organizations = await Organization.getByIDs(...organizationIdsForOrdersSet);
|
|
112
|
+
const organizationMap = new Map<string, Organization>(organizations.map(o => [o.id, o] as [string, Organization]));
|
|
113
|
+
|
|
114
|
+
const replacementOptions: ReplacementsOptions = {
|
|
115
|
+
shouldAddReplacementsForOrder: beforeFetchAllResult ? !beforeFetchAllResult.doesIncludePaymentWithoutOrders : false,
|
|
116
|
+
shouldAddReplacementsForTransfers: beforeFetchAllResult ? beforeFetchAllResult.areAllPaymentsTransfers : false,
|
|
117
|
+
orderMap,
|
|
118
|
+
webshopMap,
|
|
119
|
+
organizationMap,
|
|
120
|
+
};
|
|
121
|
+
|
|
74
122
|
if (type === EmailRecipientFilterType.Payment) {
|
|
75
|
-
recipients.push(...await getUserRecipients(userIds));
|
|
76
|
-
recipients.push(...await getMemberRecipients(memberIds));
|
|
77
|
-
recipients.push(...await
|
|
78
|
-
recipients.push(...await getOrderRecipients(orderIds));
|
|
123
|
+
recipients.push(...await getUserRecipients(userIds, replacementOptions));
|
|
124
|
+
recipients.push(...await getMemberRecipients(memberIds, replacementOptions));
|
|
125
|
+
recipients.push(...await getOrderRecipients(orderIds, replacementOptions));
|
|
79
126
|
}
|
|
80
127
|
else {
|
|
81
|
-
recipients.push(...await getOrganizationRecipients(organizationIds, subFilter));
|
|
128
|
+
recipients.push(...await getOrganizationRecipients(organizationIds, replacementOptions, subFilter));
|
|
82
129
|
}
|
|
83
130
|
|
|
84
131
|
return recipients;
|
|
85
132
|
}
|
|
86
133
|
|
|
87
|
-
async function getUserRecipients(ids: { userId: string; payment: PaymentGeneral }[]): Promise<EmailRecipient[]> {
|
|
134
|
+
async function getUserRecipients(ids: { userId: string; payment: PaymentGeneral }[], replacementOptions: ReplacementsOptions): Promise<EmailRecipient[]> {
|
|
88
135
|
if (ids.length === 0) {
|
|
89
136
|
return [];
|
|
90
137
|
}
|
|
@@ -104,7 +151,7 @@ async function getUserRecipients(ids: { userId: string; payment: PaymentGeneral
|
|
|
104
151
|
firstName: user.firstName,
|
|
105
152
|
lastName: user.lastName,
|
|
106
153
|
email: user.email,
|
|
107
|
-
replacements: getEmailReplacementsForPayment(payment),
|
|
154
|
+
replacements: getEmailReplacementsForPayment(payment, replacementOptions),
|
|
108
155
|
}));
|
|
109
156
|
}
|
|
110
157
|
}
|
|
@@ -152,15 +199,19 @@ async function getMembersForOrganizations(organizationIds: string[], filter: Sta
|
|
|
152
199
|
return result;
|
|
153
200
|
}
|
|
154
201
|
|
|
155
|
-
async function getOrganizationRecipients(ids: { organizationId: string; payment: PaymentGeneral }[], subFilter: StamhoofdFilter | null): Promise<EmailRecipient[]> {
|
|
202
|
+
async function getOrganizationRecipients(ids: { organizationId: string; payment: PaymentGeneral }[], replacementOptions: ReplacementsOptions, subFilter: StamhoofdFilter | null): Promise<EmailRecipient[]> {
|
|
156
203
|
if (ids.length === 0 || subFilter === null) {
|
|
157
204
|
return [];
|
|
158
205
|
}
|
|
159
206
|
|
|
160
207
|
const allOrganizationIds = Formatter.uniqueArray(ids.map(i => i.organizationId));
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
208
|
+
const organizationMap = replacementOptions.organizationMap;
|
|
209
|
+
|
|
210
|
+
// fetch organizations that are not in map yet
|
|
211
|
+
const fetchedOrganizations = await Organization.getByIDs(...allOrganizationIds.filter(id => !organizationMap.has(id)));
|
|
212
|
+
|
|
213
|
+
for (const organization of fetchedOrganizations) {
|
|
214
|
+
organizationMap.set(organization.id, organization);
|
|
164
215
|
}
|
|
165
216
|
|
|
166
217
|
const membersForOrganizations = await getMembersForOrganizations(allOrganizationIds, subFilter);
|
|
@@ -168,7 +219,7 @@ async function getOrganizationRecipients(ids: { organizationId: string; payment:
|
|
|
168
219
|
const results: EmailRecipient[] = [];
|
|
169
220
|
|
|
170
221
|
for (const { organizationId, payment } of ids) {
|
|
171
|
-
const organization =
|
|
222
|
+
const organization = organizationMap.get(organizationId);
|
|
172
223
|
|
|
173
224
|
if (organization) {
|
|
174
225
|
const members = membersForOrganizations.get(organizationId);
|
|
@@ -176,7 +227,7 @@ async function getOrganizationRecipients(ids: { organizationId: string; payment:
|
|
|
176
227
|
continue;
|
|
177
228
|
}
|
|
178
229
|
|
|
179
|
-
const replacements = getEmailReplacementsForPayment(payment);
|
|
230
|
+
const replacements = getEmailReplacementsForPayment(payment, replacementOptions);
|
|
180
231
|
|
|
181
232
|
for (const member of members) {
|
|
182
233
|
for (const email of member.details.getMemberEmails()) {
|
|
@@ -197,7 +248,7 @@ async function getOrganizationRecipients(ids: { organizationId: string; payment:
|
|
|
197
248
|
return results;
|
|
198
249
|
}
|
|
199
250
|
|
|
200
|
-
async function getMemberRecipients(ids: { memberId: string; payment: PaymentGeneral }[]): Promise<EmailRecipient[]> {
|
|
251
|
+
async function getMemberRecipients(ids: { memberId: string; payment: PaymentGeneral }[], replacementOptions: ReplacementsOptions): Promise<EmailRecipient[]> {
|
|
201
252
|
if (ids.length === 0) {
|
|
202
253
|
return [];
|
|
203
254
|
}
|
|
@@ -225,7 +276,7 @@ async function getMemberRecipients(ids: { memberId: string; payment: PaymentGene
|
|
|
225
276
|
firstName: user.firstName,
|
|
226
277
|
lastName: user.lastName,
|
|
227
278
|
email: user.email,
|
|
228
|
-
replacements: getEmailReplacementsForPayment(payment),
|
|
279
|
+
replacements: getEmailReplacementsForPayment(payment, replacementOptions),
|
|
229
280
|
});
|
|
230
281
|
results.push(recipient);
|
|
231
282
|
}
|
|
@@ -235,18 +286,16 @@ async function getMemberRecipients(ids: { memberId: string; payment: PaymentGene
|
|
|
235
286
|
return results;
|
|
236
287
|
}
|
|
237
288
|
|
|
238
|
-
async function getOrderRecipients(ids: { orderId: string; payment: PaymentGeneral }[]): Promise<EmailRecipient[]> {
|
|
289
|
+
async function getOrderRecipients(ids: { orderId: string; payment: PaymentGeneral }[], replacementOptions: ReplacementsOptions): Promise<EmailRecipient[]> {
|
|
239
290
|
if (ids.length === 0) {
|
|
240
291
|
return [];
|
|
241
292
|
}
|
|
242
293
|
|
|
243
|
-
const
|
|
244
|
-
const orders = await Order.getByIDs(...allOrderIds);
|
|
245
|
-
|
|
294
|
+
const orderMap = replacementOptions.orderMap;
|
|
246
295
|
const results: EmailRecipient[] = [];
|
|
247
296
|
|
|
248
297
|
for (const { orderId, payment } of ids) {
|
|
249
|
-
const order =
|
|
298
|
+
const order = orderMap.get(orderId);
|
|
250
299
|
|
|
251
300
|
if (order) {
|
|
252
301
|
const { firstName, lastName, email } = order.data.customer;
|
|
@@ -257,7 +306,7 @@ async function getOrderRecipients(ids: { orderId: string; payment: PaymentGenera
|
|
|
257
306
|
firstName,
|
|
258
307
|
lastName,
|
|
259
308
|
email,
|
|
260
|
-
replacements: getEmailReplacementsForPayment(payment),
|
|
309
|
+
replacements: getEmailReplacementsForPayment(payment, replacementOptions),
|
|
261
310
|
}));
|
|
262
311
|
}
|
|
263
312
|
}
|
|
@@ -265,24 +314,223 @@ async function getOrderRecipients(ids: { orderId: string; payment: PaymentGenera
|
|
|
265
314
|
return results;
|
|
266
315
|
}
|
|
267
316
|
|
|
268
|
-
function getEmailReplacementsForPayment(payment: PaymentGeneral): Replacement[] {
|
|
269
|
-
|
|
270
|
-
|
|
317
|
+
function getEmailReplacementsForPayment(payment: PaymentGeneral, options: ReplacementsOptions): Replacement[] {
|
|
318
|
+
const { orderMap, webshopMap, organizationMap, shouldAddReplacementsForOrder, shouldAddReplacementsForTransfers } = options;
|
|
319
|
+
const orderIds = new Set<string>();
|
|
320
|
+
|
|
321
|
+
for (const balanceItemPayment of payment.balanceItemPayments) {
|
|
322
|
+
const orderId = balanceItemPayment.balanceItem.orderId;
|
|
323
|
+
if (orderId) {
|
|
324
|
+
orderIds.add(orderId);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// will be set if only 1 order is linked
|
|
329
|
+
let singleOrder: Order | null = null;
|
|
330
|
+
|
|
331
|
+
if (orderIds.size === 1) {
|
|
332
|
+
const singleOrderId = [...orderIds][0];
|
|
333
|
+
if (singleOrderId) {
|
|
334
|
+
const order = orderMap.get(singleOrderId);
|
|
335
|
+
|
|
336
|
+
if (order) {
|
|
337
|
+
singleOrder = order;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let orderUrlReplacement: Replacement | null = null;
|
|
343
|
+
|
|
344
|
+
// add replacement for order url if only 1 order is linked
|
|
345
|
+
if (singleOrder && shouldAddReplacementsForOrder) {
|
|
346
|
+
const webshop = webshopMap.get(singleOrder.webshopId);
|
|
347
|
+
const organization = organizationMap.get(singleOrder.organizationId);
|
|
348
|
+
|
|
349
|
+
if (webshop && organization) {
|
|
350
|
+
const webshopStruct = WebshopStruct.create(webshop);
|
|
351
|
+
|
|
352
|
+
orderUrlReplacement = Replacement.create({
|
|
353
|
+
token: 'orderUrl',
|
|
354
|
+
value: 'https://' + webshopStruct.getUrl(organization) + '/order/' + (singleOrder.id),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const createPaymentDataHtml = () => {
|
|
360
|
+
if (singleOrder) {
|
|
361
|
+
const webshop = webshopMap.get(singleOrder.webshopId);
|
|
362
|
+
if (webshop) {
|
|
363
|
+
return createOrderDataHTMLTable(singleOrder, webshop);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return createPaymentDataHTMLTable(payment);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const paymentDataHtml = createPaymentDataHtml();
|
|
371
|
+
const paymentDataReplacement = paymentDataHtml
|
|
372
|
+
? Replacement.create({
|
|
373
|
+
token: 'paymentData',
|
|
374
|
+
value: '',
|
|
375
|
+
html: paymentDataHtml,
|
|
376
|
+
})
|
|
377
|
+
: null;
|
|
378
|
+
|
|
379
|
+
return ([
|
|
380
|
+
Replacement.create({
|
|
381
|
+
token: 'paymentPrice',
|
|
382
|
+
value: Formatter.price(payment.price),
|
|
383
|
+
}),
|
|
384
|
+
Replacement.create({
|
|
385
|
+
token: 'paymentMethod',
|
|
386
|
+
value: PaymentMethodHelper.getName(payment.method ?? PaymentMethod.Unknown),
|
|
387
|
+
}),
|
|
388
|
+
...(shouldAddReplacementsForTransfers
|
|
389
|
+
? [
|
|
390
|
+
Replacement.create({
|
|
391
|
+
token: 'transferDescription',
|
|
392
|
+
value: (payment.transferDescription ?? ''),
|
|
393
|
+
}),
|
|
394
|
+
Replacement.create({
|
|
395
|
+
token: 'transferBankAccount',
|
|
396
|
+
value: payment.transferSettings?.iban ?? '',
|
|
397
|
+
}),
|
|
398
|
+
Replacement.create({
|
|
399
|
+
token: 'transferBankCreditor',
|
|
400
|
+
// todo?
|
|
401
|
+
value: payment.transferSettings?.creditor ?? (payment.organizationId ? organizationMap.get(payment.organizationId)?.name : ''),
|
|
402
|
+
}),
|
|
403
|
+
]
|
|
404
|
+
: []),
|
|
405
|
+
|
|
406
|
+
Replacement.create({
|
|
407
|
+
token: 'balanceItemPaymentsTable',
|
|
408
|
+
value: '',
|
|
409
|
+
// todo: unbox orders?
|
|
410
|
+
html: payment.getBalanceItemPaymentsHtmlTable(),
|
|
411
|
+
}),
|
|
412
|
+
Replacement.create({
|
|
413
|
+
token: 'paymentTable',
|
|
414
|
+
value: '',
|
|
415
|
+
html: payment.getHTMLTable(),
|
|
416
|
+
}),
|
|
417
|
+
Replacement.create({
|
|
418
|
+
token: 'overviewContext',
|
|
419
|
+
value: getPaymentContext(payment, options),
|
|
420
|
+
}),
|
|
421
|
+
orderUrlReplacement,
|
|
422
|
+
paymentDataReplacement,
|
|
423
|
+
]).filter(replacementOrNull => replacementOrNull !== null);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function getPaymentContext(payment: PaymentGeneral, { orderMap, webshopMap }: ReplacementsOptions) {
|
|
427
|
+
const overviewContext = new Set<string>();
|
|
428
|
+
const registrationMemberNames = new Set<string>();
|
|
429
|
+
|
|
430
|
+
// only add to context if type is order or registration
|
|
431
|
+
for (const balanceItemPayment of payment.balanceItemPayments) {
|
|
432
|
+
const balanceItem = balanceItemPayment.balanceItem;
|
|
433
|
+
const type = balanceItem.type;
|
|
434
|
+
|
|
435
|
+
switch (type) {
|
|
436
|
+
case BalanceItemType.Order: {
|
|
437
|
+
if (balanceItem.orderId) {
|
|
438
|
+
const order = orderMap.get(balanceItem.orderId);
|
|
439
|
+
|
|
440
|
+
if (order) {
|
|
441
|
+
const webshop = webshopMap.get(order.webshopId);
|
|
442
|
+
if (webshop) {
|
|
443
|
+
overviewContext.add($t('{webshop} (bestelling {orderNumber})', {
|
|
444
|
+
webshop: webshop.meta.name,
|
|
445
|
+
orderNumber: order.number ?? '',
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
overviewContext.add($t('Bestelling {orderNumber}', {
|
|
450
|
+
orderNumber: order.number ?? '',
|
|
451
|
+
}));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case BalanceItemType.Registration: {
|
|
458
|
+
const memberName = balanceItem.relations.get(BalanceItemRelationType.Member)?.name.toString();
|
|
459
|
+
if (memberName) {
|
|
460
|
+
registrationMemberNames.add(memberName);
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
overviewContext.add(balanceItem.itemTitle);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
default: {
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (registrationMemberNames.size > 0) {
|
|
475
|
+
const memberNames = Formatter.joinLast([...registrationMemberNames], ', ', ' ' + $t(`6a156458-b396-4d0f-b562-adb3e38fc51b`) + ' ');
|
|
476
|
+
overviewContext.add($t(`01d5fd7e-2960-4eb4-ab3a-2ac6dcb2e39c`) + ' ' + memberNames);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (overviewContext.size === 0) {
|
|
480
|
+
// add item title if no balance items with type order or registration
|
|
481
|
+
if (payment.balanceItemPayments.length === 1) {
|
|
482
|
+
const balanceItem = payment.balanceItemPayments[0].balanceItem;
|
|
483
|
+
return balanceItem.itemTitle;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (payment.balanceItemPayments.length > 1) {
|
|
487
|
+
// return title if all balance items have the same title
|
|
488
|
+
const firstTitle = payment.balanceItemPayments[0].balanceItem.itemTitle;
|
|
489
|
+
const haveAllSameTitle = payment.balanceItemPayments.every(p => p.balanceItem.itemTitle === firstTitle);
|
|
490
|
+
|
|
491
|
+
if (haveAllSameTitle) {
|
|
492
|
+
return `${firstTitle} (${payment.balanceItemPayments.length}x)`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// else return default text for multiple items
|
|
496
|
+
return $t('Betaling voor {count} items', { count: payment.balanceItemPayments.length });
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// else return default text for single item
|
|
500
|
+
return $t('Betaling voor 1 item');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// join texts for balance items with type order or registration
|
|
504
|
+
return [...overviewContext].join(', ');
|
|
271
505
|
}
|
|
272
506
|
|
|
273
|
-
|
|
507
|
+
type BeforeFetchAllResult = {
|
|
508
|
+
doesIncludePaymentWithoutOrders: boolean;
|
|
509
|
+
areAllPaymentsTransfers: boolean;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
async function fetchPaymentRecipients(query: LimitedFilteredRequest, beforeFetchAllResult?: BeforeFetchAllResult) {
|
|
274
513
|
const result = await GetPaymentsEndpoint.buildData(query);
|
|
275
514
|
|
|
276
515
|
return new PaginatedResponse({
|
|
277
|
-
results: await getRecipients(result, EmailRecipientFilterType.Payment, null),
|
|
516
|
+
results: await getRecipients(result, EmailRecipientFilterType.Payment, null, beforeFetchAllResult),
|
|
278
517
|
next: result.next,
|
|
279
518
|
});
|
|
280
519
|
}
|
|
281
520
|
|
|
282
|
-
|
|
283
|
-
|
|
521
|
+
const paymentRecipientLoader: RecipientLoader<BeforeFetchAllResult> = {
|
|
522
|
+
beforeFetchAll: async (query: LimitedFilteredRequest) => {
|
|
523
|
+
const doesIncludePaymentWithoutOrders = await doesQueryIncludePaymentsWithoutOrder(query);
|
|
524
|
+
const areAllPaymentsTransfers = await doesQueryIncludePaymentsOtherThanTransfers(query);
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
doesIncludePaymentWithoutOrders,
|
|
528
|
+
areAllPaymentsTransfers,
|
|
529
|
+
};
|
|
530
|
+
},
|
|
531
|
+
fetch: async (query: LimitedFilteredRequest, _subfilter, beforeFetchAllResult) => fetchPaymentRecipients(query, beforeFetchAllResult),
|
|
284
532
|
// For now: only count the number of payments - not the amount of emails
|
|
285
|
-
count: async (query: LimitedFilteredRequest) => {
|
|
533
|
+
count: async (query: LimitedFilteredRequest, _subfilter) => {
|
|
286
534
|
const q = await GetPaymentsEndpoint.buildQuery(query);
|
|
287
535
|
const base = await q.count();
|
|
288
536
|
|
|
@@ -295,31 +543,78 @@ Email.recipientLoaders.set(EmailRecipientFilterType.Payment, {
|
|
|
295
543
|
|
|
296
544
|
return base;
|
|
297
545
|
},
|
|
298
|
-
}
|
|
546
|
+
};
|
|
299
547
|
|
|
300
|
-
|
|
548
|
+
Email.recipientLoaders.set(EmailRecipientFilterType.Payment, paymentRecipientLoader);
|
|
549
|
+
|
|
550
|
+
async function doesQueryIncludePaymentsWithoutOrder(filterRequest: LimitedFilteredRequest) {
|
|
551
|
+
// create count request (without limit and page filter)
|
|
552
|
+
const countRequest = new CountFilteredRequest({
|
|
553
|
+
filter: filterRequest.filter,
|
|
554
|
+
search: filterRequest.search,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const baseQuery = await GetPaymentsEndpoint.buildQuery(countRequest);
|
|
558
|
+
|
|
559
|
+
const balanceItemPaymentsJoin = SQL.innerJoin(BalanceItemPayment.table)
|
|
560
|
+
.where(
|
|
561
|
+
SQL.column(BalanceItemPayment.table, 'paymentId'),
|
|
562
|
+
SQL.column(Payment.table, 'id'),
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const balanceItemJoin = SQL.innerJoin(BalanceItem.table)
|
|
566
|
+
.where(
|
|
567
|
+
SQL.column(BalanceItem.table, 'id'),
|
|
568
|
+
SQL.column(BalanceItemPayment.table, 'balanceItemId'),
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
// check if 1 payment without order
|
|
572
|
+
const results = await baseQuery
|
|
573
|
+
.join(balanceItemPaymentsJoin)
|
|
574
|
+
.join(balanceItemJoin)
|
|
575
|
+
// where no order
|
|
576
|
+
.where(SQL.column(BalanceItem.table, 'orderId'), null)
|
|
577
|
+
.limit(1)
|
|
578
|
+
.count();
|
|
579
|
+
|
|
580
|
+
return results > 0;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function doesQueryIncludePaymentsOtherThanTransfers(filterRequest: LimitedFilteredRequest) {
|
|
584
|
+
// create count request (without limit and page filter)
|
|
585
|
+
const countRequest = new CountFilteredRequest({
|
|
586
|
+
filter: filterRequest.filter,
|
|
587
|
+
search: filterRequest.search,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const baseQuery = await GetPaymentsEndpoint.buildQuery(countRequest);
|
|
591
|
+
|
|
592
|
+
// check if 1 payment with other method than transfer
|
|
593
|
+
const results = await baseQuery
|
|
594
|
+
// where method is not transfer
|
|
595
|
+
.whereNot(SQL.column(Payment.table, 'method'), PaymentMethod.Transfer)
|
|
596
|
+
.limit(1)
|
|
597
|
+
.count();
|
|
598
|
+
|
|
599
|
+
return results === 0;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function fetchPaymentOrganizationRecipients(query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null, beforeFetchAllResult?: BeforeFetchAllResult) {
|
|
301
603
|
const result = await GetPaymentsEndpoint.buildData(query);
|
|
302
604
|
|
|
303
605
|
return new PaginatedResponse({
|
|
304
|
-
results: await getRecipients(result, EmailRecipientFilterType.PaymentOrganization, subfilter),
|
|
606
|
+
results: await getRecipients(result, EmailRecipientFilterType.PaymentOrganization, subfilter, beforeFetchAllResult),
|
|
305
607
|
next: result.next,
|
|
306
608
|
});
|
|
307
609
|
}
|
|
308
610
|
|
|
309
|
-
|
|
310
|
-
fetch: fetchPaymentOrganizationRecipients,
|
|
611
|
+
const paymentOrganizationRecipientLoader: RecipientLoader<BeforeFetchAllResult> = {
|
|
612
|
+
fetch: async (query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null, beforeFetchAllResult) => fetchPaymentOrganizationRecipients(query, subfilter, beforeFetchAllResult),
|
|
311
613
|
// For now: only count the number of payments - not the amount of emails
|
|
312
614
|
count: async (query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) => {
|
|
313
615
|
const q = await GetPaymentsEndpoint.buildQuery(query);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (base < 1000) {
|
|
317
|
-
// Do full scan
|
|
318
|
-
query.limit = 1000;
|
|
319
|
-
const result = await fetchPaymentOrganizationRecipients(query, subfilter);
|
|
320
|
-
return result.results.length;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return base;
|
|
616
|
+
return await q.count();
|
|
324
617
|
},
|
|
325
|
-
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
Email.recipientLoaders.set(EmailRecipientFilterType.PaymentOrganization, paymentOrganizationRecipientLoader);
|