@stamhoofd/models 2.4.0 → 2.6.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 (86) hide show
  1. package/dist/src/migrations/1722269236-group-waitinglist-id.sql +4 -0
  2. package/dist/src/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  3. package/dist/src/migrations/1722525787-depending-balance-item.sql +2 -0
  4. package/dist/src/migrations/1722845608-registration-stock-reservations.sql +2 -0
  5. package/dist/src/migrations/1722845609-group-stock-reservations.sql +2 -0
  6. package/dist/src/migrations/1722852362-stripe-intents-account-id.sql +2 -0
  7. package/dist/src/migrations/1722852363-stripe-checkout-sessions-account-id.sql +2 -0
  8. package/dist/src/models/BalanceItem.d.ts +8 -0
  9. package/dist/src/models/BalanceItem.d.ts.map +1 -1
  10. package/dist/src/models/BalanceItem.js +70 -44
  11. package/dist/src/models/BalanceItem.js.map +1 -1
  12. package/dist/src/models/DocumentTemplate.js +1 -1
  13. package/dist/src/models/DocumentTemplate.js.map +1 -1
  14. package/dist/src/models/Event.d.ts +7 -0
  15. package/dist/src/models/Event.d.ts.map +1 -1
  16. package/dist/src/models/Event.js +28 -0
  17. package/dist/src/models/Event.js.map +1 -1
  18. package/dist/src/models/Group.d.ts +13 -3
  19. package/dist/src/models/Group.d.ts.map +1 -1
  20. package/dist/src/models/Group.js +45 -6
  21. package/dist/src/models/Group.js.map +1 -1
  22. package/dist/src/models/Member.d.ts +1 -1
  23. package/dist/src/models/Member.d.ts.map +1 -1
  24. package/dist/src/models/Member.js +12 -9
  25. package/dist/src/models/Member.js.map +1 -1
  26. package/dist/src/models/Order.js +1 -1
  27. package/dist/src/models/Order.js.map +1 -1
  28. package/dist/src/models/Organization.d.ts +3 -11
  29. package/dist/src/models/Organization.d.ts.map +1 -1
  30. package/dist/src/models/Organization.js +4 -28
  31. package/dist/src/models/Organization.js.map +1 -1
  32. package/dist/src/models/Payment.d.ts +5 -7
  33. package/dist/src/models/Payment.d.ts.map +1 -1
  34. package/dist/src/models/Payment.js +8 -13
  35. package/dist/src/models/Payment.js.map +1 -1
  36. package/dist/src/models/Registration.d.ts +17 -2
  37. package/dist/src/models/Registration.d.ts.map +1 -1
  38. package/dist/src/models/Registration.js +59 -7
  39. package/dist/src/models/Registration.js.map +1 -1
  40. package/dist/src/models/StripeCheckoutSession.d.ts +4 -0
  41. package/dist/src/models/StripeCheckoutSession.d.ts.map +1 -1
  42. package/dist/src/models/StripeCheckoutSession.js +7 -0
  43. package/dist/src/models/StripeCheckoutSession.js.map +1 -1
  44. package/dist/src/models/StripePaymentIntent.d.ts +4 -0
  45. package/dist/src/models/StripePaymentIntent.d.ts.map +1 -1
  46. package/dist/src/models/StripePaymentIntent.js +7 -0
  47. package/dist/src/models/StripePaymentIntent.js.map +1 -1
  48. package/package.json +2 -2
  49. package/src/migrations/1722269236-group-waitinglist-id.sql +4 -0
  50. package/src/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  51. package/src/migrations/1722525787-depending-balance-item.sql +2 -0
  52. package/src/migrations/1722845608-registration-stock-reservations.sql +2 -0
  53. package/src/migrations/1722845609-group-stock-reservations.sql +2 -0
  54. package/src/migrations/1722852362-stripe-intents-account-id.sql +2 -0
  55. package/src/migrations/1722852363-stripe-checkout-sessions-account-id.sql +2 -0
  56. package/src/models/BalanceItem.ts +78 -47
  57. package/src/models/DocumentTemplate.ts +1 -1
  58. package/src/models/Event.ts +31 -0
  59. package/src/models/Group.ts +53 -14
  60. package/src/models/Member.ts +13 -10
  61. package/src/models/Order.ts +2 -2
  62. package/src/models/Organization.ts +5 -34
  63. package/src/models/Payment.ts +10 -16
  64. package/src/models/Registration.ts +71 -11
  65. package/src/models/StripeAccount.ts +1 -1
  66. package/src/models/StripeCheckoutSession.ts +6 -0
  67. package/src/models/StripePaymentIntent.ts +6 -0
  68. package/dist/src/assets/assets/Metropolis-Black.woff2 +0 -0
  69. package/dist/src/assets/assets/Metropolis-BlackItalic.woff2 +0 -0
  70. package/dist/src/assets/assets/Metropolis-Bold.woff2 +0 -0
  71. package/dist/src/assets/assets/Metropolis-BoldItalic.woff2 +0 -0
  72. package/dist/src/assets/assets/Metropolis-ExtraBold.woff2 +0 -0
  73. package/dist/src/assets/assets/Metropolis-ExtraBoldItalic.woff2 +0 -0
  74. package/dist/src/assets/assets/Metropolis-ExtraLight.woff2 +0 -0
  75. package/dist/src/assets/assets/Metropolis-ExtraLightItalic.woff2 +0 -0
  76. package/dist/src/assets/assets/Metropolis-Light.woff2 +0 -0
  77. package/dist/src/assets/assets/Metropolis-LightItalic.woff2 +0 -0
  78. package/dist/src/assets/assets/Metropolis-Medium.woff2 +0 -0
  79. package/dist/src/assets/assets/Metropolis-MediumItalic.woff2 +0 -0
  80. package/dist/src/assets/assets/Metropolis-Regular.woff2 +0 -0
  81. package/dist/src/assets/assets/Metropolis-RegularItalic.woff2 +0 -0
  82. package/dist/src/assets/assets/Metropolis-SemiBold.woff2 +0 -0
  83. package/dist/src/assets/assets/Metropolis-SemiBoldItalic.woff2 +0 -0
  84. package/dist/src/assets/assets/Metropolis-Thin.woff2 +0 -0
  85. package/dist/src/assets/assets/Metropolis-ThinItalic.woff2 +0 -0
  86. package/dist/src/assets/assets/logo.png +0 -0
