@stamhoofd/backend 2.57.1 → 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.
- package/index.ts +6 -1
- package/package.json +13 -13
- package/src/audit-logs/EventLogger.ts +30 -0
- package/src/audit-logs/GroupLogger.ts +95 -0
- package/src/audit-logs/MemberLogger.ts +24 -0
- package/src/audit-logs/MemberPlatformMembershipLogger.ts +57 -0
- package/src/audit-logs/MemberResponsibilityRecordLogger.ts +69 -0
- package/src/audit-logs/ModelLogger.ts +218 -0
- package/src/audit-logs/OrderLogger.ts +57 -0
- package/src/audit-logs/OrganizationLogger.ts +26 -0
- package/src/audit-logs/OrganizationRegistrationPeriodLogger.ts +77 -0
- package/src/audit-logs/PaymentLogger.ts +43 -0
- package/src/audit-logs/PlatformLogger.ts +13 -0
- package/src/audit-logs/RegistrationLogger.ts +53 -0
- package/src/audit-logs/RegistrationPeriodLogger.ts +21 -0
- package/src/audit-logs/StripeAccountLogger.ts +47 -0
- package/src/audit-logs/WebshopLogger.ts +35 -0
- package/src/crons.ts +2 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +12 -24
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +4 -18
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +6 -3
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +3 -18
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +0 -15
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +5 -2
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +5 -15
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +18 -28
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +6 -3
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -4
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +8 -2
- package/src/helpers/AuthenticatedStructures.ts +16 -1
- package/src/helpers/Context.ts +8 -2
- package/src/helpers/MemberUserSyncer.ts +45 -40
- package/src/helpers/PeriodHelper.ts +31 -27
- package/src/helpers/TagHelper.ts +23 -20
- package/src/seeds/1722344162-update-membership.ts +2 -2
- package/src/seeds/1726572303-schedule-stock-updates.ts +2 -1
- package/src/services/AuditLogService.ts +89 -216
- package/src/services/BalanceItemPaymentService.ts +1 -1
- package/src/services/BalanceItemService.ts +14 -5
- package/src/services/MemberNumberService.ts +120 -0
- package/src/services/PaymentService.ts +199 -193
- package/src/services/PlatformMembershipService.ts +284 -0
- package/src/services/RegistrationService.ts +76 -27
- package/src/services/explainPatch.ts +528 -316
- package/src/helpers/MembershipHelper.ts +0 -54
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Migration } from '@simonbackx/simple-database';
|
|
2
2
|
import { Registration } from '@stamhoofd/models';
|
|
3
|
+
import { RegistrationService } from '../services/RegistrationService';
|
|
3
4
|
|
|
4
5
|
export default new Migration(async () => {
|
|
5
6
|
if (STAMHOOFD.environment == 'test') {
|
|
@@ -27,7 +28,7 @@ export default new Migration(async () => {
|
|
|
27
28
|
const registrations = await Registration.getByIDs(...rawRegistrations.map(g => g.id));
|
|
28
29
|
|
|
29
30
|
for (const registration of registrations) {
|
|
30
|
-
|
|
31
|
+
RegistrationService.scheduleStockUpdate(registration.id);
|
|
31
32
|
|
|
32
33
|
c++;
|
|
33
34
|
|
|
@@ -1,232 +1,105 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { Model, ModelEvent } from '@simonbackx/simple-database';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
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;
|
|
10
28
|
};
|
|
11
29
|
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
oldMemberDetails: MemberDetails;
|
|
16
|
-
memberDetailsPatch: AutoEncoderPatchType<MemberDetails>;
|
|
17
|
-
};
|
|
30
|
+
export class AuditLogService {
|
|
31
|
+
private constructor() { }
|
|
32
|
+
static disableLocalStore = new AsyncLocalStorage<AuditLogContextSettings>();
|
|
18
33
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
group: Group;
|
|
23
|
-
registration: Registration;
|
|
24
|
-
};
|
|
34
|
+
static disable<T>(run: () => T): T {
|
|
35
|
+
return this.setContext({ disable: true }, run);
|
|
36
|
+
}
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
oldConfig: PlatformConfig;
|
|
33
|
-
patch: PlatformConfig | AutoEncoderPatchType<PlatformConfig>;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
export type OrganizationConfigChangeAuditOptions = {
|
|
37
|
-
type: AuditLogType.OrganizationSettingsChanged;
|
|
38
|
-
organization: Organization;
|
|
39
|
-
} & ({
|
|
40
|
-
oldMeta: OrganizationMetaData;
|
|
41
|
-
patch: OrganizationMetaData | AutoEncoderPatchType<OrganizationMetaData>;
|
|
42
|
-
} | {
|
|
43
|
-
oldMeta: OrganizationPrivateMetaData;
|
|
44
|
-
patch: OrganizationPrivateMetaData | AutoEncoderPatchType<OrganizationPrivateMetaData>;
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
export type EventAuditOptions = {
|
|
48
|
-
type: AuditLogType.EventAdded | AuditLogType.EventEdited | AuditLogType.EventDeleted;
|
|
49
|
-
event: Event;
|
|
50
|
-
oldData?: AutoEncoder;
|
|
51
|
-
patch?: AutoEncoder | AutoEncoderPatchType<AutoEncoder>;
|
|
52
|
-
};
|
|
38
|
+
static setContext<T>(context: AuditLogContextSettings, run: () => T): T {
|
|
39
|
+
const currentContext = this.getContext() ?? {};
|
|
40
|
+
return this.disableLocalStore.run({ ...currentContext, ...context }, () => {
|
|
41
|
+
return run();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
group: Group;
|
|
57
|
-
oldData?: AutoEncoder;
|
|
58
|
-
patch?: AutoEncoder | AutoEncoderPatchType<AutoEncoder>;
|
|
59
|
-
};
|
|
45
|
+
static isDisabled(): boolean {
|
|
46
|
+
const c = this.getContext();
|
|
60
47
|
|
|
61
|
-
|
|
48
|
+
if (c && c.disable === true) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
62
51
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const userId = Context.optionalAuth?.user?.id ?? null;
|
|
67
|
-
const organizationId = Context.organization?.id ?? null;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
68
54
|
|
|
69
|
-
|
|
55
|
+
static getContext(): AuditLogContextSettings | null {
|
|
56
|
+
const c = this.disableLocalStore.getStore();
|
|
57
|
+
return c ?? null;
|
|
58
|
+
}
|
|
70
59
|
|
|
71
|
-
|
|
72
|
-
model.userId = userId;
|
|
73
|
-
model.organizationId = organizationId;
|
|
60
|
+
static listening = false;
|
|
74
61
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
else if (options.type === AuditLogType.MemberUnregistered) {
|
|
79
|
-
this.fillForMemberRegistered(model, options);
|
|
80
|
-
}
|
|
81
|
-
else if (options.type === AuditLogType.MemberEdited) {
|
|
82
|
-
this.fillForMemberEdited(model, options);
|
|
83
|
-
}
|
|
84
|
-
else if (options.type === AuditLogType.MemberAdded) {
|
|
85
|
-
this.fillForMemberAdded(model, options);
|
|
86
|
-
}
|
|
87
|
-
else if (options.type === AuditLogType.PlatformSettingsChanged) {
|
|
88
|
-
this.fillForPlatformConfig(model, options);
|
|
89
|
-
}
|
|
90
|
-
else if (options.type === AuditLogType.OrganizationSettingsChanged) {
|
|
91
|
-
this.fillForOrganizationConfig(model, options);
|
|
92
|
-
}
|
|
93
|
-
else if (options.type === AuditLogType.EventAdded || options.type === AuditLogType.EventEdited || options.type === AuditLogType.EventDeleted) {
|
|
94
|
-
this.fillForEvent(model, options);
|
|
95
|
-
}
|
|
96
|
-
else if (options.type === AuditLogType.GroupAdded || options.type === AuditLogType.GroupEdited || options.type === AuditLogType.GroupDeleted) {
|
|
97
|
-
this.fillForGroup(model, options);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// In the future we might group these saves together in one query to improve performance
|
|
101
|
-
await model.save();
|
|
102
|
-
|
|
103
|
-
console.log('Audit log', model.id, options);
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
console.error('Failed to save log', options, e);
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
fillForMemberRegistered(model: AuditLog, options: MemberRegisteredAuditOptions) {
|
|
111
|
-
model.objectId = options.member.id;
|
|
112
|
-
model.replacements = new Map([
|
|
113
|
-
['m', AuditLogReplacement.create({
|
|
114
|
-
id: options.member.id,
|
|
115
|
-
value: options.member.details.name,
|
|
116
|
-
type: AuditLogReplacementType.Member,
|
|
117
|
-
})],
|
|
118
|
-
['g', AuditLogReplacement.create({
|
|
119
|
-
id: options.group.id,
|
|
120
|
-
value: options.group.settings.name,
|
|
121
|
-
type: AuditLogReplacementType.Group,
|
|
122
|
-
})],
|
|
123
|
-
]);
|
|
124
|
-
|
|
125
|
-
const registrationStructure = options.registration.setRelation(Registration.group, options.group).getStructure();
|
|
126
|
-
if (registrationStructure.description) {
|
|
127
|
-
model.description = registrationStructure.description;
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
fillForMemberEdited(model: AuditLog, options: MemberEditedAuditOptions) {
|
|
132
|
-
model.objectId = options.member.id;
|
|
133
|
-
|
|
134
|
-
model.replacements = new Map([
|
|
135
|
-
['m', AuditLogReplacement.create({
|
|
136
|
-
id: options.member.id,
|
|
137
|
-
value: options.member.details.name,
|
|
138
|
-
type: AuditLogReplacementType.Member,
|
|
139
|
-
})],
|
|
140
|
-
]);
|
|
141
|
-
|
|
142
|
-
// Generate changes list
|
|
143
|
-
model.patchList = explainPatch(options.oldMemberDetails, options.memberDetailsPatch);
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
fillForMemberAdded(model: AuditLog, options: MemberAddedAuditOptions) {
|
|
147
|
-
model.objectId = options.member.id;
|
|
148
|
-
|
|
149
|
-
model.replacements = new Map([
|
|
150
|
-
['m', AuditLogReplacement.create({
|
|
151
|
-
id: options.member.id,
|
|
152
|
-
value: options.member.details.name,
|
|
153
|
-
type: AuditLogReplacementType.Member,
|
|
154
|
-
})],
|
|
155
|
-
]);
|
|
156
|
-
|
|
157
|
-
// Generate changes list
|
|
158
|
-
model.patchList = explainPatch(null, options.member.details);
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
fillForPlatformConfig(model: AuditLog, options: PlatformConfigChangeAuditOptions) {
|
|
162
|
-
model.objectId = null;
|
|
163
|
-
|
|
164
|
-
// Generate changes list
|
|
165
|
-
model.patchList = explainPatch(options.oldConfig, options.patch);
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
fillForOrganizationConfig(model: AuditLog, options: OrganizationConfigChangeAuditOptions) {
|
|
169
|
-
model.objectId = options.organization.id;
|
|
170
|
-
model.organizationId = options.organization.id;
|
|
171
|
-
|
|
172
|
-
model.replacements = new Map([
|
|
173
|
-
['o', AuditLogReplacement.create({
|
|
174
|
-
id: options.organization.id,
|
|
175
|
-
value: options.organization.name,
|
|
176
|
-
type: AuditLogReplacementType.Organization,
|
|
177
|
-
})],
|
|
178
|
-
]);
|
|
179
|
-
|
|
180
|
-
// Generate changes list
|
|
181
|
-
model.patchList = explainPatch(options.oldMeta, options.patch);
|
|
182
|
-
},
|
|
183
|
-
|
|
184
|
-
fillForEvent(model: AuditLog, options: EventAuditOptions) {
|
|
185
|
-
model.objectId = options.event.id;
|
|
186
|
-
|
|
187
|
-
if (options.patch) {
|
|
188
|
-
// Generate changes list
|
|
189
|
-
model.patchList = explainPatch(options.oldData ?? null, options.patch);
|
|
62
|
+
static listen() {
|
|
63
|
+
if (this.listening) {
|
|
64
|
+
return;
|
|
190
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);
|
|
191
70
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
fillForGroup(model: AuditLog, options: GroupAuditOptions) {
|
|
202
|
-
model.objectId = options.group.id;
|
|
203
|
-
|
|
204
|
-
if (options.patch) {
|
|
205
|
-
// Generate changes list
|
|
206
|
-
model.patchList = explainPatch(options.oldData ?? null, options.patch);
|
|
207
|
-
}
|
|
71
|
+
if (!definition) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log('Model event', {
|
|
76
|
+
...event,
|
|
77
|
+
model: event.model.static.name,
|
|
78
|
+
});
|
|
208
79
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
case AuditLogType.GroupAdded:
|
|
213
|
-
model.type = AuditLogType.WaitingListAdded;
|
|
214
|
-
break;
|
|
215
|
-
case AuditLogType.GroupEdited:
|
|
216
|
-
model.type = AuditLogType.WaitingListEdited;
|
|
217
|
-
break;
|
|
218
|
-
case AuditLogType.GroupDeleted:
|
|
219
|
-
model.type = AuditLogType.WaitingListDeleted;
|
|
220
|
-
break;
|
|
80
|
+
if (this.isDisabled()) {
|
|
81
|
+
console.log('Audit log disabled');
|
|
82
|
+
return;
|
|
221
83
|
}
|
|
222
|
-
}
|
|
223
84
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
value: options.group.settings.name,
|
|
228
|
-
type: AuditLogReplacementType.Group,
|
|
229
|
-
})],
|
|
230
|
-
]);
|
|
231
|
-
},
|
|
85
|
+
await definition.logEvent(event);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
232
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|