@stamhoofd/models 2.3.0 → 2.5.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 (133) 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/migrations/1722269236-group-waitinglist-id.sql +4 -0
  21. package/dist/src/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  22. package/dist/src/migrations/1722525787-depending-balance-item.sql +2 -0
  23. package/dist/src/models/BalanceItem.d.ts +7 -0
  24. package/dist/src/models/BalanceItem.d.ts.map +1 -1
  25. package/dist/src/models/BalanceItem.js +34 -1
  26. package/dist/src/models/BalanceItem.js.map +1 -1
  27. package/dist/src/models/DocumentTemplate.js +14 -14
  28. package/dist/src/models/DocumentTemplate.js.map +1 -1
  29. package/dist/src/models/Email.d.ts +41 -0
  30. package/dist/src/models/Email.d.ts.map +1 -0
  31. package/dist/src/models/Email.js +490 -0
  32. package/dist/src/models/Email.js.map +1 -0
  33. package/dist/src/models/EmailRecipient.d.ts +20 -0
  34. package/dist/src/models/EmailRecipient.d.ts.map +1 -0
  35. package/dist/src/models/EmailRecipient.js +95 -0
  36. package/dist/src/models/EmailRecipient.js.map +1 -0
  37. package/dist/src/models/EmailTemplate.d.ts +2 -1
  38. package/dist/src/models/EmailTemplate.d.ts.map +1 -1
  39. package/dist/src/models/EmailTemplate.js +4 -0
  40. package/dist/src/models/EmailTemplate.js.map +1 -1
  41. package/dist/src/models/Event.d.ts +19 -0
  42. package/dist/src/models/Event.d.ts.map +1 -0
  43. package/dist/src/models/Event.js +78 -0
  44. package/dist/src/models/Event.js.map +1 -0
  45. package/dist/src/models/Group.d.ts +9 -1
  46. package/dist/src/models/Group.d.ts.map +1 -1
  47. package/dist/src/models/Group.js +25 -22
  48. package/dist/src/models/Group.js.map +1 -1
  49. package/dist/src/models/Member.d.ts +1 -0
  50. package/dist/src/models/Member.d.ts.map +1 -1
  51. package/dist/src/models/Member.js +42 -11
  52. package/dist/src/models/Member.js.map +1 -1
  53. package/dist/src/models/MemberPlatformMembership.d.ts +7 -0
  54. package/dist/src/models/MemberPlatformMembership.d.ts.map +1 -1
  55. package/dist/src/models/MemberPlatformMembership.js +22 -0
  56. package/dist/src/models/MemberPlatformMembership.js.map +1 -1
  57. package/dist/src/models/MemberResponsibilityRecord.d.ts +3 -0
  58. package/dist/src/models/MemberResponsibilityRecord.d.ts.map +1 -1
  59. package/dist/src/models/MemberResponsibilityRecord.js +8 -0
  60. package/dist/src/models/MemberResponsibilityRecord.js.map +1 -1
  61. package/dist/src/models/Order.js +1 -1
  62. package/dist/src/models/Order.js.map +1 -1
  63. package/dist/src/models/Organization.d.ts +3 -4
  64. package/dist/src/models/Organization.d.ts.map +1 -1
  65. package/dist/src/models/Organization.js +28 -24
  66. package/dist/src/models/Organization.js.map +1 -1
  67. package/dist/src/models/OrganizationRegistrationPeriod.d.ts +1 -0
  68. package/dist/src/models/OrganizationRegistrationPeriod.d.ts.map +1 -1
  69. package/dist/src/models/OrganizationRegistrationPeriod.js +7 -0
  70. package/dist/src/models/OrganizationRegistrationPeriod.js.map +1 -1
  71. package/dist/src/models/RegistrationPeriod.d.ts +1 -0
  72. package/dist/src/models/RegistrationPeriod.d.ts.map +1 -1
  73. package/dist/src/models/RegistrationPeriod.js +12 -0
  74. package/dist/src/models/RegistrationPeriod.js.map +1 -1
  75. package/dist/src/models/User.d.ts +2 -1
  76. package/dist/src/models/User.d.ts.map +1 -1
  77. package/dist/src/models/User.js +13 -3
  78. package/dist/src/models/User.js.map +1 -1
  79. package/dist/src/models/index.d.ts +3 -0
  80. package/dist/src/models/index.d.ts.map +1 -1
  81. package/dist/src/models/index.js +3 -0
  82. package/dist/src/models/index.js.map +1 -1
  83. package/package.json +2 -2
  84. package/src/factories/GroupFactory.ts +6 -6
  85. package/src/helpers/EmailBuilder.ts +33 -18
  86. package/src/helpers/GroupBuilder.ts +0 -23
  87. package/src/migrations/1721050380-email-table.sql +24 -0
  88. package/src/migrations/1721050381-email-recipients-table.sql +18 -0
  89. package/src/migrations/1721342679-responsibility-groupId.sql +2 -0
  90. package/src/migrations/1721342680-responsibility-groupId-foreign-key.sql +1 -0
  91. package/src/migrations/1721400546-users-memberId.sql +3 -0
  92. package/src/migrations/1721639159-membership-deleted-at.sql +2 -0
  93. package/src/migrations/1721639160-membership-generated.sql +2 -0
  94. package/src/migrations/1721841819-group-type.sql +2 -0
  95. package/src/migrations/1722090482-events.sql +18 -0
  96. package/src/migrations/1722269236-group-waitinglist-id.sql +4 -0
  97. package/src/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  98. package/src/migrations/1722525787-depending-balance-item.sql +2 -0
  99. package/src/models/BalanceItem.ts +38 -1
  100. package/src/models/DocumentTemplate.ts +2 -2
  101. package/src/models/Email.ts +556 -0
  102. package/src/models/EmailRecipient.ts +81 -0
  103. package/src/models/EmailTemplate.ts +5 -1
  104. package/src/models/Event.ts +71 -0
  105. package/src/models/Group.ts +27 -37
  106. package/src/models/Member.ts +60 -12
  107. package/src/models/MemberPlatformMembership.ts +21 -0
  108. package/src/models/MemberResponsibilityRecord.ts +7 -0
  109. package/src/models/Order.ts +2 -2
  110. package/src/models/Organization.ts +32 -28
  111. package/src/models/OrganizationRegistrationPeriod.ts +8 -0
  112. package/src/models/RegistrationPeriod.ts +14 -0
  113. package/src/models/User.ts +13 -3
  114. package/src/models/index.ts +3 -0
  115. package/dist/src/assets/assets/Metropolis-Black.woff2 +0 -0
  116. package/dist/src/assets/assets/Metropolis-BlackItalic.woff2 +0 -0
  117. package/dist/src/assets/assets/Metropolis-Bold.woff2 +0 -0
  118. package/dist/src/assets/assets/Metropolis-BoldItalic.woff2 +0 -0
  119. package/dist/src/assets/assets/Metropolis-ExtraBold.woff2 +0 -0
  120. package/dist/src/assets/assets/Metropolis-ExtraBoldItalic.woff2 +0 -0
  121. package/dist/src/assets/assets/Metropolis-ExtraLight.woff2 +0 -0
  122. package/dist/src/assets/assets/Metropolis-ExtraLightItalic.woff2 +0 -0
  123. package/dist/src/assets/assets/Metropolis-Light.woff2 +0 -0
  124. package/dist/src/assets/assets/Metropolis-LightItalic.woff2 +0 -0
  125. package/dist/src/assets/assets/Metropolis-Medium.woff2 +0 -0
  126. package/dist/src/assets/assets/Metropolis-MediumItalic.woff2 +0 -0
  127. package/dist/src/assets/assets/Metropolis-Regular.woff2 +0 -0
  128. package/dist/src/assets/assets/Metropolis-RegularItalic.woff2 +0 -0
  129. package/dist/src/assets/assets/Metropolis-SemiBold.woff2 +0 -0
  130. package/dist/src/assets/assets/Metropolis-SemiBoldItalic.woff2 +0 -0
  131. package/dist/src/assets/assets/Metropolis-Thin.woff2 +0 -0
  132. package/dist/src/assets/assets/Metropolis-ThinItalic.woff2 +0 -0
  133. package/dist/src/assets/assets/logo.png +0 -0
