@stamhoofd/models 2.8.0 → 2.10.2

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 (45) hide show
  1. package/dist/src/migrations/1723202125-member-number.sql +3 -0
  2. package/dist/src/migrations/1723218160-balance-item-unit-price.sql +2 -0
  3. package/dist/src/migrations/1723218161-balance-item-amount.sql +2 -0
  4. package/dist/src/migrations/1723218162-balance-item-type.sql +2 -0
  5. package/dist/src/migrations/1723218163-balance-item-type-defaults.sql +2 -0
  6. package/dist/src/migrations/1723218164-balance-item-relations.sql +2 -0
  7. package/dist/src/models/BalanceItem.d.ts +10 -3
  8. package/dist/src/models/BalanceItem.d.ts.map +1 -1
  9. package/dist/src/models/BalanceItem.js +25 -21
  10. package/dist/src/models/BalanceItem.js.map +1 -1
  11. package/dist/src/models/Event.d.ts.map +1 -1
  12. package/dist/src/models/Event.js +14 -0
  13. package/dist/src/models/Event.js.map +1 -1
  14. package/dist/src/models/Image.d.ts.map +1 -1
  15. package/dist/src/models/Image.js +3 -2
  16. package/dist/src/models/Image.js.map +1 -1
  17. package/dist/src/models/Member.d.ts +5 -2
  18. package/dist/src/models/Member.d.ts.map +1 -1
  19. package/dist/src/models/Member.js +141 -16
  20. package/dist/src/models/Member.js.map +1 -1
  21. package/dist/src/models/Order.d.ts.map +1 -1
  22. package/dist/src/models/Order.js +1 -1
  23. package/dist/src/models/Order.js.map +1 -1
  24. package/dist/src/models/Payment.d.ts +2 -6
  25. package/dist/src/models/Payment.d.ts.map +1 -1
  26. package/dist/src/models/Payment.js +5 -21
  27. package/dist/src/models/Payment.js.map +1 -1
  28. package/dist/src/models/Registration.d.ts +3 -1
  29. package/dist/src/models/Registration.d.ts.map +1 -1
  30. package/dist/src/models/Registration.js +9 -10
  31. package/dist/src/models/Registration.js.map +1 -1
  32. package/package.json +3 -4
  33. package/src/migrations/1723202125-member-number.sql +3 -0
  34. package/src/migrations/1723218160-balance-item-unit-price.sql +2 -0
  35. package/src/migrations/1723218161-balance-item-amount.sql +2 -0
  36. package/src/migrations/1723218162-balance-item-type.sql +2 -0
  37. package/src/migrations/1723218163-balance-item-type-defaults.sql +2 -0
  38. package/src/migrations/1723218164-balance-item-relations.sql +2 -0
  39. package/src/models/BalanceItem.ts +24 -25
  40. package/src/models/Event.ts +16 -1
  41. package/src/models/Image.ts +4 -2
  42. package/src/models/Member.ts +164 -18
  43. package/src/models/Order.ts +2 -2
  44. package/src/models/Payment.ts +6 -26
  45. package/src/models/Registration.ts +9 -11
@@ -1,11 +1,12 @@
1
1
  import { column, Database, ManyToManyRelation, ManyToOneRelation, Model, OneToManyRelation } from '@simonbackx/simple-database';
2
- import { SQL } from "@stamhoofd/sql";
2
+ import { scalarToSQLExpression, SQL, SQLWhereLike } from "@stamhoofd/sql";
3
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 { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
7
8
  import { QueueHandler } from '@stamhoofd/queues';
8
- import { Group, MemberPlatformMembership, Payment, Platform, Registration, User } from './';
9
+ import { Group, MemberPlatformMembership, Organization, Payment, Platform, Registration, User } from './';
9
10
  export type MemberWithRegistrations = Member & {
10
11
  users: User[],
11
12
  registrations: (Registration & {group: Group})[]
@@ -32,16 +33,14 @@ export class Member extends Model {
32
33
  type: "string",
33
34
  beforeSave: function() {
34
35
  return this.details?.firstName ?? ''
35
- },
36
- skipUpdate: true
36
+ }
37
37
  })
38
38
  firstName: string
39
39
 
40
40
  @column({ type: "string",
41
41
  beforeSave: function() {
42
42
  return this.details?.lastName ?? ''
43
- },
44
- skipUpdate: true })
43
+ } })
45
44
  lastName: string
46
45
 
