@stamhoofd/models 2.83.4 → 2.84.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/dist/src/factories/BalanceItemFactory.d.ts +3 -1
- package/dist/src/factories/BalanceItemFactory.d.ts.map +1 -1
- package/dist/src/factories/BalanceItemFactory.js +6 -0
- package/dist/src/factories/BalanceItemFactory.js.map +1 -1
- package/dist/src/factories/DocumentTemplateFactory.d.ts +15 -0
- package/dist/src/factories/DocumentTemplateFactory.d.ts.map +1 -0
- package/dist/src/factories/DocumentTemplateFactory.js +125 -0
- package/dist/src/factories/DocumentTemplateFactory.js.map +1 -0
- package/dist/src/factories/EventFactory.d.ts +2 -0
- package/dist/src/factories/EventFactory.d.ts.map +1 -1
- package/dist/src/factories/EventFactory.js +5 -0
- package/dist/src/factories/EventFactory.js.map +1 -1
- package/dist/src/factories/GroupFactory.d.ts +7 -2
- package/dist/src/factories/GroupFactory.d.ts.map +1 -1
- package/dist/src/factories/GroupFactory.js +30 -5
- package/dist/src/factories/GroupFactory.js.map +1 -1
- package/dist/src/factories/MemberFactory.d.ts.map +1 -1
- package/dist/src/factories/MemberFactory.js +3 -1
- package/dist/src/factories/MemberFactory.js.map +1 -1
- package/dist/src/factories/RegistrationFactory.d.ts +3 -1
- package/dist/src/factories/RegistrationFactory.d.ts.map +1 -1
- package/dist/src/factories/RegistrationFactory.js +2 -0
- package/dist/src/factories/RegistrationFactory.js.map +1 -1
- package/dist/src/factories/UserFactory.js +1 -1
- package/dist/src/factories/UserFactory.js.map +1 -1
- package/dist/src/factories/index.d.ts +1 -0
- package/dist/src/factories/index.d.ts.map +1 -1
- package/dist/src/factories/index.js +1 -0
- package/dist/src/factories/index.js.map +1 -1
- package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
- package/dist/src/helpers/EmailBuilder.js +3 -1
- package/dist/src/helpers/EmailBuilder.js.map +1 -1
- package/dist/src/migrations/1747913433-registration-discounts.sql +2 -0
- package/dist/src/migrations/1747996262-balance-items-paid-at.sql +2 -0
- package/dist/src/migrations/1747996263-balance-items-paid-at-fill.sql +1 -0
- package/dist/src/models/BalanceItem.d.ts +9 -8
- package/dist/src/models/BalanceItem.d.ts.map +1 -1
- package/dist/src/models/BalanceItem.js +36 -42
- package/dist/src/models/BalanceItem.js.map +1 -1
- package/dist/src/models/CachedBalance.d.ts.map +1 -1
- package/dist/src/models/CachedBalance.js +3 -0
- package/dist/src/models/CachedBalance.js.map +1 -1
- package/dist/src/models/Document.d.ts.map +1 -1
- package/dist/src/models/Document.js +9 -1
- package/dist/src/models/Document.js.map +1 -1
- package/dist/src/models/Email.d.ts.map +1 -1
- package/dist/src/models/Email.js +10 -3
- package/dist/src/models/Email.js.map +1 -1
- package/dist/src/models/Email.test.js +40 -40
- package/dist/src/models/Email.test.js.map +1 -1
- package/dist/src/models/Group.d.ts +1 -1
- package/dist/src/models/Group.d.ts.map +1 -1
- package/dist/src/models/Group.js +9 -4
- package/dist/src/models/Group.js.map +1 -1
- package/dist/src/models/Image.d.ts +3 -0
- package/dist/src/models/Image.d.ts.map +1 -1
- package/dist/src/models/Image.js +28 -17
- package/dist/src/models/Image.js.map +1 -1
- package/dist/src/models/Member.d.ts +3 -3
- package/dist/src/models/Member.d.ts.map +1 -1
- package/dist/src/models/Member.js +2 -2
- package/dist/src/models/Member.js.map +1 -1
- package/dist/src/models/MemberPlatformMembership.js +1 -1
- package/dist/src/models/MemberPlatformMembership.js.map +1 -1
- package/dist/src/models/Organization.d.ts.map +1 -1
- package/dist/src/models/Organization.js +26 -24
- package/dist/src/models/Organization.js.map +1 -1
- package/dist/src/models/PayconiqPayment.d.ts.map +1 -1
- package/dist/src/models/PayconiqPayment.js +6 -1
- package/dist/src/models/PayconiqPayment.js.map +1 -1
- package/dist/src/models/Platform.d.ts.map +1 -1
- package/dist/src/models/Platform.js +5 -3
- package/dist/src/models/Platform.js.map +1 -1
- package/dist/src/models/Registration.d.ts +11 -1
- package/dist/src/models/Registration.d.ts.map +1 -1
- package/dist/src/models/Registration.js +14 -1
- package/dist/src/models/Registration.js.map +1 -1
- package/dist/src/models/User.d.ts.map +1 -1
- package/dist/src/models/User.js +1 -1
- package/dist/src/models/User.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/factories/BalanceItemFactory.ts +7 -1
- package/src/factories/DocumentTemplateFactory.ts +130 -0
- package/src/factories/EventFactory.ts +7 -0
- package/src/factories/GroupFactory.ts +35 -8
- package/src/factories/MemberFactory.ts +3 -1
- package/src/factories/RegistrationFactory.ts +9 -2
- package/src/factories/UserFactory.ts +1 -1
- package/src/factories/index.ts +1 -0
- package/src/helpers/EmailBuilder.ts +3 -1
- package/src/migrations/1747913433-registration-discounts.sql +2 -0
- package/src/migrations/1747996262-balance-items-paid-at.sql +2 -0
- package/src/migrations/1747996263-balance-items-paid-at-fill.sql +1 -0
- package/src/models/BalanceItem.ts +39 -54
- package/src/models/CachedBalance.ts +4 -0
- package/src/models/Document.ts +9 -1
- package/src/models/Email.test.ts +40 -40
- package/src/models/Email.ts +12 -4
- package/src/models/Group.ts +12 -4
- package/src/models/Image.ts +32 -18
- package/src/models/Member.ts +3 -3
- package/src/models/MemberPlatformMembership.ts +1 -1
- package/src/models/Organization.ts +35 -27
- package/src/models/PayconiqPayment.ts +7 -1
- package/src/models/Platform.ts +6 -3
- package/src/models/Registration.ts +14 -2
- package/src/models/User.ts +2 -2
|
@@ -116,6 +116,12 @@ export class BalanceItem extends QueryableModel {
|
|
|
116
116
|
@column({ type: 'datetime', nullable: true })
|
|
117
117
|
dueAt: Date | null = null;
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Marking a balance item as 'paid' can have side effects. To prevent executing these side effects multiple times, we store it in here.
|
|
121
|
+
*/
|
|
122
|
+
@column({ type: 'datetime', nullable: true })
|
|
123
|
+
paidAt: Date | null = null;
|
|
124
|
+
|
|
119
125
|
@column({
|
|
120
126
|
type: 'datetime', beforeSave(old?: any) {
|
|
121
127
|
if (old !== undefined) {
|
|
@@ -142,6 +148,27 @@ export class BalanceItem extends QueryableModel {
|
|
|
142
148
|
return this.unitPrice * this.amount;
|
|
143
149
|
}
|
|
144
150
|
|
|
151
|
+
get isAfterDueDate() {
|
|
152
|
+
if (this.dueAt === null) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const now = new Date();
|
|
157
|
+
now.setMilliseconds(0);
|
|
158
|
+
return this.dueAt <= now;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Note: cancelled balance items can also return 'true', because if they have pending/paid payments, they are still due with a negative price
|
|
163
|
+
*/
|
|
164
|
+
get isDue() {
|
|
165
|
+
if (this.status === BalanceItemStatus.Hidden) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return this.isAfterDueDate;
|
|
170
|
+
}
|
|
171
|
+
|
|
145
172
|
get calculatedPriceOpen() {
|
|
146
173
|
if (this.status !== BalanceItemStatus.Due) {
|
|
147
174
|
return -this.pricePaid - this.pricePending;
|
|
@@ -197,7 +224,7 @@ export class BalanceItem extends QueryableModel {
|
|
|
197
224
|
|
|
198
225
|
// Refund the user
|
|
199
226
|
const cancellationFee = Math.round(item.price * options.cancellationFeePercentage / 10000);
|
|
200
|
-
if (cancellationFee
|
|
227
|
+
if (cancellationFee !== 0) {
|
|
201
228
|
// Create a new item
|
|
202
229
|
const cancellationItem = await item.createCancellationItem(cancellationFee);
|
|
203
230
|
deletedItems.push(cancellationItem);
|
|
@@ -205,9 +232,9 @@ export class BalanceItem extends QueryableModel {
|
|
|
205
232
|
}
|
|
206
233
|
}
|
|
207
234
|
|
|
208
|
-
if (deletedItems.length) {
|
|
209
|
-
|
|
210
|
-
}
|
|
235
|
+
// if (deletedItems.length) {
|
|
236
|
+
// await this.updateOutstanding(deletedItems);
|
|
237
|
+
// }
|
|
211
238
|
|
|
212
239
|
return deletedItems;
|
|
213
240
|
}
|
|
@@ -218,6 +245,7 @@ export class BalanceItem extends QueryableModel {
|
|
|
218
245
|
item.memberId = this.memberId;
|
|
219
246
|
item.userId = this.userId;
|
|
220
247
|
item.payingOrganizationId = this.payingOrganizationId;
|
|
248
|
+
item.registrationId = this.registrationId;
|
|
221
249
|
|
|
222
250
|
item.type = BalanceItemType.CancellationFee;
|
|
223
251
|
item.relations = this.relations;
|
|
@@ -243,9 +271,9 @@ export class BalanceItem extends QueryableModel {
|
|
|
243
271
|
}
|
|
244
272
|
}
|
|
245
273
|
|
|
246
|
-
if (needsUpdate) {
|
|
247
|
-
|
|
248
|
-
}
|
|
274
|
+
// if (needsUpdate) {
|
|
275
|
+
// await this.updateOutstanding(items);
|
|
276
|
+
// }
|
|
249
277
|
}
|
|
250
278
|
|
|
251
279
|
static async undoForDeletedOrders(orderIds: string[]) {
|
|
@@ -270,7 +298,10 @@ export class BalanceItem extends QueryableModel {
|
|
|
270
298
|
}
|
|
271
299
|
|
|
272
300
|
static async deleteForDeletedRegistration(registrationId: string, options?: { cancellationFeePercentage?: number }) {
|
|
273
|
-
const items = await BalanceItem.
|
|
301
|
+
const items = await BalanceItem.select()
|
|
302
|
+
.where('registrationId', registrationId)
|
|
303
|
+
.where('type', [BalanceItemType.Registration, BalanceItemType.RegistrationBundleDiscount])
|
|
304
|
+
.fetch();
|
|
274
305
|
return await this.deleteItems(items, options);
|
|
275
306
|
}
|
|
276
307
|
|
|
@@ -292,52 +323,6 @@ export class BalanceItem extends QueryableModel {
|
|
|
292
323
|
};
|
|
293
324
|
}
|
|
294
325
|
|
|
295
|
-
/**
|
|
296
|
-
* Update how many every object in the system owes or needs to be reimbursed
|
|
297
|
-
* and also updates the pricePaid/pricePending cached values in Balance items and members
|
|
298
|
-
*/
|
|
299
|
-
static async updateOutstanding(items: BalanceItem[], additionalItems: { memberId: string; organizationId: string }[] = []) {
|
|
300
|
-
console.log('Update outstanding balance for', items.length, 'items');
|
|
301
|
-
|
|
302
|
-
await BalanceItem.updatePricePaid(items.map(i => i.id));
|
|
303
|
-
|
|
304
|
-
const organizationIds = Formatter.uniqueArray(items.map(p => p.organizationId));
|
|
305
|
-
for (const organizationId of organizationIds) {
|
|
306
|
-
const filteredItems = items.filter(i => i.organizationId === organizationId);
|
|
307
|
-
const filteredAdditionalItems = additionalItems.filter(i => i.organizationId === organizationId);
|
|
308
|
-
|
|
309
|
-
const memberIds = Formatter.uniqueArray(
|
|
310
|
-
[
|
|
311
|
-
...filteredItems.map(p => p.memberId).filter(id => id !== null),
|
|
312
|
-
...filteredAdditionalItems.map(i => i.memberId),
|
|
313
|
-
],
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
await CachedBalance.updateForMembers(organizationId, memberIds);
|
|
317
|
-
|
|
318
|
-
let userIds = filteredItems.filter(p => p.userId !== null).map(p => p.userId!);
|
|
319
|
-
|
|
320
|
-
if (memberIds.length) {
|
|
321
|
-
// Now also include the userIds of the members
|
|
322
|
-
const userMemberIds = (await MemberUser.select().where('membersId', memberIds).fetch()).map(m => m.usersId);
|
|
323
|
-
userIds.push(...userMemberIds);
|
|
324
|
-
}
|
|
325
|
-
userIds = Formatter.uniqueArray(userIds);
|
|
326
|
-
|
|
327
|
-
await CachedBalance.updateForUsers(organizationId, userIds);
|
|
328
|
-
|
|
329
|
-
const organizationIds = Formatter.uniqueArray(filteredItems.map(p => p.payingOrganizationId).filter(id => id !== null));
|
|
330
|
-
await CachedBalance.updateForOrganizations(organizationId, organizationIds);
|
|
331
|
-
|
|
332
|
-
const registrationIds: string[] = Formatter.uniqueArray(filteredItems.map(p => p.registrationId).filter(id => id !== null));
|
|
333
|
-
await CachedBalance.updateForRegistrations(organizationId, registrationIds);
|
|
334
|
-
|
|
335
|
-
if (registrationIds.length) {
|
|
336
|
-
await Document.updateForRegistrations(registrationIds, organizationId);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
326
|
/**
|
|
342
327
|
* Update the outstanding balance of multiple members in one go (or all members)
|
|
343
328
|
*/
|
|
@@ -229,6 +229,10 @@ export class CachedBalance extends QueryableModel {
|
|
|
229
229
|
.where('dueAt', SQLWhereSign.Greater, dueOffset)
|
|
230
230
|
.groupBy(SQL.column(columnName));
|
|
231
231
|
|
|
232
|
+
if (customWhere) {
|
|
233
|
+
dueQuery.where(customWhere);
|
|
234
|
+
}
|
|
235
|
+
|
|
232
236
|
const dueResult = await dueQuery.fetch();
|
|
233
237
|
|
|
234
238
|
const results: [string, { amountPaid: number; amountOpen: number; amountPending: number; nextDueAt: Date | null }][] = [];
|
package/src/models/Document.ts
CHANGED
|
@@ -74,12 +74,20 @@ export class Document extends QueryableModel {
|
|
|
74
74
|
name: this.data.name,
|
|
75
75
|
number: this.number,
|
|
76
76
|
created_at: this.createdAt,
|
|
77
|
+
organization: {
|
|
78
|
+
name: organization.name,
|
|
79
|
+
companyName: organization.meta.companies[0]?.name || organization.name,
|
|
80
|
+
companyNumber: organization.meta.companies[0]?.companyNumber || null,
|
|
81
|
+
address: organization.address,
|
|
82
|
+
companyAddress: organization.meta.companies[0]?.address ?? organization.address,
|
|
83
|
+
},
|
|
77
84
|
};
|
|
78
85
|
const platformLogo = Platform.shared.config.logoDocuments ?? Platform.shared.config.horizontalLogo ?? Platform.shared.config.squareLogo;
|
|
79
86
|
const organizationLogo = organization.meta.horizontalLogo ?? organization.meta.squareLogo;
|
|
80
87
|
|
|
81
88
|
if (organizationLogo) {
|
|
82
89
|
data['organization'] = {
|
|
90
|
+
...data['organization'],
|
|
83
91
|
logo: organizationLogo.encode({ version: Version }) ?? null,
|
|
84
92
|
};
|
|
85
93
|
}
|
|
@@ -147,7 +155,7 @@ export class Document extends QueryableModel {
|
|
|
147
155
|
if (member) {
|
|
148
156
|
const organizationIds = Formatter.uniqueArray(member.registrations.map(r => r.organizationId));
|
|
149
157
|
for (const organizationId of organizationIds) {
|
|
150
|
-
await this.updateForRegistrations(member.registrations.filter(r => r.organizationId === organizationId).map(r => r.id), organizationId);
|
|
158
|
+
await this.updateForRegistrations(member.registrations.filter(r => r.registeredAt && r.deactivatedAt === null && r.organizationId === organizationId).map(r => r.id), organizationId);
|
|
151
159
|
}
|
|
152
160
|
}
|
|
153
161
|
}
|
package/src/models/Email.test.ts
CHANGED
|
@@ -74,8 +74,8 @@ describe('Model.Email', () => {
|
|
|
74
74
|
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
75
75
|
expect(model.recipientCount).toBe(2);
|
|
76
76
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
77
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
78
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0); // never tried to send any failed emails (whitelist)
|
|
77
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
78
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0); // never tried to send any failed emails (whitelist)
|
|
79
79
|
|
|
80
80
|
// Load recipietns
|
|
81
81
|
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
@@ -128,8 +128,8 @@ describe('Model.Email', () => {
|
|
|
128
128
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
129
129
|
|
|
130
130
|
// Both have succeeded
|
|
131
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(2);
|
|
132
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(1); // One retry
|
|
131
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(2);
|
|
132
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(1); // One retry
|
|
133
133
|
|
|
134
134
|
// Load recipietns
|
|
135
135
|
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
@@ -181,8 +181,8 @@ describe('Model.Email', () => {
|
|
|
181
181
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
182
182
|
|
|
183
183
|
// Both have succeeded
|
|
184
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(0);
|
|
185
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(6); // Two retries for each recipient
|
|
184
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(0);
|
|
185
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(6); // Two retries for each recipient
|
|
186
186
|
|
|
187
187
|
// Load recipietns
|
|
188
188
|
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
@@ -222,8 +222,8 @@ describe('Model.Email', () => {
|
|
|
222
222
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
223
223
|
|
|
224
224
|
// Both have succeeded
|
|
225
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
226
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
225
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
226
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
227
227
|
|
|
228
228
|
// Load recipietns
|
|
229
229
|
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
@@ -236,7 +236,7 @@ describe('Model.Email', () => {
|
|
|
236
236
|
]);
|
|
237
237
|
|
|
238
238
|
// Check to header
|
|
239
|
-
expect(EmailMocker.broadcast.getSucceededEmail(0).to).toEqual('example@domain.be');
|
|
239
|
+
expect(await EmailMocker.broadcast.getSucceededEmail(0).to).toEqual('example@domain.be');
|
|
240
240
|
}, 15_000);
|
|
241
241
|
|
|
242
242
|
it('Includes recipient names in mail header', async () => {
|
|
@@ -259,8 +259,8 @@ describe('Model.Email', () => {
|
|
|
259
259
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
260
260
|
|
|
261
261
|
// Both have succeeded
|
|
262
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
263
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
262
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
263
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
264
264
|
|
|
265
265
|
// Load recipietns
|
|
266
266
|
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
@@ -275,7 +275,7 @@ describe('Model.Email', () => {
|
|
|
275
275
|
]);
|
|
276
276
|
|
|
277
277
|
// Check to header
|
|
278
|
-
expect(EmailMocker.broadcast.getSucceededEmail(0).to).toEqual('"John Von Doe" <example@domain.be>');
|
|
278
|
+
expect(await EmailMocker.broadcast.getSucceededEmail(0).to).toEqual('"John Von Doe" <example@domain.be>');
|
|
279
279
|
}, 15_000);
|
|
280
280
|
|
|
281
281
|
it('Skips invalid email addresses', async () => {
|
|
@@ -298,8 +298,8 @@ describe('Model.Email', () => {
|
|
|
298
298
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
299
299
|
|
|
300
300
|
// Both have succeeded
|
|
301
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(0);
|
|
302
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
301
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(0);
|
|
302
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
303
303
|
|
|
304
304
|
// Load recipietns
|
|
305
305
|
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
@@ -341,8 +341,8 @@ describe('Model.Email', () => {
|
|
|
341
341
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
342
342
|
|
|
343
343
|
// Both have succeeded
|
|
344
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
345
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
344
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
345
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
346
346
|
|
|
347
347
|
// Load recipietns
|
|
348
348
|
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
@@ -384,11 +384,11 @@ describe('Model.Email', () => {
|
|
|
384
384
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
385
385
|
|
|
386
386
|
// Both have succeeded
|
|
387
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
388
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
387
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
388
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
389
389
|
|
|
390
390
|
// Check to header
|
|
391
|
-
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
391
|
+
expect(await EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
392
392
|
to: 'example@domain.be',
|
|
393
393
|
from: '"My Platform" <info@my-platform.com>',
|
|
394
394
|
replyTo: undefined,
|
|
@@ -425,11 +425,11 @@ describe('Model.Email', () => {
|
|
|
425
425
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
426
426
|
|
|
427
427
|
// Both have succeeded
|
|
428
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
429
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
428
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
429
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
430
430
|
|
|
431
431
|
// Check to header
|
|
432
|
-
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
432
|
+
expect(await EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
433
433
|
to: 'example@domain.be',
|
|
434
434
|
from: '"My Platform" <noreply@broadcast.my-platform.com>', // domain has changed here
|
|
435
435
|
replyTo: '"My Platform" <info@other-platform.com>', // Reply to should be set
|
|
@@ -471,11 +471,11 @@ describe('Model.Email', () => {
|
|
|
471
471
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
472
472
|
|
|
473
473
|
// Both have succeeded
|
|
474
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
475
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
474
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
475
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
476
476
|
|
|
477
477
|
// Check to header
|
|
478
|
-
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
478
|
+
expect(await EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
479
479
|
to: 'example@domain.be',
|
|
480
480
|
from: '"My Platform" <noreply-uritest@broadcast.my-platform.com>', // domain has changed here
|
|
481
481
|
replyTo: '"My Platform" <info@my-platform.com>', // Reply to should be set
|
|
@@ -519,11 +519,11 @@ describe('Model.Email', () => {
|
|
|
519
519
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
520
520
|
|
|
521
521
|
// Both have succeeded
|
|
522
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
523
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
522
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
523
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
524
524
|
|
|
525
525
|
// Check to header
|
|
526
|
-
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
526
|
+
expect(await EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
527
527
|
to: 'example@domain.be',
|
|
528
528
|
from: '"My Platform" <info@my-platform.com>', // domain has changed here
|
|
529
529
|
replyTo: undefined,
|
|
@@ -567,11 +567,11 @@ describe('Model.Email', () => {
|
|
|
567
567
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
568
568
|
|
|
569
569
|
// Both have succeeded
|
|
570
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
571
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
570
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
571
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
572
572
|
|
|
573
573
|
// Check to header
|
|
574
|
-
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
574
|
+
expect(await EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
575
575
|
to: 'example@domain.be',
|
|
576
576
|
from: '"Custom Name" <noreply-' + organization.uri + '@broadcast.my-platform.com>',
|
|
577
577
|
replyTo: '"Custom Name" <custom@customdomain.com>', // domain has changed here
|
|
@@ -623,11 +623,11 @@ describe('Model.Email', () => {
|
|
|
623
623
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
624
624
|
|
|
625
625
|
// Both have succeeded
|
|
626
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
627
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
626
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
627
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
628
628
|
|
|
629
629
|
// Check to header
|
|
630
|
-
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
630
|
+
expect(await EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
631
631
|
subject: `${brightYellow};${expectedContrastColor};${organization.name};${organization.name}`,
|
|
632
632
|
html: `${brightYellow};${expectedContrastColor};${organization.name};${organization.name}`,
|
|
633
633
|
text: `${brightYellow};${expectedContrastColor};${organization.name};${organization.name}`,
|
|
@@ -681,11 +681,11 @@ describe('Model.Email', () => {
|
|
|
681
681
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
682
682
|
|
|
683
683
|
// Both have succeeded
|
|
684
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
685
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
684
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
685
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
686
686
|
|
|
687
687
|
// Check to header
|
|
688
|
-
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
688
|
+
expect(await EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
689
689
|
subject: `${brightBlue};${expectedContrastColor};${organization.name};Custom Name`,
|
|
690
690
|
html: `${brightBlue};${expectedContrastColor};${organization.name};Custom Name`,
|
|
691
691
|
text: `${brightBlue};${expectedContrastColor};${organization.name};Custom Name`,
|
|
@@ -731,11 +731,11 @@ describe('Model.Email', () => {
|
|
|
731
731
|
expect(model.status).toBe(EmailStatus.Sent);
|
|
732
732
|
|
|
733
733
|
// Both have succeeded
|
|
734
|
-
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
735
|
-
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
734
|
+
expect(await EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
735
|
+
expect(await EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
736
736
|
|
|
737
737
|
// Check to header
|
|
738
|
-
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
738
|
+
expect(await EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
739
739
|
subject: `${darkRed};${expectedContrastColor};${platform.config.name};${platform.config.name}`,
|
|
740
740
|
html: `${darkRed};${expectedContrastColor};${platform.config.name};${platform.config.name}`,
|
|
741
741
|
text: `${darkRed};${expectedContrastColor};${platform.config.name};${platform.config.name}`,
|
package/src/models/Email.ts
CHANGED
|
@@ -240,7 +240,7 @@ export class Email extends QueryableModel {
|
|
|
240
240
|
await this.save();
|
|
241
241
|
|
|
242
242
|
const id = this.id;
|
|
243
|
-
return await QueueHandler.schedule('send-email', async function (this: unknown) {
|
|
243
|
+
return await QueueHandler.schedule('send-email', async function (this: unknown, { abort }) {
|
|
244
244
|
let upToDate = await Email.getByID(id);
|
|
245
245
|
if (!upToDate) {
|
|
246
246
|
throw new SimpleError({
|
|
@@ -291,12 +291,14 @@ export class Email extends QueryableModel {
|
|
|
291
291
|
replyTo = null;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
abort.throwIfAborted();
|
|
294
295
|
upToDate.status = EmailStatus.Sending;
|
|
295
296
|
upToDate.sentAt = upToDate.sentAt ?? new Date();
|
|
296
297
|
await upToDate.save();
|
|
297
298
|
|
|
298
299
|
// Create recipients if not yet created
|
|
299
300
|
await upToDate.buildRecipients();
|
|
301
|
+
abort.throwIfAborted();
|
|
300
302
|
|
|
301
303
|
// Refresh model
|
|
302
304
|
upToDate = await Email.getByID(id);
|
|
@@ -343,6 +345,7 @@ export class Email extends QueryableModel {
|
|
|
343
345
|
});
|
|
344
346
|
|
|
345
347
|
while (true) {
|
|
348
|
+
abort.throwIfAborted();
|
|
346
349
|
const data = await SQL.select()
|
|
347
350
|
.from('email_recipients')
|
|
348
351
|
.where('emailId', upToDate.id)
|
|
@@ -373,7 +376,6 @@ export class Email extends QueryableModel {
|
|
|
373
376
|
const promise = new Promise<void>((resolve) => {
|
|
374
377
|
promiseResolve = resolve;
|
|
375
378
|
});
|
|
376
|
-
sendingPromises.push(promise);
|
|
377
379
|
|
|
378
380
|
const virtualRecipient = recipient.getRecipient();
|
|
379
381
|
|
|
@@ -423,8 +425,9 @@ export class Email extends QueryableModel {
|
|
|
423
425
|
callback(error).catch(console.error);
|
|
424
426
|
},
|
|
425
427
|
});
|
|
426
|
-
|
|
428
|
+
abort.throwIfAborted(); // do not schedule if aborted
|
|
427
429
|
EmailClass.schedule(builder);
|
|
430
|
+
sendingPromises.push(promise);
|
|
428
431
|
}
|
|
429
432
|
|
|
430
433
|
if (sendingPromises.length > 0) {
|
|
@@ -507,7 +510,7 @@ export class Email extends QueryableModel {
|
|
|
507
510
|
|
|
508
511
|
async buildRecipients() {
|
|
509
512
|
const id = this.id;
|
|
510
|
-
await QueueHandler.schedule('email-build-recipients-' + this.id, async function () {
|
|
513
|
+
await QueueHandler.schedule('email-build-recipients-' + this.id, async function ({ abort }) {
|
|
511
514
|
const upToDate = await Email.getByID(id);
|
|
512
515
|
|
|
513
516
|
if (!upToDate || !upToDate.id) {
|
|
@@ -522,6 +525,8 @@ export class Email extends QueryableModel {
|
|
|
522
525
|
return;
|
|
523
526
|
}
|
|
524
527
|
|
|
528
|
+
abort.throwIfAborted();
|
|
529
|
+
|
|
525
530
|
// If it is already creating -> something went wrong (e.g. server restart) and we can safely try again
|
|
526
531
|
|
|
527
532
|
upToDate.recipientsStatus = EmailRecipientsStatus.Creating;
|
|
@@ -538,6 +543,8 @@ export class Email extends QueryableModel {
|
|
|
538
543
|
)
|
|
539
544
|
.where(SQL.column('emailId'), upToDate.id);
|
|
540
545
|
|
|
546
|
+
abort.throwIfAborted();
|
|
547
|
+
|
|
541
548
|
for (const subfilter of upToDate.recipientFilter.filters) {
|
|
542
549
|
// Create recipients
|
|
543
550
|
const loader = Email.recipientLoaders.get(subfilter.type);
|
|
@@ -554,6 +561,7 @@ export class Email extends QueryableModel {
|
|
|
554
561
|
});
|
|
555
562
|
|
|
556
563
|
while (request) {
|
|
564
|
+
abort.throwIfAborted();
|
|
557
565
|
const response = await loader.fetch(request, subfilter.subfilter);
|
|
558
566
|
|
|
559
567
|
for (const item of response.results) {
|
package/src/models/Group.ts
CHANGED
|
@@ -97,13 +97,21 @@ export class Group extends QueryableModel {
|
|
|
97
97
|
@column({ type: 'json', decoder: new ArrayDecoder(StockReservation) })
|
|
98
98
|
stockReservations: StockReservation[] = [];
|
|
99
99
|
|
|
100
|
-
static async getAll(organizationId: string, periodId: string | null, active = true) {
|
|
101
|
-
const
|
|
100
|
+
static async getAll(organizationId: string, periodId: string | null, active = true, types: GroupType[] = [GroupType.Membership]): Promise<Group[]> {
|
|
101
|
+
const query = Group.select()
|
|
102
|
+
.where('organizationId', organizationId);
|
|
103
|
+
|
|
102
104
|
if (active) {
|
|
103
|
-
|
|
105
|
+
query.andWhere('deletedAt', null);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (periodId) {
|
|
109
|
+
query.andWhere('periodId', periodId);
|
|
104
110
|
}
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
query.andWhere('type', types);
|
|
113
|
+
|
|
114
|
+
return await query.fetch();
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
/**
|
package/src/models/Image.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; // ES Modules import
|
|
1
2
|
import { column } from '@simonbackx/simple-database';
|
|
2
3
|
import { ArrayDecoder } from '@simonbackx/simple-encoding';
|
|
3
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
5
|
import { QueryableModel } from '@stamhoofd/sql';
|
|
5
6
|
import { File, Resolution, ResolutionRequest } from '@stamhoofd/structures';
|
|
6
|
-
import AWS from 'aws-sdk';
|
|
7
7
|
import sharp from 'sharp';
|
|
8
8
|
import { v4 as uuidv4 } from 'uuid';
|
|
9
9
|
|
|
@@ -24,6 +24,23 @@ export class Image extends QueryableModel {
|
|
|
24
24
|
@column({ type: 'datetime' })
|
|
25
25
|
createdAt: Date = new Date();
|
|
26
26
|
|
|
27
|
+
static s3Client: S3Client | null = null;
|
|
28
|
+
|
|
29
|
+
static getS3Client(): S3Client {
|
|
30
|
+
if (!this.s3Client) {
|
|
31
|
+
this.s3Client = new S3Client({
|
|
32
|
+
forcePathStyle: false, // Configures to use subdomain/virtual calling format.
|
|
33
|
+
endpoint: 'https://' + STAMHOOFD.SPACES_ENDPOINT,
|
|
34
|
+
credentials: {
|
|
35
|
+
accessKeyId: STAMHOOFD.SPACES_KEY,
|
|
36
|
+
secretAccessKey: STAMHOOFD.SPACES_SECRET,
|
|
37
|
+
},
|
|
38
|
+
region: 'eu-west-1', // Not used, but required by the S3Client
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return this.s3Client;
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
static async create(fileContent: string | Buffer, type: string | undefined, resolutions: ResolutionRequest[], isPrivateFile: boolean = false, user: { id: string } | null = null): Promise<Image> {
|
|
28
45
|
if (!STAMHOOFD.SPACES_BUCKET || !STAMHOOFD.SPACES_ENDPOINT || !STAMHOOFD.SPACES_KEY || !STAMHOOFD.SPACES_SECRET) {
|
|
29
46
|
throw new SimpleError({
|
|
@@ -81,11 +98,7 @@ export class Image extends QueryableModel {
|
|
|
81
98
|
|
|
82
99
|
const files = await Promise.all(promises);
|
|
83
100
|
|
|
84
|
-
const
|
|
85
|
-
endpoint: STAMHOOFD.SPACES_ENDPOINT,
|
|
86
|
-
accessKeyId: STAMHOOFD.SPACES_KEY,
|
|
87
|
-
secretAccessKey: STAMHOOFD.SPACES_SECRET,
|
|
88
|
-
});
|
|
101
|
+
const client = this.getS3Client();
|
|
89
102
|
|
|
90
103
|
let prefix = (STAMHOOFD.SPACES_PREFIX ?? '');
|
|
91
104
|
if (prefix.length > 0) {
|
|
@@ -112,15 +125,17 @@ export class Image extends QueryableModel {
|
|
|
112
125
|
const fileId = uuidv4();
|
|
113
126
|
|
|
114
127
|
const key = prefix + image.id + '/' + fileId + (!supportsTransparency ? '.jpg' : '.png');
|
|
115
|
-
const
|
|
128
|
+
const cmd = new PutObjectCommand({
|
|
116
129
|
Bucket: STAMHOOFD.SPACES_BUCKET,
|
|
117
130
|
Key: key,
|
|
118
131
|
Body: f.data,
|
|
119
132
|
ContentType: !supportsTransparency ? 'image/jpeg' : 'image/png',
|
|
120
133
|
ACL: isPrivateFile ? 'private' : 'public-read',
|
|
121
|
-
};
|
|
134
|
+
});
|
|
122
135
|
|
|
123
|
-
uploadPromises.push(
|
|
136
|
+
uploadPromises.push(
|
|
137
|
+
client.send(cmd),
|
|
138
|
+
);
|
|
124
139
|
|
|
125
140
|
const _file = new File({
|
|
126
141
|
id: fileId,
|
|
@@ -153,14 +168,6 @@ export class Image extends QueryableModel {
|
|
|
153
168
|
const fileId = uuidv4();
|
|
154
169
|
const uploadExt = fileType;
|
|
155
170
|
const key = prefix + (STAMHOOFD.environment ?? 'development') + '/' + image.id + '/' + fileId + '.' + uploadExt;
|
|
156
|
-
const params = {
|
|
157
|
-
Bucket: STAMHOOFD.SPACES_BUCKET,
|
|
158
|
-
Key: key,
|
|
159
|
-
Body: fileContent,
|
|
160
|
-
ContentType: type ?? 'image/jpeg',
|
|
161
|
-
ACL: 'private',
|
|
162
|
-
};
|
|
163
|
-
|
|
164
171
|
image.source = new File({
|
|
165
172
|
id: fileId,
|
|
166
173
|
server: 'https://' + STAMHOOFD.SPACES_BUCKET + '.' + STAMHOOFD.SPACES_ENDPOINT,
|
|
@@ -169,7 +176,14 @@ export class Image extends QueryableModel {
|
|
|
169
176
|
// Don't set private here, as we don't allow to download this file
|
|
170
177
|
});
|
|
171
178
|
|
|
172
|
-
|
|
179
|
+
const cmd = new PutObjectCommand({
|
|
180
|
+
Bucket: STAMHOOFD.SPACES_BUCKET,
|
|
181
|
+
Key: key,
|
|
182
|
+
Body: fileContent,
|
|
183
|
+
ContentType: type ?? 'image/jpeg',
|
|
184
|
+
ACL: 'private',
|
|
185
|
+
});
|
|
186
|
+
uploadPromises.push(client.send(cmd));
|
|
173
187
|
|
|
174
188
|
await Promise.all(uploadPromises);
|
|
175
189
|
await image.save();
|
package/src/models/Member.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { column, Database, ManyToManyRelation, ManyToOneRelation, OneToManyRelation } from '@simonbackx/simple-database';
|
|
2
2
|
import { QueryableModel, SQL } from '@stamhoofd/sql';
|
|
3
|
-
import { MemberDetails, NationalRegisterNumberOptOut,
|
|
3
|
+
import { MemberDetails, NationalRegisterNumberOptOut, RegistrationWithTinyMember, TinyMember } from '@stamhoofd/structures';
|
|
4
4
|
import { Formatter } from '@stamhoofd/utility';
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
6
|
|
|
@@ -342,8 +342,8 @@ export class Member extends QueryableModel {
|
|
|
342
342
|
return this.getBlobByIds(...(await this.getMemberIdsWithRegistrationForUser(user)));
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
static
|
|
346
|
-
return
|
|
345
|
+
static getRegistrationWithTinyMemberStructure(registration: RegistrationWithMember & { group: import('./Group').Group }): RegistrationWithTinyMember {
|
|
346
|
+
return RegistrationWithTinyMember.create({
|
|
347
347
|
...registration.getStructure(),
|
|
348
348
|
cycle: registration.cycle,
|
|
349
349
|
member: TinyMember.create({
|
|
@@ -366,7 +366,7 @@ export class MemberPlatformMembership extends QueryableModel {
|
|
|
366
366
|
balanceItem.unitPrice = this.price;
|
|
367
367
|
await balanceItem.save();
|
|
368
368
|
|
|
369
|
-
await BalanceItem.updateOutstanding([balanceItem]);
|
|
369
|
+
// await BalanceItem.updateOutstanding([balanceItem]);
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
}
|