@@ -8,6 +8,10 @@ class StripePaymentIntent extends simple_database_1.Model {
8
8
  constructor() {
9
9
  super(...arguments);
10
10
  this.organizationId = null;
11
+ /**
12
+ * For direct charges, this should be set
13
+ */
14
+ this.accountId = null;
11
15
  }
12
16
  }
13
17
  exports.StripePaymentIntent = StripePaymentIntent;
@@ -28,4 +32,7 @@ tslib_1.__decorate([
28
32
  tslib_1.__decorate([
29
33
  (0, simple_database_1.column)({ type: "string", nullable: true })
30
34
  ], StripePaymentIntent.prototype, "organizationId", void 0);
35
+ tslib_1.__decorate([
36
+ (0, simple_database_1.column)({ type: "string", nullable: true })
37
+ ], StripePaymentIntent.prototype, "accountId", void 0);
31
38
  //# sourceMappingURL=StripePaymentIntent.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"StripePaymentIntent.js","sourceRoot":"","sources":["../../../src/models/StripePaymentIntent.ts"],"names":[],"mappings":";;;;AAAA,iEAA2D;AAC3D,+BAAoC;AAEpC,MAAa,mBAAoB,SAAQ,uBAAK;IAA9C;;QAiBI,mBAAc,GAAkB,IAAI,CAAC;IACzC,CAAC;;AAlBD,kDAkBC;AAjBU,yBAAK,GAAG,wBAAwB,AAA3B,CAA4B;AAOxC;IALC,IAAA,wBAAM,EAAC;QACJ,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,CAAC,KAAK;YAC3C,OAAO,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,IAAA,SAAM,GAAE,CAAC;QAC7B,CAAC;KACJ,CAAC;+CACU;AAGZ;IADC,IAAA,wBAAM,EAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;sDACT;AAGlB;IADC,IAAA,wBAAM,EAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;2DACJ;AAGvB;IADC,IAAA,wBAAM,EAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;2DACN"}
1
+ {"version":3,"file":"StripePaymentIntent.js","sourceRoot":"","sources":["../../../src/models/StripePaymentIntent.ts"],"names":[],"mappings":";;;;AAAA,iEAA2D;AAC3D,+BAAoC;AAEpC,MAAa,mBAAoB,SAAQ,uBAAK;IAA9C;;QAiBI,mBAAc,GAAkB,IAAI,CAAC;QAErC;;WAEG;QAEH,cAAS,GAAgB,IAAI,CAAA;IACjC,CAAC;;AAxBD,kDAwBC;AAvBU,yBAAK,GAAG,wBAAwB,AAA3B,CAA4B;AAOxC;IALC,IAAA,wBAAM,EAAC;QACJ,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,CAAC,KAAK;YAC3C,OAAO,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,IAAA,SAAM,GAAE,CAAC;QAC7B,CAAC;KACJ,CAAC;+CACU;AAGZ;IADC,IAAA,wBAAM,EAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;sDACT;AAGlB;IADC,IAAA,wBAAM,EAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;2DACJ;AAGvB;IADC,IAAA,wBAAM,EAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;2DACN;AAMrC;IADC,IAAA,wBAAM,EAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sDACd"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/models",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "main": "./dist/src/index.js",
5
5
  "types": "./dist/src/index.d.ts",
6
6
  "license": "UNLICENCED",
@@ -27,5 +27,5 @@
27
27
  "devDependencies": {
28
28
  "@simonbackx/simple-database": "1.24.0"
29
29
  },
30
- "gitHead": "5313c328ca0e90544bf198bc6bebc90d7dced2aa"
30
+ "gitHead": "7a3f9f6c08058dc8b671befbfad73184afdc6d7c"
31
31
  }
@@ -0,0 +1,4 @@
1
+ ALTER TABLE `groups`
2
+ ADD COLUMN `waitingListId` varchar(36) NULL AFTER `defaultAgeGroupId`;
3
+
4
+ ALTER TABLE `groups` ADD FOREIGN KEY (`waitingListId`) REFERENCES `groups` (`id`) ON UPDATE CASCADE ON DELETE SET NULL;
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `balance_items` ADD COLUMN `payingOrganizationId` varchar(36) NULL;
2
+ ALTER TABLE `balance_items` ADD FOREIGN KEY (`payingOrganizationId`) REFERENCES `organizations` (`id`) ON UPDATE CASCADE ON DELETE SET NULL;
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `balance_items` ADD COLUMN `dependingBalanceItemId` varchar(36) NULL;
2
+ ALTER TABLE `balance_items` ADD FOREIGN KEY (`dependingBalanceItemId`) REFERENCES `balance_items` (`id`) ON UPDATE CASCADE ON DELETE SET NULL;
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `registrations`
2
+ ADD COLUMN `stockReservations` json NOT NULL DEFAULT ('{"value": [], "version": 0}');
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `groups`
2
+ ADD COLUMN `stockReservations` json NOT NULL DEFAULT ('{"value": [], "version": 0}');
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `stripe_payment_intents`
2
+ ADD COLUMN `accountId` varchar(100) NULL;
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `stripe_checkout_sessions`
2
+ ADD COLUMN `accountId` varchar(100) NULL;
@@ -18,15 +18,22 @@ export class BalanceItem extends Model {
18
18
  })
19
19
  id!: string;
20
20
 
21
+ // Receiving organization
22
+
21
23
  @column({ type: "string" })
22
24
  organizationId: string
23
25
 
26
+ // Payer: memberId, userId or payingOrganizationId
27
+
24
28
  @column({ type: "string", nullable: true })
25
29
  memberId: string | null = null;
26
30
 
27
31
  @column({ type: "string", nullable: true })
28
32
  userId: string | null = null;
29
33
 
34
+ @column({ type: "string", nullable: true })
35
+ payingOrganizationId: string | null = null;
36
+
30
37
  /**
31
38
  * The registration ID that is linked to this balance item
32
39
  */
@@ -39,6 +46,14 @@ export class BalanceItem extends Model {
39
46
  @column({ type: "string", nullable: true })
40
47
  orderId: string | null = null;
41
48
 
49
+ /**
50
+ * The depending balance item ID that is linked to this balance item
51
+ * -> as soon as this balance item is paid, we'll mark this balance item as pending if it is still hidden
52
+ * -> allows for a pay back system where one user needs to pay back a different user
53
+ */
54
+ @column({ type: "string", nullable: true })
55
+ dependingBalanceItemId: string | null = null;
56
+
42
57
  @column({ type: "string" })
43
58
  description = "";
44
59
 
@@ -92,6 +107,12 @@ export class BalanceItem extends Model {
92
107
 
93
108
  async markPaid(payment: Payment, organization: Organization) {
94
109
  // status and pricePaid changes are handled inside balanceitempayment
110
+ if (this.dependingBalanceItemId) {
111
+ const depending = await BalanceItem.getByID(this.dependingBalanceItemId)
112
+ if (depending && depending.status === BalanceItemStatus.Hidden) {
113
+ await BalanceItem.reactivateItems([depending])
114
+ }
115
+ }
95
116
 
96
117
  // If registration
97
118
  if (this.registrationId) {
@@ -100,7 +121,7 @@ export class BalanceItem extends Model {
100
121
 
101
122
  if (registration) {
102
123
  // 1. Mark registration as being valid
103
- if (registration.registeredAt === null) {
124
+ if (registration.registeredAt === null || registration.deactivatedAt) {
104
125
  await registration.markValid()
105
126
 
106
127
  const {Group} = await import("./Group");
@@ -137,6 +158,28 @@ export class BalanceItem extends Model {
137
158
  }
138
159
  }
139
160
  }
161
+
162
+ // Do we have a different connected balance item?
163
+ // Make it visible if this one is paid
164
+ if (this.dependingBalanceItemId) {
165
+ const depending = await BalanceItem.getByID(this.dependingBalanceItemId)
166
+ if (depending) {
167
+ if (this.status === BalanceItemStatus.Hidden) {
168
+ depending.status = BalanceItemStatus.Pending
169
+ await depending.save()
170
+
171
+ if (depending.memberId) {
172
+ const {Member} = await import("./Member");
173
+ await Member.updateOutstandingBalance([depending.memberId])
174
+ }
175
+
176
+ if (depending.registrationId) {
177
+ const {Registration} = await import("./Registration");
178
+ await Registration.updateOutstandingBalance([depending.registrationId], depending.organizationId)
179
+ }
180
+ }
181
+ }
182
+ }
140
183
  }
141
184
 
142
185
  async undoPaid(payment: Payment, organization: Organization) {
@@ -178,76 +221,52 @@ export class BalanceItem extends Model {
178
221
  }
179
222
 
180
223
  updateStatus() {
181
- this.status = this.pricePaid >= this.price ? BalanceItemStatus.Paid : BalanceItemStatus.Pending;
224
+ this.status = this.pricePaid >= this.price ? BalanceItemStatus.Paid : (this.pricePaid > 0 ? BalanceItemStatus.Pending : (this.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Pending));
182
225
  }
183
226
 
184
227
  static async deleteItems(items: BalanceItem[]) {
185
- const {payments} = await BalanceItem.loadPayments(items)
228
+ const {balanceItemPayments} = await BalanceItem.loadPayments(items)
186
229
 
187
- // Load all balance items
188
- const {balanceItems, balanceItemPayments: allBalanceItemPayments} = await Payment.loadBalanceItems(payments)
189
- for (const payment of payments) {
190
- if (payment.status === PaymentStatus.Succeeded) {
191
- continue;
192
- }
193
- if (!(payment.method === PaymentMethod.PointOfSale || payment.method === PaymentMethod.Transfer || payment.method === PaymentMethod.Unknown)) {
194
- continue;
195
- }
196
- const bip = allBalanceItemPayments.filter(p => p.paymentId == payment.id)
197
- const bis = balanceItems.filter(b => b.status !== BalanceItemStatus.Hidden && bip.find(p => p.balanceItemId == b.id))
198
-
199
- const remainingAfterDelete = bis.filter(b => !items.find(i => i.id == b.id))
200
- if (remainingAfterDelete.length == 0) {
201
- // Delete payment
202
- payment.status = PaymentStatus.Failed
203
- payment._forceUpdatedAt = new Date(1900, 0, 1)
204
- await payment.save()
205
- }
206
- }
230
+ // todo: in the future we could automatically delete payments that are not needed anymore and weren't paid yet -> to prevent leaving ghost payments
231
+ // for now, an admin can manually cancel those payments
232
+ let needsUpdate = false
207
233
 
208
234
  // Set other items to zero (the balance item payments keep the real price)
209
235
  for (const item of items) {
236
+ needsUpdate = needsUpdate || (item.price > 0 && item.status !== BalanceItemStatus.Hidden)
237
+
210
238
  // Don't change status of items that are already paid or are partially paid
211
239
  // Not using item.paidPrice, since this is cached
212
- const bip = allBalanceItemPayments.filter(p => p.balanceItemId == item.id)
213
- const relatedPayments = payments.filter(p => bip.find(b => b.paymentId == p.id))
240
+ const bip = balanceItemPayments.filter(p => p.balanceItemId == item.id)
214
241
 
215
- if (relatedPayments.length === 0 || !relatedPayments.find(p => p.status === PaymentStatus.Succeeded)) {
216
- // No paid payments associated with this item
242
+ if (bip.length === 0) {
243
+ // No payments associated with this item
217
244
  item.status = BalanceItemStatus.Hidden
245
+ item.price = 0
246
+ await item.save()
247
+ } else {
248
+ item.price = 0
218
249
  await item.save()
219
250
  }
220
251
  }
252
+
253
+ if (needsUpdate) {
254
+ await this.updateOutstanding(items)
255
+ }
221
256
  }
222
257
 
223
258
  static async reactivateItems(items: BalanceItem[]) {
224
- // Set other items to zero (the balance item payments keep the real price)
259
+ let needsUpdate = false
225
260
  for (const item of items) {
226
261
  if (item.status === BalanceItemStatus.Hidden) {
227
262
  item.status = BalanceItemStatus.Pending
263
+ needsUpdate = needsUpdate || item.price > 0
228
264
  await item.save()
229
265
  }
230
266
  }
231
267
 
232
- const {payments} = await BalanceItem.loadPayments(items)
233
-
234
- // Load all balance items
235
- const {balanceItems, balanceItemPayments: allBalanceItemPayments} = await Payment.loadBalanceItems(payments)
236
- for (const payment of payments) {
237
- if (payment.status !== PaymentStatus.Failed) {
238
- continue;
239
- }
240
- if (!(payment.method === PaymentMethod.PointOfSale || payment.method === PaymentMethod.Transfer || payment.method === PaymentMethod.Unknown)) {
241
- continue;
242
- }
243
- const bip = allBalanceItemPayments.filter(p => p.paymentId == payment.id)
244
- const bis = balanceItems.filter(b => b.status !== BalanceItemStatus.Hidden && bip.find(p => p.balanceItemId == b.id))
245
-
246
- if (bis.length > 0) {
247
- // Undo failed
248
- payment.status = PaymentStatus.Created
249
- await payment.save()
250
- }
268
+ if (needsUpdate) {
269
+ await this.updateOutstanding(items)
251
270
  }
252
271
  }
253
272
 
@@ -285,6 +304,18 @@ export class BalanceItem extends Model {
285
304
  }
286
305
  }
287
306
 
307
+ static async updateOutstanding(items: BalanceItem[], organizationId?: string) {
308
+ const Member = (await import('./Member')).Member;
309
+
310
+ // Update outstanding amount of related members and registrations
311
+ const memberIds: string[] = Formatter.uniqueArray(items.map(p => p.memberId).filter(id => id !== null)) as any
312
+ await Member.updateOutstandingBalance(memberIds)
313
+
314
+ const {Registration} = await import('./Registration');
315
+ const registrationIds: string[] = Formatter.uniqueArray(items.map(p => p.registrationId).filter(id => id !== null)) as any
316
+ await Registration.updateOutstandingBalance(registrationIds, organizationId)
317
+ }
318
+
288
319
  static async loadPayments(items: BalanceItem[]) {
289
320
  if (items.length == 0) {
290
321
  return {balanceItemPayments: [], payments: []}
@@ -426,7 +426,7 @@ export class DocumentTemplate extends Model {
426
426
 
427
427
  for (const groupDefinition of this.privateSettings.groups) {
428
428
  // Get the registrations for this group with this cycle
429
- const registrations = await Member.getRegistrationWithMembersForGroup(groupDefinition.groupId, groupDefinition.cycle)
429
+ const registrations = await Member.getRegistrationWithMembersForGroup(groupDefinition.groupId)
430
430
 
431
431
  for (const registration of registrations) {
432
432
  const document = await this.generateForRegistration(registration)
@@ -55,6 +55,9 @@ export class Event extends Model {
55
55
  })
56
56
  updatedAt: Date
57
57
 
58
+ /**
59
+ * @deprecated
60
+ */
58
61
  getStructure(group?: Group|null) {
59
62
  return EventStruct.create({
60
63
  ...this,
@@ -62,10 +65,38 @@ export class Event extends Model {
62
65
  })
63
66
  }
64
67
 
68
+ /**
69
+ * @deprecated
70
+ */
65
71
  getPrivateStructure(group?: Group|null) {
66
72
  return EventStruct.create({
67
73
  ...this,
68
74
  group: group ? group.getPrivateStructure() : null
69
75
  })
70
76
  }
77
+
78
+ async syncGroupRequirements(group: Group|null) {
79
+ if (!group) {
80
+ return;
81
+ }
82
+
83
+ group.settings.requireDefaultAgeGroupIds = this.meta.defaultAgeGroupIds ?? []
84
+ group.settings.requireGroupIds = this.meta.groupIds ?? []
85
+
86
+ if (this.organizationId) {
87
+ // This is a not-national event, so require the organization
88
+ group.settings.requireOrganizationIds = this.meta.organizationTagIds ?? []
89
+ group.settings.requireOrganizationTags = []
90
+ group.settings.requirePlatformMembershipOn = null
91
+ } else {
92
+ group.settings.requireOrganizationTags = this.meta.organizationTagIds ?? []
93
+
94
+ // Everyone can register
95
+ group.settings.requireOrganizationIds = []
96
+
97
+ // But they need a valid platform membership
98
+ group.settings.requirePlatformMembershipOn = this.endDate
99
+ }
100
+ await group.save()
101
+ }
71
102
  }
@@ -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")
@@ -42,6 +44,9 @@ export class Group extends Model {
42
44
  @column({ type: "string" })
43
45
  organizationId: string;
44
46
 
47
+ @column({ type: "string", nullable: true })
48
+ waitingListId: string | null = null
49
+
45
50
  @column({ type: "string" })
46
51
  periodId: string;
47
52
 
@@ -82,17 +87,20 @@ export class Group extends Model {
82
87
  })
83
88
  deletedAt: Date | null = null
84
89
 
85
- /**
86
- * Every time a new registration period starts, this number increases. This is used to mark all older registrations as 'out of date' automatically
87
- */
88
90
  @column({ type: "string" })
89
91
  status = GroupStatus.Open;
90
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
+
91
99
  static async getAll(organizationId: string, periodId: string|null, active = true) {
92
100
  const w: any = periodId ? {periodId} : {}
93
101
  if (active) {
94
102
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
95
- return await Group.where({ organizationId, deletedAt: null, ...w, status: {sign: '!=', value: GroupStatus.Archived} })
103
+ return await Group.where({ organizationId, deletedAt: null, ...w })
96
104
  }
97
105
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
98
106
  return await Group.where({ organizationId, ...w })
@@ -186,10 +194,16 @@ export class Group extends Model {
186
194
 
187
195
  }
188
196
 
197
+ /**
198
+ * @deprecated
199
+ */
189
200
  getStructure() {
190
201
  return GroupStruct.create({ ...this, privateSettings: null })
191
202
  }
192
203
 
204
+ /**
205
+ * @deprecated
206
+ */
193
207
  getPrivateStructure() {
194
208
  return GroupStruct.create(this)
195
209
  }
@@ -207,18 +221,13 @@ export class Group extends Model {
207
221
 
208
222
  async updateOccupancy() {
209
223
  this.settings.registeredMembers = await Group.getCount(
210
- "groupId = ? and cycle = ? and waitingList = 0 and registeredAt is not null",
211
- [this.id, this.cycle]
224
+ "groupId = ? and registeredAt is not null AND deactivatedAt is null",
225
+ [this.id]
212
226
  )
213
227
 
214
228
  this.settings.reservedMembers = await Group.getCount(
215
- "groupId = ? and cycle = ? and ((waitingList = 0 and registeredAt is null AND reservedUntil >= ?) OR (waitingList = 1 and canRegister = 1))",
216
- [this.id, this.cycle, new Date()]
217
- )
218
-
219
- this.settings.waitingListSize = await Group.getCount(
220
- "groupId = ? and cycle = ? and waitingList = 1",
221
- [this.id, this.cycle, new Date()]
229
+ "groupId = ? and registeredAt is null AND (canRegister = 1 OR reservedUntil >= ?)",
230
+ [this.id, new Date()]
222
231
  )
223
232
  }
224
233
 
@@ -253,6 +262,14 @@ export class Group extends Model {
253
262
  }
254
263
 
255
264
  for (const group of allGroups) {
265
+ if (group.periodId !== period.periodId) {
266
+ continue
267
+ }
268
+
269
+ if (group.type !== GroupType.Membership) {
270
+ continue
271
+ }
272
+
256
273
  if (!reachable.get(group.id) && group.deletedAt === null) {
257
274
  console.log("Deleting unreachable group "+group.id+" from organization "+organizationId + " org period "+period.id)
258
275
  group.deletedAt = new Date()
@@ -263,6 +280,28 @@ export class Group extends Model {
263
280
  }
264
281
  }
265
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
+
266
305
  }
267
306
 
268
307
  Registration.group = new ManyToOneRelation(Group, "group")
@@ -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, GroupStatus } from '@stamhoofd/structures';
3
+ import { Member as MemberStruct, MemberDetails, MemberWithRegistrationsBlob, RegistrationWithMember as RegistrationWithMemberStruct, User as UserStruct, GroupStatus, TinyMember } from '@stamhoofd/structures';
4
4
  import { Formatter, Sorter } from '@stamhoofd/utility';
5
5
  import { v4 as uuidv4 } from "uuid";
6
6
 
@@ -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
 
@@ -415,10 +419,9 @@ export class Member extends Model {
415
419
  }
416
420
 
417
421
  async updateMemberships(this: MemberWithRegistrations) {
418
- console.log('Updating memberships for member: ' + this.id)
419
422
  return await QueueHandler.schedule('updateMemberships-' + this.id, async () => {
420
423
  const platform = await Platform.getShared()
421
- const registrations = this.registrations.filter(r => r.group.periodId == platform.periodId && !r.waitingList && r.registeredAt && !r.deactivatedAt)
424
+ const registrations = this.registrations.filter(r => r.group.periodId == platform.periodId && r.registeredAt && !r.deactivatedAt)
422
425
 
423
426
  const defaultMemberships = registrations.flatMap(r => {
424
427
  if (!r.group.defaultAgeGroupId) {
@@ -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
+ }
@@ -264,14 +264,9 @@ export class Organization extends Model {
264
264
  return this.id+"." + defaultDomain;
265
265
  }
266
266
 
267
- async getPeriod({emptyGroups} = {emptyGroups: false}) {
267
+ async getPeriod() {
268
268
  const oPeriods = await OrganizationRegistrationPeriod.where({ periodId: this.periodId, organizationId: this.id }, {limit: 1})
269
- const period = await RegistrationPeriod.getByID(this.periodId)
270
-
271
- if (!period) {
272
- throw new Error("Period not found")
273
- }
274
-
269
+
275
270
  let oPeriod: OrganizationRegistrationPeriod;
276
271
  if (oPeriods.length == 0) {
277
272
  // Automatically create a period
@@ -293,19 +288,12 @@ export class Organization extends Model {
293
288
  } else {
294
289
  oPeriod = oPeriods[0];
295
290
  }
296
- const groups = emptyGroups ? [] : (await (await import("./Group")).Group.getAll(this.id, this.periodId))
297
291
 
298
- return {
299
- organizationPeriod: oPeriod,
300
- period,
301
- groups
302
- }
292
+ return oPeriod
303
293
  }
304
294
 
305
- async getStructure({emptyGroups} = {emptyGroups: false}): Promise<OrganizationStruct> {
306
- const {groups, organizationPeriod, period} = await this.getPeriod({emptyGroups})
307
-
308
- const struct = OrganizationStruct.create({
295
+ getBaseStructure(): OrganizationStruct {
296
+ return OrganizationStruct.create({
309
297
  id: this.id,
310
298
  name: this.name,
311
299
  meta: this.meta,
@@ -313,25 +301,8 @@ export class Organization extends Model {
313
301
  registerDomain: this.registerDomain,
314
302
  uri: this.uri,
315
303
  website: this.website,
316
- groups: groups.map(g => g.getStructure()),
317
304
  createdAt: this.createdAt,
318
- period: organizationPeriod.getStructure(period, groups)
319
305
  })
320
-
321
- if (this.meta.modules.disableActivities) {
322
- // Only show groups that are in a given category
323
- struct.groups = struct.categoryTree.categories[0]?.groups ?? []
324
- }
325
-
326
- if (emptyGroups) {
327
- // Reduce data
328
- struct.meta = struct.meta.clone()
329
- struct.meta.categories = []
330
- struct.meta.recordsConfiguration = OrganizationRecordsConfiguration.create({})
331
-
332
- }
333
-
334
- return struct
335
306
  }
336
307
 
337
308
  async updateDNSRecords() {