@stamhoofd/backend 2.114.0 → 2.114.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.114.0",
3
+ "version": "2.114.1",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -54,14 +54,14 @@
54
54
  "@simonbackx/simple-encoding": "2.23.1",
55
55
  "@simonbackx/simple-endpoints": "1.20.1",
56
56
  "@simonbackx/simple-logging": "^1.0.1",
57
- "@stamhoofd/backend-i18n": "2.114.0",
58
- "@stamhoofd/backend-middleware": "2.114.0",
59
- "@stamhoofd/email": "2.114.0",
60
- "@stamhoofd/models": "2.114.0",
61
- "@stamhoofd/queues": "2.114.0",
62
- "@stamhoofd/sql": "2.114.0",
63
- "@stamhoofd/structures": "2.114.0",
64
- "@stamhoofd/utility": "2.114.0",
57
+ "@stamhoofd/backend-i18n": "2.114.1",
58
+ "@stamhoofd/backend-middleware": "2.114.1",
59
+ "@stamhoofd/email": "2.114.1",
60
+ "@stamhoofd/models": "2.114.1",
61
+ "@stamhoofd/queues": "2.114.1",
62
+ "@stamhoofd/sql": "2.114.1",
63
+ "@stamhoofd/structures": "2.114.1",
64
+ "@stamhoofd/utility": "2.114.1",
65
65
  "archiver": "^7.0.1",
66
66
  "axios": "^1.13.2",
67
67
  "cookie": "^0.7.0",
@@ -79,5 +79,5 @@
79
79
  "publishConfig": {
80
80
  "access": "public"
81
81
  },
82
- "gitHead": "89f437d82d2c21b1741c9c7efdbdafd6c8b98318"
82
+ "gitHead": "0394c203f4133023b2a813d96554ea8b4a0a73d8"
83
83
  }
