@stamhoofd/models 2.63.0 → 2.64.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 (63) hide show
  1. package/dist/src/migrations/1605262045-import-postcodes.d.ts +1 -1
  2. package/dist/src/migrations/1605262045-import-postcodes.js +1 -4
  3. package/dist/src/migrations/1734429094-registration-trial-until.sql +3 -0
  4. package/dist/src/migrations/1734429095-membership-trial-until.sql +2 -0
  5. package/dist/src/migrations/1734535120-registration-period-previous-period-id.sql +3 -0
  6. package/dist/src/migrations/1734535121-platform-previous-period-id.sql +3 -0
  7. package/dist/src/migrations/1734626607-cached-balance-amount-open.sql +2 -0
  8. package/dist/src/migrations/1734698906-cached-balance-amount-paid.sql +2 -0
  9. package/dist/src/models/BalanceItem.d.ts +2 -2
  10. package/dist/src/models/BalanceItem.d.ts.map +1 -1
  11. package/dist/src/models/BalanceItem.js +13 -11
  12. package/dist/src/models/BalanceItem.js.map +1 -1
  13. package/dist/src/models/BalanceItemPayment.d.ts +5 -0
  14. package/dist/src/models/BalanceItemPayment.d.ts.map +1 -1
  15. package/dist/src/models/BalanceItemPayment.js +15 -0
  16. package/dist/src/models/BalanceItemPayment.js.map +1 -1
  17. package/dist/src/models/CachedBalance.d.ts +6 -2
  18. package/dist/src/models/CachedBalance.d.ts.map +1 -1
  19. package/dist/src/models/CachedBalance.js +64 -20
  20. package/dist/src/models/CachedBalance.js.map +1 -1
  21. package/dist/src/models/DocumentTemplate.d.ts.map +1 -1
  22. package/dist/src/models/DocumentTemplate.js +36 -9
  23. package/dist/src/models/DocumentTemplate.js.map +1 -1
  24. package/dist/src/models/Member.d.ts +3 -7
  25. package/dist/src/models/Member.d.ts.map +1 -1
  26. package/dist/src/models/Member.js +2 -41
  27. package/dist/src/models/Member.js.map +1 -1
  28. package/dist/src/models/MemberPlatformMembership.d.ts +10 -1
  29. package/dist/src/models/MemberPlatformMembership.d.ts.map +1 -1
  30. package/dist/src/models/MemberPlatformMembership.js +79 -5
  31. package/dist/src/models/MemberPlatformMembership.js.map +1 -1
  32. package/dist/src/models/MergedMember.d.ts +1 -1
  33. package/dist/src/models/MergedMember.js +1 -1
  34. package/dist/src/models/Platform.d.ts +2 -0
  35. package/dist/src/models/Platform.d.ts.map +1 -1
  36. package/dist/src/models/Platform.js +9 -1
  37. package/dist/src/models/Platform.js.map +1 -1
  38. package/dist/src/models/Registration.d.ts +17 -5
  39. package/dist/src/models/Registration.d.ts.map +1 -1
  40. package/dist/src/models/Registration.js +23 -34
  41. package/dist/src/models/Registration.js.map +1 -1
  42. package/dist/src/models/RegistrationPeriod.d.ts +7 -0
  43. package/dist/src/models/RegistrationPeriod.d.ts.map +1 -1
  44. package/dist/src/models/RegistrationPeriod.js +36 -0
  45. package/dist/src/models/RegistrationPeriod.js.map +1 -1
  46. package/package.json +2 -2
  47. package/src/migrations/1605262045-import-postcodes.ts +2 -4
  48. package/src/migrations/1734429094-registration-trial-until.sql +3 -0
  49. package/src/migrations/1734429095-membership-trial-until.sql +2 -0
  50. package/src/migrations/1734535120-registration-period-previous-period-id.sql +3 -0
  51. package/src/migrations/1734535121-platform-previous-period-id.sql +3 -0
  52. package/src/migrations/1734626607-cached-balance-amount-open.sql +2 -0
  53. package/src/migrations/1734698906-cached-balance-amount-paid.sql +2 -0
  54. package/src/models/BalanceItem.ts +18 -17
  55. package/src/models/BalanceItemPayment.ts +20 -1
  56. package/src/models/CachedBalance.ts +88 -23
  57. package/src/models/DocumentTemplate.ts +39 -9
  58. package/src/models/Member.ts +2 -48
  59. package/src/models/MemberPlatformMembership.ts +92 -5
  60. package/src/models/MergedMember.ts +1 -1
  61. package/src/models/Platform.ts +9 -1
  62. package/src/models/Registration.ts +21 -40
  63. package/src/models/RegistrationPeriod.ts +47 -2
