@stamhoofd/models 2.83.5 → 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.
Files changed (108) hide show
  1. package/dist/src/factories/BalanceItemFactory.d.ts +3 -1
  2. package/dist/src/factories/BalanceItemFactory.d.ts.map +1 -1
  3. package/dist/src/factories/BalanceItemFactory.js +6 -0
  4. package/dist/src/factories/BalanceItemFactory.js.map +1 -1
  5. package/dist/src/factories/DocumentTemplateFactory.d.ts +15 -0
  6. package/dist/src/factories/DocumentTemplateFactory.d.ts.map +1 -0
  7. package/dist/src/factories/DocumentTemplateFactory.js +125 -0
  8. package/dist/src/factories/DocumentTemplateFactory.js.map +1 -0
  9. package/dist/src/factories/EventFactory.d.ts +2 -0
  10. package/dist/src/factories/EventFactory.d.ts.map +1 -1
  11. package/dist/src/factories/EventFactory.js +5 -0
  12. package/dist/src/factories/EventFactory.js.map +1 -1
  13. package/dist/src/factories/GroupFactory.d.ts +7 -2
  14. package/dist/src/factories/GroupFactory.d.ts.map +1 -1
  15. package/dist/src/factories/GroupFactory.js +30 -5
  16. package/dist/src/factories/GroupFactory.js.map +1 -1
  17. package/dist/src/factories/MemberFactory.d.ts.map +1 -1
  18. package/dist/src/factories/MemberFactory.js +3 -1
  19. package/dist/src/factories/MemberFactory.js.map +1 -1
  20. package/dist/src/factories/RegistrationFactory.d.ts +3 -1
  21. package/dist/src/factories/RegistrationFactory.d.ts.map +1 -1
  22. package/dist/src/factories/RegistrationFactory.js +2 -0
  23. package/dist/src/factories/RegistrationFactory.js.map +1 -1
  24. package/dist/src/factories/UserFactory.js +1 -1
  25. package/dist/src/factories/UserFactory.js.map +1 -1
  26. package/dist/src/factories/index.d.ts +1 -0
  27. package/dist/src/factories/index.d.ts.map +1 -1
  28. package/dist/src/factories/index.js +1 -0
  29. package/dist/src/factories/index.js.map +1 -1
  30. package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
  31. package/dist/src/helpers/EmailBuilder.js +3 -1
  32. package/dist/src/helpers/EmailBuilder.js.map +1 -1
  33. package/dist/src/migrations/1747913433-registration-discounts.sql +2 -0
  34. package/dist/src/migrations/1747996262-balance-items-paid-at.sql +2 -0
  35. package/dist/src/migrations/1747996263-balance-items-paid-at-fill.sql +1 -0
  36. package/dist/src/models/BalanceItem.d.ts +9 -8
  37. package/dist/src/models/BalanceItem.d.ts.map +1 -1
  38. package/dist/src/models/BalanceItem.js +36 -42
  39. package/dist/src/models/BalanceItem.js.map +1 -1
  40. package/dist/src/models/CachedBalance.d.ts.map +1 -1
  41. package/dist/src/models/CachedBalance.js +3 -0
  42. package/dist/src/models/CachedBalance.js.map +1 -1
  43. package/dist/src/models/Document.d.ts.map +1 -1
  44. package/dist/src/models/Document.js +9 -1
  45. package/dist/src/models/Document.js.map +1 -1
  46. package/dist/src/models/Email.d.ts.map +1 -1
  47. package/dist/src/models/Email.js +10 -3
  48. package/dist/src/models/Email.js.map +1 -1
  49. package/dist/src/models/Email.test.js +40 -40
  50. package/dist/src/models/Email.test.js.map +1 -1
  51. package/dist/src/models/Group.d.ts +1 -1
  52. package/dist/src/models/Group.d.ts.map +1 -1
  53. package/dist/src/models/Group.js +9 -4
  54. package/dist/src/models/Group.js.map +1 -1
  55. package/dist/src/models/Image.d.ts +3 -0
  56. package/dist/src/models/Image.d.ts.map +1 -1
  57. package/dist/src/models/Image.js +28 -17
  58. package/dist/src/models/Image.js.map +1 -1
  59. package/dist/src/models/Member.d.ts +3 -3
  60. package/dist/src/models/Member.d.ts.map +1 -1
  61. package/dist/src/models/Member.js +2 -2
  62. package/dist/src/models/Member.js.map +1 -1
  63. package/dist/src/models/MemberPlatformMembership.js +1 -1
  64. package/dist/src/models/MemberPlatformMembership.js.map +1 -1
  65. package/dist/src/models/Organization.d.ts.map +1 -1
  66. package/dist/src/models/Organization.js +26 -24
  67. package/dist/src/models/Organization.js.map +1 -1
  68. package/dist/src/models/PayconiqPayment.d.ts.map +1 -1
  69. package/dist/src/models/PayconiqPayment.js +6 -1
  70. package/dist/src/models/PayconiqPayment.js.map +1 -1
  71. package/dist/src/models/Platform.d.ts.map +1 -1
  72. package/dist/src/models/Platform.js +5 -3
  73. package/dist/src/models/Platform.js.map +1 -1
  74. package/dist/src/models/Registration.d.ts +11 -1
  75. package/dist/src/models/Registration.d.ts.map +1 -1
  76. package/dist/src/models/Registration.js +14 -1
  77. package/dist/src/models/Registration.js.map +1 -1
  78. package/dist/src/models/User.d.ts.map +1 -1
  79. package/dist/src/models/User.js +1 -1
  80. package/dist/src/models/User.js.map +1 -1
  81. package/dist/tsconfig.tsbuildinfo +1 -1
  82. package/package.json +5 -5
  83. package/src/factories/BalanceItemFactory.ts +7 -1
  84. package/src/factories/DocumentTemplateFactory.ts +130 -0
  85. package/src/factories/EventFactory.ts +7 -0
  86. package/src/factories/GroupFactory.ts +35 -8
  87. package/src/factories/MemberFactory.ts +3 -1
  88. package/src/factories/RegistrationFactory.ts +9 -2
  89. package/src/factories/UserFactory.ts +1 -1
  90. package/src/factories/index.ts +1 -0
  91. package/src/helpers/EmailBuilder.ts +3 -1
  92. package/src/migrations/1747913433-registration-discounts.sql +2 -0
  93. package/src/migrations/1747996262-balance-items-paid-at.sql +2 -0
  94. package/src/migrations/1747996263-balance-items-paid-at-fill.sql +1 -0
  95. package/src/models/BalanceItem.ts +39 -54
  96. package/src/models/CachedBalance.ts +4 -0
  97. package/src/models/Document.ts +9 -1
  98. package/src/models/Email.test.ts +40 -40
  99. package/src/models/Email.ts +12 -4
  100. package/src/models/Group.ts +12 -4
  101. package/src/models/Image.ts +32 -18
  102. package/src/models/Member.ts +3 -3
  103. package/src/models/MemberPlatformMembership.ts +1 -1
  104. package/src/models/Organization.ts +35 -27
  105. package/src/models/PayconiqPayment.ts +7 -1
  106. package/src/models/Platform.ts +6 -3
  107. package/src/models/Registration.ts +14 -2
  108. 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 > 0) {
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
- await this.updateOutstanding(deletedItems);
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
- await this.updateOutstanding(items);
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.where({ registrationId });
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 }][] = [];
@@ -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
  }
@@ -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}`,
@@ -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) {
@@ -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 w: any = periodId ? { periodId } : {};
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
- return await Group.where({ organizationId, deletedAt: null, ...w });
105
+ query.andWhere('deletedAt', null);
106
+ }
107
+
108
+ if (periodId) {
109
+ query.andWhere('periodId', periodId);
104
110
  }
105
111
 
106
- return await Group.where({ organizationId, ...w });
112
+ query.andWhere('type', types);
113
+
114
+ return await query.fetch();
107
115
  }
108
116
 
109
117
  /**
@@ -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 s3 = new AWS.S3({
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 params = {
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(s3.putObject(params).promise());
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
- uploadPromises.push(s3.putObject(params).promise());
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();
@@ -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, RegistrationWithMember as RegistrationWithMemberStruct, TinyMember } from '@stamhoofd/structures';
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 getRegistrationWithMemberStructure(registration: RegistrationWithMember & { group: import('./Group').Group }): RegistrationWithMemberStruct {
346
- return RegistrationWithMemberStruct.create({
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
  }