package/src/boot.ts CHANGED
@@ -132,6 +132,7 @@ export const boot = async (options: { killProcess: boolean }) => {
132
132
  await import('./email-recipient-loaders/receivable-balances');
133
133
  await import('./excel-loaders/registrations');
134
134
  await import('./email-recipient-loaders/documents');
135
+ await import ('./email-recipient-loaders/payments');
135
136
 
136
137
  productionLog('Opening port...');
137
138
  routerServer.listen(STAMHOOFD.PORT ?? 9090);
@@ -0,0 +1,325 @@
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';
4
+ import { Formatter } from '@stamhoofd/utility';
5
+ import { GetPaymentsEndpoint } from '../endpoints/organization/dashboard/payments/GetPaymentsEndpoint.js';
6
+ import { memberResponsibilityRecordFilterCompilers } from '../sql-filters/member-responsibility-records.js';
7
+
8
+ async function getRecipients(result: PaginatedResponse<PaymentGeneral[], LimitedFilteredRequest>, type: EmailRecipientFilterType.Payment | EmailRecipientFilterType.PaymentOrganization, subFilter: StamhoofdFilter | null) {
9
+ const recipients: EmailRecipient[] = [];
10
+ const userIds: { userId: string; payment: PaymentGeneral }[] = [];
11
+ const organizationIds: { organizationId: string; payment: PaymentGeneral }[] = [];
12
+ const memberIds: { memberId: string; payment: PaymentGeneral }[] = [];
13
+ const orderIds: { orderId: string; payment: PaymentGeneral }[] = [];
14
+
15
+ for (const payment of result.results) {
16
+ if (payment.payingOrganizationId) {
17
+ organizationIds.push({ organizationId: payment.payingOrganizationId, payment });
18
+ continue;
19
+ }
20
+
21
+ if (payment.payingUserId) {
22
+ userIds.push({ userId: payment.payingUserId, payment });
23
+ continue;
24
+ }
25
+
26
+ const balanceItemOrganizationIds = new Set<string>();
27
+ const balanceItemUserIds = new Set<string>();
28
+ const balanceItemMemberIds = new Set<string>();
29
+ const balanceItemOrderIds = new Set<string>();
30
+
31
+ for (const balanceItemPayment of payment.balanceItemPayments) {
32
+ const balanceItem = balanceItemPayment.balanceItem;
33
+
34
+ if (balanceItem.payingOrganizationId) {
35
+ balanceItemOrganizationIds.add(balanceItem.payingOrganizationId);
36
+ continue;
37
+ }
38
+
39
+ if (balanceItem.userId) {
40
+ balanceItemUserIds.add(balanceItem.userId);
41
+ continue;
42
+ }
43
+
44
+ if (balanceItem.memberId) {
45
+ balanceItemMemberIds.add(balanceItem.memberId);
46
+ continue;
47
+ }
48
+
49
+ if (balanceItem.orderId) {
50
+ balanceItemOrderIds.add(balanceItem.orderId);
51
+ continue;
52
+ }
53
+ }
54
+
55
+ const totalRecipientsForPayment = balanceItemOrganizationIds.size + balanceItemUserIds.size + balanceItemMemberIds.size + balanceItemOrderIds.size;
56
+ if (totalRecipientsForPayment > 1) {
57
+ console.warn('Multiple recipients found for payment: ', payment.id);
58
+ }
59
+
60
+ if (balanceItemOrganizationIds.size > 0) {
61
+ organizationIds.push(...Array.from(balanceItemOrganizationIds).map(organizationId => ({ organizationId, payment })));
62
+ }
63
+ if (balanceItemUserIds.size > 0) {
64
+ userIds.push(...Array.from(balanceItemUserIds).map(userId => ({ userId, payment })));
65
+ }
66
+ if (balanceItemMemberIds.size > 0) {
67
+ memberIds.push(...Array.from(balanceItemMemberIds).map(memberId => ({ memberId, payment })));
68
+ }
69
+ if (balanceItemOrderIds.size > 0) {
70
+ orderIds.push(...Array.from(balanceItemOrderIds).map(orderId => ({ orderId, payment })));
71
+ }
72
+ }
73
+
74
+ if (type === EmailRecipientFilterType.Payment) {
75
+ recipients.push(...await getUserRecipients(userIds));
76
+ recipients.push(...await getMemberRecipients(memberIds));
77
+ recipients.push(...await getUserRecipients(userIds));
78
+ recipients.push(...await getOrderRecipients(orderIds));
79
+ }
80
+ else {
81
+ recipients.push(...await getOrganizationRecipients(organizationIds, subFilter));
82
+ }
83
+
84
+ return recipients;
85
+ }
86
+
87
+ async function getUserRecipients(ids: { userId: string; payment: PaymentGeneral }[]): Promise<EmailRecipient[]> {
88
+ if (ids.length === 0) {
89
+ return [];
90
+ }
91
+
92
+ const allUserIds = Formatter.uniqueArray(ids.map(i => i.userId));
93
+ const users = await User.getByIDs(...allUserIds);
94
+
95
+ const results: EmailRecipient[] = [];
96
+
97
+ for (const { userId, payment } of ids) {
98
+ const user = users.find(u => u.id === userId);
99
+
100
+ if (user) {
101
+ results.push(EmailRecipient.create({
102
+ objectId: payment.id,
103
+ userId,
104
+ firstName: user.firstName,
105
+ lastName: user.lastName,
106
+ email: user.email,
107
+ replacements: getEmailReplacementsForPayment(payment),
108
+ }));
109
+ }
110
+ }
111
+
112
+ return results;
113
+ }
114
+
115
+ async function getMembersForOrganizations(organizationIds: string[], filter: StamhoofdFilter | null): Promise<Map<string, Member[]>> {
116
+ const query = MemberResponsibilityRecord.select()
117
+ .where('organizationId', organizationIds)
118
+ .where('endDate', null)
119
+ .where(await compileToSQLFilter(filter,
120
+ memberResponsibilityRecordFilterCompilers));
121
+
122
+ const responsibilites = await query
123
+ .fetch();
124
+
125
+ const allMemberIds = Formatter.uniqueArray(responsibilites.map(r => r.memberId));
126
+ const members = await Member.getByIDs(...allMemberIds);
127
+
128
+ const result = new Map<string, Member[]>();
129
+
130
+ for (const responsibility of responsibilites) {
131
+ const organizationId = responsibility.organizationId;
132
+
133
+ if (!organizationId) {
134
+ continue;
135
+ }
136
+
137
+ const member = members.find(m => m.id === responsibility.memberId);
138
+
139
+ if (!member) {
140
+ continue;
141
+ }
142
+
143
+ const membersForOrganization = result.get(organizationId);
144
+ if (membersForOrganization) {
145
+ membersForOrganization.push(member);
146
+ }
147
+ else {
148
+ result.set(organizationId, [member]);
149
+ }
150
+ }
151
+
152
+ return result;
153
+ }
154
+
155
+ async function getOrganizationRecipients(ids: { organizationId: string; payment: PaymentGeneral }[], subFilter: StamhoofdFilter | null): Promise<EmailRecipient[]> {
156
+ if (ids.length === 0 || subFilter === null) {
157
+ return [];
158
+ }
159
+
160
+ const allOrganizationIds = Formatter.uniqueArray(ids.map(i => i.organizationId));
161
+ const organizations = await Organization.getByIDs(...allOrganizationIds);
162
+ if (!organizations.length) {
163
+ return [];
164
+ }
165
+
166
+ const membersForOrganizations = await getMembersForOrganizations(allOrganizationIds, subFilter);
167
+
168
+ const results: EmailRecipient[] = [];
169
+
170
+ for (const { organizationId, payment } of ids) {
171
+ const organization = organizations.find(o => o.id === organizationId);
172
+
173
+ if (organization) {
174
+ const members = membersForOrganizations.get(organizationId);
175
+ if (!members) {
176
+ continue;
177
+ }
178
+
179
+ const replacements = getEmailReplacementsForPayment(payment);
180
+
181
+ for (const member of members) {
182
+ for (const email of member.details.getMemberEmails()) {
183
+ results.push(EmailRecipient.create({
184
+ objectId: payment.id,
185
+ name: organization.name,
186
+ memberId: member.id,
187
+ firstName: member.details.firstName,
188
+ lastName: member.details.lastName,
189
+ email,
190
+ replacements,
191
+ }));
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ return results;
198
+ }
199
+
200
+ async function getMemberRecipients(ids: { memberId: string; payment: PaymentGeneral }[]): Promise<EmailRecipient[]> {
201
+ if (ids.length === 0) {
202
+ return [];
203
+ }
204
+
205
+ const allMemberIds = Formatter.uniqueArray(ids.map(i => i.memberId));
206
+ const members = await Member.getBlobByIds(...allMemberIds);
207
+
208
+ const results: EmailRecipient[] = [];
209
+
210
+ for (const { memberId, payment } of ids) {
211
+ const member = members.find(m => m.id === memberId);
212
+
213
+ if (member) {
214
+ const emails = member.details.getNotificationEmails();
215
+
216
+ for (const user of member.users) {
217
+ if (!emails.includes(user.email.toLocaleLowerCase())) {
218
+ continue;
219
+ }
220
+
221
+ const recipient = EmailRecipient.create({
222
+ objectId: payment.id,
223
+ userId: user.id,
224
+ memberId,
225
+ firstName: user.firstName,
226
+ lastName: user.lastName,
227
+ email: user.email,
228
+ replacements: getEmailReplacementsForPayment(payment),
229
+ });
230
+ results.push(recipient);
231
+ }
232
+ }
233
+ }
234
+
235
+ return results;
236
+ }
237
+
238
+ async function getOrderRecipients(ids: { orderId: string; payment: PaymentGeneral }[]): Promise<EmailRecipient[]> {
239
+ if (ids.length === 0) {
240
+ return [];
241
+ }
242
+
243
+ const allOrderIds = Formatter.uniqueArray(ids.map(i => i.orderId));
244
+ const orders = await Order.getByIDs(...allOrderIds);
245
+
246
+ const results: EmailRecipient[] = [];
247
+
248
+ for (const { orderId, payment } of ids) {
249
+ const order = orders.find(o => o.id === orderId);
250
+
251
+ if (order) {
252
+ const { firstName, lastName, email } = order.data.customer;
253
+
254
+ results.push(EmailRecipient.create({
255
+ objectId: payment.id,
256
+ userId: order.userId,
257
+ firstName,
258
+ lastName,
259
+ email,
260
+ replacements: getEmailReplacementsForPayment(payment),
261
+ }));
262
+ }
263
+ }
264
+
265
+ return results;
266
+ }
267
+
268
+ function getEmailReplacementsForPayment(payment: PaymentGeneral): Replacement[] {
269
+ // todo
270
+ return [];
271
+ }
272
+
273
+ async function fetchPaymentRecipients(query: LimitedFilteredRequest) {
274
+ const result = await GetPaymentsEndpoint.buildData(query);
275
+
276
+ return new PaginatedResponse({
277
+ results: await getRecipients(result, EmailRecipientFilterType.Payment, null),
278
+ next: result.next,
279
+ });
280
+ }
281
+
282
+ Email.recipientLoaders.set(EmailRecipientFilterType.Payment, {
283
+ fetch: fetchPaymentRecipients,
284
+ // For now: only count the number of payments - not the amount of emails
285
+ count: async (query: LimitedFilteredRequest) => {
286
+ const q = await GetPaymentsEndpoint.buildQuery(query);
287
+ const base = await q.count();
288
+
289
+ if (base < 1000) {
290
+ // Do full scan
291
+ query.limit = 1000;
292
+ const result = await fetchPaymentRecipients(query);
293
+ return result.results.length;
294
+ }
295
+
296
+ return base;
297
+ },
298
+ });
299
+
300
+ async function fetchPaymentOrganizationRecipients(query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) {
301
+ const result = await GetPaymentsEndpoint.buildData(query);
302
+
303
+ return new PaginatedResponse({
304
+ results: await getRecipients(result, EmailRecipientFilterType.PaymentOrganization, subfilter),
305
+ next: result.next,
306
+ });
307
+ }
308
+
309
+ Email.recipientLoaders.set(EmailRecipientFilterType.PaymentOrganization, {
310
+ fetch: fetchPaymentOrganizationRecipients,
311
+ // For now: only count the number of payments - not the amount of emails
312
+ count: async (query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) => {
313
+ const q = await GetPaymentsEndpoint.buildQuery(query);
314
+ const base = await q.count();
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;
324
+ },
325
+ });
@@ -24,5 +24,10 @@ export const balanceItemPaymentsCompilers: SQLFilterDefinitions = {
24
24
  type: SQLValueType.String,
25
25
  nullable: false,
26
26
  }),
27
+ payingOrganizationId: createColumnFilter({
28
+ expression: SQL.column('balance_items', 'payingOrganizationId'),
29
+ type: SQLValueType.String,
30
+ nullable: true,
31
+ }),
27
32
  },
28
33
  };
@@ -0,0 +1,16 @@
1
+ import { MemberResponsibilityRecord } from '@stamhoofd/models';
2
+ import { baseSQLFilterCompilers, createColumnFilter, SQL, SQLFilterDefinitions, SQLValueType } from '@stamhoofd/sql';
3
+
4
+ const baseTable = SQL.table(MemberResponsibilityRecord.table);
5
+
6
+ /**
7
+ * Defines how to filter member responsibility records in the database from StamhoofdFilter objects
8
+ */
9
+ export const memberResponsibilityRecordFilterCompilers: SQLFilterDefinitions = {
10
+ ...baseSQLFilterCompilers,
11
+ responsibilityId: createColumnFilter({
12
+ expression: SQL.column(baseTable, 'responsibilityId'),
13
+ type: SQLValueType.String,
14
+ nullable: false,
15
+ }),
16
+ };
@@ -31,6 +31,11 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
31
31
  type: SQLValueType.String,
32
32
  nullable: true,
33
33
  }),
34
+ payingOrganizationId: createColumnFilter({
35
+ expression: SQL.column('payingOrganizationId'),
36
+ type: SQLValueType.String,
37
+ nullable: true,
38
+ }),
34
39
  createdAt: createColumnFilter({
35
40
  expression: SQL.column('createdAt'),
36
41
  type: SQLValueType.Datetime,