@stamhoofd/backend 2.114.0 → 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 -19
- package/src/email-recipient-loaders/documents.ts +1 -14
- package/src/email-recipient-loaders/payments.ts +620 -0
- 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-filters/balance-item-payments.ts +5 -0
- package/src/sql-filters/member-responsibility-records.ts +16 -0
- package/src/sql-filters/payments.ts +5 -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/src/{seeds → seeds-temporary}/1769088653-uitpas-status.ts +0 -0
|
@@ -0,0 +1,620 @@
|
|
|
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
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
5
|
+
import { GetPaymentsEndpoint } from '../endpoints/organization/dashboard/payments/GetPaymentsEndpoint.js';
|
|
6
|
+
import { createOrderDataHTMLTable, createPaymentDataHTMLTable } from '../helpers/email-html-helpers.js';
|
|
7
|
+
import { memberResponsibilityRecordFilterCompilers } from '../sql-filters/member-responsibility-records.js';
|
|
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) {
|
|
18
|
+
const recipients: EmailRecipient[] = [];
|
|
19
|
+
const userIds: { userId: string; payment: PaymentGeneral }[] = [];
|
|
20
|
+
const organizationIds: { organizationId: string; payment: PaymentGeneral }[] = [];
|
|
21
|
+
const memberIds: { memberId: string; payment: PaymentGeneral }[] = [];
|
|
22
|
+
const orderIds: { orderId: string; payment: PaymentGeneral }[] = [];
|
|
23
|
+
|
|
24
|
+
for (const payment of result.results) {
|
|
25
|
+
if (payment.payingOrganizationId) {
|
|
26
|
+
organizationIds.push({ organizationId: payment.payingOrganizationId, payment });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (payment.payingUserId) {
|
|
31
|
+
userIds.push({ userId: payment.payingUserId, payment });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const balanceItemOrganizationIds = new Set<string>();
|
|
36
|
+
const balanceItemUserIds = new Set<string>();
|
|
37
|
+
const balanceItemMemberIds = new Set<string>();
|
|
38
|
+
const balanceItemOrderIds = new Set<string>();
|
|
39
|
+
|
|
40
|
+
for (const balanceItemPayment of payment.balanceItemPayments) {
|
|
41
|
+
const balanceItem = balanceItemPayment.balanceItem;
|
|
42
|
+
|
|
43
|
+
if (balanceItem.payingOrganizationId) {
|
|
44
|
+
balanceItemOrganizationIds.add(balanceItem.payingOrganizationId);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (balanceItem.userId) {
|
|
49
|
+
balanceItemUserIds.add(balanceItem.userId);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (balanceItem.memberId) {
|
|
54
|
+
balanceItemMemberIds.add(balanceItem.memberId);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (balanceItem.orderId) {
|
|
59
|
+
balanceItemOrderIds.add(balanceItem.orderId);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const totalRecipientsForPayment = balanceItemOrganizationIds.size + balanceItemUserIds.size + balanceItemMemberIds.size + balanceItemOrderIds.size;
|
|
65
|
+
if (totalRecipientsForPayment > 1) {
|
|
66
|
+
console.warn('Multiple recipients found for payment: ', payment.id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (balanceItemOrganizationIds.size > 0) {
|
|
70
|
+
organizationIds.push(...Array.from(balanceItemOrganizationIds).map(organizationId => ({ organizationId, payment })));
|
|
71
|
+
}
|
|
72
|
+
if (balanceItemUserIds.size > 0) {
|
|
73
|
+
userIds.push(...Array.from(balanceItemUserIds).map(userId => ({ userId, payment })));
|
|
74
|
+
}
|
|
75
|
+
if (balanceItemMemberIds.size > 0) {
|
|
76
|
+
memberIds.push(...Array.from(balanceItemMemberIds).map(memberId => ({ memberId, payment })));
|
|
77
|
+
}
|
|
78
|
+
if (balanceItemOrderIds.size > 0) {
|
|
79
|
+
orderIds.push(...Array.from(balanceItemOrderIds).map(orderId => ({ orderId, payment })));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
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
|
+
|
|
122
|
+
if (type === EmailRecipientFilterType.Payment) {
|
|
123
|
+
recipients.push(...await getUserRecipients(userIds, replacementOptions));
|
|
124
|
+
recipients.push(...await getMemberRecipients(memberIds, replacementOptions));
|
|
125
|
+
recipients.push(...await getOrderRecipients(orderIds, replacementOptions));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
recipients.push(...await getOrganizationRecipients(organizationIds, replacementOptions, subFilter));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return recipients;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function getUserRecipients(ids: { userId: string; payment: PaymentGeneral }[], replacementOptions: ReplacementsOptions): Promise<EmailRecipient[]> {
|
|
135
|
+
if (ids.length === 0) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const allUserIds = Formatter.uniqueArray(ids.map(i => i.userId));
|
|
140
|
+
const users = await User.getByIDs(...allUserIds);
|
|
141
|
+
|
|
142
|
+
const results: EmailRecipient[] = [];
|
|
143
|
+
|
|
144
|
+
for (const { userId, payment } of ids) {
|
|
145
|
+
const user = users.find(u => u.id === userId);
|
|
146
|
+
|
|
147
|
+
if (user) {
|
|
148
|
+
results.push(EmailRecipient.create({
|
|
149
|
+
objectId: payment.id,
|
|
150
|
+
userId,
|
|
151
|
+
firstName: user.firstName,
|
|
152
|
+
lastName: user.lastName,
|
|
153
|
+
email: user.email,
|
|
154
|
+
replacements: getEmailReplacementsForPayment(payment, replacementOptions),
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function getMembersForOrganizations(organizationIds: string[], filter: StamhoofdFilter | null): Promise<Map<string, Member[]>> {
|
|
163
|
+
const query = MemberResponsibilityRecord.select()
|
|
164
|
+
.where('organizationId', organizationIds)
|
|
165
|
+
.where('endDate', null)
|
|
166
|
+
.where(await compileToSQLFilter(filter,
|
|
167
|
+
memberResponsibilityRecordFilterCompilers));
|
|
168
|
+
|
|
169
|
+
const responsibilites = await query
|
|
170
|
+
.fetch();
|
|
171
|
+
|
|
172
|
+
const allMemberIds = Formatter.uniqueArray(responsibilites.map(r => r.memberId));
|
|
173
|
+
const members = await Member.getByIDs(...allMemberIds);
|
|
174
|
+
|
|
175
|
+
const result = new Map<string, Member[]>();
|
|
176
|
+
|
|
177
|
+
for (const responsibility of responsibilites) {
|
|
178
|
+
const organizationId = responsibility.organizationId;
|
|
179
|
+
|
|
180
|
+
if (!organizationId) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const member = members.find(m => m.id === responsibility.memberId);
|
|
185
|
+
|
|
186
|
+
if (!member) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const membersForOrganization = result.get(organizationId);
|
|
191
|
+
if (membersForOrganization) {
|
|
192
|
+
membersForOrganization.push(member);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
result.set(organizationId, [member]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function getOrganizationRecipients(ids: { organizationId: string; payment: PaymentGeneral }[], replacementOptions: ReplacementsOptions, subFilter: StamhoofdFilter | null): Promise<EmailRecipient[]> {
|
|
203
|
+
if (ids.length === 0 || subFilter === null) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const allOrganizationIds = Formatter.uniqueArray(ids.map(i => i.organizationId));
|
|
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);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const membersForOrganizations = await getMembersForOrganizations(allOrganizationIds, subFilter);
|
|
218
|
+
|
|
219
|
+
const results: EmailRecipient[] = [];
|
|
220
|
+
|
|
221
|
+
for (const { organizationId, payment } of ids) {
|
|
222
|
+
const organization = organizationMap.get(organizationId);
|
|
223
|
+
|
|
224
|
+
if (organization) {
|
|
225
|
+
const members = membersForOrganizations.get(organizationId);
|
|
226
|
+
if (!members) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const replacements = getEmailReplacementsForPayment(payment, replacementOptions);
|
|
231
|
+
|
|
232
|
+
for (const member of members) {
|
|
233
|
+
for (const email of member.details.getMemberEmails()) {
|
|
234
|
+
results.push(EmailRecipient.create({
|
|
235
|
+
objectId: payment.id,
|
|
236
|
+
name: organization.name,
|
|
237
|
+
memberId: member.id,
|
|
238
|
+
firstName: member.details.firstName,
|
|
239
|
+
lastName: member.details.lastName,
|
|
240
|
+
email,
|
|
241
|
+
replacements,
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function getMemberRecipients(ids: { memberId: string; payment: PaymentGeneral }[], replacementOptions: ReplacementsOptions): Promise<EmailRecipient[]> {
|
|
252
|
+
if (ids.length === 0) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const allMemberIds = Formatter.uniqueArray(ids.map(i => i.memberId));
|
|
257
|
+
const members = await Member.getBlobByIds(...allMemberIds);
|
|
258
|
+
|
|
259
|
+
const results: EmailRecipient[] = [];
|
|
260
|
+
|
|
261
|
+
for (const { memberId, payment } of ids) {
|
|
262
|
+
const member = members.find(m => m.id === memberId);
|
|
263
|
+
|
|
264
|
+
if (member) {
|
|
265
|
+
const emails = member.details.getNotificationEmails();
|
|
266
|
+
|
|
267
|
+
for (const user of member.users) {
|
|
268
|
+
if (!emails.includes(user.email.toLocaleLowerCase())) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const recipient = EmailRecipient.create({
|
|
273
|
+
objectId: payment.id,
|
|
274
|
+
userId: user.id,
|
|
275
|
+
memberId,
|
|
276
|
+
firstName: user.firstName,
|
|
277
|
+
lastName: user.lastName,
|
|
278
|
+
email: user.email,
|
|
279
|
+
replacements: getEmailReplacementsForPayment(payment, replacementOptions),
|
|
280
|
+
});
|
|
281
|
+
results.push(recipient);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return results;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function getOrderRecipients(ids: { orderId: string; payment: PaymentGeneral }[], replacementOptions: ReplacementsOptions): Promise<EmailRecipient[]> {
|
|
290
|
+
if (ids.length === 0) {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const orderMap = replacementOptions.orderMap;
|
|
295
|
+
const results: EmailRecipient[] = [];
|
|
296
|
+
|
|
297
|
+
for (const { orderId, payment } of ids) {
|
|
298
|
+
const order = orderMap.get(orderId);
|
|
299
|
+
|
|
300
|
+
if (order) {
|
|
301
|
+
const { firstName, lastName, email } = order.data.customer;
|
|
302
|
+
|
|
303
|
+
results.push(EmailRecipient.create({
|
|
304
|
+
objectId: payment.id,
|
|
305
|
+
userId: order.userId,
|
|
306
|
+
firstName,
|
|
307
|
+
lastName,
|
|
308
|
+
email,
|
|
309
|
+
replacements: getEmailReplacementsForPayment(payment, replacementOptions),
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
|
|
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(', ');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
type BeforeFetchAllResult = {
|
|
508
|
+
doesIncludePaymentWithoutOrders: boolean;
|
|
509
|
+
areAllPaymentsTransfers: boolean;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
async function fetchPaymentRecipients(query: LimitedFilteredRequest, beforeFetchAllResult?: BeforeFetchAllResult) {
|
|
513
|
+
const result = await GetPaymentsEndpoint.buildData(query);
|
|
514
|
+
|
|
515
|
+
return new PaginatedResponse({
|
|
516
|
+
results: await getRecipients(result, EmailRecipientFilterType.Payment, null, beforeFetchAllResult),
|
|
517
|
+
next: result.next,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
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),
|
|
532
|
+
// For now: only count the number of payments - not the amount of emails
|
|
533
|
+
count: async (query: LimitedFilteredRequest, _subfilter) => {
|
|
534
|
+
const q = await GetPaymentsEndpoint.buildQuery(query);
|
|
535
|
+
const base = await q.count();
|
|
536
|
+
|
|
537
|
+
if (base < 1000) {
|
|
538
|
+
// Do full scan
|
|
539
|
+
query.limit = 1000;
|
|
540
|
+
const result = await fetchPaymentRecipients(query);
|
|
541
|
+
return result.results.length;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return base;
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
|
|
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) {
|
|
603
|
+
const result = await GetPaymentsEndpoint.buildData(query);
|
|
604
|
+
|
|
605
|
+
return new PaginatedResponse({
|
|
606
|
+
results: await getRecipients(result, EmailRecipientFilterType.PaymentOrganization, subfilter, beforeFetchAllResult),
|
|
607
|
+
next: result.next,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const paymentOrganizationRecipientLoader: RecipientLoader<BeforeFetchAllResult> = {
|
|
612
|
+
fetch: async (query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null, beforeFetchAllResult) => fetchPaymentOrganizationRecipients(query, subfilter, beforeFetchAllResult),
|
|
613
|
+
// For now: only count the number of payments - not the amount of emails
|
|
614
|
+
count: async (query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) => {
|
|
615
|
+
const q = await GetPaymentsEndpoint.buildQuery(query);
|
|
616
|
+
return await q.count();
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
Email.recipientLoaders.set(EmailRecipientFilterType.PaymentOrganization, paymentOrganizationRecipientLoader);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CachedBalance, Email } from '@stamhoofd/models';
|
|
2
2
|
import { BalanceItem as BalanceItemStruct, compileToInMemoryFilter, EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, receivableBalanceObjectContactInMemoryFilterCompilers, ReceivableBalanceType, Replacement, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
3
|
import { Formatter } from '@stamhoofd/utility';
|
|
4
|
-
import { GetReceivableBalancesEndpoint } from '../endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint';
|
|
4
|
+
import { GetReceivableBalancesEndpoint } from '../endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.js';
|
|
5
5
|
|
|
6
6
|
async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) {
|
|
7
7
|
const result = await GetReceivableBalancesEndpoint.buildData(query);
|
|
@@ -73,15 +73,6 @@ Email.recipientLoaders.set(EmailRecipientFilterType.ReceivableBalances, {
|
|
|
73
73
|
// For now: only count the number of organizations - not the amount of emails
|
|
74
74
|
count: async (query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) => {
|
|
75
75
|
const q = await GetReceivableBalancesEndpoint.buildQuery(query);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (base < 1000) {
|
|
79
|
-
// Do full scan
|
|
80
|
-
query.limit = 1000;
|
|
81
|
-
const result = await fetch(query, subfilter);
|
|
82
|
-
return result.results.length;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return base;
|
|
76
|
+
return await q.count();
|
|
86
77
|
},
|
|
87
78
|
});
|
|
@@ -127,7 +127,7 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
await model.save();
|
|
130
|
-
await model.buildExampleRecipient();
|
|
130
|
+
await model.buildExampleRecipient(true);
|
|
131
131
|
await model.updateCount();
|
|
132
132
|
|
|
133
133
|
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
|
|
@@ -210,7 +210,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
// Update documents
|
|
213
|
-
await Document.updateForMember(member
|
|
213
|
+
await Document.updateForMember(member);
|
|
214
214
|
|
|
215
215
|
// Update responsibilities
|
|
216
216
|
for (const patchResponsibility of patch.responsibilities.getPatches()) {
|
|
@@ -871,7 +871,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
871
871
|
await MemberUserSyncer.onChangeMember(m);
|
|
872
872
|
|
|
873
873
|
// Update documents
|
|
874
|
-
await Document.updateForMember(m
|
|
874
|
+
await Document.updateForMember(m);
|
|
875
875
|
}
|
|
876
876
|
}
|
|
877
877
|
}
|
|
@@ -149,7 +149,7 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
149
149
|
await MemberUserSyncer.onChangeMember(member);
|
|
150
150
|
|
|
151
151
|
// Update documents
|
|
152
|
-
await Document.updateForMember(member
|
|
152
|
+
await Document.updateForMember(member);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// Modify members
|
|
@@ -173,7 +173,7 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
173
173
|
await MemberUserSyncer.linkUser(user.email, updatedMember, true);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
await Document.updateForMember(updatedMember
|
|
176
|
+
await Document.updateForMember(updatedMember);
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|