@stamhoofd/backend 2.55.2 → 2.57.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 (27) hide show
  1. package/index.ts +4 -0
  2. package/package.json +12 -11
  3. package/src/crons.ts +4 -3
  4. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +150 -0
  5. package/src/endpoints/global/events/PatchEventsEndpoint.ts +27 -3
  6. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +27 -9
  7. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +2 -1
  8. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +17 -2
  9. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +17 -1
  10. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +12 -9
  11. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -1
  12. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +5 -3
  13. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +21 -1
  14. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +4 -306
  15. package/src/helpers/AdminPermissionChecker.ts +102 -1
  16. package/src/helpers/AuthenticatedStructures.ts +46 -2
  17. package/src/helpers/EmailResumer.ts +8 -3
  18. package/src/seeds/1732117645-move-rrn.ts +77 -0
  19. package/src/services/AuditLogService.ts +232 -0
  20. package/src/services/BalanceItemPaymentService.ts +45 -0
  21. package/src/services/BalanceItemService.ts +88 -0
  22. package/src/services/GroupService.ts +13 -0
  23. package/src/services/PaymentService.ts +308 -0
  24. package/src/services/RegistrationService.ts +78 -0
  25. package/src/services/explainPatch.ts +639 -0
  26. package/src/sql-filters/audit-logs.ts +10 -0
  27. package/src/sql-sorters/audit-logs.ts +35 -0
