@stamhoofd/models 2.3.0 → 2.4.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 (100) hide show
  1. package/dist/src/factories/GroupFactory.d.ts.map +1 -1
  2. package/dist/src/factories/GroupFactory.js +5 -5
  3. package/dist/src/factories/GroupFactory.js.map +1 -1
  4. package/dist/src/helpers/EmailBuilder.d.ts +3 -2
  5. package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
  6. package/dist/src/helpers/EmailBuilder.js +29 -16
  7. package/dist/src/helpers/EmailBuilder.js.map +1 -1
  8. package/dist/src/helpers/GroupBuilder.d.ts.map +1 -1
  9. package/dist/src/helpers/GroupBuilder.js +0 -23
  10. package/dist/src/helpers/GroupBuilder.js.map +1 -1
  11. package/dist/src/migrations/1721050380-email-table.sql +24 -0
  12. package/dist/src/migrations/1721050381-email-recipients-table.sql +18 -0
  13. package/dist/src/migrations/1721342679-responsibility-groupId.sql +2 -0
  14. package/dist/src/migrations/1721342680-responsibility-groupId-foreign-key.sql +1 -0
  15. package/dist/src/migrations/1721400546-users-memberId.sql +3 -0
  16. package/dist/src/migrations/1721639159-membership-deleted-at.sql +2 -0
  17. package/dist/src/migrations/1721639160-membership-generated.sql +2 -0
  18. package/dist/src/migrations/1721841819-group-type.sql +2 -0
  19. package/dist/src/migrations/1722090482-events.sql +18 -0
  20. package/dist/src/models/DocumentTemplate.js +14 -14
  21. package/dist/src/models/DocumentTemplate.js.map +1 -1
  22. package/dist/src/models/Email.d.ts +41 -0
  23. package/dist/src/models/Email.d.ts.map +1 -0
  24. package/dist/src/models/Email.js +490 -0
  25. package/dist/src/models/Email.js.map +1 -0
  26. package/dist/src/models/EmailRecipient.d.ts +20 -0
  27. package/dist/src/models/EmailRecipient.d.ts.map +1 -0
  28. package/dist/src/models/EmailRecipient.js +95 -0
  29. package/dist/src/models/EmailRecipient.js.map +1 -0
  30. package/dist/src/models/EmailTemplate.d.ts +2 -1
  31. package/dist/src/models/EmailTemplate.d.ts.map +1 -1
  32. package/dist/src/models/EmailTemplate.js +4 -0
  33. package/dist/src/models/EmailTemplate.js.map +1 -1
  34. package/dist/src/models/Event.d.ts +19 -0
  35. package/dist/src/models/Event.d.ts.map +1 -0
  36. package/dist/src/models/Event.js +78 -0
  37. package/dist/src/models/Event.js.map +1 -0
  38. package/dist/src/models/Group.d.ts +2 -1
  39. package/dist/src/models/Group.d.ts.map +1 -1
  40. package/dist/src/models/Group.js +9 -22
  41. package/dist/src/models/Group.js.map +1 -1
  42. package/dist/src/models/Member.d.ts +1 -0
  43. package/dist/src/models/Member.d.ts.map +1 -1
  44. package/dist/src/models/Member.js +42 -11
  45. package/dist/src/models/Member.js.map +1 -1
  46. package/dist/src/models/MemberPlatformMembership.d.ts +7 -0
  47. package/dist/src/models/MemberPlatformMembership.d.ts.map +1 -1
  48. package/dist/src/models/MemberPlatformMembership.js +22 -0
  49. package/dist/src/models/MemberPlatformMembership.js.map +1 -1
  50. package/dist/src/models/MemberResponsibilityRecord.d.ts +3 -0
  51. package/dist/src/models/MemberResponsibilityRecord.d.ts.map +1 -1
  52. package/dist/src/models/MemberResponsibilityRecord.js +8 -0
  53. package/dist/src/models/MemberResponsibilityRecord.js.map +1 -1
  54. package/dist/src/models/Organization.d.ts +8 -1
  55. package/dist/src/models/Organization.d.ts.map +1 -1
  56. package/dist/src/models/Organization.js +37 -9
  57. package/dist/src/models/Organization.js.map +1 -1
  58. package/dist/src/models/OrganizationRegistrationPeriod.d.ts +1 -0
  59. package/dist/src/models/OrganizationRegistrationPeriod.d.ts.map +1 -1
  60. package/dist/src/models/OrganizationRegistrationPeriod.js +7 -0
  61. package/dist/src/models/OrganizationRegistrationPeriod.js.map +1 -1
  62. package/dist/src/models/RegistrationPeriod.d.ts +1 -0
  63. package/dist/src/models/RegistrationPeriod.d.ts.map +1 -1
  64. package/dist/src/models/RegistrationPeriod.js +12 -0
  65. package/dist/src/models/RegistrationPeriod.js.map +1 -1
  66. package/dist/src/models/User.d.ts +2 -1
  67. package/dist/src/models/User.d.ts.map +1 -1
  68. package/dist/src/models/User.js +13 -3
  69. package/dist/src/models/User.js.map +1 -1
  70. package/dist/src/models/index.d.ts +3 -0
  71. package/dist/src/models/index.d.ts.map +1 -1
  72. package/dist/src/models/index.js +3 -0
  73. package/dist/src/models/index.js.map +1 -1
  74. package/package.json +2 -2
  75. package/src/factories/GroupFactory.ts +6 -6
  76. package/src/helpers/EmailBuilder.ts +33 -18
  77. package/src/helpers/GroupBuilder.ts +0 -23
  78. package/src/migrations/1721050380-email-table.sql +24 -0
  79. package/src/migrations/1721050381-email-recipients-table.sql +18 -0
  80. package/src/migrations/1721342679-responsibility-groupId.sql +2 -0
  81. package/src/migrations/1721342680-responsibility-groupId-foreign-key.sql +1 -0
  82. package/src/migrations/1721400546-users-memberId.sql +3 -0
  83. package/src/migrations/1721639159-membership-deleted-at.sql +2 -0
  84. package/src/migrations/1721639160-membership-generated.sql +2 -0
  85. package/src/migrations/1721841819-group-type.sql +2 -0
  86. package/src/migrations/1722090482-events.sql +18 -0
  87. package/src/models/DocumentTemplate.ts +2 -2
  88. package/src/models/Email.ts +556 -0
  89. package/src/models/EmailRecipient.ts +81 -0
  90. package/src/models/EmailTemplate.ts +5 -1
  91. package/src/models/Event.ts +71 -0
  92. package/src/models/Group.ts +10 -37
  93. package/src/models/Member.ts +60 -12
  94. package/src/models/MemberPlatformMembership.ts +21 -0
  95. package/src/models/MemberResponsibilityRecord.ts +7 -0
  96. package/src/models/Organization.ts +42 -9
  97. package/src/models/OrganizationRegistrationPeriod.ts +8 -0
  98. package/src/models/RegistrationPeriod.ts +14 -0
  99. package/src/models/User.ts +13 -3
  100. package/src/models/index.ts +3 -0
