@stamhoofd/models 2.5.0 → 2.7.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/1722845608-registration-stock-reservations.sql +2 -0
- package/dist/src/migrations/1722845609-group-stock-reservations.sql +2 -0
- package/dist/src/migrations/1722852362-stripe-intents-account-id.sql +2 -0
- package/dist/src/migrations/1722852363-stripe-checkout-sessions-account-id.sql +2 -0
- package/dist/src/models/BalanceItem.d.ts +1 -0
- package/dist/src/models/BalanceItem.d.ts.map +1 -1
- package/dist/src/models/BalanceItem.js +36 -43
- package/dist/src/models/BalanceItem.js.map +1 -1
- package/dist/src/models/DocumentTemplate.js +1 -1
- package/dist/src/models/DocumentTemplate.js.map +1 -1
- package/dist/src/models/Email.d.ts.map +1 -1
- package/dist/src/models/Email.js +16 -8
- package/dist/src/models/Email.js.map +1 -1
- package/dist/src/models/EmailTemplate.d.ts.map +1 -1
- package/dist/src/models/EmailTemplate.js.map +1 -1
- package/dist/src/models/Event.d.ts +7 -0
- package/dist/src/models/Event.d.ts.map +1 -1
- package/dist/src/models/Event.js +28 -0
- package/dist/src/models/Event.js.map +1 -1
- package/dist/src/models/Group.d.ts +6 -3
- package/dist/src/models/Group.d.ts.map +1 -1
- package/dist/src/models/Group.js +29 -6
- package/dist/src/models/Group.js.map +1 -1
- package/dist/src/models/Member.d.ts +3 -2
- package/dist/src/models/Member.d.ts.map +1 -1
- package/dist/src/models/Member.js +32 -22
- package/dist/src/models/Member.js.map +1 -1
- package/dist/src/models/Payment.d.ts +5 -7
- package/dist/src/models/Payment.d.ts.map +1 -1
- package/dist/src/models/Payment.js +8 -13
- package/dist/src/models/Payment.js.map +1 -1
- package/dist/src/models/Registration.d.ts +18 -2
- package/dist/src/models/Registration.d.ts.map +1 -1
- package/dist/src/models/Registration.js +72 -8
- package/dist/src/models/Registration.js.map +1 -1
- package/dist/src/models/StripeCheckoutSession.d.ts +4 -0
- package/dist/src/models/StripeCheckoutSession.d.ts.map +1 -1
- package/dist/src/models/StripeCheckoutSession.js +7 -0
- package/dist/src/models/StripeCheckoutSession.js.map +1 -1
- package/dist/src/models/StripePaymentIntent.d.ts +4 -0
- package/dist/src/models/StripePaymentIntent.d.ts.map +1 -1
- package/dist/src/models/StripePaymentIntent.js +7 -0
- package/dist/src/models/StripePaymentIntent.js.map +1 -1
- package/package.json +2 -2
- package/src/migrations/1722845608-registration-stock-reservations.sql +2 -0
- package/src/migrations/1722845609-group-stock-reservations.sql +2 -0
- package/src/migrations/1722852362-stripe-intents-account-id.sql +2 -0
- package/src/migrations/1722852363-stripe-checkout-sessions-account-id.sql +2 -0
- package/src/models/BalanceItem.ts +40 -46
- package/src/models/DocumentTemplate.ts +1 -1
- package/src/models/Email.ts +28 -17
- package/src/models/EmailTemplate.ts +1 -1
- package/src/models/Event.ts +31 -0
- package/src/models/Group.ts +36 -14
- package/src/models/Member.ts +34 -23
- package/src/models/Payment.ts +10 -16
- package/src/models/Registration.ts +88 -12
- package/src/models/StripeAccount.ts +1 -1
- package/src/models/StripeCheckoutSession.ts +6 -0
- package/src/models/StripePaymentIntent.ts +6 -0
package/src/models/Group.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { column, Database, ManyToOneRelation, Model, OneToManyRelation } from '@simonbackx/simple-database';
|
|
2
|
-
import { GroupCategory, GroupPrivateSettings, GroupSettings, GroupStatus, Group as GroupStruct, GroupType } from '@stamhoofd/structures';
|
|
2
|
+
import { GroupCategory, GroupPrivateSettings, GroupSettings, GroupStatus, Group as GroupStruct, GroupType, StockReservation } from '@stamhoofd/structures';
|
|
3
3
|
import { v4 as uuidv4 } from "uuid";
|
|
4
4
|
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { Member, MemberWithRegistrations, OrganizationRegistrationPeriod, Payment, Registration, User } from './';
|
|
7
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
|
+
import { ArrayDecoder } from '@simonbackx/simple-encoding';
|
|
7
9
|
|
|
8
10
|
if (Member === undefined) {
|
|
9
11
|
throw new Error("Import Member is undefined")
|
|
@@ -85,17 +87,20 @@ export class Group extends Model {
|
|
|
85
87
|
})
|
|
86
88
|
deletedAt: Date | null = null
|
|
87
89
|
|
|
88
|
-
/**
|
|
89
|
-
* Every time a new registration period starts, this number increases. This is used to mark all older registrations as 'out of date' automatically
|
|
90
|
-
*/
|
|
91
90
|
@column({ type: "string" })
|
|
92
91
|
status = GroupStatus.Open;
|
|
93
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Editing this field is only allowed when running inside the QueueHandler
|
|
95
|
+
*/
|
|
96
|
+
@column({ type: "json", decoder: new ArrayDecoder(StockReservation) })
|
|
97
|
+
stockReservations: StockReservation[] = []
|
|
98
|
+
|
|
94
99
|
static async getAll(organizationId: string, periodId: string|null, active = true) {
|
|
95
100
|
const w: any = periodId ? {periodId} : {}
|
|
96
101
|
if (active) {
|
|
97
102
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
98
|
-
return await Group.where({ organizationId, deletedAt: null, ...w
|
|
103
|
+
return await Group.where({ organizationId, deletedAt: null, ...w })
|
|
99
104
|
}
|
|
100
105
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
101
106
|
return await Group.where({ organizationId, ...w })
|
|
@@ -216,18 +221,13 @@ export class Group extends Model {
|
|
|
216
221
|
|
|
217
222
|
async updateOccupancy() {
|
|
218
223
|
this.settings.registeredMembers = await Group.getCount(
|
|
219
|
-
"groupId = ? and
|
|
220
|
-
[this.id
|
|
224
|
+
"groupId = ? and registeredAt is not null AND deactivatedAt is null",
|
|
225
|
+
[this.id]
|
|
221
226
|
)
|
|
222
227
|
|
|
223
228
|
this.settings.reservedMembers = await Group.getCount(
|
|
224
|
-
"groupId = ? and
|
|
225
|
-
[this.id,
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
this.settings.waitingListSize = await Group.getCount(
|
|
229
|
-
"groupId = ? and cycle = ? and waitingList = 1",
|
|
230
|
-
[this.id, this.cycle, new Date()]
|
|
229
|
+
"groupId = ? and registeredAt is null AND (canRegister = 1 OR reservedUntil >= ?)",
|
|
230
|
+
[this.id, new Date()]
|
|
231
231
|
)
|
|
232
232
|
}
|
|
233
233
|
|
|
@@ -280,6 +280,28 @@ export class Group extends Model {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
static async applyStockReservations(groupId: string, addStockReservations: StockReservation[], free = false) {
|
|
284
|
+
await QueueHandler.schedule('group-stock-update-'+groupId, async () => {
|
|
285
|
+
const updatedGroup = await Group.getByID(groupId)
|
|
286
|
+
if (!updatedGroup) {
|
|
287
|
+
throw new Error("Expected group")
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!free) {
|
|
291
|
+
updatedGroup.stockReservations = StockReservation.added(updatedGroup.stockReservations, addStockReservations)
|
|
292
|
+
} else {
|
|
293
|
+
updatedGroup.stockReservations = StockReservation.removed(updatedGroup.stockReservations, addStockReservations)
|
|
294
|
+
}
|
|
295
|
+
await updatedGroup.save()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
static async freeStockReservations(groupId: string, reservations: StockReservation[]) {
|
|
301
|
+
return await this.applyStockReservations(groupId, reservations, true)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
283
305
|
}
|
|
284
306
|
|
|
285
307
|
Registration.group = new ManyToOneRelation(Group, "group")
|
package/src/models/Member.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { column, Database, ManyToManyRelation, ManyToOneRelation, Model, OneToManyRelation } from '@simonbackx/simple-database';
|
|
2
2
|
import { SQL } from "@stamhoofd/sql";
|
|
3
|
-
import {
|
|
3
|
+
import { MemberDetails, MemberWithRegistrationsBlob, RegistrationWithMember as RegistrationWithMemberStruct, TinyMember } from '@stamhoofd/structures';
|
|
4
4
|
import { Formatter, Sorter } from '@stamhoofd/utility';
|
|
5
5
|
import { v4 as uuidv4 } from "uuid";
|
|
6
6
|
|
|
7
|
-
import { Group, MemberPlatformMembership, Payment, Platform, Registration, User } from './';
|
|
8
7
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
|
+
import { Group, MemberPlatformMembership, Payment, Platform, Registration, User } from './';
|
|
9
9
|
export type MemberWithRegistrations = Member & {
|
|
10
10
|
users: User[],
|
|
11
11
|
registrations: (Registration & {group: Group})[]
|
|
@@ -144,7 +144,7 @@ export class Member extends Model {
|
|
|
144
144
|
}
|
|
145
145
|
let query = `SELECT ${Member.getDefaultSelect()}, ${Registration.getDefaultSelect()} from \`${Member.table}\`\n`;
|
|
146
146
|
|
|
147
|
-
query += `JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND (\`${Registration.table}\`.\`registeredAt\` is not null OR \`${Registration.table}\`.\`
|
|
147
|
+
query += `JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND (\`${Registration.table}\`.\`registeredAt\` is not null OR \`${Registration.table}\`.\`canRegister\` = 1)\n`
|
|
148
148
|
|
|
149
149
|
// We do an extra join because we also need to get the other registrations of each member (only one regitration has to match the query)
|
|
150
150
|
query += `where \`${Registration.table}\`.\`${Registration.primary.name}\` IN (?)`
|
|
@@ -177,15 +177,15 @@ export class Member extends Model {
|
|
|
177
177
|
/**
|
|
178
178
|
* Fetch all registrations with members with their corresponding (valid) registrations
|
|
179
179
|
*/
|
|
180
|
-
static async getRegistrationWithMembersForGroup(groupId: string
|
|
180
|
+
static async getRegistrationWithMembersForGroup(groupId: string): Promise<RegistrationWithMember[]> {
|
|
181
181
|
let query = `SELECT ${Member.getDefaultSelect()}, ${Registration.getDefaultSelect()} from \`${Member.table}\`\n`;
|
|
182
182
|
|
|
183
|
-
query += `JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND (\`${Registration.table}\`.\`registeredAt\` is not null OR \`${Registration.table}\`.\`
|
|
183
|
+
query += `JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND (\`${Registration.table}\`.\`registeredAt\` is not null OR \`${Registration.table}\`.\`canRegister\` = 1)\n`
|
|
184
184
|
|
|
185
185
|
// We do an extra join because we also need to get the other registrations of each member (only one regitration has to match the query)
|
|
186
|
-
query += `where \`${Registration.table}\`.\`groupId\` =
|
|
186
|
+
query += `where \`${Registration.table}\`.\`groupId\` = ?`
|
|
187
187
|
|
|
188
|
-
const [results] = await Database.select(query, [groupId
|
|
188
|
+
const [results] = await Database.select(query, [groupId])
|
|
189
189
|
const registrations: RegistrationWithMember[] = []
|
|
190
190
|
|
|
191
191
|
// In the future we might add a 'reverse' method on manytoone relation, instead of defining the new relation. But then we need to store 2 model types in the many to one relation.
|
|
@@ -261,7 +261,7 @@ export class Member extends Model {
|
|
|
261
261
|
return []
|
|
262
262
|
}
|
|
263
263
|
let query = `SELECT ${Member.getDefaultSelect()}, ${Registration.getDefaultSelect()}, ${User.getDefaultSelect()} from \`${Member.table}\`\n`;
|
|
264
|
-
query += `LEFT JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND (\`${Registration.table}\`.\`registeredAt\` is not null OR \`${Registration.table}\`.\`
|
|
264
|
+
query += `LEFT JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND (\`${Registration.table}\`.\`registeredAt\` is not null OR \`${Registration.table}\`.\`canRegister\` = 1)\n`
|
|
265
265
|
query += Member.users.joinQuery(Member.table, User.table)+"\n"
|
|
266
266
|
|
|
267
267
|
// We do an extra join because we also need to get the other registrations of each member (only one regitration has to match the query)
|
|
@@ -380,7 +380,11 @@ export class Member extends Model {
|
|
|
380
380
|
return RegistrationWithMemberStruct.create({
|
|
381
381
|
...registration.getStructure(),
|
|
382
382
|
cycle: registration.cycle,
|
|
383
|
-
member:
|
|
383
|
+
member: TinyMember.create({
|
|
384
|
+
id: registration.member.id,
|
|
385
|
+
firstName: registration.member.firstName,
|
|
386
|
+
lastName: registration.member.lastName,
|
|
387
|
+
}),
|
|
384
388
|
})
|
|
385
389
|
}
|
|
386
390
|
|
|
@@ -406,19 +410,22 @@ export class Member extends Model {
|
|
|
406
410
|
).fetch()).flatMap(r => (r.members && (typeof r.members.id) === 'string') ? [r.members.id as string] : [])
|
|
407
411
|
|
|
408
412
|
for (const id of memberIds) {
|
|
409
|
-
|
|
410
|
-
await member?.updateMemberships()
|
|
413
|
+
await Member.updateMembershipsForId(id)
|
|
411
414
|
}
|
|
412
415
|
}).catch((e) => {
|
|
413
416
|
console.error('Failed to update memberships for group id ', id), e
|
|
414
417
|
});
|
|
415
418
|
}
|
|
416
419
|
|
|
417
|
-
async
|
|
418
|
-
|
|
419
|
-
|
|
420
|
+
static async updateMembershipsForId(id: string) {
|
|
421
|
+
return await QueueHandler.schedule('updateMemberships-' + id, async function (this: undefined) {
|
|
422
|
+
const me = await Member.getWithRegistrations(id)
|
|
423
|
+
if (!me) {
|
|
424
|
+
console.log('Skipping automatic membership for: ' + id, ' - member not found')
|
|
425
|
+
return
|
|
426
|
+
}
|
|
420
427
|
const platform = await Platform.getShared()
|
|
421
|
-
const registrations =
|
|
428
|
+
const registrations = me.registrations.filter(r => r.group.periodId == platform.periodId && r.registeredAt && !r.deactivatedAt)
|
|
422
429
|
|
|
423
430
|
const defaultMemberships = registrations.flatMap(r => {
|
|
424
431
|
if (!r.group.defaultAgeGroupId) {
|
|
@@ -440,7 +447,7 @@ export class Member extends Model {
|
|
|
440
447
|
}]
|
|
441
448
|
})
|
|
442
449
|
// Get active memberships for this member that
|
|
443
|
-
const memberships = await MemberPlatformMembership.where({memberId:
|
|
450
|
+
const memberships = await MemberPlatformMembership.where({memberId: me.id, periodId: platform.periodId })
|
|
444
451
|
const now = new Date()
|
|
445
452
|
const activeMemberships = memberships.filter(m => m.startDate <= now && m.endDate >= now && m.deletedAt === null)
|
|
446
453
|
const activeMembershipsUndeletable = activeMemberships.filter(m => !m.canDelete() || !m.generated)
|
|
@@ -449,20 +456,20 @@ export class Member extends Model {
|
|
|
449
456
|
// Stop all active memberships taht were added automatically
|
|
450
457
|
for (const membership of activeMemberships) {
|
|
451
458
|
if (membership.canDelete() && membership.generated) {
|
|
452
|
-
console.log('Removing membership because no longer registered member and not yet invoiced for: ' +
|
|
459
|
+
console.log('Removing membership because no longer registered member and not yet invoiced for: ' + me.id + ' - membership ' + membership.id)
|
|
453
460
|
membership.deletedAt = new Date()
|
|
454
461
|
await membership.save()
|
|
455
462
|
}
|
|
456
463
|
}
|
|
457
464
|
|
|
458
|
-
console.log('Skipping automatic membership for: ' +
|
|
465
|
+
console.log('Skipping automatic membership for: ' + me.id, ' - no default memberships found')
|
|
459
466
|
return
|
|
460
467
|
}
|
|
461
468
|
|
|
462
469
|
|
|
463
470
|
if (activeMembershipsUndeletable.length) {
|
|
464
471
|
// Skip automatic additions
|
|
465
|
-
console.log('Skipping automatic membership for: ' +
|
|
472
|
+
console.log('Skipping automatic membership for: ' + me.id, ' - already has active memberships')
|
|
466
473
|
return
|
|
467
474
|
}
|
|
468
475
|
|
|
@@ -480,7 +487,7 @@ export class Member extends Model {
|
|
|
480
487
|
|
|
481
488
|
// Check if already have the same membership
|
|
482
489
|
if (activeMemberships.find(m => m.membershipTypeId == cheapestMembership.membership.id)) {
|
|
483
|
-
console.log('Skipping automatic membership for: ' +
|
|
490
|
+
console.log('Skipping automatic membership for: ' + me.id, ' - already has this membership')
|
|
484
491
|
return
|
|
485
492
|
}
|
|
486
493
|
|
|
@@ -490,9 +497,9 @@ export class Member extends Model {
|
|
|
490
497
|
}
|
|
491
498
|
|
|
492
499
|
// Can we revive an earlier deleted membership?
|
|
493
|
-
console.log('Creating automatic membership for: ' +
|
|
500
|
+
console.log('Creating automatic membership for: ' + me.id + ' - membership type ' + cheapestMembership.membership.id)
|
|
494
501
|
const membership = new MemberPlatformMembership();
|
|
495
|
-
membership.memberId =
|
|
502
|
+
membership.memberId = me.id
|
|
496
503
|
membership.membershipTypeId = cheapestMembership.membership.id
|
|
497
504
|
membership.organizationId = cheapestMembership.registration.organizationId
|
|
498
505
|
membership.periodId = platform.periodId
|
|
@@ -508,11 +515,15 @@ export class Member extends Model {
|
|
|
508
515
|
// This reasoning allows us to replace an existing membership with a cheaper one (not date based ones, but type based ones)
|
|
509
516
|
for (const toDelete of activeMemberships) {
|
|
510
517
|
if (toDelete.canDelete() && toDelete.generated) {
|
|
511
|
-
console.log('Removing membership because cheaper membership found for: ' +
|
|
518
|
+
console.log('Removing membership because cheaper membership found for: ' + me.id + ' - membership ' + toDelete.id)
|
|
512
519
|
toDelete.deletedAt = new Date()
|
|
513
520
|
await toDelete.save()
|
|
514
521
|
}
|
|
515
522
|
}
|
|
516
523
|
});
|
|
517
524
|
}
|
|
525
|
+
|
|
526
|
+
async updateMemberships() {
|
|
527
|
+
return await Member.updateMembershipsForId(this.id)
|
|
528
|
+
}
|
|
518
529
|
}
|
package/src/models/Payment.ts
CHANGED
|
@@ -110,39 +110,37 @@ export class Payment extends Model {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
const {balanceItemPayments, balanceItems} = await Payment.loadBalanceItems(payments)
|
|
113
|
-
const {registrations, orders,
|
|
113
|
+
const {registrations, orders, groups} = await Payment.loadBalanceItemRelations(balanceItems);
|
|
114
114
|
|
|
115
|
-
return this.getGeneralStructureFromRelations({
|
|
115
|
+
return await this.getGeneralStructureFromRelations({
|
|
116
116
|
payments,
|
|
117
117
|
registrations,
|
|
118
118
|
orders,
|
|
119
|
-
members,
|
|
120
119
|
balanceItemPayments,
|
|
121
120
|
balanceItems,
|
|
122
121
|
groups
|
|
123
122
|
}, includeSettlements)
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
static getGeneralStructureFromRelations({payments, registrations, orders,
|
|
125
|
+
static async getGeneralStructureFromRelations({payments, registrations, orders, balanceItemPayments, balanceItems, groups}: {
|
|
127
126
|
payments: Payment[];
|
|
128
|
-
registrations: import("./
|
|
127
|
+
registrations: import("./Member").RegistrationWithMember[];
|
|
129
128
|
orders: import("./Order").Order[];
|
|
130
|
-
members: import("./Member").Member[];
|
|
131
129
|
balanceItemPayments: import("./BalanceItemPayment").BalanceItemPayment[];
|
|
132
130
|
balanceItems: import("./BalanceItem").BalanceItem[];
|
|
133
131
|
groups: import("./Group").Group[];
|
|
134
|
-
}, includeSettlements = false): PaymentGeneral[] {
|
|
132
|
+
}, includeSettlements = false): Promise<PaymentGeneral[]> {
|
|
135
133
|
if (payments.length === 0) {
|
|
136
134
|
return []
|
|
137
135
|
}
|
|
136
|
+
const {Member} = (await import("./Member"));
|
|
138
137
|
|
|
139
138
|
return payments.map(payment => {
|
|
140
139
|
return PaymentGeneral.create({
|
|
141
140
|
...payment,
|
|
142
141
|
balanceItemPayments: balanceItemPayments.filter(item => item.paymentId === payment.id).map((item) => {
|
|
143
142
|
const balanceItem = balanceItems.find(b => b.id === item.balanceItemId)
|
|
144
|
-
const registration = balanceItem?.registrationId
|
|
145
|
-
const member = balanceItem?.memberId ? members.find(r => r.id === balanceItem.memberId) : undefined
|
|
143
|
+
const registration = balanceItem?.registrationId ? registrations.find(r => r.id === balanceItem.registrationId) : null
|
|
146
144
|
const order = balanceItem?.orderId && orders.find(r => r.id === balanceItem.orderId)
|
|
147
145
|
const group = registration ? groups.find(g => g.id === registration.groupId) : null
|
|
148
146
|
|
|
@@ -154,8 +152,7 @@ export class Payment extends Model {
|
|
|
154
152
|
...item,
|
|
155
153
|
balanceItem: BalanceItemDetailed.create({
|
|
156
154
|
...balanceItem,
|
|
157
|
-
registration: registration ? registration.setRelation(Registration.group, group!)
|
|
158
|
-
member: member ? MemberStruct.create(member) : null,
|
|
155
|
+
registration: registration ? Member.getRegistrationWithMemberStructure(registration.setRelation(Registration.group, group!)) : null,
|
|
159
156
|
order: order ? OrderStruct.create({...order, payment: null}) : null
|
|
160
157
|
})
|
|
161
158
|
})
|
|
@@ -195,22 +192,19 @@ export class Payment extends Model {
|
|
|
195
192
|
}
|
|
196
193
|
|
|
197
194
|
static async loadBalanceItemRelations(balanceItems: import("./BalanceItem").BalanceItem[]) {
|
|
198
|
-
const {Registration} = await import("./Registration");
|
|
199
195
|
const {Order} = await import("./Order");
|
|
200
196
|
const {Member} = await import("./Member");
|
|
201
197
|
|
|
202
198
|
// Load members and orders
|
|
203
199
|
const registrationIds = Formatter.uniqueArray(balanceItems.flatMap(b => b.registrationId ? [b.registrationId] : []))
|
|
204
200
|
const orderIds = Formatter.uniqueArray(balanceItems.flatMap(b => b.orderId ? [b.orderId] : []))
|
|
205
|
-
const memberIds = Formatter.uniqueArray(balanceItems.flatMap(b => b.memberId ? [b.memberId] : []))
|
|
206
201
|
|
|
207
|
-
const registrations = await
|
|
202
|
+
const registrations = await Member.getRegistrationWithMembersByIDs(registrationIds)
|
|
208
203
|
const orders = await Order.getByIDs(...orderIds)
|
|
209
|
-
const members = await Member.getByIDs(...memberIds)
|
|
210
204
|
|
|
211
205
|
const groupIds = Formatter.uniqueArray(registrations.map(r => r.groupId))
|
|
212
206
|
const groups = await (await import("./Group")).Group.getByIDs(...groupIds)
|
|
213
207
|
|
|
214
|
-
return {registrations, orders,
|
|
208
|
+
return {registrations, orders, groups}
|
|
215
209
|
}
|
|
216
210
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { column, Database, ManyToOneRelation, Model } from '@simonbackx/simple-database';
|
|
2
2
|
import { Email } from '@stamhoofd/email';
|
|
3
|
-
import { EmailTemplateType, PaymentMethod, PaymentMethodHelper, Recipient, Registration as RegistrationStructure, Replacement } from '@stamhoofd/structures';
|
|
3
|
+
import { EmailTemplateType, PaymentMethod, PaymentMethodHelper, Recipient, Registration as RegistrationStructure, Replacement, StockReservation } from '@stamhoofd/structures';
|
|
4
4
|
import { Formatter } from '@stamhoofd/utility';
|
|
5
5
|
import { v4 as uuidv4 } from "uuid";
|
|
6
6
|
|
|
7
7
|
import { getEmailBuilder } from '../helpers/EmailBuilder';
|
|
8
|
-
import { Document, EmailTemplate, Organization, User } from './';
|
|
8
|
+
import { Document, EmailTemplate, Group, Organization, User } from './';
|
|
9
|
+
import { ArrayDecoder } from '@simonbackx/simple-encoding';
|
|
10
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
9
11
|
|
|
10
12
|
export class Registration extends Model {
|
|
11
13
|
static table = "registrations"
|
|
@@ -69,6 +71,9 @@ export class Registration extends Model {
|
|
|
69
71
|
@column({ type: "datetime", nullable: true })
|
|
70
72
|
reservedUntil: Date | null = null
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* @deprecated - replaced by group type
|
|
76
|
+
*/
|
|
72
77
|
@column({ type: "boolean" })
|
|
73
78
|
waitingList = false
|
|
74
79
|
|
|
@@ -88,6 +93,12 @@ export class Registration extends Model {
|
|
|
88
93
|
@column({ type: "integer" })
|
|
89
94
|
pricePaid = 0
|
|
90
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Set to null if no reservations are made, to help faster querying
|
|
98
|
+
*/
|
|
99
|
+
@column({ type: "json", decoder: new ArrayDecoder(StockReservation), nullable: true })
|
|
100
|
+
stockReservations: StockReservation[] = []
|
|
101
|
+
|
|
91
102
|
static group: ManyToOneRelation<"group", import('./Group').Group>
|
|
92
103
|
|
|
93
104
|
getStructure(this: Registration & {group: import('./Group').Group}) {
|
|
@@ -101,7 +112,7 @@ export class Registration extends Model {
|
|
|
101
112
|
/**
|
|
102
113
|
* Update the outstanding balance of multiple members in one go (or all members)
|
|
103
114
|
*/
|
|
104
|
-
static async updateOutstandingBalance(registrationIds: string[] | 'all', organizationId
|
|
115
|
+
static async updateOutstandingBalance(registrationIds: string[] | 'all', organizationId?: string) {
|
|
105
116
|
if (registrationIds !== 'all' && registrationIds.length == 0) {
|
|
106
117
|
return
|
|
107
118
|
}
|
|
@@ -136,7 +147,7 @@ export class Registration extends Model {
|
|
|
136
147
|
|
|
137
148
|
await Database.update(query, params)
|
|
138
149
|
|
|
139
|
-
if (registrationIds !== 'all') {
|
|
150
|
+
if (registrationIds !== 'all' && organizationId) {
|
|
140
151
|
await Document.updateForRegistrations(registrationIds, organizationId)
|
|
141
152
|
}
|
|
142
153
|
}
|
|
@@ -149,7 +160,7 @@ export class Registration extends Model {
|
|
|
149
160
|
const query = `
|
|
150
161
|
SELECT COUNT(DISTINCT \`${Registration.table}\`.memberId) as c FROM \`${Registration.table}\`
|
|
151
162
|
JOIN \`groups\` ON \`groups\`.id = \`${Registration.table}\`.groupId
|
|
152
|
-
WHERE \`groups\`.organizationId = ? AND \`${Registration.table}\`.cycle = \`groups\`.cycle AND \`groups\`.deletedAt is null AND
|
|
163
|
+
WHERE \`groups\`.organizationId = ? AND \`${Registration.table}\`.cycle = \`groups\`.cycle AND \`groups\`.deletedAt is null AND \`${Registration.table}\`.registeredAt is not null AND \`${Registration.table}\`.deactivatedAt is null`
|
|
153
164
|
|
|
154
165
|
const [results] = await Database.select(query, [organizationId])
|
|
155
166
|
const count = results[0]['']['c'];
|
|
@@ -162,26 +173,40 @@ export class Registration extends Model {
|
|
|
162
173
|
}
|
|
163
174
|
}
|
|
164
175
|
|
|
176
|
+
async deactivate() {
|
|
177
|
+
if (this.deactivatedAt !== null) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Clear the registration
|
|
182
|
+
this.deactivatedAt = new Date()
|
|
183
|
+
await this.save()
|
|
184
|
+
this.scheduleStockUpdate()
|
|
185
|
+
|
|
186
|
+
const {Member} = await import('./Member');
|
|
187
|
+
await Member.updateMembershipsForId(this.memberId)
|
|
188
|
+
}
|
|
189
|
+
|
|
165
190
|
async markValid(this: Registration) {
|
|
166
|
-
if (this.registeredAt !== null) {
|
|
191
|
+
if (this.registeredAt !== null && this.deactivatedAt === null) {
|
|
167
192
|
await this.save();
|
|
168
193
|
return false;
|
|
169
194
|
}
|
|
170
195
|
|
|
171
|
-
if (this.waitingList && this.canRegister) {
|
|
172
|
-
this.waitingList = false
|
|
173
|
-
}
|
|
174
|
-
|
|
175
196
|
this.reservedUntil = null
|
|
176
|
-
this.registeredAt = new Date()
|
|
197
|
+
this.registeredAt = this.registeredAt ?? new Date()
|
|
198
|
+
this.deactivatedAt = null
|
|
177
199
|
this.canRegister = false
|
|
178
200
|
await this.save();
|
|
201
|
+
this.scheduleStockUpdate()
|
|
202
|
+
|
|
203
|
+
const {Member} = await import('./Member');
|
|
204
|
+
await Member.updateMembershipsForId(this.memberId)
|
|
179
205
|
|
|
180
206
|
await this.sendEmailTemplate({
|
|
181
207
|
type: EmailTemplateType.RegistrationConfirmation
|
|
182
208
|
});
|
|
183
209
|
|
|
184
|
-
const {Member} = await import('./Member');
|
|
185
210
|
const member = await Member.getByID(this.memberId);
|
|
186
211
|
if (member) {
|
|
187
212
|
const registrationMemberRelation = new ManyToOneRelation(Member, "member")
|
|
@@ -408,4 +433,55 @@ export class Registration extends Model {
|
|
|
408
433
|
|
|
409
434
|
Email.schedule(builder)
|
|
410
435
|
}
|
|
436
|
+
|
|
437
|
+
shouldIncludeStock() {
|
|
438
|
+
return (this.registeredAt !== null && this.deactivatedAt === null) || this.canRegister || (this.reservedUntil && this.reservedUntil > new Date())
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Adds or removes the order to the stock of the webshop (if it wasn't already included). If amounts were changed, only those
|
|
445
|
+
* changes will get added
|
|
446
|
+
* Should always happen in the webshop-stock queue to prevent multiple webshop writes at the same time
|
|
447
|
+
* + in combination with validation and reading the webshop
|
|
448
|
+
*/
|
|
449
|
+
scheduleStockUpdate() {
|
|
450
|
+
const id = this.id;
|
|
451
|
+
|
|
452
|
+
QueueHandler.cancel('registration-stock-update-'+id);
|
|
453
|
+
QueueHandler.schedule('registration-stock-update-'+id, async function(this: undefined) {
|
|
454
|
+
const updated = await Registration.getByID(id);
|
|
455
|
+
|
|
456
|
+
if (!updated) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Start with clearing all the stock reservations we've already made
|
|
461
|
+
if (updated.stockReservations) {
|
|
462
|
+
const groupIds = Formatter.uniqueArray(updated.stockReservations.flatMap(r => r.objectType === 'Group' ? [r.objectId] : []));
|
|
463
|
+
for (const groupId of groupIds) {
|
|
464
|
+
const stocks = StockReservation.filter('Group', groupId, updated.stockReservations);
|
|
465
|
+
|
|
466
|
+
// Technically we don't need to await this, but okay...
|
|
467
|
+
await Group.freeStockReservations(groupId, stocks);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (updated.shouldIncludeStock()) {
|
|
472
|
+
const myStockReservations: StockReservation[] = [];
|
|
473
|
+
|
|
474
|
+
// todo: build
|
|
475
|
+
|
|
476
|
+
updated.stockReservations = myStockReservations;
|
|
477
|
+
await updated.save();
|
|
478
|
+
} else {
|
|
479
|
+
if (updated.stockReservations.length) {
|
|
480
|
+
updated.stockReservations = [];
|
|
481
|
+
await updated.save();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
}).catch(console.error)
|
|
486
|
+
}
|
|
411
487
|
}
|
|
@@ -19,4 +19,10 @@ export class StripeCheckoutSession extends Model {
|
|
|
19
19
|
|
|
20
20
|
@column({ type: "string", nullable: true })
|
|
21
21
|
organizationId: string | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* For direct charges, this should be set
|
|
25
|
+
*/
|
|
26
|
+
@column({ type: "string", nullable: true })
|
|
27
|
+
accountId: string|null = null
|
|
22
28
|
}
|
|
@@ -19,4 +19,10 @@ export class StripePaymentIntent extends Model {
|
|
|
19
19
|
|
|
20
20
|
@column({ type: "string", nullable: true })
|
|
21
21
|
organizationId: string | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* For direct charges, this should be set
|
|
25
|
+
*/
|
|
26
|
+
@column({ type: "string", nullable: true })
|
|
27
|
+
accountId: string|null = null
|
|
22
28
|
}
|