@@ -0,0 +1,81 @@
1
+ import { column, Model } from '@simonbackx/simple-database';
2
+ import { EmailRecipient as EmailRecipientStruct, Replacement } from '@stamhoofd/structures';
3
+ import { v4 as uuidv4 } from "uuid";
4
+
5
+ import { ArrayDecoder } from '@simonbackx/simple-encoding';
6
+
7
+ export class EmailRecipient extends Model {
8
+ static table = "email_recipients";
9
+
10
+ @column({
11
+ primary: true, type: "string", beforeSave(value) {
12
+ return value ?? uuidv4();
13
+ }
14
+ })
15
+ id!: string;
16
+
17
+ @column({ type: "string" })
18
+ emailId: string
19
+
20
+ @column({ type: "string", nullable: true })
21
+ firstName: string | null = null;
22
+
23
+ @column({ type: "string", nullable: true})
24
+ lastName: string | null = null;
25
+
26
+ @column({ type: "string" })
27
+ email: string;
28
+
29
+ @column({ type: "json", decoder: new ArrayDecoder(Replacement) })
30
+ replacements: Replacement[] = []
31
+
32
+ @column({ type: "string", nullable: true})
33
+ failErrorMessage: string|null = null
34
+
35
+ @column({ type: "integer" })
36
+ failCount = 0
37
+
38
+ @column({
39
+ type: "datetime",
40
+ nullable: true
41
+ })
42
+ firstFailedAt: Date|null = null
43
+
44
+ @column({
45
+ type: "datetime",
46
+ nullable: true
47
+ })
48
+ lastFailedAt: Date|null = null
49
+
50
+ @column({
51
+ type: "datetime",
52
+ nullable: true
53
+ })
54
+ sentAt: Date|null = null
55
+
56
+ @column({
57
+ type: "datetime", beforeSave(old?: any) {
58
+ if (old !== undefined) {
59
+ return old;
60
+ }
61
+ const date = new Date()
62
+ date.setMilliseconds(0)
63
+ return date
64
+ }
65
+ })
66
+ createdAt: Date
67
+
68
+ @column({
69
+ type: "datetime", beforeSave() {
70
+ const date = new Date()
71
+ date.setMilliseconds(0)
72
+ return date
73
+ },
74
+ skipUpdate: true
75
+ })
76
+ updatedAt: Date
77
+
78
+ getStructure() {
79
+ return EmailRecipientStruct.create(this)
80
+ }
81
+ }
@@ -1,6 +1,6 @@
1
1
  import { column, Model } from "@simonbackx/simple-database";