47
46
  @column({
@@ -49,11 +48,19 @@ export class Member extends Model {
49
48
  nullable: true,
50
49
  beforeSave: function(this: Member) {
51
50
  return this.details?.birthDay ? Formatter.dateIso(this.details.birthDay) : null
52
- },
53
- skipUpdate: true
51
+ }
54
52
  })
55
53
  birthDay: string | null
56
54
 
55
+ @column({
56
+ type: "string",
57
+ nullable: true,
58
+ beforeSave: function() {
59
+ return this.details?.memberNumber ?? null
60
+ }
61
+ })
62
+ memberNumber: string | null
63
+
57
64
  @column({ type: "json", decoder: MemberDetails })
58
65
  details: MemberDetails
59
66
 
@@ -122,7 +129,7 @@ export class Member extends Model {
122
129
  LEFT JOIN (
123
130
  SELECT
124
131
  memberId,
125
- sum(price) - sum(pricePaid) AS outstandingBalance
132
+ sum(unitPrice * amount) - sum(pricePaid) AS outstandingBalance
126
133
  FROM
127
134
  balance_items
128
135
  WHERE status != 'Hidden'${firstWhere}
@@ -417,11 +424,15 @@ export class Member extends Model {
417
424
  });
418
425
  }
419
426
 
420
- static async updateMembershipsForId(id: string) {
427
+ static async updateMembershipsForId(id: string, silent = false) {
421
428
  return await QueueHandler.schedule('updateMemberships-' + id, async function (this: undefined) {
429
+ console.log('update memberships for id ', id);
430
+
422
431
  const me = await Member.getWithRegistrations(id)
423
432
  if (!me) {
424
- console.log('Skipping automatic membership for: ' + id, ' - member not found')
433
+ if (!silent) {
434
+ console.log('Skipping automatic membership for: ' + id, ' - member not found')
435
+ }
425
436
  return
426
437
  }
427
438
  const platform = await Platform.getShared()
@@ -453,23 +464,29 @@ export class Member extends Model {
453
464
  const activeMembershipsUndeletable = activeMemberships.filter(m => !m.canDelete() || !m.generated)
454
465
 
455
466
  if (defaultMemberships.length == 0) {
456
- // Stop all active memberships taht were added automatically
467
+ // Stop all active memberships that were added automatically
457
468
  for (const membership of activeMemberships) {
458
469
  if (membership.canDelete() && membership.generated) {
459
- console.log('Removing membership because no longer registered member and not yet invoiced for: ' + me.id + ' - membership ' + membership.id)
470
+ if (!silent) {
471
+ console.log('Removing membership because no longer registered member and not yet invoiced for: ' + me.id + ' - membership ' + membership.id)
472
+ }
460
473
  membership.deletedAt = new Date()
461
474
  await membership.save()
462
475
  }
463
476
  }
464
477
 
465
- console.log('Skipping automatic membership for: ' + me.id, ' - no default memberships found')
478
+ if (!silent) {
479
+ console.log('Skipping automatic membership for: ' + me.id, ' - no default memberships found')
480
+ }
466
481
  return
467
482
  }
468
483
 
469
484
 
470
485
  if (activeMembershipsUndeletable.length) {
471
486
  // Skip automatic additions
472
- console.log('Skipping automatic membership for: ' + me.id, ' - already has active memberships')
487
+ if (!silent) {
488
+ console.log('Skipping automatic membership for: ' + me.id, ' - already has active memberships')
489
+ }
473
490
  return
474
491
  }
475
492
 
@@ -487,7 +504,9 @@ export class Member extends Model {
487
504
 
488
505
  // Check if already have the same membership
489
506
  if (activeMemberships.find(m => m.membershipTypeId == cheapestMembership.membership.id)) {
490
- console.log('Skipping automatic membership for: ' + me.id, ' - already has this membership')
507
+ if (!silent) {
508
+ console.log('Skipping automatic membership for: ' + me.id, ' - already has this membership')
509
+ }
491
510
  return
492
511
  }
493
512
 
@@ -497,7 +516,9 @@ export class Member extends Model {
497
516
  }
498
517
 
499
518
  // Can we revive an earlier deleted membership?
500
- console.log('Creating automatic membership for: ' + me.id + ' - membership type ' + cheapestMembership.membership.id)
519
+ if (!silent) {
520
+ console.log('Creating automatic membership for: ' + me.id + ' - membership type ' + cheapestMembership.membership.id)
521
+ }
501
522
  const membership = new MemberPlatformMembership();
502
523
  membership.memberId = me.id
503
524
  membership.membershipTypeId = cheapestMembership.membership.id
@@ -509,13 +530,25 @@ export class Member extends Model {
509
530
  membership.expireDate = periodConfig.expireDate
510
531
  membership.generated = true;
511
532
 
533
+ if(me.details.memberNumber === null) {
534
+ try {
535
+ await me.assignMemberNumber(membership);
536
+ } catch(error) {
537
+ console.error(`Failed to assign member number for id ${me.id}: ${error.message}`);
538
+ // If the assignment of the member number fails the membership is not created but the member is registered
539
+ return;
540
+ }
541
+ }
542
+
512
543
  await membership.calculatePrice()
513
544
  await membership.save()
514
545
 
515
546
  // This reasoning allows us to replace an existing membership with a cheaper one (not date based ones, but type based ones)
516
547
  for (const toDelete of activeMemberships) {
517
548
  if (toDelete.canDelete() && toDelete.generated) {
518
- console.log('Removing membership because cheaper membership found for: ' + me.id + ' - membership ' + toDelete.id)
549
+ if (!silent) {
550
+ console.log('Removing membership because cheaper membership found for: ' + me.id + ' - membership ' + toDelete.id)
551
+ }
519
552
  toDelete.deletedAt = new Date()
520
553
  await toDelete.save()
521
554
  }
@@ -523,6 +556,119 @@ export class Member extends Model {
523
556
  });
524
557
  }
525
558
 
559
+ private async assignMemberNumber(membership: MemberPlatformMembership) {
560
+ const member: Member = this;
561
+
562
+ if (member.details?.memberNumber) {
563
+ console.log('Member already has member number, should not happen');
564
+ return
565
+ }
566
+
567
+ return await QueueHandler.schedule('assignMemberNumber', async function (this: undefined) {
568
+ try {
569
+ const memberNumber = await member.createMemberNumber(membership);
570
+ member.details.memberNumber = memberNumber;
571
+ await member.save();
572
+ } catch(error) {
573
+ if(isSimpleError(error) || isSimpleErrors(error)) {
574
+ throw error;
575
+ } else {
576
+ console.error(error);
577
+ throw new SimpleError({
578
+ code: 'assign_member_number',
579
+ message: error.message,
580
+ human: "Er is iets misgegaan bij het aanmaken van het lidnummer.",
581
+ })
582
+ }
583
+ }
584
+ });
585
+ }
586
+
587
+ async createMemberNumber(membership: MemberPlatformMembership): Promise<string> {
588
+ // example: 5301-101012-1
589
+
590
+ //#region get birth date part (ddmmjj)
591
+ const birthDay = this.details?.birthDay;
592
+ if(!birthDay) {
593
+ throw new SimpleError({
594
+ code: 'assign_member_number',
595
+ message: "Missing birthDay",
596
+ human: "Er kon geen lidnummer aangemaakt worden omdat er geen geboortedatum is ingesteld.",
597
+ });
598
+ }
599
+
600
+ const dayPart = birthDay.getDate().toString().padStart(2, '0');
601
+ const monthPart = (birthDay.getMonth() + 1).toString().padStart(2, '0');
602
+ const yearPart = birthDay.getFullYear().toString().slice(2, 4);
603
+ const birthDatePart = `${dayPart}${monthPart}${yearPart}`;
604
+ //#endregion
605
+
606
+ //#region get group number
607
+ const organizationId = membership.organizationId;
608
+ const organization = await Organization.getByID(organizationId);
609
+ if(!organization) {
610
+ throw new Error(`Organization with id ${organizationId} not found`);
611
+ }
612
+ const groupNumber = organization.uri;
613
+ //#endregion
614
+
615
+ //#region get follow up number
616
+ const firstPart = `${groupNumber}-${birthDatePart}-`;
617
+
618
+ const query = SQL.select()
619
+ .from(SQL.table('members'))
620
+ .where(
621
+ new SQLWhereLike(
622
+ SQL.column('members', 'memberNumber'),
623
+ scalarToSQLExpression(`${SQLWhereLike.escape(firstPart)}%`)
624
+ )
625
+ );
626
+
627
+ const count = await query.count();
628
+ console.log(`Found ${count} members with a memberNumber starting with ${firstPart}`);
629
+
630
+ let followUpNumber = count;
631
+ //#endregion
632
+
633
+ //#region check if memberNumber is unique
634
+ let doesExist = true;
635
+ let memberNumber: string = '';
636
+ let tries = 0;
637
+
638
+ while(doesExist) {
639
+ followUpNumber++;
640
+ memberNumber = firstPart + followUpNumber;
641
+
642
+ const result = await SQL.select()
643
+ .from(SQL.table('members'))
644
+ .where(
645
+ SQL.column('members', 'memberNumber'),
646
+ scalarToSQLExpression(memberNumber)
647
+ )
648
+ .first(false);
649
+
650
+ console.log(`Is ${memberNumber} unique? ${result === null}`);
651
+
652
+ if(result !== null) {
653
+ tries++;
654
+ if(tries > 9) {
655
+ throw new SimpleError({
656
+ code: 'assign_member_number',
657
+ message: `Duplicate member numbers (last try: ${memberNumber}, tries: ${tries})`,
658
+ human: "Er kon geen uniek lidnummer aangemaakt worden. Mogelijks zijn er teveel leden met dezelfde geboortedatum. Neem contact op met de vereniging.",
659
+ });
660
+ }
661
+ } else {
662
+ doesExist = false;
663
+ }
664
+ }
665
+ //#endregion
666
+
667
+ console.log(`Created member number: ${memberNumber}`);
668
+
669
+ return memberNumber;
670
+ }
671
+
526
672
  async updateMemberships() {
527
673
  return await Member.updateMembershipsForId(this.id)
528
674
  }
@@ -2,7 +2,7 @@ import { column, ManyToOneRelation, Model } from "@simonbackx/simple-database";
2
2
  import { SimpleError } from "@simonbackx/simple-errors";
3
3
  import { Email } from '@stamhoofd/email';
4
4
  import { QueueHandler } from "@stamhoofd/queues";
5
- import { BalanceItemPaymentWithPrivatePayment,BalanceItemWithPayments, BalanceItemWithPrivatePayments, EmailTemplateType, MemberBalanceItemPayment, Order as OrderStruct, OrderData, OrderStatus, Payment as PaymentStruct, PaymentMethod, PrivateOrder, PrivatePayment, ProductType, Recipient, Replacement, WebshopPreview, WebshopStatus, WebshopTicketType, WebshopTimeSlot } from '@stamhoofd/structures';
5
+ import { BalanceItemPaymentWithPrivatePayment,BalanceItemWithPayments, BalanceItemWithPrivatePayments, EmailTemplateType, BalanceItemPaymentWithPayment, Order as OrderStruct, OrderData, OrderStatus, Payment as PaymentStruct, PaymentMethod, PrivateOrder, PrivatePayment, ProductType, Recipient, Replacement, WebshopPreview, WebshopStatus, WebshopTicketType, WebshopTimeSlot } from '@stamhoofd/structures';
6
6
  import { Formatter } from "@stamhoofd/utility";
7
7
  import { v4 as uuidv4 } from "uuid";
8
8
 
@@ -780,7 +780,7 @@ export class Order extends Model {
780
780
  ...balanceItem,
781
781
  payments: balanceItemPayments.filter(b => b.balanceItemId === balanceItem.id).map(balanceItemPayment => {
782
782
  const payment = payments.find(pp => pp.id === balanceItemPayment.paymentId)!
783
- return MemberBalanceItemPayment.create({
783
+ return BalanceItemPaymentWithPayment.create({
784
784
  ...balanceItemPayment,
785
785
  payment: PaymentStruct.create(payment)
786
786
  })
@@ -110,50 +110,33 @@ export class Payment extends Model {
110
110
  }
111
111
 
112
112
  const {balanceItemPayments, balanceItems} = await Payment.loadBalanceItems(payments)
113
- const {registrations, orders, groups} = await Payment.loadBalanceItemRelations(balanceItems);
114
113
 
115
- return await this.getGeneralStructureFromRelations({
114
+ return this.getGeneralStructureFromRelations({
116
115
  payments,
117
- registrations,
118
- orders,
119
116
  balanceItemPayments,
120
- balanceItems,
121
- groups
117
+ balanceItems
122
118
  }, includeSettlements)
123
119
  }
124
120
 
125
- static async getGeneralStructureFromRelations({payments, registrations, orders, balanceItemPayments, balanceItems, groups}: {
121
+ static getGeneralStructureFromRelations({payments, balanceItemPayments, balanceItems}: {
126
122
  payments: Payment[];
127
- registrations: import("./Member").RegistrationWithMember[];
128
- orders: import("./Order").Order[];
129
123
  balanceItemPayments: import("./BalanceItemPayment").BalanceItemPayment[];
130
124
  balanceItems: import("./BalanceItem").BalanceItem[];
131
- groups: import("./Group").Group[];
132
- }, includeSettlements = false): Promise<PaymentGeneral[]> {
125
+ }, includeSettlements = false): PaymentGeneral[] {
133
126
  if (payments.length === 0) {
134
127
  return []
135
128
  }
136
- const {Member} = (await import("./Member"));
137
129
 
138
130
  return payments.map(payment => {
139
131
  return PaymentGeneral.create({
140
132
  ...payment,
141
133
  balanceItemPayments: balanceItemPayments.filter(item => item.paymentId === payment.id).map((item) => {
142
134
  const balanceItem = balanceItems.find(b => b.id === item.balanceItemId)
143
- const registration = balanceItem?.registrationId ? registrations.find(r => r.id === balanceItem.registrationId) : null
144
- const order = balanceItem?.orderId && orders.find(r => r.id === balanceItem.orderId)
145
- const group = registration ? groups.find(g => g.id === registration.groupId) : null
146
-
147
- if (registration && !group) {
148
- throw new Error("Group "+registration.groupId+" not found")
149
- }
150
135
 
151
136
  return BalanceItemPaymentDetailed.create({
152
137
  ...item,
153
138
  balanceItem: BalanceItemDetailed.create({
154
- ...balanceItem,
155
- registration: registration ? Member.getRegistrationWithMemberStructure(registration.setRelation(Registration.group, group!)) : null,
156
- order: order ? OrderStruct.create({...order, payment: null}) : null
139
+ ...balanceItem
157
140
  })
158
141
  })
159
142
  }),
@@ -202,9 +185,6 @@ export class Payment extends Model {
202
185
  const registrations = await Member.getRegistrationWithMembersByIDs(registrationIds)
203
186
  const orders = await Order.getByIDs(...orderIds)
204
187
 
205
- const groupIds = Formatter.uniqueArray(registrations.map(r => r.groupId))
206
- const groups = await (await import("./Group")).Group.getByIDs(...groupIds)
207
-
208
- return {registrations, orders, groups}
188
+ return {registrations, orders}
209
189
  }
210
190
  }
@@ -143,7 +143,7 @@ export class Registration extends Model {
143
143
  LEFT JOIN (
144
144
  SELECT
145
145
  registrationId,
146
- sum(price) AS price,
146
+ sum(unitPrice * amount) AS price,
147
147
  sum(pricePaid) AS pricePaid
148
148
  FROM
149
149
  balance_items
@@ -196,7 +196,7 @@ export class Registration extends Model {
196
196
  await Member.updateMembershipsForId(this.memberId)
197
197
  }
198
198
 
199
- async markValid(this: Registration) {
199
+ async markValid(this: Registration, options?: {skipEmail?: boolean}) {
200
200
  if (this.registeredAt !== null && this.deactivatedAt === null) {
201
201
  await this.save();
202
202
  return false;
@@ -212,9 +212,11 @@ export class Registration extends Model {
212
212
  const {Member} = await import('./Member');
213
213
  await Member.updateMembershipsForId(this.memberId)
214
214
 
215
- await this.sendEmailTemplate({
216
- type: EmailTemplateType.RegistrationConfirmation
217
- });
215
+ if (options?.skipEmail !== true) {
216
+ await this.sendEmailTemplate({
217
+ type: EmailTemplateType.RegistrationConfirmation
218
+ });
219
+ }
218
220
 
219
221
  const member = await Member.getByID(this.memberId);
220
222
  if (member) {
@@ -346,11 +348,7 @@ export class Registration extends Model {
346
348
  const template = templates[0]
347
349
 
348
350
  const paymentGeneral = await payment.getGeneralStructure();
349
- const registrations = paymentGeneral.registrations
350
- let groupIds = registrations.map(r => r.groupId);
351
-
352
- // Remove duplicate groupIds
353
- groupIds = groupIds.filter((v, i, a) => a.indexOf(v) === i);
351
+ const groupIds = paymentGeneral.groupIds;
354
352
 
355
353
  const recipients = [
356
354
  Recipient.create({
@@ -386,7 +384,7 @@ export class Registration extends Model {
386
384
  }),
387
385
  Replacement.create({
388
386
  token: "overviewContext",
389
- value: "Inschrijving van " + paymentGeneral.memberFirstNames
387
+ value: "Inschrijving van " + paymentGeneral.memberNames
390
388
  }),
391
389
  Replacement.create({
392
390
  token: "memberNames",