@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.
Files changed (48) hide show
  1. package/index.ts +6 -1
  2. package/package.json +13 -13
  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 +6 -3
  22. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +3 -18
  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 +2 -2
  26. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +5 -15
  27. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +18 -28
  28. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +2 -1
  29. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +2 -1
  30. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +6 -3
  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 +31 -27
  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 +89 -216
  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 +528 -316
  48. package/src/helpers/MembershipHelper.ts +0 -54
package/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import backendEnv from '@stamhoofd/backend-env';
2
2
  backendEnv.load();
3
3
 
4
- import { Column, Database, Migration } from '@simonbackx/simple-database';
4
+ import { Column, Database, Migration, Model } from '@simonbackx/simple-database';
5
5
  import { CORSPreflightEndpoint, Router, RouterServer } from '@simonbackx/simple-endpoints';
6
6
  import { I18n } from '@stamhoofd/backend-i18n';
7
7
  import { CORSMiddleware, LogMiddleware, VersionMiddleware } from '@stamhoofd/backend-middleware';
@@ -14,6 +14,8 @@ import { stopCrons, startCrons, waitForCrons } from '@stamhoofd/crons';
14
14
  import { resumeEmails } from './src/helpers/EmailResumer';
15
15
  import { ContextMiddleware } from './src/middleware/ContextMiddleware';
16
16
  import { Platform } from '@stamhoofd/models';
17
+ import { AuditLogService } from './src/services/AuditLogService';
18
+ import { PlatformMembershipService } from './src/services/PlatformMembershipService';
17
19
 