2
2
  import { AnyDecoder } from "@simonbackx/simple-encoding";
3
- import { EmailTemplateType } from "@stamhoofd/structures";
3
+ import { EmailRecipientFilterType, EmailTemplate as EmailTemplateStruct, EmailTemplateType } from "@stamhoofd/structures";
4
4
  import { v4 as uuidv4 } from "uuid";
5
5
 
6
6
 
@@ -61,4 +61,8 @@ export class EmailTemplate extends Model {
61
61
  skipUpdate: true
62
62
  })
63
63
  updatedAt: Date
64
+
65
+ getStructure() {
66
+ return EmailTemplateStruct.create(this)
67
+ }
64
68
  }
@@ -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
 
@@ -39,6 +42,9 @@ export class Group extends Model {
39
42
  @column({ type: "string" })
40
43
  organizationId: string;
41
44
 
45
+ @column({ type: "string", nullable: true })
46
+ waitingListId: string | null = null
47
+
42
48
  @column({ type: "string" })
43
49
  periodId: string;
44
50
 
@@ -183,10 +189,16 @@ export class Group extends Model {
183
189
 
184
190
  }
185
191
 
192
+ /**
193
+ * @deprecated
194
+ */
186
195
  getStructure() {
187
196
  return GroupStruct.create({ ...this, privateSettings: null })
188
197
  }
189
198
 
199
+ /**
200
+ * @deprecated
201
+ */
190
202
  getPrivateStructure() {
191
203
  return GroupStruct.create(this)
192
204
  }
@@ -207,7 +219,6 @@ export class Group extends Model {
207
219
  "groupId = ? and cycle = ? and waitingList = 0 and registeredAt is not null",
208
220
  [this.id, this.cycle]
209
221
  )
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
222
 
212
223
  this.settings.reservedMembers = await Group.getCount(
213
224
  "groupId = ? and cycle = ? and ((waitingList = 0 and registeredAt is null AND reservedUntil >= ?) OR (waitingList = 1 and canRegister = 1))",
@@ -218,37 +229,6 @@ export class Group extends Model {
218
229
  "groupId = ? and cycle = ? and waitingList = 1",
219
230
  [this.id, this.cycle, new Date()]
220
231
  )
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
232
  }
253
233
 
254
234
  static async deleteUnreachable(organizationId: string, period: OrganizationRegistrationPeriod, allGroups: Group[]) {
@@ -282,10 +262,20 @@ export class Group extends Model {
282
262
  }
283
263
 
284
264
  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
265
+ if (group.periodId !== period.periodId) {
266
+ continue
267
+ }
268
+
269
+ if (group.type !== GroupType.Membership) {
270
+ continue
271
+ }
272
+
273
+ if (!reachable.get(group.id) && group.deletedAt === null) {
274
+ console.log("Deleting unreachable group "+group.id+" from organization "+organizationId + " org period "+period.id)
275
+ group.deletedAt = new Date()
288
276
  await group.save()
277
+
278
+ Member.updateMembershipsForGroupId(group.id)
289
279
  }
290
280
  }
291
281
  }