@@ -0,0 +1,71 @@
1
+
2
+ import { column, Model } from "@simonbackx/simple-database";
3
+ import { EventMeta, Event as EventStruct } from '@stamhoofd/structures';
4
+ import { v4 as uuidv4 } from "uuid";
5
+ import { Group } from "./Group";
6
+
7
+ export class Event extends Model {
8
+ static table = "events";
9
+
10
+ @column({ primary: true, type: "string", beforeSave(value) {
11
+ return value ?? uuidv4();
12
+ } })
13
+ id!: string;
14
+
15
+ @column({ type: "string" })
16
+ name: string
17
+
18
+ @column({ type: "string" })
19
+ typeId: string
20
+
21
+ @column({ type: "string", nullable: true })
22
+ organizationId: string|null = null
23
+
24
+ @column({ type: "string", nullable: true })
25
+ groupId: string|null = null
26
+
27
+ @column({ type: "datetime" })
28
+ startDate: Date
29
+
30
+ @column({ type: "datetime" })
31
+ endDate: Date
32
+
33
+ @column({ type: "json", decoder: EventMeta })
34
+ meta = EventMeta.create({})
35
+
36
+ @column({
37
+ type: "datetime", beforeSave(old?: any) {
38
+ if (old !== undefined) {
39
+ return old;
40
+ }
41
+ const date = new Date()
42
+ date.setMilliseconds(0)
43
+ return date
44
+ }
45
+ })
46
+ createdAt: Date
47
+
48
+ @column({
49
+ type: "datetime", beforeSave() {
50
+ const date = new Date()
51
+ date.setMilliseconds(0)
52
+ return date
53
+ },
54
+ skipUpdate: true
55
+ })
56
+ updatedAt: Date
57
+
58
+ getStructure(group?: Group|null) {
59
+ return EventStruct.create({
60
+ ...this,
61
+ group: group ? group.getStructure() : null
62
+ })
63
+ }
64
+
65
+ getPrivateStructure(group?: Group|null) {
66
+ return EventStruct.create({
67
+ ...this,
68
+ group: group ? group.getPrivateStructure() : null
69
+ })
70
+ }
71
+ }
@@ -1,9 +1,9 @@
1
1
  import { column, Database, ManyToOneRelation, Model, OneToManyRelation } from '@simonbackx/simple-database';
