@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.
- package/dist/src/migrations/1605262045-import-postcodes.d.ts +1 -1
- package/dist/src/migrations/1605262045-import-postcodes.js +1 -4
- package/dist/src/migrations/1734429094-registration-trial-until.sql +3 -0
- package/dist/src/migrations/1734429095-membership-trial-until.sql +2 -0
- package/dist/src/migrations/1734535120-registration-period-previous-period-id.sql +3 -0
- package/dist/src/migrations/1734535121-platform-previous-period-id.sql +3 -0
- package/dist/src/migrations/1734626607-cached-balance-amount-open.sql +2 -0
- package/dist/src/migrations/1734698906-cached-balance-amount-paid.sql +2 -0
- package/dist/src/models/BalanceItem.d.ts +2 -2
- package/dist/src/models/BalanceItem.d.ts.map +1 -1
- package/dist/src/models/BalanceItem.js +13 -11
- package/dist/src/models/BalanceItem.js.map +1 -1
- package/dist/src/models/BalanceItemPayment.d.ts +5 -0
- package/dist/src/models/BalanceItemPayment.d.ts.map +1 -1
- package/dist/src/models/BalanceItemPayment.js +15 -0
- package/dist/src/models/BalanceItemPayment.js.map +1 -1
- package/dist/src/models/CachedBalance.d.ts +6 -2
- package/dist/src/models/CachedBalance.d.ts.map +1 -1
- package/dist/src/models/CachedBalance.js +64 -20
- package/dist/src/models/CachedBalance.js.map +1 -1
- package/dist/src/models/DocumentTemplate.d.ts.map +1 -1
- package/dist/src/models/DocumentTemplate.js +36 -9
- package/dist/src/models/DocumentTemplate.js.map +1 -1
- package/dist/src/models/Member.d.ts +3 -7
- package/dist/src/models/Member.d.ts.map +1 -1
- package/dist/src/models/Member.js +2 -41
- package/dist/src/models/Member.js.map +1 -1
- package/dist/src/models/MemberPlatformMembership.d.ts +10 -1
- package/dist/src/models/MemberPlatformMembership.d.ts.map +1 -1
- package/dist/src/models/MemberPlatformMembership.js +79 -5
- package/dist/src/models/MemberPlatformMembership.js.map +1 -1
- package/dist/src/models/MergedMember.d.ts +1 -1
- package/dist/src/models/MergedMember.js +1 -1
- package/dist/src/models/Platform.d.ts +2 -0
- package/dist/src/models/Platform.d.ts.map +1 -1
- package/dist/src/models/Platform.js +9 -1
- package/dist/src/models/Platform.js.map +1 -1
- package/dist/src/models/Registration.d.ts +17 -5
- package/dist/src/models/Registration.d.ts.map +1 -1
- package/dist/src/models/Registration.js +23 -34
- package/dist/src/models/Registration.js.map +1 -1
- package/dist/src/models/RegistrationPeriod.d.ts +7 -0
- package/dist/src/models/RegistrationPeriod.d.ts.map +1 -1
- package/dist/src/models/RegistrationPeriod.js +36 -0
- package/dist/src/models/RegistrationPeriod.js.map +1 -1
- package/package.json +2 -2
- package/src/migrations/1605262045-import-postcodes.ts +2 -4
- package/src/migrations/1734429094-registration-trial-until.sql +3 -0
- package/src/migrations/1734429095-membership-trial-until.sql +2 -0
- package/src/migrations/1734535120-registration-period-previous-period-id.sql +3 -0
- package/src/migrations/1734535121-platform-previous-period-id.sql +3 -0
- package/src/migrations/1734626607-cached-balance-amount-open.sql +2 -0
- package/src/migrations/1734698906-cached-balance-amount-paid.sql +2 -0
- package/src/models/BalanceItem.ts +18 -17
- package/src/models/BalanceItemPayment.ts +20 -1
- package/src/models/CachedBalance.ts +88 -23
- package/src/models/DocumentTemplate.ts +39 -9
- package/src/models/Member.ts +2 -48
- package/src/models/MemberPlatformMembership.ts +92 -5
- package/src/models/MergedMember.ts +1 -1
- package/src/models/Platform.ts +9 -1
- package/src/models/Registration.ts +21 -40
- 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,
|
|
3
|
-
import { BalanceItem as BalanceItemStruct,
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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, {
|
|
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
|
|
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
|
|
207
|
-
throw new Error('Invalid
|
|
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
|
-
|
|
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
|
|
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
|
|
240
|
-
throw new Error('Invalid
|
|
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].
|
|
252
|
-
if (
|
|
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].
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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
|
-
'
|
|
332
|
+
'amountPaid',
|
|
333
|
+
'amountOpen',
|
|
286
334
|
'amountPending',
|
|
287
335
|
'nextDueAt',
|
|
288
336
|
'createdAt',
|
|
289
337
|
'updatedAt',
|
|
290
338
|
)
|
|
291
|
-
.values(...result.map(([objectId, {
|
|
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
|
-
|
|
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('
|
|
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:
|
|
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:
|
|
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
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/models/Member.ts
CHANGED
|
@@ -63,7 +63,8 @@ export class Member extends Model {
|
|
|
63
63
|
details: MemberDetails;
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
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 {
|
|
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
|
|
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
|
|
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: '
|
|
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) {
|
package/src/models/Platform.ts
CHANGED
|
@@ -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();
|