18
20
  process.on('unhandledRejection', (error: Error) => {
19
21
  console.error('unhandledRejection');
@@ -184,6 +186,9 @@ const start = async () => {
184
186
  await import('./src/crons');
185
187
  startCrons();
186
188
  seeds().catch(console.error);
189
+
190
+ AuditLogService.listen();
191
+ PlatformMembershipService.listen();
187
192
  };
188
193
 
189
194
  start().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.57.1",
3
+ "version": "2.59.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -33,18 +33,18 @@
33
33
  "dependencies": {
34
34
  "@bwip-js/node": "^4.5.1",
35
35
  "@mollie/api-client": "3.7.0",
36
- "@simonbackx/simple-database": "1.25.0",
37
- "@simonbackx/simple-encoding": "2.17.0",
38
- "@simonbackx/simple-endpoints": "1.14.0",
36
+ "@simonbackx/simple-database": "1.27.0",
37
+ "@simonbackx/simple-encoding": "2.18.0",
38
+ "@simonbackx/simple-endpoints": "1.15.0",
39
39
  "@simonbackx/simple-logging": "^1.0.1",
40
- "@stamhoofd/backend-i18n": "2.57.1",
41
- "@stamhoofd/backend-middleware": "2.57.1",
42
- "@stamhoofd/email": "2.57.1",
43
- "@stamhoofd/models": "2.57.1",
44
- "@stamhoofd/queues": "2.57.1",
45
- "@stamhoofd/sql": "2.57.1",
46
- "@stamhoofd/structures": "2.57.1",
47
- "@stamhoofd/utility": "2.57.1",
40
+ "@stamhoofd/backend-i18n": "2.59.0",
41
+ "@stamhoofd/backend-middleware": "2.59.0",
42
+ "@stamhoofd/email": "2.59.0",
43
+ "@stamhoofd/models": "2.59.0",
44
+ "@stamhoofd/queues": "2.59.0",
45
+ "@stamhoofd/sql": "2.59.0",
46
+ "@stamhoofd/structures": "2.59.0",
47
+ "@stamhoofd/utility": "2.59.0",
48
48
  "archiver": "^7.0.1",
49
49
  "aws-sdk": "^2.885.0",
50
50
  "axios": "1.6.8",
@@ -64,5 +64,5 @@
64
64
  "publishConfig": {
65
65
  "access": "public"
66
66
  },
67
- "gitHead": "f109d195d0bbb0e49c7b7bcef403b710c68c025b"
67
+ "gitHead": "a7043be8f8fde4fd1c91943495132a21921ebbe1"
68
68
  }
@@ -0,0 +1,30 @@
1
+ import { Event } from '@stamhoofd/models';
2
+ import { getDefaultGenerator, ModelLogger } from './ModelLogger';
3
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
4
+
5
+ export const EventLogger = new ModelLogger(Event, {
6
+ optionsGenerator: getDefaultGenerator({
7
+ created: AuditLogType.EventAdded,
8
+ updated: AuditLogType.EventEdited,
9
+ deleted: AuditLogType.EventDeleted,
10
+ }),
11
+
12
+ createReplacements(model) {
13
+ return new Map([
14
+ ['e', AuditLogReplacement.create({
15
+ id: model.id,
16
+ value: model.name,
17
+ type: AuditLogReplacementType.Event,
18
+ })],
19
+ ]);
20
+ },
21
+
22
+ postProcess(event, options, log) {
23
+ // Replace groupId key by 'registrations'
24
+ for (const item of log.patchList) {
25
+ if (item.key.value === 'groupId') {
26
+ item.key = AuditLogReplacement.key('registrations');
27
+ }
28
+ }
29
+ },
30
+ });
@@ -0,0 +1,95 @@
1
+ import { Group } from '@stamhoofd/models';
2
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, GroupType } from '@stamhoofd/structures';
3
+ import { getDefaultGenerator, ModelLogger } from './ModelLogger';
4
+
5
+ const defaultGenerator = getDefaultGenerator({
6
+ created: AuditLogType.GroupAdded,
7
+ updated: AuditLogType.GroupEdited,
8
+ deleted: AuditLogType.GroupDeleted,
9
+ });
10
+
11
+ export const GroupLogger = new ModelLogger(Group, {
12
+ skipKeys: ['periodId', 'cycle', 'organizationId', 'stockReservations', 'settings.registeredMembers'],
13
+ async optionsGenerator(event) {
14
+ const result = await defaultGenerator(event);
15
+
16
+ if (!result) {
17
+ return;
18
+ }
19
+
20
+ const model = event.model;
21
+ if (model.type === GroupType.WaitingList) {
22
+ // Change event type
23
+ switch (result.type) {
24
+ case AuditLogType.GroupAdded:
25
+ result.type = AuditLogType.WaitingListAdded;
26
+ break;
27
+ case AuditLogType.GroupEdited:
28
+ result.type = AuditLogType.WaitingListEdited;
29
+ break;
30
+ case AuditLogType.GroupDeleted:
31
+ result.type = AuditLogType.WaitingListDeleted;
32
+ break;
33
+ }
34
+ }
35
+
36
+ if (model.type === GroupType.EventRegistration) {
37
+ // Change event type
38
+ switch (result.type) {
39
+ case AuditLogType.GroupAdded:
40
+ // do not log
41
+ return;
42
+ case AuditLogType.GroupEdited:
43
+ result.type = AuditLogType.EventEdited;
44
+
45
+ if (model.settings.eventId) {
46
+ result.objectId = model.settings.eventId;
47
+ }
48
+ break;
49
+ case AuditLogType.GroupDeleted:
50
+ // do not log
51
+ return;
52
+ }
53
+ }
54
+
55
+ return result;
56
+ },
57
+
58
+ postProcess(event, _options, log) {
59
+ if (log.type === AuditLogType.EventEdited) {
60
+ // Prefix changes
61
+ for (const item of log.patchList) {
62
+ item.key = item.key.prepend(AuditLogReplacement.key('registrations'));
63
+ }
64
+ }
65
+ },
66
+
67
+ createReplacements: (model) => {
68
+ if (model.type === GroupType.EventRegistration) {
69
+ if (model.settings.eventId) {
70
+ return new Map([
71
+ ['e', AuditLogReplacement.create({
72
+ id: model.settings.eventId,
73
+ value: model.settings.name,
74
+ type: AuditLogReplacementType.Event,
75
+ })],
76
+ ]);
77
+ }
78
+ return new Map([
79
+ ['e', AuditLogReplacement.create({
80
+ id: model.id,
81
+ value: model.settings.name,
82
+ type: AuditLogReplacementType.Group,
83
+ })],
84
+ ]);
85
+ }
86
+
87
+ return new Map([
88
+ ['g', AuditLogReplacement.create({
89
+ id: model.id,
90
+ value: model.settings.name,
91
+ type: AuditLogReplacementType.Group,
92
+ })],
93
+ ]);
94
+ },
95
+ });
@@ -0,0 +1,24 @@
1
+ import { Member } from '@stamhoofd/models';
2
+ import { getDefaultGenerator, ModelLogger } from './ModelLogger';
3
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
4
+
5
+ export const MemberLogger = new ModelLogger(Member, {
6
+ // Skip repeated auto generated fields
7
+ skipKeys: ['firstName', 'lastName', 'birthDay', 'memberNumber'],
8
+
9
+ optionsGenerator: getDefaultGenerator({
10
+ created: AuditLogType.MemberAdded,
11
+ updated: AuditLogType.MemberEdited,
12
+ deleted: AuditLogType.MemberDeleted,
13
+ }),
14
+
15
+ createReplacements(model) {
16
+ return new Map([
17
+ ['m', AuditLogReplacement.create({
18
+ id: model.id,
19
+ value: model.details.name,
20
+ type: AuditLogReplacementType.Member,
21
+ })],
22
+ ]);
23
+ },
24
+ });
@@ -0,0 +1,57 @@
1
+ import { Member, MemberPlatformMembership } from '@stamhoofd/models';
2
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, uuidToName } from '@stamhoofd/structures';
3
+ import { Formatter } from '@stamhoofd/utility';
4
+ import { getDefaultGenerator, ModelLogger } from './ModelLogger';
5
+
6
+ const defaultGenerator = getDefaultGenerator({
7
+ created: AuditLogType.MemberPlatformMembershipAdded,
8
+ updated: AuditLogType.MemberPlatformMembershipEdited,
9
+ deleted: AuditLogType.MemberPlatformMembershipDeleted,
10
+ });
11
+
12
+ export const MemberPlatformMembershipLogger = new ModelLogger(MemberPlatformMembership, {
13
+ skipKeys: ['balanceItemId'],
14
+ async optionsGenerator(event) {
15
+ const result = await defaultGenerator(event);
16
+
17
+ if (!result) {
18
+ return;
19
+ }
20
+
21
+ const member = await Member.getByID(event.model.memberId);
22
+
23
+ if (!member) {
24
+ console.log('No member found for MemberPlatformMembership', event.model.id);
25
+ return;
26
+ }
27
+
28
+ return {
29
+ ...result,
30
+ data: {
31
+ member,
32
+ },
33
+ objectId: event.model.memberId,
34
+ };
35
+ },
36
+
37
+ generateDescription(event, options) {
38
+ return Formatter.capitalizeFirstLetter(Formatter.dateRange(event.model.startDate, event.model.endDate));
39
+ },
40
+
41
+ createReplacements(model, options) {
42
+ const map = new Map([
43
+ ['pm', AuditLogReplacement.create({
44
+ id: model.membershipTypeId,
45
+ value: uuidToName(model.membershipTypeId) || undefined,
46
+ type: AuditLogReplacementType.PlatformMembershipType,
47
+ })],
48
+ ['m', AuditLogReplacement.create({
49
+ id: options.data.member.id,
50
+ value: options.data.member.details.name,
51
+ type: AuditLogReplacementType.Member,
52
+ })],
53
+ ]);
54
+
55
+ return map;
56
+ },
57
+ });
@@ -0,0 +1,69 @@
1
+ import { Group, Member, MemberResponsibilityRecord, Organization, Platform } from '@stamhoofd/models';
2
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
3
+ import { getDefaultGenerator, ModelLogger } from './ModelLogger';
4
+
5
+ const defaultGenerator = getDefaultGenerator({
6
+ created: AuditLogType.MemberResponsibilityRecordAdded,
7
+ updated: AuditLogType.MemberResponsibilityRecordEdited,
8
+ deleted: AuditLogType.MemberResponsibilityRecordDeleted,
9
+ });
10
+
11
+ export const MemberResponsibilityRecordLogger = new ModelLogger(MemberResponsibilityRecord, {
12
+ skipKeys: ['balanceItemId'],
13
+ async optionsGenerator(event) {
14
+ const result = await defaultGenerator(event);
15
+
16
+ if (!result) {
17
+ return;
18
+ }
19
+
20
+ const member = await Member.getByID(event.model.memberId);
21
+ const group = event.model.groupId ? (await Group.getByID(event.model.groupId)) : null;
22
+
23
+ if (!member) {
24
+ console.log('No member found for MemberResponsibilityRecord', event.model.id);
25
+ return;
26
+ }
27
+
28
+ if (event.type === 'updated' && event.changedFields.endDate && event.originalFields.endDate === null) {
29
+ result.type = AuditLogType.MemberResponsibilityRecordDeleted;
30
+ result.generatePatchList = false;
31
+ }
32
+
33
+ return {
34
+ ...result,
35
+ data: {
36
+ member,
37
+ group,
38
+ platform: await Platform.getSharedStruct(),
39
+ },
40
+ objectId: event.model.memberId,
41
+ };
42
+ },
43
+
44
+ createReplacements(model, options) {
45
+ const name = options.data.platform.config.responsibilities.find(r => r.id === model.responsibilityId)?.name;
46
+ const map = new Map([
47
+ ['r', AuditLogReplacement.create({
48
+ id: model.responsibilityId,
49
+ value: name,
50
+ type: AuditLogReplacementType.MemberResponsibility,
51
+ })],
52
+ ['m', AuditLogReplacement.create({
53
+ id: options.data.member.id,
54
+ value: options.data.member.details.name,
55
+ type: AuditLogReplacementType.Member,
56
+ })],
57
+ ]);
58
+
59
+ if (options.data.group) {
60
+ map.set('g', AuditLogReplacement.create({
61
+ id: options.data.group.id,
62
+ value: options.data.group.settings.name,
63
+ type: AuditLogReplacementType.Group,
64
+ }));
65
+ }
66
+
67
+ return map;
68
+ },
69
+ });
@@ -0,0 +1,218 @@
1
+ import { Model, ModelEvent } from '@simonbackx/simple-database';
2
+ import { AuditLog } from '@stamhoofd/models';
3
+ import { AuditLogReplacement, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
4
+ import { ContextInstance } from '../helpers/Context';
5
+ import { AuditLogService } from '../services/AuditLogService';
6
+ import { createUnknownChangeHandler } from '../services/explainPatch';
7
+
8
+ export type ModelEventLogOptions<D> = {
9
+ type: AuditLogType;
10
+ generatePatchList?: boolean;
11
+ data: D;
12
+ mergeInto?: AuditLog;
13
+ objectId?: string;
14
+ };
15
+
16
+ type DefaultLogOptionsType = {
17
+ created?: AuditLogType;
18
+ updated?: AuditLogType;
19
+ deleted?: AuditLogType;
20
+ };
21
+
22
+ type EventOptionsGenerator<M extends Model, D> = (event: ModelEvent<M>) => ModelEventLogOptions<D> | Promise<ModelEventLogOptions<D> | undefined> | undefined;
23
+
24
+ export type ModelLoggerOptions<M extends Model, D = undefined> = {
25
+ optionsGenerator: EventOptionsGenerator<M, D>;
26
+ skipKeys?: string[];
27
+ generateDescription?(event: ModelEvent<M>, options: ModelEventLogOptions<D>): string | null | undefined;
28
+ createReplacements?(model: M, options: ModelEventLogOptions<D>): Map<string, AuditLogReplacement>;
29
+ postProcess?(event: ModelEvent<M>, options: ModelEventLogOptions<D>, log: AuditLog): Promise<void> | void;
30
+ };
31
+
32
+ const ModelEventsMap = new WeakMap<Model, { events: AuditLog[] }>();
33
+
34
+ export function getDefaultGenerator(types: DefaultLogOptionsType): EventOptionsGenerator<Model, undefined> {
35
+ return (event: ModelEvent<Model>) => {
36
+ if (event.type === 'created') {
37
+ if (types.created) {
38
+ return { type: types.created, data: undefined };
39
+ }
40
+ return;
41
+ }
42
+
43
+ let mergeInto: AuditLog | undefined = undefined;
44
+
45
+ if (event.type === 'updated') {
46
+ // Generate changes list
47
+ if ('deletedAt' in event.changedFields && ((event.model as any).deletedAt)) {
48
+ if (!('deletedAt' in event.originalFields) || !event.originalFields.deletedAt) {
49
+ event = {
50
+ ...event,
51
+ type: 'deleted',
52
+ };
53
+
54
+ if (types.deleted) {
55
+ return { type: types.deleted, data: undefined };
56
+ }
57
+ }
58
+ }
59
+
60
+ if (types.updated) {
61
+ if ('createdAt' in event.model && event.model.createdAt instanceof Date && event.model.createdAt.getTime() >= Date.now() - 1000) {
62
+ // Ignore quick model changes after insertion: these are often side effects of code that should not be logged
63
+ // (the created event normally doesn't contain what has been added - so no need to log what has been changed since creation)
64
+ if (types.created && types.created !== types.updated) {
65
+ // Merge into create event
66
+ const events = ModelEventsMap.get(event.model)?.events;
67
+ if (events && events.length > 0) {
68
+ mergeInto = events[0];
69
+ }
70
+ }
71
+ }
72
+
73
+ return { type: types.updated, generatePatchList: true, data: undefined, mergeInto };
74
+ }
75
+ return;
76
+ }
77
+
78
+ if (event.type === 'deleted') {
79
+ if (types.deleted) {
80
+ return { type: types.deleted, data: undefined };
81
+ }
82
+ return;
83
+ }
84
+ };
85
+ }
86
+
87
+ export class ModelLogger<ModelType extends typeof Model, M extends InstanceType<ModelType>, D> {
88
+ model: ModelType;
89
+ optionsGenerator: EventOptionsGenerator<M, D>;
90
+ skipKeys: string[] = [];
91
+ generateDescription?: (event: ModelEvent<M>, options: ModelEventLogOptions<D>) => string | null | undefined;
92
+ createReplacements?: (model: M, options: ModelEventLogOptions<D>) => Map<string, AuditLogReplacement>;
93
+ postProcess?: (event: ModelEvent<M>, options: ModelEventLogOptions<D>, log: AuditLog) => Promise<void> | void;
94
+ static sharedSkipKeys = ['id', 'createdAt', 'updatedAt', 'deletedAt'];
95
+
96
+ constructor(model: ModelType, options: ModelLoggerOptions<M, D>) {
97
+ this.model = model;
98
+ this.optionsGenerator = options.optionsGenerator;
99
+ this.skipKeys = options.skipKeys ?? [];
100
+ this.generateDescription = options.generateDescription;
101
+ this.createReplacements = options.createReplacements;
102
+ this.postProcess = options.postProcess;
103
+ }
104
+
105
+ async logEvent(event: ModelEvent<M>) {
106
+ try {
107
+ const context = ContextInstance.optional;
108
+ const log = new AuditLog();
109
+ const settings = AuditLogService.getContext();
110
+ const userId = settings?.userId !== undefined ? settings?.userId : (context?.optionalAuth?.user?.id ?? settings?.fallbackUserId ?? null);
111
+ log.userId = userId;
112
+
113
+ log.organizationId = context?.organization?.id ?? settings?.fallbackOrganizationId ?? null;
114
+
115
+ if ('organizationId' in event.model && typeof event.model['organizationId'] === 'string') {
116
+ // Always override
117
+ log.organizationId = event.model.organizationId;
118
+ }
119
+
120
+ if (settings?.source) {
121
+ log.source = settings.source;
122
+ }
123
+ else if (context) {
124
+ log.source = userId ? AuditLogSource.User : AuditLogSource.Anonymous;
125
+ }
126
+ else {
127
+ log.source = AuditLogSource.System;
128
+ }
129
+
130
+ const options = await this.optionsGenerator(event);
131
+
132
+ if (!options) {
133
+ return false;
134
+ }
135
+
136
+ log.type = options.type;
137
+ log.objectId = options.objectId ?? event.model.getPrimaryKey()?.toString() ?? null;
138
+ log.replacements = this.createReplacements ? this.createReplacements(event.model as M, options) : new Map();
139
+ log.description = (this.generateDescription ? this.generateDescription(event, options) : '') ?? '';
140
+
141
+ if (options.generatePatchList && event.type === 'updated') {
142
+ const oldModel = event.getOldModel();
143
+
144
+ // Generate changes list
145
+ const changedProperties = Object.keys(event.changedFields).filter(k => !this.skipKeys?.includes(k) && !ModelLogger.sharedSkipKeys.includes(k));
146
+
147
+ if (changedProperties.length > 0) {
148
+ for (const key of changedProperties) {
149
+ if (changedProperties[key] instanceof Model) {
150
+ // Ignore relations
151
+ continue;
152
+ }
153
+ log.patchList.push(...createUnknownChangeHandler(key)(
154
+ key in oldModel ? oldModel[key] : undefined,
155
+ key in event.model ? event.model[key] : undefined,
156
+ ));
157
+ }
158
+
159
+ // Remove skipped keys
160
+ log.patchList = log.patchList.filter(p => !this.skipKeys?.includes(p.key.toKey()));
161
+
162
+ if (log.patchList.length === 0 && !log.description) {
163
+ // No changes or all skipped
164
+ console.log('No changes after secundary filtering');
165
+ return false;
166
+ }
167
+ }
168
+ else {
169
+ console.log('No changes');
170
+ return false;
171
+ }
172
+ }
173
+
174
+ if (this.postProcess) {
175
+ await this.postProcess(event, options, log);
176
+ }
177
+
178
+ if (options.mergeInto) {
179
+ // Merge into
180
+ console.log('Merging new audit log into existing log', options.mergeInto.id);
181
+
182
+ if (!options.mergeInto.description) {
183
+ options.mergeInto.description = log.description;
184
+ }
185
+
186
+ if (log.replacements.size) {
187
+ options.mergeInto.replacements = log.replacements;
188
+ }
189
+
190
+ if (!options.mergeInto.patchList.length) {
191
+ options.mergeInto.patchList = log.patchList;
192
+ }
193
+
194
+ if (options.mergeInto.userId === null) {
195
+ options.mergeInto.userId = log.userId;
196
+ }
197
+
198
+ if (options.mergeInto.organizationId === null) {
199
+ options.mergeInto.organizationId = log.organizationId;
200
+ }
201
+
202
+ return await options.mergeInto.save();
203
+ }
204
+
205
+ const saved = await log.save();
206
+
207
+ // Assign to map
208
+ ModelEventsMap.set(event.model, {
209
+ events: [...(ModelEventsMap.get(event.model)?.events ?? []), log],
210
+ });
211
+ return saved;
212
+ }
213
+ catch (e) {
214
+ console.error('Failed to save log', e);
215
+ }
216
+ return false;
217
+ };
218
+ }
@@ -0,0 +1,57 @@
1
+ import { Order, Webshop } from '@stamhoofd/models';
2
+ import { getDefaultGenerator, ModelLogger } from './ModelLogger';
3
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, OrderStatus } from '@stamhoofd/structures';
4
+
5
+ const defaultGenerator = getDefaultGenerator({
6
+ created: AuditLogType.OrderAdded,
7
+ updated: AuditLogType.OrderEdited,
8
+ deleted: AuditLogType.OrderDeleted,
9
+ });
10
+
11
+ export const OrderLogger = new ModelLogger(Order, {
12
+ skipKeys: ['amount', 'timeSlotTime', 'validAt', 'paymentId'],
13
+ async optionsGenerator(event) {
14
+ const result = await defaultGenerator(event);
15
+
16
+ if (!result) {
17
+ return;
18
+ }
19
+
20
+ // todo: when placing an order / marking an order as paid
21
+ // we should not log any stock changes
22
+
23
+ const webshop = Order.webshop.isLoaded(event.model) ? (event.model as (Order & Record<'webshop', Webshop>)).webshop : (await Webshop.getByID(event.model.webshopId));
24
+ if (!webshop) {
25
+ console.log('No webshop found for order', event.model.id);
26
+ return;
27
+ }
28
+
29
+ if (event.type === 'updated' && event.changedFields.status === OrderStatus.Deleted) {
30
+ result.type = AuditLogType.OrderDeleted;
31
+ result.generatePatchList = false;
32
+ }
33
+
34
+ return {
35
+ ...result,
36
+ data: {
37
+ webshop,
38
+ },
39
+ };
40
+ },
41
+
42
+ createReplacements: (model, options) => {
43
+ return new Map([
44
+ ['w', AuditLogReplacement.create({
45
+ id: options.data.webshop.id,
46
+ value: options.data.webshop.meta.name,
47
+ type: AuditLogReplacementType.Webshop,
48
+ })],
49
+ ['org', AuditLogReplacement.create({
50
+ id: model.id,
51
+ value: model.number ? `bestelling #${model.number}` : `bestelling van ${model.data.customer.name}`,
52
+ type: AuditLogReplacementType.Order,
53
+ description: model.number ? model.data.customer.name : 'Deze bestelling heeft nog geen nummer',
54
+ })],
55
+ ]);
56
+ },
57
+ });
@@ -0,0 +1,26 @@
1
+ import { Organization } from '@stamhoofd/models';
2
+ import { getDefaultGenerator, ModelLogger } from './ModelLogger';
3
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
4
+
5
+ export const OrganizationLogger = new ModelLogger(Organization, {
6
+ skipKeys: ['searchIndex'],
7
+ optionsGenerator: getDefaultGenerator({
8
+ created: AuditLogType.OrganizationAdded,
9
+ updated: AuditLogType.OrganizationEdited,
10
+ deleted: AuditLogType.OrganizationDeleted,
11
+ }),
12
+
13
+ createReplacements(model) {
14
+ return new Map([
15
+ ['o', AuditLogReplacement.create({
16
+ id: model.id,
17
+ value: model.name,
18
+ type: AuditLogReplacementType.Organization,
19
+ })],
20
+ ]);
21
+ },
22
+
23
+ postProcess(event, options, log) {
24
+ log.organizationId = event.model.id;
25
+ },
26
+ });