@stamhoofd/backend 2.58.0 → 2.59.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 (48) hide show
  1. package/index.ts +6 -1
  2. package/package.json +12 -12
  3. package/src/audit-logs/EventLogger.ts +30 -0
  4. package/src/audit-logs/GroupLogger.ts +95 -0
  5. package/src/audit-logs/MemberLogger.ts +24 -0
  6. package/src/audit-logs/MemberPlatformMembershipLogger.ts +57 -0
  7. package/src/audit-logs/MemberResponsibilityRecordLogger.ts +69 -0
  8. package/src/audit-logs/ModelLogger.ts +218 -0
  9. package/src/audit-logs/OrderLogger.ts +57 -0
  10. package/src/audit-logs/OrganizationLogger.ts +26 -0
  11. package/src/audit-logs/OrganizationRegistrationPeriodLogger.ts +77 -0
  12. package/src/audit-logs/PaymentLogger.ts +43 -0
  13. package/src/audit-logs/PlatformLogger.ts +13 -0
  14. package/src/audit-logs/RegistrationLogger.ts +53 -0
  15. package/src/audit-logs/RegistrationPeriodLogger.ts +21 -0
  16. package/src/audit-logs/StripeAccountLogger.ts +47 -0
  17. package/src/audit-logs/WebshopLogger.ts +35 -0
  18. package/src/crons.ts +2 -1
  19. package/src/endpoints/global/events/PatchEventsEndpoint.ts +12 -24
  20. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +4 -18
  21. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +5 -13
  22. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +3 -11
  23. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +0 -15
  24. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +5 -2
  25. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +0 -19
  26. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -12
  27. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +18 -33
  28. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +0 -6
  29. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +0 -6
  30. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +5 -14
  31. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -4
  32. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +8 -2
  33. package/src/helpers/AuthenticatedStructures.ts +16 -1
  34. package/src/helpers/Context.ts +8 -2
  35. package/src/helpers/MemberUserSyncer.ts +45 -40
  36. package/src/helpers/PeriodHelper.ts +3 -4
  37. package/src/helpers/TagHelper.ts +23 -20
  38. package/src/seeds/1722344162-update-membership.ts +2 -2
  39. package/src/seeds/1726572303-schedule-stock-updates.ts +2 -1
  40. package/src/services/AuditLogService.ts +83 -295
  41. package/src/services/BalanceItemPaymentService.ts +1 -1
  42. package/src/services/BalanceItemService.ts +14 -5
  43. package/src/services/MemberNumberService.ts +120 -0
  44. package/src/services/PaymentService.ts +199 -193
  45. package/src/services/PlatformMembershipService.ts +284 -0
  46. package/src/services/RegistrationService.ts +76 -27
  47. package/src/services/explainPatch.ts +110 -41
  48. package/src/helpers/MembershipHelper.ts +0 -54
@@ -1,317 +1,105 @@
1
- import { AutoEncoder, AutoEncoderPatchType } from '@simonbackx/simple-encoding';
2
- import { AuditLog, Group, Member, Organization, Registration, Event, RegistrationPeriod, StripeAccount } from '@stamhoofd/models';
3
- import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, GroupType, MemberDetails, OrganizationMetaData, OrganizationPrivateMetaData, PlatformConfig, PlatformPrivateConfig } from '@stamhoofd/structures';
4
- import { Context } from '../helpers/Context';
5
- import { explainPatch } from './explainPatch';
1
+ import { Model, ModelEvent } from '@simonbackx/simple-database';
6
2
  import { AsyncLocalStorage } from 'node:async_hooks';