2
- import { CycleInformation, Group as GroupStruct, GroupCategory, GroupPrivateSettings, GroupSettings, GroupStatus } from '@stamhoofd/structures';
2
+ import { GroupCategory, GroupPrivateSettings, GroupSettings, GroupStatus, Group as GroupStruct, GroupType } from '@stamhoofd/structures';
3
3
  import { v4 as uuidv4 } from "uuid";
4
4
 
5
- import { Member, MemberWithRegistrations, OrganizationRegistrationPeriod, Payment, Registration, User } from './';
6
5
  import { Formatter } from '@stamhoofd/utility';
6
+ import { Member, MemberWithRegistrations, OrganizationRegistrationPeriod, Payment, Registration, User } from './';
7
7
 
8
8
  if (Member === undefined) {
9
9
  throw new Error("Import Member is undefined")
@@ -28,6 +28,9 @@ export class Group extends Model {
28
28
  })
29
29
  id!: string;
30
30
 
31
+ @column({ type: "string" })
32
+ type = GroupType.Membership;
33
+
31
34
  @column({ type: "json", decoder: GroupSettings })
32
35
  settings: GroupSettings;
33
36
 
@@ -207,7 +210,6 @@ export class Group extends Model {
207
210
  "groupId = ? and cycle = ? and waitingList = 0 and registeredAt is not null",
208
211
  [this.id, this.cycle]
209
212
  )
210
- //const query = `select count(*) as c from \`${Registration.table}\` where groupId = ? and cycle = ? and (((registeredAt is not null or reservedUntil >= ?) and waitingList = 0) OR (waitingList = 1 AND canRegister = 1))`
211
213
 
212
214
  this.settings.reservedMembers = await Group.getCount(
213
215
  "groupId = ? and cycle = ? and ((waitingList = 0 and registeredAt is null AND reservedUntil >= ?) OR (waitingList = 1 and canRegister = 1))",
@@ -218,37 +220,6 @@ export class Group extends Model {
218
220
  "groupId = ? and cycle = ? and waitingList = 1",
219
221
  [this.id, this.cycle, new Date()]
220
222
  )
221
-
222
- // Loop cycle -1 until current (excluding current)
223
- for (let cycle = -1; cycle < this.cycle; cycle++) {
224
- if (!this.settings.cycleSettings.has(cycle)) {
225
- this.settings.cycleSettings.set(cycle, CycleInformation.create({
226
- registeredMembers: 0,
227
- reservedMembers: 0,
228
- waitingListSize: 0
229
- }))
230
- }
231
- }
232
-
233
- // Older cycles
234
- // todo: optimize this a bit
235
- for (const [cycle, info] of this.settings.cycleSettings) {
236
-
237
- info.registeredMembers = await Group.getCount(
238
- "groupId = ? and cycle = ? and waitingList = 0 and registeredAt is not null",
239
- [this.id, cycle]
240
- )
241
-
242
- info.reservedMembers = await Group.getCount(
243
- "groupId = ? and cycle = ? and ((waitingList = 0 and registeredAt is null AND reservedUntil >= ?) OR (waitingList = 1 and canRegister = 1))",
244
- [this.id, cycle, new Date()]
245
- )
246
-
247
- info.waitingListSize = await Group.getCount(
248
- "groupId = ? and cycle = ? and waitingList = 1",
249
- [this.id, cycle, new Date()]
250
- )
251
- }
252
223
  }
