@stamhoofd/backend 2.113.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 +10 -10
- package/src/boot.ts +1 -0
- package/src/email-recipient-loaders/payments.ts +325 -0
- package/src/email-recipient-loaders/registrations.ts +3 -3
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -2
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +12 -6
- package/src/helpers/CheckSettlements.ts +2 -2
- package/src/helpers/StripePayoutChecker.ts +5 -5
- package/src/{seeds → seeds-temporary}/1769088653-uitpas-status.ts +6 -8
- package/src/sql-filters/balance-item-payments.ts +5 -0
- package/src/sql-filters/member-responsibility-records.ts +16 -0
- package/src/sql-filters/members.ts +35 -23
- package/src/sql-filters/payments.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
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.
|
|
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": "
|
|
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
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Email, Member } from '@stamhoofd/models';
|
|
2
2
|
import { SQL } from '@stamhoofd/sql';
|
|
3
3
|
import { EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, RegistrationsBlob, mergeFilters } from '@stamhoofd/structures';
|
|
4
|
-
import { GetRegistrationsEndpoint } from '../endpoints/global/registration/GetRegistrationsEndpoint';
|
|
5
|
-
import { memberJoin } from '../sql-filters/registrations';
|
|
4
|
+
import { GetRegistrationsEndpoint } from '../endpoints/global/registration/GetRegistrationsEndpoint.js';
|
|
5
|
+
import { memberJoin } from '../sql-filters/registrations.js';
|
|
6
6
|
|
|
7
7
|
async function getRecipients(result: PaginatedResponse<RegistrationsBlob, LimitedFilteredRequest>, type: 'member' | 'parents' | 'unverified') {
|
|
8
8
|
const recipients: EmailRecipient[] = [];
|
|
@@ -81,7 +81,7 @@ Email.recipientLoaders.set(EmailRecipientFilterType.RegistrationUnverified, {
|
|
|
81
81
|
const q = (await GetRegistrationsEndpoint.buildQuery(query)).join(memberJoin);
|
|
82
82
|
|
|
83
83
|
return await q.sum(
|
|
84
|
-
SQL.jsonLength(SQL.column('details'), '$.value.unverifiedEmails'),
|
|
84
|
+
SQL.jsonLength(SQL.column(Member.table, 'details'), '$.value.unverifiedEmails'),
|
|
85
85
|
);
|
|
86
86
|
},
|
|
87
87
|
});
|
|
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
3
3
|
import { Email, Platform, RateLimiter } from '@stamhoofd/models';
|
|
4
4
|
import { EmailPreview, EmailStatus, Email as EmailStruct, EmailTemplate as EmailTemplateStruct } from '@stamhoofd/structures';
|
|
5
5
|
|
|
6
|
-
import { Context } from '../../../helpers/Context';
|
|
6
|
+
import { Context } from '../../../helpers/Context.js';
|
|
7
7
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
8
8
|
|
|
9
9
|
type Params = Record<string, never>;
|
|
@@ -128,7 +128,7 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
128
128
|
|
|
129
129
|
await model.save();
|
|
130
130
|
await model.buildExampleRecipient();
|
|
131
|
-
model.updateCount();
|
|
131
|
+
await model.updateCount();
|
|
132
132
|
|
|
133
133
|
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
|
|
134
134
|
if (!await Context.auth.canSendEmail(model)) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { Email, Platform } from '@stamhoofd/models';
|
|
3
|
-
import { EmailPreview, EmailStatus, Email as EmailStruct, PermissionLevel } from '@stamhoofd/structures';
|
|
3
|
+
import { EmailPreview, EmailRecipientsStatus, EmailStatus, Email as EmailStruct, PermissionLevel } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { AutoEncoderPatchType, Decoder, patchObject } from '@simonbackx/simple-encoding';
|
|
6
6
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
|
-
import { Context } from '../../../helpers/Context';
|
|
7
|
+
import { Context } from '../../../helpers/Context.js';
|
|
8
8
|
|
|
9
9
|
type Params = { id: string };
|
|
10
10
|
type Query = undefined;
|
|
@@ -124,6 +124,15 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
if (model.recipientsStatus === EmailRecipientsStatus.Created) {
|
|
128
|
+
throw new SimpleError({
|
|
129
|
+
code: 'already_created',
|
|
130
|
+
message: 'Recipients already created',
|
|
131
|
+
human: $t(`457ecdaf-d1de-4136-9e82-682c18c5fa76`),
|
|
132
|
+
statusCode: 400,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
127
136
|
model.recipientFilter = patchObject(model.recipientFilter, request.body.recipientFilter);
|
|
128
137
|
rebuild = true;
|
|
129
138
|
}
|
|
@@ -168,10 +177,7 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
168
177
|
|
|
169
178
|
if (rebuild) {
|
|
170
179
|
await model.buildExampleRecipient();
|
|
171
|
-
|
|
172
|
-
// Force null - because we have stale data
|
|
173
|
-
model.emailRecipientsCount = null;
|
|
174
|
-
model.updateCount();
|
|
180
|
+
await model.updateCount();
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
|
|
@@ -2,7 +2,7 @@ import { MolliePayment, MollieToken, Order, Organization, PayconiqPayment, Payme
|
|
|
2
2
|
import { Settlement } from '@stamhoofd/structures';
|
|
3
3
|
import axios from 'axios';
|
|
4
4
|
|
|
5
|
-
import { StripePayoutChecker } from './StripePayoutChecker';
|
|
5
|
+
import { StripePayoutChecker } from './StripePayoutChecker.js';
|
|
6
6
|
|
|
7
7
|
type MollieSettlement = {
|
|
8
8
|
id: string;
|
|
@@ -179,7 +179,7 @@ async function updateSettlement(token: string, settlement: MollieSettlement, fro
|
|
|
179
179
|
id: settlement.id,
|
|
180
180
|
reference: settlement.reference,
|
|
181
181
|
settledAt: new Date(settlement.settledAt),
|
|
182
|
-
amount: Math.round(parseFloat(settlement.amount.value) * 100),
|
|
182
|
+
amount: Math.round(parseFloat(settlement.amount.value) * 100) * 100,
|
|
183
183
|
});
|
|
184
184
|
const saved = await payment.save();
|
|
185
185
|
|
|
@@ -171,8 +171,8 @@ export class StripePayoutChecker {
|
|
|
171
171
|
return;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
if (payment.price !== balanceItem.amount) {
|
|
175
|
-
console.log('Amount mismatch for payment ' + payment.id + ': ' + payment.price + ' !== ' + balanceItem.amount);
|
|
174
|
+
if (payment.price !== balanceItem.amount * 100) {
|
|
175
|
+
console.log('Amount mismatch for payment ' + payment.id + ': ' + payment.price + ' !== ' + (balanceItem.amount * 100));
|
|
176
176
|
return;
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -180,13 +180,13 @@ export class StripePayoutChecker {
|
|
|
180
180
|
id: payout.id,
|
|
181
181
|
reference: payout.statement_descriptor ?? '',
|
|
182
182
|
settledAt: new Date(payout.arrival_date * 1000),
|
|
183
|
-
amount: payout.amount,
|
|
183
|
+
amount: payout.amount * 100,
|
|
184
184
|
// Set only if application fee is witheld
|
|
185
|
-
fee: totalFees,
|
|
185
|
+
fee: totalFees * 100,
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
payment.settlement = settlement;
|
|
189
|
-
payment.transferFee = totalFees - payment.serviceFeePayout;
|
|
189
|
+
payment.transferFee = totalFees * 100 - payment.serviceFeePayout;
|
|
190
190
|
|
|
191
191
|
// Force an updatedAt timestamp of the related order
|
|
192
192
|
// Mark order as 'updated', or the frontend won't pull in the updates
|
|
@@ -91,14 +91,9 @@ async function migrateMember(member: Member) {
|
|
|
91
91
|
if (error.hasCode('https://api.publiq.be/probs/uitpas/pass-not-found') || error.hasCode('https://api.publiq.be/probs/uitpas/invalid-uitpas-number')) {
|
|
92
92
|
console.log(`Uitpas number ${member.details.uitpasNumberDetails?.uitpasNumber} is not known by the uitpas api for member with id ${member.id}.`);
|
|
93
93
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
status: UitpasSocialTariffStatus.Unknown,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// remove review
|
|
94
|
+
// remove the uitpas number
|
|
95
|
+
member.details.uitpasNumberDetails = null;
|
|
96
|
+
member.details.cleanData();
|
|
102
97
|
member.details.reviewTimes.removeReview('uitpasNumber');
|
|
103
98
|
await member.save();
|
|
104
99
|
|
|
@@ -113,6 +108,9 @@ async function migrateMember(member: Member) {
|
|
|
113
108
|
// remove the uitpas number
|
|
114
109
|
member.details.uitpasNumberDetails = null;
|
|
115
110
|
member.details.cleanData();
|
|
111
|
+
|
|
112
|
+
// remove review
|
|
113
|
+
member.details.reviewTimes.removeReview('uitpasNumber');
|
|
116
114
|
await member.save();
|
|
117
115
|
|
|
118
116
|
// do not throw
|
|
@@ -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
|
+
};
|
|
@@ -68,34 +68,20 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
|
|
|
68
68
|
type: SQLValueType.String,
|
|
69
69
|
nullable: false,
|
|
70
70
|
}),
|
|
71
|
+
'details.uitpasNumberDetails.socialTariff.status': createColumnFilter({
|
|
72
|
+
expression: SQL.jsonExtract(SQL.column(membersTable, 'details'), '$.value.uitpasNumberDetails.socialTariff.status'),
|
|
73
|
+
type: SQLValueType.JSONString,
|
|
74
|
+
nullable: true,
|
|
75
|
+
checkPermission: async () => {
|
|
76
|
+
await throwIfNoFinancialReadAccess();
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
71
79
|
'details.requiresFinancialSupport': createColumnFilter({
|
|
72
80
|
expression: SQL.jsonExtract(SQL.column(membersTable, 'details'), '$.value.requiresFinancialSupport.value'),
|
|
73
81
|
type: SQLValueType.JSONBoolean,
|
|
74
82
|
nullable: true,
|
|
75
83
|
checkPermission: async () => {
|
|
76
|
-
|
|
77
|
-
if (!organization) {
|
|
78
|
-
if (!Context.auth.hasPlatformFullAccess()) {
|
|
79
|
-
throw new SimpleError({
|
|
80
|
-
code: 'permission_denied',
|
|
81
|
-
message: 'No permissions for financial support filter.',
|
|
82
|
-
human: $t(`64d658fa-0727-4924-9448-b243fe8e10a1`),
|
|
83
|
-
statusCode: 400,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const permissions = await Context.auth.getOrganizationPermissions(organization);
|
|
90
|
-
|
|
91
|
-
if (!permissions || !permissions.hasAccessRight(AccessRight.MemberReadFinancialData)) {
|
|
92
|
-
throw new SimpleError({
|
|
93
|
-
code: 'permission_denied',
|
|
94
|
-
message: 'No permissions for financial support filter (organization scope).',
|
|
95
|
-
human: $t(`64d658fa-0727-4924-9448-b243fe8e10a1`),
|
|
96
|
-
statusCode: 400,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
84
|
+
await throwIfNoFinancialReadAccess();
|
|
99
85
|
},
|
|
100
86
|
}),
|
|
101
87
|
'email': createColumnFilter({
|
|
@@ -512,3 +498,29 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
|
|
|
512
498
|
),
|
|
513
499
|
},
|
|
514
500
|
};
|
|
501
|
+
|
|
502
|
+
async function throwIfNoFinancialReadAccess() {
|
|
503
|
+
const organization = Context.organization;
|
|
504
|
+
if (!organization) {
|
|
505
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
506
|
+
throw new SimpleError({
|
|
507
|
+
code: 'permission_denied',
|
|
508
|
+
message: 'No permissions for financial support filter.',
|
|
509
|
+
human: $t(`64d658fa-0727-4924-9448-b243fe8e10a1`),
|
|
510
|
+
statusCode: 400,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const permissions = await Context.auth.getOrganizationPermissions(organization);
|
|
517
|
+
|
|
518
|
+
if (!permissions || !permissions.hasAccessRight(AccessRight.MemberReadFinancialData)) {
|
|
519
|
+
throw new SimpleError({
|
|
520
|
+
code: 'permission_denied',
|
|
521
|
+
message: 'No permissions for financial support filter (organization scope).',
|
|
522
|
+
human: $t(`64d658fa-0727-4924-9448-b243fe8e10a1`),
|
|
523
|
+
statusCode: 400,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
@@ -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,
|