@@ -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
  }
@@ -875,7 +875,7 @@ export class Order extends Model {
875
875
  const template = templates[0]
876
876
 
877
877
  let recipient = (await this.getStructure()).getRecipient(
878
- await this.webshop.organization.getStructure({emptyGroups: true}),
878
+ this.webshop.organization.getBaseStructure(),
879
879
  WebshopPreview.create(this.webshop)
880
880
  )
881
881
 
@@ -1027,4 +1027,4 @@ export class Order extends Model {
1027
1027
  }
1028
1028
  }
1029
1029
  }
1030
- }
1030
+ }
@@ -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,13 +264,36 @@ 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];
269
- const period = await RegistrationPeriod.getByID(this.periodId)
270
- const groups = emptyGroups ? [] : (await (await import("./Group")).Group.getAll(this.id, this.periodId))
267
+ async getPeriod() {
268
+ const oPeriods = await OrganizationRegistrationPeriod.where({ periodId: this.periodId, organizationId: this.id }, {limit: 1})
269
+
270
+ let oPeriod: OrganizationRegistrationPeriod;
271
+ if (oPeriods.length == 0) {
272
+ // Automatically create a period
273
+ oPeriod = await QueueHandler.schedule('create-missing-organization-period', async () => {
274
+ // Race condition check
275
+ const updatedPeriods = await OrganizationRegistrationPeriod.where({ periodId: this.periodId, organizationId: this.id }, {limit: 1})
276
+
277
+ if (updatedPeriods.length) {
278
+ return updatedPeriods[0]
279
+ }
280
+
281
+ console.log('Automatically creating new organization registration period for organization ' + this.id + ' and period ' + this.periodId + ' - organization period is missing')
282
+ const created = new OrganizationRegistrationPeriod()
283
+ created.organizationId = this.id
284
+ created.periodId = this.periodId
285
+ await created.save()
286
+ return created
287
+ })
288
+ } else {
289
+ oPeriod = oPeriods[0];
290
+ }
291
+
292
+ return oPeriod
293
+ }
271
294
 
272
- const struct = OrganizationStruct.create({
295
+ getBaseStructure(): OrganizationStruct {
296
+ return OrganizationStruct.create({
273
297
  id: this.id,
274
298
  name: this.name,
275
299
  meta: this.meta,
@@ -277,28 +301,8 @@ export class Organization extends Model {
277
301
  registerDomain: this.registerDomain,
278
302
  uri: this.uri,
279
303
  website: this.website,
280
- groups: groups.map(g => g.getStructure()),
281
304
  createdAt: this.createdAt,
282
- period: OrganizationRegistrationPeriodStruct.create({
283
- ...oPeriod,
284
- period: period!.getStructure()
285
- })
286
305
  })
287
-
288
- if (this.meta.modules.disableActivities) {
289
- // Only show groups that are in a given category
290
- struct.groups = struct.categoryTree.categories[0]?.groups ?? []
291
- }
292
-
293
- if (emptyGroups) {
294
- // Reduce data
295
- struct.meta = struct.meta.clone()
296
- struct.meta.categories = []
297
- struct.meta.recordsConfiguration = OrganizationRecordsConfiguration.create({})
298
-
299
- }
300
-
301
- return struct
302
306
  }
303
307
 
304
308
  async updateDNSRecords() {
@@ -879,7 +883,7 @@ export class Organization extends Model {
879
883
  // Circular reference fix
880
884
  const User = (await import('./User')).User;
881
885
  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)))
886
+ const filtered = admins.filter(a => a.permissions?.forOrganization(this)?.hasAccessRight(AccessRight.OrganizationFinanceDirector))
883
887
 
884
888
  if (filtered.length > 0) {
885
889
  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
  }