@@ -0,0 +1,77 @@
1
+ import { Migration } from '@simonbackx/simple-database';
2
+ import { Member } from '@stamhoofd/models';
3
+ import { MemberUserSyncer } from '../helpers/MemberUserSyncer';
4
+ import { logger } from '@simonbackx/simple-logging';
5
+
6
+ export default new Migration(async () => {
7
+ if (STAMHOOFD.environment == 'test') {
8
+ console.log('skipped in tests');
9
+ return;
10
+ }
11
+
12
+ if (STAMHOOFD.userMode !== 'platform') {
13
+ console.log('skipped seed update-membership because usermode not platform');
14
+ return;
15
+ }
16
+
17
+ process.stdout.write('\n');
18
+ let c = 0;
19
+ let id: string = '';
20
+
21
+ await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
22
+ while (true) {
23
+ const rawMembers = await Member.where({
24
+ id: {
25
+ value: id,
26
+ sign: '>',
27
+ },
28
+ }, { limit: 500, sort: ['id'] });
29
+
30
+ if (rawMembers.length === 0) {
31
+ break;
32
+ }
33
+
34
+ const promises: Promise<any>[] = [];
35
+
36
+ for (const member of rawMembers) {
37
+ promises.push((async () => {
38
+ const idR = '2b7d8f1c-ce67-4612-880b-fb1fb19affbb';
39
+ const valR = member.details.recordAnswers.get(idR);
40
+
41
+ const idRS = 'd381acce-9603-4246-af62-f3ea5292eec7';
42
+ const valRS = member.details.recordAnswers.get(idRS);
43
+
44
+ let save = false;
45
+
46
+ if (valR && !member.details.nationalRegisterNumber) {
47
+ member.details.nationalRegisterNumber = valR.stringValue;
48
+ save = true;
49
+ }
50
+
51
+ if (valRS && member.details.parents.length > 0 && !member.details.parents.find(p => p.nationalRegisterNumber)) {
52
+ member.details.parents[0].nationalRegisterNumber = valRS.stringValue;
53
+ save = true;
54
+ }
55
+
56
+ if (save) {
57
+ await member.save();
58
+ }
59
+ c++;
60
+
61
+ if (c % 1000 === 0) {
62
+ process.stdout.write('.');
63
+ }
64
+ if (c % 10000 === 0) {
65
+ process.stdout.write('\n');
66
+ }
67
+ })());
68
+ }
69
+
70
+ await Promise.all(promises);
71
+ id = rawMembers[rawMembers.length - 1].id;
72
+ }
73
+ });
74
+
75
+ // Do something here
76
+ return Promise.resolve();
77
+ });
@@ -0,0 +1,232 @@
1
+ import { AutoEncoder, AutoEncoderPatchType } from '@simonbackx/simple-encoding';
2
+ import { AuditLog, Group, Member, Organization, Registration, Event } 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';
6
+
7
+ export type MemberAddedAuditOptions = {
8
+ type: AuditLogType.MemberAdded;
9
+ member: Member;
10
+ };
11
+
12
+ export type MemberEditedAuditOptions = {
13
+ type: AuditLogType.MemberEdited;
14
+ member: Member;
15
+ oldMemberDetails: MemberDetails;
16
+ memberDetailsPatch: AutoEncoderPatchType<MemberDetails>;
17
+ };
18
+
19
+ export type MemberRegisteredAuditOptions = {
20
+ type: AuditLogType.MemberRegistered | AuditLogType.MemberUnregistered;
21
+ member: Member;
22
+ group: Group;
23
+ registration: Registration;
24
+ };
25
+
26
+ export type PlatformConfigChangeAuditOptions = {
27
+ type: AuditLogType.PlatformSettingsChanged;
28
+ } & ({
29
+ oldConfig: PlatformPrivateConfig;
30
+ patch: PlatformPrivateConfig | AutoEncoderPatchType<PlatformPrivateConfig>;
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
+ };
53
+
54
+ export type GroupAuditOptions = {
55
+ type: AuditLogType.GroupAdded | AuditLogType.GroupEdited | AuditLogType.GroupDeleted;
56
+ group: Group;
57
+ oldData?: AutoEncoder;
58
+ patch?: AutoEncoder | AutoEncoderPatchType<AutoEncoder>;
59
+ };
60
+
61
+ export type AuditLogOptions = GroupAuditOptions | EventAuditOptions | MemberAddedAuditOptions | MemberEditedAuditOptions | MemberRegisteredAuditOptions | PlatformConfigChangeAuditOptions | OrganizationConfigChangeAuditOptions;
62
+
63
+ export const AuditLogService = {
64
+ async log(options: AuditLogOptions) {
65
+ try {
66
+ const userId = Context.optionalAuth?.user?.id ?? null;
67
+ const organizationId = Context.organization?.id ?? null;
68
+
69
+ const model = new AuditLog();
70
+
71
+ model.type = options.type;
72
+ model.userId = userId;
73
+ model.organizationId = organizationId;
74
+
75
+ if (options.type === AuditLogType.MemberRegistered) {
76
+ this.fillForMemberRegistered(model, options);
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);
190
+ }
191
+
192
+ model.replacements = new Map([
193
+ ['e', AuditLogReplacement.create({
194
+ id: options.event.id,
195
+ value: options.event.name,
196
+ type: AuditLogReplacementType.Event,
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
+ }
208
+
209
+ if (options.group.type === GroupType.WaitingList) {
210
+ // Change event type
211
+ switch (options.type) {
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;
221
+ }
222
+ }
223
+
224
+ model.replacements = new Map([
225
+ ['g', AuditLogReplacement.create({
226
+ id: options.group.id,
227
+ value: options.group.settings.name,
228
+ type: AuditLogReplacementType.Group,
229
+ })],
230
+ ]);
231
+ },
232
+ };
@@ -0,0 +1,45 @@
1
+ import { ManyToOneRelation } from '@simonbackx/simple-database';
2
+ import { BalanceItemPayment, Organization } from '@stamhoofd/models';
3
+ import { BalanceItemStatus } from '@stamhoofd/structures';
4
+ import { BalanceItemService } from './BalanceItemService';
5
+
6
+ type Loaded<T> = (T) extends ManyToOneRelation<infer Key, infer Model> ? Record<Key, Model> : never;
7
+
8
+ export const BalanceItemPaymentService = {
9
+ async markPaid(balanceItemPayment: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
10
+ // Update cached amountPaid of the balance item (balanceItemPayment will get overwritten later, but we need it to calculate the status)
11
+ balanceItemPayment.balanceItem.pricePaid += balanceItemPayment.price;
12
+
13
+ // Update status
14
+ const old = balanceItemPayment.balanceItem.status;
15
+ balanceItemPayment.balanceItem.updateStatus();
16
+ await balanceItemPayment.balanceItem.save();
17
+
18
+ // Do logic of balance item
19
+ if (balanceItemPayment.balanceItem.status === BalanceItemStatus.Paid && old !== BalanceItemStatus.Paid) {
20
+ // Only call markPaid once (if it wasn't (partially) paid before)
21
+ await BalanceItemService.markPaid(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
22
+ }
23
+ else {
24
+ await BalanceItemService.markUpdated(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
25
+ }
26
+ },
27
+
28
+ /**
29
+ * Call balanceItemPayment once a earlier succeeded payment is no longer succeeded
30
+ */
31
+ async undoPaid(balanceItemPayment: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
32
+ await BalanceItemService.undoPaid(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
33
+ },
34
+
35
+ async markFailed(balanceItemPayment: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
36
+ // Do logic of balance item
37
+ await BalanceItemService.markFailed(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
38
+ },
39
+
40
+ async undoFailed(balanceItemPayment: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
41
+ // Reactivate deleted items
42
+ await BalanceItemService.undoFailed(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
43
+ },
44
+
45
+ };
@@ -0,0 +1,88 @@
1
+ import { BalanceItem, Order, Organization, Payment, Webshop } from '@stamhoofd/models';
2
+ import { BalanceItemStatus, OrderStatus } from '@stamhoofd/structures';
3
+ import { RegistrationService } from './RegistrationService';
4
+
5
+ export const BalanceItemService = {
6
+ async markPaid(balanceItem: BalanceItem, payment: Payment | null, organization: Organization) {
7
+ if (balanceItem.status === BalanceItemStatus.Hidden) {
8
+ await BalanceItem.reactivateItems([balanceItem]);
9
+ }
10
+
11
+ // status and pricePaid changes are handled inside balanceitempayment
12
+ if (balanceItem.dependingBalanceItemId) {
13
+ const depending = await BalanceItem.getByID(balanceItem.dependingBalanceItemId);
14
+ if (depending && depending.status === BalanceItemStatus.Hidden) {
15
+ await BalanceItem.reactivateItems([depending]);
16
+ }
17
+ }
18
+
19
+ // If registration
20
+ if (balanceItem.registrationId) {
21
+ await RegistrationService.markValid(balanceItem.registrationId);
22
+ }
23
+
24
+ // If order
25
+ if (balanceItem.orderId) {
26
+ const order = await Order.getByID(balanceItem.orderId);
27
+ if (order) {
28
+ await order.markPaid(payment, organization);
29
+
30
+ // Save number in balance description
31
+ if (order.number !== null) {
32
+ const webshop = await Webshop.getByID(order.webshopId);
33
+
34
+ if (webshop) {
35
+ balanceItem.description = order.generateBalanceDescription(webshop);
36
+ await balanceItem.save();
37
+ }
38
+ }
39
+ }
40
+ }
41
+ },
42
+
43
+ async markUpdated(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
44
+ // For orders: mark order as changed (so they are refetched in front ends)
45
+ if (balanceItem.orderId) {
46
+ const order = await Order.getByID(balanceItem.orderId);
47
+ if (order) {
48
+ await order.paymentChanged(payment, organization);
49
+ }
50
+ }
51
+ },
52
+
53
+ async undoPaid(balanceItem: BalanceItem, payment: Payment | null, organization: Organization) {
54
+ // If order
55
+ if (balanceItem.orderId) {
56
+ const order = await Order.getByID(balanceItem.orderId);
57
+ if (order) {
58
+ await order.undoPaid(payment, organization);
59
+ }
60
+ }
61
+ },
62
+
63
+ async markFailed(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
64
+ // If order
65
+ if (balanceItem.orderId) {
66
+ const order = await Order.getByID(balanceItem.orderId);
67
+ if (order) {
68
+ await order.onPaymentFailed(payment, organization);
69
+
70
+ if (order.status === OrderStatus.Deleted) {
71
+ balanceItem.status = BalanceItemStatus.Hidden;
72
+ await balanceItem.save();
73
+ }
74
+ }
75
+ }
76
+ },
77
+
78
+ async undoFailed(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
79
+ // If order
80
+ if (balanceItem.orderId) {
81
+ const order = await Order.getByID(balanceItem.orderId);
82
+ if (order) {
83
+ await order.undoPaymentFailed(payment, organization);
84
+ }
85
+ }
86
+ },
87
+
88
+ };
@@ -0,0 +1,13 @@
1
+ import { Group } from '@stamhoofd/models';
2
+
3
+ export const GroupService = {
4
+ async updateOccupancy(groupId: string) {
5
+ const group = await Group.getByID(groupId);
6
+ if (group) {
7
+ // todo: implementation should move to the service
8
+ await group.updateOccupancy();
9
+ await group.save();
10
+ }
11
+ return group;
12
+ },
13
+ };