253
224
 
254
225
  static async deleteUnreachable(organizationId: string, period: OrganizationRegistrationPeriod, allGroups: Group[]) {
@@ -282,10 +253,12 @@ export class Group extends Model {
282
253
  }
283
254
 
284
255
  for (const group of allGroups) {
285
- if (!reachable.get(group.id) && group.status !== GroupStatus.Archived) {
286
- console.log("Archiving unreachable group "+group.id+" from organization "+organizationId + " org period "+period.id)
287
- group.status = GroupStatus.Archived
256
+ if (!reachable.get(group.id) && group.deletedAt === null) {
257
+ console.log("Deleting unreachable group "+group.id+" from organization "+organizationId + " org period "+period.id)
258
+ group.deletedAt = new Date()
288
259
  await group.save()
260
+
261
+ Member.updateMembershipsForGroupId(group.id)
289
262
  }
290
263
  }
291
264
  }
@@ -1,6 +1,6 @@
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 } from '@stamhoofd/structures';
3
+ import { Member as MemberStruct, MemberDetails, MemberWithRegistrationsBlob, RegistrationWithMember as RegistrationWithMemberStruct, User as UserStruct, GroupStatus } from '@stamhoofd/structures';
4
4
  import { Formatter, Sorter } from '@stamhoofd/utility';
5
5
  import { v4 as uuidv4 } from "uuid";
6
6
 
@@ -300,7 +300,9 @@ export class Member extends Model {
300
300
  if (!g) {
301
301
  throw new Error("Group not found")
302
302
  }
303
- member.registrations.push(registration.setRelation(Registration.group, g))
303
+ if (g.deletedAt === null) {
304
+ member.registrations.push(registration.setRelation(Registration.group, g))
305
+ }
304
306
  }
305
307
  }
306
308
 
@@ -370,10 +372,7 @@ export class Member extends Model {
370
372
  ...this,
371
373
  registrations: this.registrations.map(r => r.getStructure()),
372
374
  details: this.details,
373
- users: this.users.map(u => UserStruct.create({
374
- ...u,
375
- hasAccount: u.hasAccount()
376
- })),
375
+ users: this.users.map(u => u.getStructure()),
377
376
  })
378
377
  }
379
378
 
@@ -385,6 +384,36 @@ export class Member extends Model {
385
384
  })
386
385
  }
387
386
 
387
+ static updateMembershipsForGroupId(id: string) {
388
+ QueueHandler.schedule('bulk-update-memberships', async () => {
389
+ console.log('Bulk updating memberships for group id ', id)
390
+
391
+ // Get all members that are registered in this group
392
+ const memberIds = (await SQL.select(
393
+ SQL.column('members', 'id')
394
+ )
395
+ .from(SQL.table(Member.table))
396
+ .join(
397
+ SQL.leftJoin(
398
+ SQL.table(Registration.table)
399
+ ).where(
400
+ SQL.column(Registration.table, 'memberId'),
401
+ SQL.column(Member.table, 'id')
402
+ )
403
+ ).where(
404
+ SQL.column(Registration.table, 'groupId'),
405
+ id
406
+ ).fetch()).flatMap(r => (r.members && (typeof r.members.id) === 'string') ? [r.members.id as string] : [])
407
+
408
+ for (const id of memberIds) {
409
+ const member = await Member.getWithRegistrations(id)
410
+ await member?.updateMemberships()
411
+ }
412
+ }).catch((e) => {
413
+ console.error('Failed to update memberships for group id ', id), e
414
+ });
415
+ }
416
+
388
417
  async updateMemberships(this: MemberWithRegistrations) {
389
418
  console.log('Updating memberships for member: ' + this.id)
390
419
  return await QueueHandler.schedule('updateMemberships-' + this.id, async () => {
@@ -410,17 +439,19 @@ export class Member extends Model {
410
439
  membership: defaultMembership,
411
440
  }]
412
441
  })
