@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.
Files changed (60) hide show
  1. package/dist/src/migrations/1722845608-registration-stock-reservations.sql +2 -0
  2. package/dist/src/migrations/1722845609-group-stock-reservations.sql +2 -0
  3. package/dist/src/migrations/1722852362-stripe-intents-account-id.sql +2 -0
  4. package/dist/src/migrations/1722852363-stripe-checkout-sessions-account-id.sql +2 -0
  5. package/dist/src/models/BalanceItem.d.ts +1 -0
  6. package/dist/src/models/BalanceItem.d.ts.map +1 -1
  7. package/dist/src/models/BalanceItem.js +36 -43
  8. package/dist/src/models/BalanceItem.js.map +1 -1
  9. package/dist/src/models/DocumentTemplate.js +1 -1
  10. package/dist/src/models/DocumentTemplate.js.map +1 -1
  11. package/dist/src/models/Email.d.ts.map +1 -1
  12. package/dist/src/models/Email.js +16 -8
  13. package/dist/src/models/Email.js.map +1 -1
  14. package/dist/src/models/EmailTemplate.d.ts.map +1 -1
  15. package/dist/src/models/EmailTemplate.js.map +1 -1
  16. package/dist/src/models/Event.d.ts +7 -0
  17. package/dist/src/models/Event.d.ts.map +1 -1
  18. package/dist/src/models/Event.js +28 -0
  19. package/dist/src/models/Event.js.map +1 -1
  20. package/dist/src/models/Group.d.ts +6 -3
  21. package/dist/src/models/Group.d.ts.map +1 -1
  22. package/dist/src/models/Group.js +29 -6
  23. package/dist/src/models/Group.js.map +1 -1
  24. package/dist/src/models/Member.d.ts +3 -2
  25. package/dist/src/models/Member.d.ts.map +1 -1
  26. package/dist/src/models/Member.js +32 -22
  27. package/dist/src/models/Member.js.map +1 -1
  28. package/dist/src/models/Payment.d.ts +5 -7
  29. package/dist/src/models/Payment.d.ts.map +1 -1
  30. package/dist/src/models/Payment.js +8 -13
  31. package/dist/src/models/Payment.js.map +1 -1
  32. package/dist/src/models/Registration.d.ts +18 -2
  33. package/dist/src/models/Registration.d.ts.map +1 -1
  34. package/dist/src/models/Registration.js +72 -8
  35. package/dist/src/models/Registration.js.map +1 -1
  36. package/dist/src/models/StripeCheckoutSession.d.ts +4 -0
  37. package/dist/src/models/StripeCheckoutSession.d.ts.map +1 -1
  38. package/dist/src/models/StripeCheckoutSession.js +7 -0
  39. package/dist/src/models/StripeCheckoutSession.js.map +1 -1
  40. package/dist/src/models/StripePaymentIntent.d.ts +4 -0
  41. package/dist/src/models/StripePaymentIntent.d.ts.map +1 -1
  42. package/dist/src/models/StripePaymentIntent.js +7 -0
  43. package/dist/src/models/StripePaymentIntent.js.map +1 -1
  44. package/package.json +2 -2
  45. package/src/migrations/1722845608-registration-stock-reservations.sql +2 -0
  46. package/src/migrations/1722845609-group-stock-reservations.sql +2 -0
  47. package/src/migrations/1722852362-stripe-intents-account-id.sql +2 -0
  48. package/src/migrations/1722852363-stripe-checkout-sessions-account-id.sql +2 -0
  49. package/src/models/BalanceItem.ts +40 -46
  50. package/src/models/DocumentTemplate.ts +1 -1
  51. package/src/models/Email.ts +28 -17
  52. package/src/models/EmailTemplate.ts +1 -1
  53. package/src/models/Event.ts +31 -0
  54. package/src/models/Group.ts +36 -14
  55. package/src/models/Member.ts +34 -23
  56. package/src/models/Payment.ts +10 -16
  57. package/src/models/Registration.ts +88 -12
  58. package/src/models/StripeAccount.ts +1 -1
  59. package/src/models/StripeCheckoutSession.ts +6 -0
  60. package/src/models/StripePaymentIntent.ts +6 -0
@@ -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, status: {sign: '!=', value: GroupStatus.Archived} })
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 cycle = ? and waitingList = 0 and registeredAt is not null",
220
- [this.id, this.cycle]
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 cycle = ? and ((waitingList = 0 and registeredAt is null AND reservedUntil >= ?) OR (waitingList = 1 and canRegister = 1))",
225
- [this.id, this.cycle, new Date()]
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")
@@ -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 { Member as MemberStruct, MemberDetails, MemberWithRegistrationsBlob, RegistrationWithMember as RegistrationWithMemberStruct, User as UserStruct, GroupStatus } from '@stamhoofd/structures';
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}\`.\`waitingList\` = 1)\n`
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, cycle: number): Promise<RegistrationWithMember[]> {
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}\`.\`waitingList\` = 1)\n`
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\` = ? AND \`${Registration.table}\`.\`cycle\` = ?`
186
+ query += `where \`${Registration.table}\`.\`groupId\` = ?`
187
187
 
188
- const [results] = await Database.select(query, [groupId, cycle])
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}\`.\`waitingList\` = 1)\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}\`.\`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: MemberStruct.create(registration.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
- const member = await Member.getWithRegistrations(id)
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 updateMemberships(this: MemberWithRegistrations) {
418
- console.log('Updating memberships for member: ' + this.id)
419
- return await QueueHandler.schedule('updateMemberships-' + this.id, async () => {
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 = this.registrations.filter(r => r.group.periodId == platform.periodId && !r.waitingList && r.registeredAt && !r.deactivatedAt)
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: this.id, periodId: platform.periodId })
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: ' + this.id + ' - membership ' + membership.id)
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: ' + this.id, ' - no default memberships found')
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: ' + this.id, ' - already has active memberships')
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: ' + this.id, ' - already has this membership')
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: ' + this.id + ' - membership type ' + cheapestMembership.membership.id)
500
+ console.log('Creating automatic membership for: ' + me.id + ' - membership type ' + cheapestMembership.membership.id)
494
501
  const membership = new MemberPlatformMembership();
495
- membership.memberId = this.id
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: ' + this.id + ' - membership ' + toDelete.id)
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
  }
@@ -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, members, groups} = await Payment.loadBalanceItemRelations(balanceItems);
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, members, balanceItemPayments, balanceItems, groups}: {
125
+ static async getGeneralStructureFromRelations({payments, registrations, orders, balanceItemPayments, balanceItems, groups}: {
127
126
  payments: Payment[];
128
- registrations: import("./Registration").Registration[];
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 && registrations.find(r => r.id === 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!).getStructure() : null,
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 Registration.getByIDs(...registrationIds)
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, members, groups}
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: string) {
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 \`groups\`.status != 'Archived' AND \`${Registration.table}\`.registeredAt is not null AND \`${Registration.table}\`.waitingList = 0`
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
  }
@@ -68,4 +68,4 @@ export class StripeAccount extends Model {
68
68
  bank_account_bank_name: account.external_accounts?.data[0]?.bank_name ?? this.meta.bank_account_bank_name ?? "",
69
69
  });
70
70
  }
71
- }
71
+ }
@@ -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
  }