@@ -1,6 +1,6 @@
1
1
  import { column, Model, SQLResultNamespacedRow } from '@simonbackx/simple-database';
2
- import { SQL, SQLAlias, SQLCalculation, SQLGreatest, SQLMin, SQLMinusSign, SQLMultiplicationSign, SQLSelect, SQLSelectAs, SQLSum, SQLWhere, SQLWhereSign } from '@stamhoofd/sql';
3
- import { BalanceItem as BalanceItemStruct, BalanceItemStatus, ReceivableBalanceType } from '@stamhoofd/structures';
2
+ import { SQL, SQLAlias, SQLMin, SQLSelect, SQLSelectAs, SQLSum, SQLWhere, SQLWhereSign } from '@stamhoofd/sql';
3
+ import { BalanceItemStatus, BalanceItem as BalanceItemStruct, ReceivableBalanceType } from '@stamhoofd/structures';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
  import { BalanceItem } from './BalanceItem';
6
6
 
@@ -33,7 +33,10 @@ export class CachedBalance extends Model {
33
33
  objectType: ReceivableBalanceType;
34
34
 
35
35
  @column({ type: 'integer' })
36
- amount = 0;
36
+ amountPaid = 0;
37
+
38
+ @column({ type: 'integer' })
39
+ amountOpen = 0;
37
40
 
38
41
  /**
39
42
  * The sum of unconfirmed payments
@@ -70,6 +73,10 @@ export class CachedBalance extends Model {
70
73
  updatedAt: Date;
71
74
 
72
75
  static async getForObjects(objectIds: string[], limitOrganizationId?: string | null): Promise<CachedBalance[]> {
76
+ if (objectIds.length === 0) {
77
+ return [];
78
+ }
79
+
73
80
  const query = this.select()
74
81
  .where('objectId', objectIds);
75
82
 
@@ -81,6 +88,10 @@ export class CachedBalance extends Model {
81
88
  }
82
89
 
83
90
  static async updateForObjects(organizationId: string, objectIds: string[], objectType: ReceivableBalanceType) {
91
+ if (objectIds.length === 0) {
92
+ return;
93
+ }
94
+
84
95
  switch (objectType) {
85
96
  case ReceivableBalanceType.organization:
86
97
  await this.updateForOrganizations(organizationId, objectIds);
@@ -91,10 +102,17 @@ export class CachedBalance extends Model {
91
102
  case ReceivableBalanceType.user:
92
103
  await this.updateForUsers(organizationId, objectIds);
93
104
  break;
105
+ case ReceivableBalanceType.registration:
106
+ await this.updateForRegistrations(organizationId, objectIds);
107
+ break;
94
108
  }
95
109
  }
96
110
 
97
111
  static async balanceForObjects(organizationId: string, objectIds: string[], objectType: ReceivableBalanceType) {
112
+ if (objectIds.length === 0) {
113
+ return [];
114
+ }
115
+
98
116
  switch (objectType) {
99
117
  case ReceivableBalanceType.organization:
100
118
  return await this.balanceForOrganizations(organizationId, objectIds);
@@ -102,6 +120,8 @@ export class CachedBalance extends Model {
102
120
  return await this.balanceForMembers(organizationId, objectIds);
103
121
  case ReceivableBalanceType.user:
104
122
  return await this.balanceForUsers(organizationId, objectIds);
123
+ case ReceivableBalanceType.registration:
124
+ return await this.balanceForRegistrations(organizationId, objectIds);
105
125
  }
106
126
  }
107
127
 
@@ -122,15 +142,25 @@ export class CachedBalance extends Model {
122
142
  return await query.fetch();
123
143
  }
124
144
 
145
+ static whereNeedsUpdate() {
146
+ return SQL.where('nextDueAt', SQLWhereSign.LessEqual, BalanceItemStruct.getDueOffset());
147
+ }
148
+
125
149
  private static async fetchForObjects(organizationId: string, objectIds: string[], columnName: string, customWhere?: SQLWhere) {
126
150
  const dueOffset = BalanceItemStruct.getDueOffset();
127
151
  const query = SQL.select(
128
152
  SQL.column(columnName),
153
+ new SQLSelectAs(
154
+ new SQLSum(
155
+ SQL.column('pricePaid'),
156
+ ),
157
+ new SQLAlias('data__amountPaid'),
158
+ ),
129
159
  new SQLSelectAs(
130
160
  new SQLSum(
131
161
  SQL.column('priceOpen'),
132
162
  ),
133
- new SQLAlias('data__amount'),
163
+ new SQLAlias('data__amountOpen'),
134
164
  ),
135
165
  new SQLSelectAs(
136
166
  new SQLSum(
@@ -162,11 +192,17 @@ export class CachedBalance extends Model {
162
192
  new SQLAlias('data__dueAt'),
163
193
  ),
164
194
  // If the current amount_due is negative, we can ignore that negative part if there is a future due item
195
+ new SQLSelectAs(
196
+ new SQLSum(
197
+ SQL.column('pricePaid'),
198
+ ),
199
+ new SQLAlias('data__amountPaid'),
200
+ ),
165
201
  new SQLSelectAs(
166
202
  new SQLSum(
167
203
  SQL.column('priceOpen'),
168
204
  ),
169
- new SQLAlias('data__amount'),
205
+ new SQLAlias('data__amountOpen'),
170
206
  ),
171
207
  new SQLSelectAs(
172
208
  new SQLSum(
@@ -185,7 +221,7 @@ export class CachedBalance extends Model {
185
221
 
186
222
  const dueResult = await dueQuery.fetch();
187
223
 
188
- const results: [string, { amount: number; amountPending: number; nextDueAt: Date | null }][] = [];
224
+ const results: [string, { amountPaid: number; amountOpen: number; amountPending: number; nextDueAt: Date | null }][] = [];
189
225
  for (const row of result) {
190
226
  if (!row['data']) {
191
227
  throw new Error('Invalid data namespace');
@@ -196,22 +232,27 @@ export class CachedBalance extends Model {
196
232
  }
197
233
 
198
234
  const objectId = row[BalanceItem.table][columnName];
199
- const amount = row['data']['amount'];
235
+ const amountOpen = row['data']['amountOpen'];
200
236
  const amountPending = row['data']['amountPending'];
237
+ const amountPaid = row['data']['amountPaid'];
201
238
 
202
239
  if (typeof objectId !== 'string') {
203
240
  throw new Error('Invalid objectId');
204
241
  }
205
242
 
206
- if (typeof amount !== 'number') {
207
- throw new Error('Invalid amount');
243
+ if (typeof amountOpen !== 'number') {
244
+ throw new Error('Invalid amountOpen');
208
245
  }
209
246
 
210
247
  if (typeof amountPending !== 'number') {
211
248
  throw new Error('Invalid amountPending');
212
249
  }
213
250
 
214
- results.push([objectId, { amount, amountPending, nextDueAt: null }]);
251
+ if (typeof amountPaid !== 'number') {
252
+ throw new Error('Invalid amountPaid');
253
+ }
254
+
255
+ results.push([objectId, { amountPaid, amountOpen, amountPending, nextDueAt: null }]);
215
256
  }
216
257
 
217
258
  for (const row of dueResult) {
@@ -225,8 +266,9 @@ export class CachedBalance extends Model {
225
266
 
226
267
  const objectId = row[BalanceItem.table][columnName];
227
268
  const dueAt = row['data']['dueAt'];
228
- const amount = row['data']['amount'];
269
+ const amountOpen = row['data']['amountOpen'];
229
270
  const amountPending = row['data']['amountPending'];
271
+ const amountPaid = row['data']['amountPaid'];
230
272
 
231
273
  if (typeof objectId !== 'string') {
232
274
  throw new Error('Invalid objectId');
@@ -236,43 +278,48 @@ export class CachedBalance extends Model {
236
278
  throw new Error('Invalid dueAt');
237
279
  }
238
280
 
239
- if (typeof amount !== 'number') {
240
- throw new Error('Invalid amount');
281
+ if (typeof amountOpen !== 'number') {
282
+ throw new Error('Invalid amountOpen');
241
283
  }
242
284
 
243
285
  if (typeof amountPending !== 'number') {
244
286
  throw new Error('Invalid amountPending');
245
287
  }
246
288
 
289
+ if (typeof amountPaid !== 'number') {
290
+ throw new Error('Invalid amountPaid');
291
+ }
292
+
247
293
  const result = results.find(r => r[0] === objectId);
248
294
  if (result) {
249
295
  result[1].nextDueAt = dueAt;
250
296
 
251
- if (result[1].amount < 0) {
252
- if (amount > 0) {
297
+ if (result[1].amountOpen < 0) {
298
+ if (amountOpen > 0) {
253
299
  // Let the future due amount fill in the gap until maximum 0
254
- result[1].amount = Math.min(0, result[1].amount + amount);
300
+ result[1].amountOpen = Math.min(0, result[1].amountOpen + amountOpen);
255
301
  }
256
302
  }
257
303
 
258
304
  result[1].amountPending += amountPending;
305
+ result[1].amountPaid += amountPaid;
259
306
  }
260
307
  else {
261
- results.push([objectId, { amount: 0, amountPending: amountPending, nextDueAt: dueAt }]);
308
+ results.push([objectId, { amountPaid, amountOpen: 0, amountPending, nextDueAt: dueAt }]);
262
309
  }
263
310
  }
264
311
 
265
312
  // Add missing object ids (with 0 amount, otherwise we don't reset the amounts back to zero when all the balance items are hidden)
266
313
  for (const objectId of objectIds) {
267
314
  if (!results.find(([id]) => id === objectId)) {
268
- results.push([objectId, { amount: 0, amountPending: 0, nextDueAt: null }]);
315
+ results.push([objectId, { amountPaid: 0, amountOpen: 0, amountPending: 0, nextDueAt: null }]);
269
316
  }
270
317
  }
271
318
 
272
319
  return results;
273
320
  }
274
321
 
275
- private static async setForResults(organizationId: string, result: [string, { amount: number; amountPending: number; nextDueAt: null | Date }][], objectType: ReceivableBalanceType) {
322
+ private static async setForResults(organizationId: string, result: [string, { amountPaid: number; amountOpen: number; amountPending: number; nextDueAt: null | Date }][], objectType: ReceivableBalanceType) {
276
323
  if (result.length === 0) {
277
324
  return;
278
325
  }
@@ -282,19 +329,21 @@ export class CachedBalance extends Model {
282
329
  'organizationId',
283
330
  'objectId',
284
331
  'objectType',
285
- 'amount',
332
+ 'amountPaid',
333
+ 'amountOpen',
286
334
  'amountPending',
287
335
  'nextDueAt',
288
336
  'createdAt',
289
337
  'updatedAt',
290
338
  )
291
- .values(...result.map(([objectId, { amount, amountPending, nextDueAt }]) => {
339
+ .values(...result.map(([objectId, { amountPaid, amountOpen, amountPending, nextDueAt }]) => {
292
340
  return [
293
341
  uuidv4(),
294
342
  organizationId,
295
343
  objectId,
296
344
  objectType,
297
- amount,
345
+ amountPaid,
346
+ amountOpen,
298
347
  amountPending,
299
348
  nextDueAt,
300
349
  new Date(),
@@ -303,7 +352,8 @@ export class CachedBalance extends Model {
303
352
  }))
304
353
  .as('v')
305
354
  .onDuplicateKeyUpdate(
306
- SQL.assignment('amount', SQL.column('v', 'amount')),
355
+ SQL.assignment('amountPaid', SQL.column('v', 'amountPaid')),
356
+ SQL.assignment('amountOpen', SQL.column('v', 'amountOpen')),
307
357
  SQL.assignment('amountPending', SQL.column('v', 'amountPending')),
308
358
  SQL.assignment('nextDueAt', SQL.column('v', 'nextDueAt')),
309
359
  SQL.assignment('updatedAt', SQL.column('v', 'updatedAt')),
@@ -328,6 +378,14 @@ export class CachedBalance extends Model {
328
378
  await this.setForResults(organizationId, results, ReceivableBalanceType.member);
329
379
  }
330
380
 
381
+ static async updateForRegistrations(organizationId: string, registrationIds: string[]) {
382
+ if (registrationIds.length === 0) {
383
+ return;
384
+ }
385
+ const results = await this.fetchForObjects(organizationId, registrationIds, 'registrationId');
386
+ await this.setForResults(organizationId, results, ReceivableBalanceType.registration);
387
+ }
388
+
331
389
  static async updateForUsers(organizationId: string, userIds: string[]) {
332
390
  if (userIds.length === 0) {
333
391
  return;
@@ -357,6 +415,13 @@ export class CachedBalance extends Model {
357
415
  return await this.fetchBalanceItems(organizationId, userIds, 'userId', SQL.where('memberId', null));
358
416
  }
359
417
 
418
+ static async balanceForRegistrations(organizationId: string, registrationIds: string[]) {
419
+ if (registrationIds.length === 0) {
420
+ return [];
421
+ }
422
+ return await this.fetchBalanceItems(organizationId, registrationIds, 'registrationId');
423
+ }
424
+
360
425
  /**
361
426
  * Experimental: needs to move to library
362
427
  */
@@ -74,9 +74,11 @@ export class DocumentTemplate extends Model {
74
74
  let missingData = false;
75
75
 
76
76
  const group = await Group.getByID(registration.groupId);
77
- const { items: balanceItems, payments } = await BalanceItem.getForRegistration(registration.id);
77
+ const { items: balanceItems, payments } = await BalanceItem.getForRegistration(registration.id, this.organizationId);
78
78
 
79
79
  const paidAtDates = payments.flatMap(p => p.paidAt ? [p.paidAt?.getTime()] : []);
80
+ const price = balanceItems.reduce((sum, item) => sum + item.price, 0);
81
+ const pricePaid = balanceItems.reduce((sum, item) => sum + item.pricePaid, 0);
80
82
 
81
83
  // We take the minimum date here, because there is a highter change of later paymetns to be for other things than the registration itself
82
84
  const paidAt = paidAtDates.length ? new Date(Math.min(...paidAtDates)) : null;
@@ -102,7 +104,7 @@ export class DocumentTemplate extends Model {
102
104
  id: 'registration.startDate',
103
105
  type: RecordType.Date,
104
106
  }), // settings will be overwritten
105
- dateValue: group?.settings?.startDate,
107
+ dateValue: registration.startDate ?? group?.settings?.startDate,
106
108
  }),