7
-
8
- export type MemberAddedAuditOptions = {
9
- type: AuditLogType.MemberAdded;
10
- member: Member;
11
- };
12
-
13
- export type MemberEditedAuditOptions = {
14
- type: AuditLogType.MemberEdited;
15
- member: Member;
16
- oldMemberDetails: MemberDetails;
17
- memberDetailsPatch: AutoEncoderPatchType<MemberDetails>;
18
- };
19
-
20
- export type MemberRegisteredAuditOptions = {
21
- type: AuditLogType.MemberRegistered | AuditLogType.MemberUnregistered;
22
- member: Member;
23
- group: Group;
24
- registration: Registration;
3
+ import { RegistrationLogger } from '../audit-logs/RegistrationLogger';
4
+ import { GroupLogger } from '../audit-logs/GroupLogger';
5
+ import { OrganizationLogger } from '../audit-logs/OrganizationLogger';
6
+ import { PlatformLogger } from '../audit-logs/PlatformLogger';
7
+ import { EventLogger } from '../audit-logs/EventLogger';
8
+ import { RegistrationPeriodLogger } from '../audit-logs/RegistrationPeriodLogger';
9
+ import { OrganizationRegistrationPeriodLogger } from '../audit-logs/OrganizationRegistrationPeriodLogger';
10
+ import { StripeAccountLogger } from '../audit-logs/StripeAccountLogger';
11
+ import { MemberLogger } from '../audit-logs/MemberLogger';
12
+ import { WebshopLogger } from '../audit-logs/WebshopLogger';
13
+ import { OrderLogger } from '../audit-logs/OrderLogger';
14
+ import { AuditLogSource } from '@stamhoofd/structures';
15
+ import { PaymentLogger } from '../audit-logs/PaymentLogger';
16
+ import { MemberPlatformMembershipLogger } from '../audit-logs/MemberPlatformMembershipLogger';
17
+ import { MemberResponsibilityRecordLogger } from '../audit-logs/MemberResponsibilityRecordLogger';
18
+
19
+ export type AuditLogContextSettings = {
20
+ disable?: boolean;
21
+ source?: AuditLogSource;
22
+ userId?: string | null;
23
+
24
+ // If no userId is known, fallback to this userId
25
+ // this is useful e.g. for side effects of webhooks where the webhook calls but we don't have the userid in the request, still the action is tied to a user
26
+ fallbackUserId?: string | null;
27
+ fallbackOrganizationId?: string | null;
25
28
  };
26
29
 
27
- export type PlatformConfigChangeAuditOptions = {
28
- type: AuditLogType.PlatformSettingsChanged;
29
- oldData?: AutoEncoder;
30
- patch?: AutoEncoder | AutoEncoderPatchType<AutoEncoder>;
31
- };
30
+ export class AuditLogService {
31
+ private constructor() { }
32
+ static disableLocalStore = new AsyncLocalStorage<AuditLogContextSettings>();
32
33
 
33
- export type OrganizationConfigChangeAuditOptions = {
34
- type: AuditLogType.OrganizationSettingsChanged;
35
- organization: Organization;
36
- oldData?: AutoEncoder;
37
- patch?: AutoEncoder | AutoEncoderPatchType<AutoEncoder>;
38
- };
34
+ static disable<T>(run: () => T): T {
35
+ return this.setContext({ disable: true }, run);
36
+ }
39
37
 
40
- export type EventAuditOptions = {
41
- type: AuditLogType.EventAdded | AuditLogType.EventEdited | AuditLogType.EventDeleted;
42
- event: Event;
43
- oldData?: AutoEncoder;
44
- patch?: AutoEncoder | AutoEncoderPatchType<AutoEncoder>;
45
- };
46
-
47
- export type GroupAuditOptions = {
48
- type: AuditLogType.GroupAdded | AuditLogType.GroupEdited | AuditLogType.GroupDeleted;
49
- group: Group;
50
- oldData?: AutoEncoder;
51
- patch?: AutoEncoder | AutoEncoderPatchType<AutoEncoder>;
52
- };
53
-
54
- export type PeriodAuditOptions = {
55
- type: AuditLogType.RegistrationPeriodAdded | AuditLogType.RegistrationPeriodEdited | AuditLogType.RegistrationPeriodDeleted;
56
- period: RegistrationPeriod;
57
- oldData?: AutoEncoder;
58
- patch?: AutoEncoder | AutoEncoderPatchType<AutoEncoder>;
59
- };
60
-
61
- export type StripeAccountAuditOptions = {
62
- type: AuditLogType.StripeAccountAdded | AuditLogType.StripeAccountEdited | AuditLogType.StripeAccountDeleted;
63
- stripeAccount: StripeAccount;
64
- oldData?: AutoEncoder;
65
- patch?: AutoEncoder | AutoEncoderPatchType<AutoEncoder>;
66
- };
67
-
68
- export type AuditLogOptions = StripeAccountAuditOptions | PeriodAuditOptions | GroupAuditOptions | EventAuditOptions | MemberAddedAuditOptions | MemberEditedAuditOptions | MemberRegisteredAuditOptions | PlatformConfigChangeAuditOptions | OrganizationConfigChangeAuditOptions;
69
-
70
- export const AuditLogService = {
71
- disableLocalStore: new AsyncLocalStorage<boolean>(),
72
-
73
- disable<T extends Promise<void> | void>(run: () => T): T {
74
- return this.disableLocalStore.run(true, () => {
38
+ static setContext<T>(context: AuditLogContextSettings, run: () => T): T {
39
+ const currentContext = this.getContext() ?? {};
40
+ return this.disableLocalStore.run({ ...currentContext, ...context }, () => {
75
41
  return run();
76
42
  });
77
- },
43
+ }
78
44
 
79
- isDisabled(): boolean {
80
- const c = this.disableLocalStore.getStore();
45
+ static isDisabled(): boolean {
46
+ const c = this.getContext();
81
47
 
82
- if (!c) {
83
- return false;
48
+ if (c && c.disable === true) {
49
+ return true;
84
50
  }
85
51
 
86
- return true;
87
- },
52
+ return false;
53
+ }
88
54
 
89
- async log(options: AuditLogOptions) {
90
- try {
91
- if (this.isDisabled()) {
92
- return;
93
- }
94
- const userId = Context.optionalAuth?.user?.id ?? null;
95
- const organizationId = Context.organization?.id ?? null;
96
-
97
- const model = new AuditLog();
98
-
99
- model.type = options.type;
100
- model.userId = userId;
101
- model.organizationId = organizationId;
102
-
103
- if (options.type === AuditLogType.MemberRegistered) {
104
- this.fillForMemberRegistered(model, options);
105
- }
106
- else if (options.type === AuditLogType.MemberUnregistered) {
107
- this.fillForMemberRegistered(model, options);
108
- }
109
- else if (options.type === AuditLogType.MemberEdited) {
110
- this.fillForMemberEdited(model, options);
111
- }
112
- else if (options.type === AuditLogType.MemberAdded) {
113
- this.fillForMemberAdded(model, options);
114
- }
115
- else if (options.type === AuditLogType.PlatformSettingsChanged) {
116
- this.fillForPlatformConfig(model, options);
117
- }
118
- else if (options.type === AuditLogType.OrganizationSettingsChanged) {
119
- this.fillForOrganizationConfig(model, options);
120
- }
121
- else if (options.type === AuditLogType.EventAdded || options.type === AuditLogType.EventEdited || options.type === AuditLogType.EventDeleted) {
122
- this.fillForEvent(model, options);
123
- }
124
- else if (options.type === AuditLogType.GroupAdded || options.type === AuditLogType.GroupEdited || options.type === AuditLogType.GroupDeleted) {
125
- this.fillForGroup(model, options);
126
- }
127
- else if (options.type === AuditLogType.RegistrationPeriodAdded || options.type === AuditLogType.RegistrationPeriodEdited || options.type === AuditLogType.RegistrationPeriodDeleted) {
128
- this.fillForPeriod(model, options);
129
- }
130
- else if (options.type === AuditLogType.StripeAccountAdded || options.type === AuditLogType.StripeAccountEdited || options.type === AuditLogType.StripeAccountDeleted) {
131
- if (this.fillForStripeAccount(model, options) === false) {
132
- // do not save
133
- return;
134
- }
135
- }
136
-
137
- // In the future we might group these saves together in one query to improve performance
138
- await model.save();
139
- }
140
- catch (e) {
141
- console.error('Failed to save log', options, e);
142
- }
143
- },
144
-
145
- fillForMemberRegistered(model: AuditLog, options: MemberRegisteredAuditOptions) {
146
- model.objectId = options.member.id;
147
- model.replacements = new Map([
148
- ['m', AuditLogReplacement.create({
149
- id: options.member.id,
150
- value: options.member.details.name,
151
- type: AuditLogReplacementType.Member,
152
- })],
153
- ['g', AuditLogReplacement.create({
154
- id: options.group.id,
155
- value: options.group.settings.name,
156
- type: AuditLogReplacementType.Group,
157
- })],
158
- ]);
159
-
160
- const registrationStructure = options.registration.setRelation(Registration.group, options.group).getStructure();
161
- if (registrationStructure.description) {
162
- model.description = registrationStructure.description;
163
- }
164
- },
165
-
166
- fillForMemberEdited(model: AuditLog, options: MemberEditedAuditOptions) {
167
- model.objectId = options.member.id;
168
-
169
- model.replacements = new Map([
170
- ['m', AuditLogReplacement.create({
171
- id: options.member.id,
172
- value: options.member.details.name,
173
- type: AuditLogReplacementType.Member,
174
- })],
175
- ]);
176
-
177
- // Generate changes list
178
- model.patchList = explainPatch(options.oldMemberDetails, options.memberDetailsPatch);
179
- },
180
-
181
- fillForMemberAdded(model: AuditLog, options: MemberAddedAuditOptions) {
182
- model.objectId = options.member.id;
183
-
184
- model.replacements = new Map([
185
- ['m', AuditLogReplacement.create({
186
- id: options.member.id,
187
- value: options.member.details.name,
188
- type: AuditLogReplacementType.Member,
189
- })],
190
- ]);
191
-
192
- // Generate changes list
193
- model.patchList = explainPatch(null, options.member.details);
194
- },
195
-
196
- fillForPlatformConfig(model: AuditLog, options: PlatformConfigChangeAuditOptions) {
197
- model.objectId = null;
198
-
199
- // Generate changes list
200
- if (options.patch) {
201
- // Generate changes list
202
- model.patchList = explainPatch(options.oldData ?? null, options.patch);
203
- }
204
- },
205
-
206
- fillForOrganizationConfig(model: AuditLog, options: OrganizationConfigChangeAuditOptions) {
207
- model.objectId = options.organization.id;
208
- model.organizationId = options.organization.id;
209
-
210
- model.replacements = new Map([
211
- ['o', AuditLogReplacement.create({
212
- id: options.organization.id,
213
- value: options.organization.name,
214
- type: AuditLogReplacementType.Organization,
215
- })],
216
- ]);
217
-
218
- // Generate changes list
219
- if (options.patch) {
220
- // Generate changes list
221
- model.patchList = explainPatch(options.oldData ?? null, options.patch);
222
- }
223
- },
224
-
225
- fillForEvent(model: AuditLog, options: EventAuditOptions) {
226
- model.objectId = options.event.id;
227
-
228
- if (options.patch) {
229
- // Generate changes list
230
- model.patchList = explainPatch(options.oldData ?? null, options.patch);
231
- }
232
-
233
- model.replacements = new Map([
234
- ['e', AuditLogReplacement.create({
235
- id: options.event.id,
236
- value: options.event.name,
237
- type: AuditLogReplacementType.Event,
238
- })],
239
- ]);
240
- },
55
+ static getContext(): AuditLogContextSettings | null {
56
+ const c = this.disableLocalStore.getStore();
57
+ return c ?? null;
58
+ }
241
59
 
242
- fillForGroup(model: AuditLog, options: GroupAuditOptions) {
243
- model.objectId = options.group.id;
60
+ static listening = false;
244
61
 
245
- if (options.patch) {
246
- // Generate changes list
247
- model.patchList = explainPatch(options.oldData ?? null, options.patch);
62
+ static listen() {
63
+ if (this.listening) {
64
+ return;
248
65
  }
66
+ this.listening = true;
67
+ Model.modelEventBus.addListener(this, async (event) => {
68
+ const modelType = event.model.static as typeof Model;
69
+ const definition = modelLogDefinitions.get(modelType);
249
70
 
250
- if (options.group.type === GroupType.WaitingList) {
251
- // Change event type
252
- switch (options.type) {
253
- case AuditLogType.GroupAdded:
254
- model.type = AuditLogType.WaitingListAdded;
255
- break;
256
- case AuditLogType.GroupEdited:
257
- model.type = AuditLogType.WaitingListEdited;
258
- break;
259
- case AuditLogType.GroupDeleted:
260
- model.type = AuditLogType.WaitingListDeleted;
261
- break;
71
+ if (!definition) {
72
+ return;
262
73
  }
263
- }
264
-
265
- model.replacements = new Map([
266
- ['g', AuditLogReplacement.create({
267
- id: options.group.id,
268
- value: options.group.settings.name,
269
- type: AuditLogReplacementType.Group,
270
- })],
271
- ]);
272
- },
273
-
274
- fillForPeriod(model: AuditLog, options: PeriodAuditOptions) {
275
- model.objectId = options.period.id;
276
-
277
- if (options.patch) {
278
- // Generate changes list
279
- model.patchList = explainPatch(options.oldData ?? null, options.patch);
280
- }
281
74
 
282
- model.replacements = new Map([
283
- ['p', AuditLogReplacement.create({
284
- id: options.period.id,
285
- value: options.period.getStructure().nameShort,
286
- type: AuditLogReplacementType.RegistrationPeriod,
287
- })],
288
- ]);
289
- },
75
+ console.log('Model event', {
76
+ ...event,
77
+ model: event.model.static.name,
78
+ });
290
79
 
291
- fillForStripeAccount(model: AuditLog, options: StripeAccountAuditOptions) {
292
- model.objectId = options.stripeAccount.id;
293
-
294
- if (options.patch) {
295
- // Generate changes list
296
- model.patchList = explainPatch(options.oldData ?? null, options.patch);
297
-
298
- if (model.patchList.length === 0) {
299
- // No changes, ignore (only for stripe)
300
- return false;
80
+ if (this.isDisabled()) {
81
+ console.log('Audit log disabled');
82
+ return;
301
83
  }
302
- }
303
84
 
304
- if (options.type === AuditLogType.StripeAccountEdited) {
305
- // Never caused by a user
306
- model.userId = null;
307
- }
308
-
309
- model.replacements = new Map([
310
- ['a', AuditLogReplacement.create({
311
- id: options.stripeAccount.id,
312
- value: options.stripeAccount.accountId,
313
- type: AuditLogReplacementType.StripeAccount,
314
- })],
315
- ]);
316
- },
85
+ await definition.logEvent(event);
86
+ });
87
+ }
317
88
  };
89
+
90
+ const modelLogDefinitions = new Map<typeof Model, { logEvent: (event: ModelEvent) => Promise<any> }>();
91
+
92
+ modelLogDefinitions.set(RegistrationLogger.model, RegistrationLogger);
93
+ modelLogDefinitions.set(GroupLogger.model, GroupLogger);
94
+ modelLogDefinitions.set(OrganizationLogger.model, OrganizationLogger);
95
+ modelLogDefinitions.set(PlatformLogger.model, PlatformLogger);
96
+ modelLogDefinitions.set(EventLogger.model, EventLogger);
97
+ modelLogDefinitions.set(RegistrationPeriodLogger.model, RegistrationPeriodLogger);
98
+ modelLogDefinitions.set(OrganizationRegistrationPeriodLogger.model, OrganizationRegistrationPeriodLogger);
99
+ modelLogDefinitions.set(StripeAccountLogger.model, StripeAccountLogger);
100
+ modelLogDefinitions.set(MemberLogger.model, MemberLogger);
101
+ modelLogDefinitions.set(WebshopLogger.model, WebshopLogger);
102
+ modelLogDefinitions.set(OrderLogger.model, OrderLogger);
103
+ modelLogDefinitions.set(PaymentLogger.model, PaymentLogger);
104
+ modelLogDefinitions.set(MemberPlatformMembershipLogger.model, MemberPlatformMembershipLogger);
105
+ modelLogDefinitions.set(MemberResponsibilityRecordLogger.model, MemberResponsibilityRecordLogger);
@@ -16,7 +16,7 @@ export const BalanceItemPaymentService = {
16
16
  await balanceItemPayment.balanceItem.save();
17
17
 
18
18
  // Do logic of balance item
19
- if (balanceItemPayment.balanceItem.status === BalanceItemStatus.Paid && old !== BalanceItemStatus.Paid) {
19
+ if (balanceItemPayment.balanceItem.status === BalanceItemStatus.Paid && old !== BalanceItemStatus.Paid && balanceItemPayment.price >= 0) {
20
20
  // Only call markPaid once (if it wasn't (partially) paid before)
21
21
  await BalanceItemService.markPaid(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
22
22
  }
@@ -1,6 +1,7 @@
1
1
  import { BalanceItem, Order, Organization, Payment, Webshop } from '@stamhoofd/models';
2
- import { BalanceItemStatus, OrderStatus } from '@stamhoofd/structures';
2
+ import { AuditLogSource, BalanceItemStatus, OrderStatus } from '@stamhoofd/structures';
3
3
  import { RegistrationService } from './RegistrationService';
4
+ import { AuditLogService } from './AuditLogService';
4
5
 
5
6
  export const BalanceItemService = {
6
7
  async markPaid(balanceItem: BalanceItem, payment: Payment | null, organization: Organization) {
@@ -16,6 +17,10 @@ export const BalanceItemService = {
16
17
  }
17
18
  }
18
19
 
20
+ // It is possible this balance item was earlier paid
21
+ // and later the regigstration / order has been canceled and it became a negative balance item - which as some point has been reembursed and marked as 'paid'
22
+ // in that case, we should be careful not to mark the registration as valid again
23
+
19
24
  // If registration
20
25
  if (balanceItem.registrationId) {
21
26
  await RegistrationService.markValid(balanceItem.registrationId);
@@ -43,10 +48,14 @@ export const BalanceItemService = {
43
48
  async markUpdated(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
44
49
  // For orders: mark order as changed (so they are refetched in front ends)
45
50
  if (balanceItem.orderId) {
46
- const order = await Order.getByID(balanceItem.orderId);
47
- if (order) {
48
- await order.paymentChanged(payment, organization);
49
- }
51
+ await AuditLogService.setContext({ source: AuditLogSource.Payment }, async () => {
52
+ if (balanceItem.orderId) {
53
+ const order = await Order.getByID(balanceItem.orderId);
54
+ if (order) {
55
+ await order.paymentChanged(payment, organization);
56
+ }
57
+ }
58
+ });
50
59
  }
51
60
  },
52
61
 
@@ -0,0 +1,120 @@
1
+ import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
2
+ import { Member, MemberPlatformMembership, Organization } from '@stamhoofd/models';
3
+ import { QueueHandler } from '@stamhoofd/queues';
4
+ import { SQL, SQLWhereLike, scalarToSQLExpression } from '@stamhoofd/sql';
5
+
6
+ export class MemberNumberService {
7
+ static async assignMemberNumber(member: Member, membership: MemberPlatformMembership) {
8
+ if (member.details?.memberNumber) {
9
+ console.log('Member already has member number, should not happen');
10
+ return;
11
+ }
12
+
13
+ return await QueueHandler.schedule('assignMemberNumber', async function (this: undefined) {
14
+ try {
15
+ const memberNumber = await MemberNumberService.createMemberNumber(member, membership);
16
+ member.details.memberNumber = memberNumber;
17
+ await member.save();
18
+ }
19
+ catch (error) {
20
+ if (isSimpleError(error) || isSimpleErrors(error)) {
21
+ throw error;
22
+ }
23
+ else {
24
+ console.error(error);
25
+ throw new SimpleError({
26
+ code: 'assign_member_number',
27
+ message: error.message,
28
+ human: 'Er is iets misgegaan bij het aanmaken van het lidnummer.',
29
+ });
30
+ }
31
+ }
32
+ });
33
+ }
34
+
35
+ static async createMemberNumber(member: Member, membership: MemberPlatformMembership): Promise<string> {
36
+ // example: 5301-101012-1
37
+
38
+ // #region get birth date part (ddmmjj)
39
+ const birthDay = member.details?.birthDay;
40
+ if (!birthDay) {
41
+ throw new SimpleError({
42
+ code: 'assign_member_number',
43
+ message: 'Missing birthDay',
44
+ human: 'Er kon geen lidnummer aangemaakt worden omdat er geen geboortedatum is ingesteld.',
45
+ });
46
+ }
47
+
48
+ const dayPart = birthDay.getDate().toString().padStart(2, '0');
49
+ const monthPart = (birthDay.getMonth() + 1).toString().padStart(2, '0');
50
+ const yearPart = birthDay.getFullYear().toString().slice(2, 4);
51
+ const birthDatePart = `${dayPart}${monthPart}${yearPart}`;
52
+ // #endregion
53
+
54
+ // #region get group number
55
+ const organizationId = membership.organizationId;
56
+ const organization = await Organization.getByID(organizationId);
57
+ if (!organization) {
58
+ throw new Error(`Organization with id ${organizationId} not found`);
59
+ }
60
+ const groupNumber = organization.uri;
61
+ // #endregion
62
+
63
+ // #region get follow up number
64
+ const firstPart = `${groupNumber}-${birthDatePart}-`;
65
+
66
+ const query = SQL.select()
67
+ .from(SQL.table('members'))
68
+ .where(
69
+ new SQLWhereLike(
70
+ SQL.column('members', 'memberNumber'),
71
+ scalarToSQLExpression(`${SQLWhereLike.escape(firstPart)}%`),
72
+ ),
73
+ );
74
+
75
+ const count = await query.count();
76
+ console.log(`Found ${count} members with a memberNumber starting with ${firstPart}`);
77
+
78
+ let followUpNumber = count;
79
+ // #endregion
80
+
81
+ // #region check if memberNumber is unique
82
+ let doesExist = true;
83
+ let memberNumber: string = '';
84
+ let tries = 0;
85
+
86
+ while (doesExist) {
87
+ followUpNumber++;
88
+ memberNumber = firstPart + followUpNumber;
89
+
90
+ const result = await SQL.select()
91
+ .from(SQL.table('members'))
92
+ .where(
93
+ SQL.column('members', 'memberNumber'),
94
+ scalarToSQLExpression(memberNumber),
95
+ )
96
+ .first(false);
97
+
98
+ console.log(`Is ${memberNumber} unique? ${result === null}`);
99
+
100
+ if (result !== null) {
101
+ tries++;
102
+ if (tries > 9) {
103
+ throw new SimpleError({
104
+ code: 'assign_member_number',
105
+ message: `Duplicate member numbers (last try: ${memberNumber}, tries: ${tries})`,
106
+ human: 'Er kon geen uniek lidnummer aangemaakt worden. Mogelijks zijn er teveel leden met dezelfde geboortedatum. Neem contact op met de vereniging.',
107
+ });
108
+ }
109
+ }
110
+ else {
111
+ doesExist = false;
112
+ }
113
+ }
114
+ // #endregion
115
+
116
+ console.log(`Created member number: ${memberNumber}`);
117
+
118
+ return memberNumber;
119
+ }
120
+ }