413
- // Get active memberships for this member
442
+ // Get active memberships for this member that
414
443
  const memberships = await MemberPlatformMembership.where({memberId: this.id, periodId: platform.periodId })
415
444
  const now = new Date()
416
- const activeMemberships = memberships.filter(m => m.startDate <= now && m.endDate >= now)
445
+ const activeMemberships = memberships.filter(m => m.startDate <= now && m.endDate >= now && m.deletedAt === null)
446
+ const activeMembershipsUndeletable = activeMemberships.filter(m => !m.canDelete() || !m.generated)
417
447
 
418
448
  if (defaultMemberships.length == 0) {
419
- // Stop all active memberships
449
+ // Stop all active memberships taht were added automatically
420
450
  for (const membership of activeMemberships) {
421
- if (!membership.invoiceId && !membership.invoiceItemDetailId) {
451
+ if (membership.canDelete() && membership.generated) {
422
452
  console.log('Removing membership because no longer registered member and not yet invoiced for: ' + this.id + ' - membership ' + membership.id)
423
- await membership.delete()
453
+ membership.deletedAt = new Date()
454
+ await membership.save()
424
455
  }
425
456
  }
426
457
 
@@ -429,7 +460,7 @@ export class Member extends Model {
429
460
  }
430
461
 
431
462
 
432
- if (activeMemberships.length) {
463
+ if (activeMembershipsUndeletable.length) {
433
464
  // Skip automatic additions
434
465
  console.log('Skipping automatic membership for: ' + this.id, ' - already has active memberships')
435
466
  return
@@ -447,11 +478,18 @@ export class Member extends Model {
447
478
  throw new Error("No membership found")
448
479
  }
449
480
 
481
+ // Check if already have the same membership
482
+ if (activeMemberships.find(m => m.membershipTypeId == cheapestMembership.membership.id)) {
483
+ console.log('Skipping automatic membership for: ' + this.id, ' - already has this membership')
484
+ return
485
+ }
486
+
450
487
  const periodConfig = cheapestMembership.membership.periods.get(platform.periodId)
451
488
  if (!periodConfig) {
452
489
  throw new Error("Period config not found")
453
490
  }
454
491
 
492
+ // Can we revive an earlier deleted membership?
455
493
  console.log('Creating automatic membership for: ' + this.id + ' - membership type ' + cheapestMembership.membership.id)
456
494
  const membership = new MemberPlatformMembership();
457
495
  membership.memberId = this.id
@@ -462,9 +500,19 @@ export class Member extends Model {
462
500
  membership.startDate = periodConfig.startDate
463
501
  membership.endDate = periodConfig.endDate
464
502
  membership.expireDate = periodConfig.expireDate
503
+ membership.generated = true;
465
504
 
466
505
  await membership.calculatePrice()
467
506
  await membership.save()
507
+
508
+ // This reasoning allows us to replace an existing membership with a cheaper one (not date based ones, but type based ones)
509
+ for (const toDelete of activeMemberships) {
510
+ if (toDelete.canDelete() && toDelete.generated) {
511
+ console.log('Removing membership because cheaper membership found for: ' + this.id + ' - membership ' + toDelete.id)
512
+ toDelete.deletedAt = new Date()
513
+ await toDelete.save()
514
+ }
515
+ }
468
516
  });
469
517
  }
470
518
  }
@@ -47,6 +47,17 @@ export class MemberPlatformMembership extends Model {
47
47
  @column({ type: "integer" })
48
48
  price = 0;
49
49
 
50
+ /**
51
+ * Whether this was added automatically by the system
52
+ */
53
+ @column({ type: "boolean" })
54
+ generated = false
55
+
56
+ @column({
57
+ type: "datetime", nullable: true
58
+ })
59
+ deletedAt: Date|null = null
60
+
50
61
  @column({
51
62
  type: "datetime", beforeSave(old?: any) {
52
63
  if (old !== undefined) {
@@ -69,6 +80,16 @@ export class MemberPlatformMembership extends Model {
69
80
  })
70
81
  updatedAt: Date
71
82
 
83
+ canDelete() {
84
+ if (this.invoiceId || this.invoiceItemDetailId) {
85
+ return false;
86
+ }
87
+ return true;
88
+ }
89
+
90
+ delete(): Promise<void> {
91
+ throw new Error('Cannot delete a membership. Use the deletedAt column.');
92
+ }
72
93
 
73
94
  async calculatePrice() {
74
95
  if (this.invoiceId || this.invoiceItemDetailId) {
@@ -1,5 +1,6 @@
1
1
  import { column, Model } from '@simonbackx/simple-database';
2
2
  import { v4 as uuidv4 } from "uuid";
3
+ import { MemberResponsibilityRecord as MemberResponsibilityRecordStruct } from '@stamhoofd/structures';
3
4
 
4
5
  export class MemberResponsibilityRecord extends Model {
5
6
  static table = "member_responsibility_records"
@@ -15,6 +16,9 @@ export class MemberResponsibilityRecord extends Model {
15
16
  @column({ type: "string", nullable: true })
16
17
  organizationId: string|null = null;
17
18
 
19
+ @column({ type: "string", nullable: true })
20
+ groupId: string|null = null;
21
+
18
22
  @column({ type: "string" })
19
23
  memberId: string
20
24
 
@@ -36,4 +40,7 @@ export class MemberResponsibilityRecord extends Model {
36
40
  @column({ type: "datetime", nullable: true })
37
41
  endDate: Date | null = null
38
42
 
43
+ getStructure() {
44
+ return MemberResponsibilityRecordStruct.create(this)
45
+ }
39
46
  }
@@ -3,7 +3,7 @@ import { DecodedRequest } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { I18n } from "@stamhoofd/backend-i18n";
5
5
  import { Email, EmailInterfaceRecipient } from "@stamhoofd/email";
6
- import { Address, Country, DNSRecordStatus, EmailTemplateType, OrganizationEmail, OrganizationMetaData, OrganizationPrivateMetaData, OrganizationRecordsConfiguration, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentMethod, PaymentProvider, PrivatePaymentConfiguration, Recipient, Replacement, STPackageType, TransferSettings } from "@stamhoofd/structures";
6
+ import { AccessRight, Address, Country, DNSRecordStatus, EmailTemplateType, OrganizationEmail, OrganizationMetaData, OrganizationPrivateMetaData, OrganizationRecordsConfiguration, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentMethod, PaymentProvider, PrivatePaymentConfiguration, Recipient, Replacement, STPackageType, TransferSettings } from "@stamhoofd/structures";
7
7
  import { AWSError } from 'aws-sdk';
8
8
  import SES from 'aws-sdk/clients/sesv2';
9
9
  import { PromiseResult } from 'aws-sdk/lib/request';
@@ -13,6 +13,7 @@ import { validateDNSRecords } from "../helpers/DNSValidator";
13
13
  import { getEmailBuilder } from "../helpers/EmailBuilder";
14
14
  import { OrganizationServerMetaData } from '../structures/OrganizationServerMetaData';
15
15
  import { EmailTemplate, Group, OrganizationRegistrationPeriod, RegistrationPeriod, StripeAccount } from "./";
16
+ import { QueueHandler } from "@stamhoofd/queues";
16
17
 
17
18
  export class Organization extends Model {
18
19
  static table = "organizations";
@@ -263,12 +264,47 @@ export class Organization extends Model {
263
264
  return this.id+"." + defaultDomain;
264
265
  }
265
266
 
266
- async getStructure({emptyGroups} = {emptyGroups: false}): Promise<OrganizationStruct> {
267
- const oPeriods = await OrganizationRegistrationPeriod.where({ periodId: this.periodId }, {limit: 1})
268
- const oPeriod = oPeriods[0];
267
+ async getPeriod({emptyGroups} = {emptyGroups: false}) {
268
+ const oPeriods = await OrganizationRegistrationPeriod.where({ periodId: this.periodId, organizationId: this.id }, {limit: 1})
269
269
  const period = await RegistrationPeriod.getByID(this.periodId)
270
+
271
+ if (!period) {
272
+ throw new Error("Period not found")
273
+ }
274
+
275
+ let oPeriod: OrganizationRegistrationPeriod;
276
+ if (oPeriods.length == 0) {
277
+ // Automatically create a period
278
+ oPeriod = await QueueHandler.schedule('create-missing-organization-period', async () => {
279
+ // Race condition check
280
+ const updatedPeriods = await OrganizationRegistrationPeriod.where({ periodId: this.periodId, organizationId: this.id }, {limit: 1})
281
+
282
+ if (updatedPeriods.length) {
283
+ return updatedPeriods[0]
284
+ }
285
+
286
+ console.log('Automatically creating new organization registration period for organization ' + this.id + ' and period ' + this.periodId + ' - organization period is missing')
287
+ const created = new OrganizationRegistrationPeriod()
288
+ created.organizationId = this.id
289
+ created.periodId = this.periodId
290
+ await created.save()
291
+ return created
292
+ })
293
+ } else {
294
+ oPeriod = oPeriods[0];
295
+ }
270
296
  const groups = emptyGroups ? [] : (await (await import("./Group")).Group.getAll(this.id, this.periodId))
271
297
 
298
+ return {
299
+ organizationPeriod: oPeriod,
300
+ period,
301
+ groups
302
+ }
303
+ }
304
+
305
+ async getStructure({emptyGroups} = {emptyGroups: false}): Promise<OrganizationStruct> {
306
+ const {groups, organizationPeriod, period} = await this.getPeriod({emptyGroups})
307
+
272
308
  const struct = OrganizationStruct.create({
273
309
  id: this.id,
274
310
  name: this.name,
@@ -279,10 +315,7 @@ export class Organization extends Model {
279
315
  website: this.website,
280
316
  groups: groups.map(g => g.getStructure()),
281
317
  createdAt: this.createdAt,
282
- period: OrganizationRegistrationPeriodStruct.create({
283
- ...oPeriod,
284
- period: period!.getStructure()
285
- })
318
+ period: organizationPeriod.getStructure(period, groups)
286
319
  })
287
320
 
288
321
  if (this.meta.modules.disableActivities) {
@@ -879,7 +912,7 @@ export class Organization extends Model {
879
912
  // Circular reference fix
880
913
  const User = (await import('./User')).User;
881
914
  const admins = await User.where({ organizationId: this.id, permissions: { sign: "!=", value: null }})
882
- const filtered = admins.filter(a => a.organizationPermissions && (a.organizationPermissions.hasFullAccess(this.privateMeta.roles) || a.organizationPermissions.hasFinanceAccess(this.privateMeta.roles)))
915
+ const filtered = admins.filter(a => a.permissions?.forOrganization(this)?.hasAccessRight(AccessRight.OrganizationFinanceDirector))
883
916
 
884
917
  if (filtered.length > 0) {
885
918
  return filtered.map(f => f.getEmailTo() ).join(", ")
@@ -53,6 +53,14 @@ export class OrganizationRegistrationPeriod extends Model {
53
53
  })
54
54
  }
55
55
 
56
+ getPrivateStructure(this: OrganizationRegistrationPeriod, period: RegistrationPeriod, groups: Group[]) {
57
+ return OrganizationRegistrationPeriodStruct.create({
58
+ ...this,
59
+ period: period.getStructure(),
60
+ groups: groups.map(g => g.getPrivateStructure()).sort(GroupStruct.defaultSort)
61
+ })
62
+ }
63
+
56
64
  async cleanCategories(groups: {id: string}[]) {
57
65
  const reachable = new Map<string, boolean>()
58
66
  const queue = [this.settings.rootCategoryId]
@@ -1,4 +1,5 @@
1
1
  import { column, Model } from '@simonbackx/simple-database';
2
+ import { SQL, SQLWhereSign } from '@stamhoofd/sql';
2
3
  import { RegistrationPeriodSettings, RegistrationPeriod as RegistrationPeriodStruct } from '@stamhoofd/structures';
3
4
  import { v4 as uuidv4 } from "uuid";
4
5
 
@@ -52,4 +53,17 @@ export class RegistrationPeriod extends Model {
52
53
  getStructure() {
53
54
  return RegistrationPeriodStruct.create(this)
54
55
  }
56
+
57
+ static async getByDate(date: Date): Promise<RegistrationPeriod|null> {
58
+ const result = await SQL.select().from(SQL.table(this.table))
59
+ .where(SQL.column('startDate'), SQLWhereSign.LessEqual, date)
60
+ .where(SQL.column('endDate'), SQLWhereSign.GreaterEqual, date)
61
+ .first(false);
62
+
63
+ if (result === null || !result[this.table]) {
64
+ return null;
65
+ }
66
+
67
+ return RegistrationPeriod.fromRow(result[this.table]) ?? null
68
+ }
55
69
  }
@@ -1,11 +1,11 @@
1
1
 
2
2
  import { column, Database, ManyToOneRelation, Model } from "@simonbackx/simple-database";
3
3
  import { EmailInterfaceRecipient } from "@stamhoofd/email";
4
- import { LoginProviderType, NewUser, Permissions, User as UserStruct,UserMeta, UserPermissions } from "@stamhoofd/structures";
4
+ import { LoginProviderType, NewUser, Permissions, UserMeta, UserPermissions, User as UserStruct } from "@stamhoofd/structures";
5
5
  import argon2 from "argon2";
6
6
  import { v4 as uuidv4 } from "uuid";
7
7
 
8
- import { Organization, Platform } from "./";
8
+ import { Organization } from "./";
9
9
 
10
10
  export class User extends Model {
11
11
  static table = "users";
@@ -21,6 +21,9 @@ export class User extends Model {
21
21
  @column({ foreignKey: User.organization, type: "string", nullable: true })
22
22
  organizationId: string|null;
23
23
 
24
+ @column({ type: "string", nullable: true })
25
+ memberId: string|null = null
26
+
24
27
  @column({ type: "string", nullable: true })
25
28
  firstName: string | null = null;
26
29
 
@@ -347,6 +350,11 @@ export class User extends Model {
347
350
  throw new Error("Missing organization")
348
351
  }
349
352
 
353
+ if (await User.getForAuthentication(organization?.id ?? null, email, {allowWithoutAccount: true})) {
354
+ // Already exists
355
+ return;
356
+ }
357
+
350
358
  const user = new User();
351
359
  user.organizationId = STAMHOOFD.userMode === 'platform' ? null : (organization?.id ?? null)
352
360
  user.id = id ?? uuidv4()
@@ -429,7 +437,9 @@ export class User extends Model {
429
437
  email: this.email,
430
438
  verified: this.verified,
431
439
  permissions: this.permissions,
432
- hasAccount: this.hasAccount()
440
+ hasAccount: this.hasAccount(),
441
+ memberId: this.memberId,
442
+ organizationId: this.organizationId
433
443
  });
434
444
  }
435
445
 
@@ -48,3 +48,6 @@ export * from "./MemberResponsibilityRecord"
48
48
  export * from "./OrganizationRegistrationPeriod"
49
49
  export * from "./RegistrationPeriod"
50
50
  export * from "./MemberPlatformMembership"
51
+ export * from "./Email"
52
+ export * from "./EmailRecipient"
53
+ export * from "./Event"