107
109
  'registration.endDate': RecordDateAnswer.create({
108
110
  settings: RecordSettings.create({
@@ -114,16 +116,36 @@ export class DocumentTemplate extends Model {
114
116
  'registration.price':
115
117
  RecordPriceAnswer.create({
116
118
  settings: RecordSettings.create({
119
+ id: 'registration.price',
117
120
  type: RecordType.Price,
118
121
  }), // settings will be overwritten
119
- value: registration.price,
122
+ value: price,
123
+ }),
124
+ // This one is duplicated in case it got disabled (we need to use it to check if document is included)
125
+ 'registration.priceOriginal':
126
+ RecordPriceAnswer.create({
127
+ settings: RecordSettings.create({
128
+ id: 'registration.priceOriginal',
129
+ type: RecordType.Price,
130
+ }), // settings will be overwritten
131
+ value: price,
132
+ }),
133
+ // This one is duplicated in case it got disabled (we need to use it to check if document is included)
134
+ 'registration.pricePaidOriginal':
135
+ RecordPriceAnswer.create({
136
+ settings: RecordSettings.create({
137
+ id: 'registration.pricePaidOriginal',
138
+ type: RecordType.Price,
139
+ }), // settings will be overwritten
140
+ value: pricePaid,
120
141
  }),
121
142
  'registration.pricePaid':
122
143
  RecordPriceAnswer.create({
123
144
  settings: RecordSettings.create({
145
+ id: 'registration.pricePaid',
124
146
  type: RecordType.Price,
125
147
  }), // settings will be overwritten
126
- value: registration.pricePaid,
148
+ value: pricePaid,
127
149
  }),
128
150
  'registration.paidAt':
129
151
  RecordDateAnswer.create({
@@ -483,14 +505,22 @@ export class DocumentTemplate extends Model {
483
505
  }
484
506
  }
485
507
 
486
- if (this.settings.minPrice !== null) {
487
- if ((registration.price ?? 0) < this.settings.minPrice) {
488
- return false;
508
+ if (this.settings.minPrice !== null && this.settings.minPrice > 0) {
509
+ const priceAnswer = fieldAnswers.get('registration.priceOriginal');
510
+ if (priceAnswer && priceAnswer instanceof RecordPriceAnswer) {
511
+ if ((priceAnswer.value ?? 0) < this.settings.minPrice) {
512
+ return false;
513
+ }
489
514
  }
490
515
  }
491
516
 
492
- if (this.settings.minPricePaid !== null) {
493
- if ((registration.pricePaid ?? 0) < this.settings.minPricePaid && (registration.price ?? 0) > 0) {
517
+ if (this.settings.minPricePaid !== null && this.settings.minPricePaid > 0) {
518
+ const pricePaidAnswer = fieldAnswers.get('registration.pricePaidOriginal');
519
+ const priceAnswer = fieldAnswers.get('registration.priceOriginal');
520
+ const price = (priceAnswer instanceof RecordPriceAnswer ? priceAnswer.value : 0) ?? 0;
521
+ const pricePaid = (pricePaidAnswer instanceof RecordPriceAnswer ? pricePaidAnswer.value : 0) ?? 0;
522
+
523
+ if (pricePaid < this.settings.minPricePaid && price > 0) {
494
524
  return false;
495
525
  }
496
526
  }
@@ -63,7 +63,8 @@ export class Member extends Model {
63
63
  details: MemberDetails;
64
64
 
65
65
  /**
66
- * Not yet paid balance
66
+ * @deprecated
67
+ * Unreliable since a member can have outstanding balance to multiple organizations now
67
68
  */
68
69
  @column({ type: 'integer' })
69
70
  outstandingBalance = 0;
@@ -103,44 +104,6 @@ export class Member extends Model {
103
104
  return (await this.getBlobByIds(id))[0] ?? null;
104
105
  }
105
106
 
106
- /**
107
- * Update the outstanding balance of multiple members in one go (or all members)
108
- */
109
- static async updateOutstandingBalance(memberIds: string[] | 'all') {
110
- if (memberIds !== 'all' && memberIds.length == 0) {
111
- return;
112
- }
113
-
114
- const params: any[] = [];
115
- let firstWhere = '';
116
- let secondWhere = '';
117
-
118
- if (memberIds !== 'all') {
119
- firstWhere = ` AND memberId IN (?)`;
120
- params.push(memberIds);
121
-
122
- secondWhere = `WHERE members.id IN (?)`;
123
- params.push(memberIds);
124
- }
125
-
126
- const query = `UPDATE
127
- members
128
- LEFT JOIN (
129
- SELECT
130
- memberId,
131
- sum(unitPrice * amount) - sum(pricePaid) AS outstandingBalance
132
- FROM
133
- balance_items
134
- WHERE status != 'Hidden'${firstWhere}
135
- GROUP BY
136
- memberId
137
- ) i ON i.memberId = members.id
138
- SET members.outstandingBalance = COALESCE(i.outstandingBalance, 0)
139
- ${secondWhere}`;
140
-
141
- await Database.update(query, params);
142
- }
143
-
144
107
  /**
145
108
  * Fetch all registrations with members with their corresponding (valid) registrations
146
109
  */
@@ -377,15 +340,6 @@ export class Member extends Model {
377
340
  return this.getBlobByIds(...(await this.getMemberIdsWithRegistrationForUser(user)));
378
341
  }
379
342
 
380
- getStructureWithRegistrations(this: MemberWithRegistrations, forOrganization: null | boolean = null) {
381
- return MemberWithRegistrationsBlob.create({
382
- ...this,
383
- registrations: this.registrations.map(r => r.getStructure()),
384
- details: this.details,
385
- users: this.users.map(u => u.getStructure()),
386
- });
387
- }
388
-
389
343
  static getRegistrationWithMemberStructure(registration: RegistrationWithMember & { group: import('./Group').Group }): RegistrationWithMemberStruct {
390
344
  return RegistrationWithMemberStruct.create({
391
345
  ...registration.getStructure(),
@@ -4,10 +4,12 @@ import { SQL, SQLSelect, SQLWhereSign } from '@stamhoofd/sql';
4
4
  import { PlatformMembershipTypeBehaviour } from '@stamhoofd/structures';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
+ import { BalanceItem } from './BalanceItem';
7
8
  import { Member } from './Member';
8
9
  import { Organization } from './Organization';
9
10
  import { Platform } from './Platform';
10
- import { BalanceItem } from './BalanceItem';
11
+ import { Registration } from './Registration';
12
+ import { RegistrationPeriod } from './RegistrationPeriod';
11
13
 
12
14
  export class MemberPlatformMembership extends Model {
13
15
  static table = 'member_platform_memberships';
@@ -38,6 +40,15 @@ export class MemberPlatformMembership extends Model {
38
40
  @column({ type: 'date' })
39
41
  endDate: Date;
40
42
 
43
+ /**
44
+ * This membership won't get charged before this day.
45
+ * The membership can still get removed before this day.
46
+ *
47
+ * If a membership is deleted during trial -> do not set deletedAt, but set price to 0 and set trialUntil and endDate to the current date
48
+ */
49
+ @column({ type: 'date', nullable: true })
50
+ trialUntil: Date | null = null;
51
+
41
52
  @column({ type: 'date', nullable: true })
42
53
  expireDate: Date | null = null;
43
54
 
@@ -104,14 +115,44 @@ export class MemberPlatformMembership extends Model {
104
115
  throw new Error('Cannot delete a membership. Use the deletedAt column.');
105
116
  }
106
117
 
107
- async calculatePrice(member: Member) {
118
+ async isElegibleForTrial(member: Member) {
119
+ const period = await RegistrationPeriod.getByID(this.periodId);
120
+ if (!period) {
121
+ return false;
122
+ }
123
+
124
+ if (!period.previousPeriodId) {
125
+ // We have no previous period = no data = no trials
126
+ return false;
127
+ }
128
+
129
+ const platform = await Platform.getSharedStruct();
130
+ const typeIds = platform.config.membershipTypes.filter(m => m.behaviour === PlatformMembershipTypeBehaviour.Period).map(m => m.id);
131
+
132
+ const membership = await MemberPlatformMembership.select()
133
+ .where('memberId', member.id)
134
+ .where('deletedAt', null)
135
+ .where('periodId', period.previousPeriodId)
136
+ .where('membershipTypeId', typeIds)
137
+ .first(false);
138
+
139
+ const hasBlockingMemberships = !!membership;
140
+
141
+ if (hasBlockingMemberships) {
142
+ return false;
143
+ }
144
+
145
+ return true;
146
+ }
147
+
148
+ async calculatePrice(member: Member, registration?: Registration) {
108
149
  const platform = await Platform.getSharedPrivateStruct();
109
- const membershipType = platform.config.membershipTypes.find(m => m.id == this.membershipTypeId);
150
+ const membershipType = platform.config.membershipTypes.find(m => m.id === this.membershipTypeId);
110
151
 
111
152
  if (!membershipType) {
112
153
  throw new SimpleError({
113
154
  code: 'invalid_membership_type',
114
- message: 'Uknown membership type',
155
+ message: 'Unknown membership type',
115
156
  human: 'Deze aansluiting is niet (meer) beschikbaar',
116
157
  });
117
158
  }
@@ -192,7 +233,6 @@ export class MemberPlatformMembership extends Model {
192
233
  else {
193
234
  this.priceWithoutDiscount = earliestPriceConfig.getBasePrice(tagIds, false);
194
235
  this.price = priceConfig.getBasePrice(tagIds, shouldApplyReducedPrice);
195
- this.startDate = periodConfig.startDate;
196
236
  this.endDate = periodConfig.endDate;
197
237
  this.expireDate = periodConfig.expireDate;
198
238
  this.maximumFreeAmount = this.price > 0 ? 1 : 0;
@@ -202,6 +242,53 @@ export class MemberPlatformMembership extends Model {
202
242
  this.price = 0;
203
243
  this.freeAmount = 1;
204
244
  }
245
+
246
+ // Alter start date
247
+ if (registration && registration.startDate) {
248
+ this.startDate = periodConfig.startDate;
249
+
250
+ if (registration.startDate > periodConfig.startDate && registration.startDate < periodConfig.endDate) {
251
+ let startBrussels = Formatter.luxon(registration.startDate);
252
+ startBrussels = startBrussels.set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
253
+ this.startDate = startBrussels.toJSDate();
254
+ }
255
+ }
256
+ else {
257
+ let startBrussels = Formatter.luxon(this.startDate);
258
+ startBrussels = startBrussels.set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
259
+ this.startDate = startBrussels.toJSDate();
260
+ }
261
+ }
262
+
263
+ if (periodConfig.trialDays) {
264
+ // Check whether you are elegible for a trial
265
+ if (await this.isElegibleForTrial(member)) {
266
+ // Allowed to set trial until, maximum periodConfig.trialDays after startDate
267
+ let trialUntil = Formatter.luxon(this.startDate).plus({ days: periodConfig.trialDays });
268
+ trialUntil = trialUntil.set({ hour: 23, minute: 59, second: 59, millisecond: 0 });
269
+
270
+ // Max end date
271
+ if (trialUntil.toJSDate() > this.endDate) {
272
+ trialUntil = Formatter.luxon(this.endDate).set({ hour: 23, minute: 59, second: 59, millisecond: 0 });
273
+ }
274
+
275
+ this.trialUntil = trialUntil.toJSDate();
276
+ }
277
+ else {
278
+ this.trialUntil = null;
279
+ }
280
+ }
281
+ else {
282
+ // No trial
283
+ this.trialUntil = null;
284
+ }
285
+
286
+ // Never charge itself
287
+ const chargeVia = platform.membershipOrganizationId;
288
+ if (this.organizationId === chargeVia) {
289
+ this.price = 0;
290
+ this.priceWithoutDiscount = 0;
291
+ this.freeAmount = 0;
205
292
  }
206
293
 
207
294
  if (this.balanceItemId) {
@@ -41,7 +41,7 @@ export class MergedMember extends Model {
41
41
  details: MemberDetails;
42
42
 
43
43
  /**
44
- * Not yet paid balance
44
+ * @deprecated
45
45
  */
46
46
  @column({ type: 'integer' })
47
47
  outstandingBalance = 0;
@@ -20,6 +20,9 @@ export class Platform extends Model {
20
20
  @column({ type: 'string' })
21
21
  periodId: string;
22
22
 
23
+ @column({ type: 'string', nullable: true })
24
+ previousPeriodId: string | null = null;
25
+
23
26
  @column({ type: 'string', nullable: true })
24
27
  membershipOrganizationId: string | null = null;
25
28
 
@@ -37,6 +40,11 @@ export class Platform extends Model {
37
40
  return clone;
38
41
  }
39
42
 
43
+ async setPreviousPeriodId() {
44
+ const period = await RegistrationPeriod.getByID(this.periodId);
45
+ this.previousPeriodId = period?.previousPeriodId ?? null;
46
+ }
47
+
40
48
  static async getSharedPrivateStruct(): Promise<PlatformStruct & { privateConfig: PlatformPrivateConfig }> {
41
49
  if (this.sharedStruct && this.sharedStruct.privateConfig) {
42
50
  return this.sharedStruct as any;
@@ -76,8 +84,8 @@ export class Platform extends Model {
76
84
  }
77
85
 
78
86
  async save() {
79
- Platform.clearCache();
80
87
  const s = await super.save();
88
+ Platform.clearCache();
81
89
 
82
90
  // Force update cache immediately
83
91
  await Platform.getSharedStruct();