@stamhoofd/backend 2.78.4 → 2.79.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 (29) hide show
  1. package/index.ts +1 -1
  2. package/package.json +13 -12
  3. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +1 -1
  4. package/src/endpoints/auth/CreateAdminEndpoint.ts +1 -1
  5. package/src/endpoints/auth/CreateTokenEndpoint.ts +1 -1
  6. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
  7. package/src/endpoints/auth/PatchUserEndpoint.ts +1 -1
  8. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +1964 -4
  9. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +175 -2
  10. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +1 -1
  11. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +2 -2
  12. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +7 -1
  13. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +3 -23
  14. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +12 -0
  15. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -3
  16. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +164 -0
  17. package/src/helpers/AdminPermissionChecker.ts +22 -1
  18. package/src/helpers/AuthenticatedStructures.ts +77 -20
  19. package/src/helpers/ForwardHandler.test.ts +16 -5
  20. package/src/helpers/ForwardHandler.ts +21 -9
  21. package/src/helpers/MemberUserSyncer.test.ts +822 -0
  22. package/src/helpers/MemberUserSyncer.ts +137 -108
  23. package/src/helpers/TagHelper.ts +3 -3
  24. package/src/seeds/1734596144-fill-previous-period-id.ts +1 -1
  25. package/src/seeds/1741008870-fix-auditlog-description.ts +50 -0
  26. package/src/seeds/1741011468-fix-auditlog-description-uft16.ts +88 -0
  27. package/src/services/PlatformMembershipService.ts +7 -2
  28. package/src/services/SSOService.ts +1 -1
  29. package/tests/e2e/register.test.ts +2 -2
@@ -1,6 +1,6 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
2
  import { AuditLog, BalanceItem, CachedBalance, Document, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
3
- import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
3
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, GroupType, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
4
  import { Sorter } from '@stamhoofd/utility';
5
5
 
6
6
  import { SQL } from '@stamhoofd/sql';
@@ -58,11 +58,29 @@ export class AuthenticatedStructures {
58
58
 
59
59
  static async groups(groups: Group[]) {
60
60
  const waitingListIds = Formatter.uniqueArray(groups.map(g => g.waitingListId).filter(id => id !== null));
61
- const waitingLists = waitingListIds.length > 0 ? await Group.getByIDs(...waitingListIds) : [];
61
+
62
+ const waitingLists: Group[] = [];
63
+ const waitingListsToFetch: string[] = [];
64
+
65
+ for (const waitingListId of waitingListIds) {
66
+ const existingGroup = groups.find(g => g.id === waitingListId);
67
+
68
+ if (existingGroup) {
69
+ waitingLists.push(existingGroup);
70
+ }
71
+ else {
72
+ waitingListsToFetch.push(waitingListId);
73
+ }
74
+ }
75
+
76
+ if (waitingListsToFetch.length) {
77
+ const fetchedWaitingLists = await Group.getByIDs(...waitingListsToFetch);
78
+ waitingLists.push(...fetchedWaitingLists);
79
+ }
62
80
 
63
81
  const structs: GroupStruct[] = [];
64
82
  for (const group of groups) {
65
- const waitingList = waitingLists.find(g => g.id == group.waitingListId) ?? null;
83
+ const waitingList = waitingLists.find(g => g.id === group.waitingListId) ?? null;
66
84
  const waitingListStruct = waitingList ? GroupStruct.create(waitingList) : null;
67
85
  if (waitingList && waitingListStruct && !await Context.optionalAuth?.canAccessGroup(waitingList)) {
68
86
  waitingListStruct.privateSettings = null;
@@ -88,31 +106,70 @@ export class AuthenticatedStructures {
88
106
  return [];
89
107
  }
90
108
 
109
+ const periodIds = periods ? Formatter.uniqueArray(periods.map(p => p.id)) : Formatter.uniqueArray(organizationRegistrationPeriods.map(p => p.periodId));
110
+
91
111
  if (!periods) {
92
- const periodIds = Formatter.uniqueArray(organizationRegistrationPeriods.map(p => p.periodId));
93
112
  periods = await RegistrationPeriod.getByIDs(...periodIds);
94
113
  }
95
114
 
115
+ const organizationIds = Formatter.uniqueArray(organizationRegistrationPeriods.map(r => r.organizationId));
116
+
96
117
  const groupIds = Formatter.uniqueArray(organizationRegistrationPeriods.flatMap(p => p.settings.categories.flatMap(c => c.groupIds)));
97
- const groups = groupIds.length ? await Group.getByIDs(...groupIds) : [];
118
+
119
+ let groups: Group[] = [];
120
+
121
+ const whereWaitingList = SQL.where('organizationId', organizationIds)
122
+ .and('periodId', periodIds)
123
+ .and('type', GroupType.WaitingList)
124
+ .and('deletedAt', null);
125
+
126
+ if (groupIds.length) {
127
+ const whereGroupIds = SQL.where('id', groupIds);
128
+
129
+ if (organizationIds.length && periodIds.length) {
130
+ groups = await Group.select()
131
+ .where(whereGroupIds)
132
+ .orWhere(whereWaitingList)
133
+ .fetch();
134
+ }
135
+ else {
136
+ groups = await Group.select()
137
+ .where(whereGroupIds).fetch();
138
+ }
139
+ }
140
+ else if (organizationIds.length && periodIds.length) {
141
+ groups = await Group.select()
142
+ .where(whereWaitingList)
143
+ .fetch();
144
+ }
98
145
 
99
146
  const groupStructs = await this.groups(groups);
100
147
 
101
148
  const structs: OrganizationRegistrationPeriodStruct[] = [];
102
149
  for (const organizationPeriod of organizationRegistrationPeriods) {
103
- const period = periods.find(p => p.id == organizationPeriod.periodId) ?? null;
150
+ const period = periods.find(p => p.id === organizationPeriod.periodId) ?? null;
104
151
  if (!period) {
105
152
  continue;
106
153
  }
107
- const groupIds = Formatter.uniqueArray(organizationPeriod.settings.categories.flatMap(c => c.groupIds));
108
154
 
109
- structs.push(
110
- OrganizationRegistrationPeriodStruct.create({
111
- ...organizationPeriod,
112
- period: period.getStructure(),
113
- groups: groupStructs.filter(gg => groupIds.includes(gg.id)).sort(GroupStruct.defaultSort),
114
- }),
115
- );
155
+ const struct = OrganizationRegistrationPeriodStruct.create({
156
+ ...organizationPeriod,
157
+ period: period.getStructure(),
158
+ groups: groupStructs.filter((gg) => {
159
+ if (gg.organizationId !== organizationPeriod.organizationId) {
160
+ return false;
161
+ }
162
+
163
+ if (gg.periodId !== organizationPeriod.periodId) {
164
+ return false;
165
+ }
166
+
167
+ return true;
168
+ })
169
+ .sort(GroupStruct.defaultSort),
170
+ });
171
+
172
+ structs.push(struct);
116
173
  }
117
174
 
118
175
  return structs;
@@ -767,11 +824,11 @@ export class AuthenticatedStructures {
767
824
  ]
768
825
  : []),
769
826
 
770
- ...(member.details.parentsHaveAccess
771
- ? member.details.parents.filter(p => !!p.email).map(p => ReceivableBalanceObjectContact.create({
827
+ ...((member.details.defaultAge <= 18 || member.details.getMemberEmails().length === 0)
828
+ ? member.details.parents.filter(p => p.getEmails().length > 0).map(p => ReceivableBalanceObjectContact.create({
772
829
  firstName: p.firstName ?? '',
773
830
  lastName: p.lastName ?? '',
774
- emails: [p.email!],
831
+ emails: p.getEmails(),
775
832
  meta: {
776
833
  type: 'parent',
777
834
  responsibilityIds: [],
@@ -810,11 +867,11 @@ export class AuthenticatedStructures {
810
867
  ]
811
868
  : []),
812
869
 
813
- ...(member.details.parentsHaveAccess
814
- ? member.details.parents.filter(p => !!p.email).map(p => ReceivableBalanceObjectContact.create({
870
+ ...((member.details.defaultAge <= 18 || member.details.getMemberEmails().length === 0)
871
+ ? member.details.parents.filter(p => p.getEmails().length > 0).map(p => ReceivableBalanceObjectContact.create({
815
872
  firstName: p.firstName ?? '',
816
873
  lastName: p.lastName ?? '',
817
- emails: [p.email!],
874
+ emails: p.getEmails(),
818
875
  meta: {
819
876
  type: 'parent',
820
877
  responsibilityIds: [],
@@ -34,7 +34,9 @@ describe('ForwardHandler', () => {
34
34
  },
35
35
  ],
36
36
  subject: 'Hello',
37
- replyTo: 'someone@example.com',
37
+ replyTo: {
38
+ email: 'someone@example.com',
39
+ },
38
40
  });
39
41
  expect(options!.text).toContain('Content hier');
40
42
  });
@@ -67,7 +69,9 @@ describe('ForwardHandler', () => {
67
69
  },
68
70
  ],
69
71
  subject: 'Hello',
70
- replyTo: 'someone@example.com',
72
+ replyTo: {
73
+ email: 'someone@example.com',
74
+ },
71
75
  });
72
76
  expect(options!.text).toContain('Content hier');
73
77
  });
@@ -92,7 +96,9 @@ describe('ForwardHandler', () => {
92
96
  },
93
97
  ],
94
98
  subject: 'Hello',
95
- replyTo: 'someone@example.com',
99
+ replyTo: {
100
+ email: 'someone@example.com',
101
+ },
96
102
  });
97
103
  expect(options!.text).toContain('Content hier');
98
104
 
@@ -119,7 +125,9 @@ describe('ForwardHandler', () => {
119
125
 
120
126
  expect(options).toMatchObject({
121
127
  subject: 'Hello',
122
- replyTo: 'someone@example.com',
128
+ replyTo: {
129
+ email: 'someone@example.com',
130
+ },
123
131
  });
124
132
  expect(options!.to).toIncludeAllMembers([
125
133
  {
@@ -207,7 +215,10 @@ describe('ForwardHandler', () => {
207
215
  dmarcVerdict: { status: 'PASS' },
208
216
  });
209
217
  expect(options).toMatchObject({
210
- to: 'hallo@stamhoofd.be',
218
+ to: [{
219
+ email: 'hallo@stamhoofd.be',
220
+ name: 'Stamhoofd',
221
+ }],
211
222
  subject: 'E-mail unsubscribe mislukt',
212
223
  });
213
224
  });
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
- /* eslint-disable @typescript-eslint/no-redundant-type-constituents */
3
- import { Email, EmailAddress, EmailInterfaceRecipient } from '@stamhoofd/email';
2
+
3
+ import { Email, EmailAddress, EmailInterface, EmailInterfaceRecipient } from '@stamhoofd/email';
4
4
  import { Organization } from '@stamhoofd/models';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { simpleParser } from 'mailparser';
@@ -49,7 +49,7 @@ export class ForwardHandler {
49
49
  // Forward
50
50
  return {
51
51
  from: Email.getWebmasterFromEmail(),
52
- to: Email.getWebmasterToEmail(),
52
+ to: [Email.getWebmasterToEmail()],
53
53
  subject: 'E-mail unsubscribe mislukt',
54
54
  text: 'Beste,\n\nEr werd een unsubscribe gemeld op ' + email + ' die niet kon worden verwerkt. Gelieve dit na te kijken.\n\nStamhoofd',
55
55
  };
@@ -64,7 +64,7 @@ export class ForwardHandler {
64
64
  }
65
65
 
66
66
  // Send a new e-mail
67
- let defaultEmail: EmailInterfaceRecipient[] | string = Email.getWebmasterToEmail();
67
+ let defaultEmail: EmailInterfaceRecipient[] = [Email.getWebmasterToEmail()];
68
68
  let organizationEmails: EmailInterfaceRecipient[] = [];
69
69
  const extraDescription = 'Dit bericht werd verstuurd naar ' + email + ', en werd automatisch doorgestuurd naar alle beheerders. Stel in Stamhoofd de e-mailadressen in om ervoor te zorgen dat antwoorden naar een specifiek e-mailadres worden verstuurd.';
70
70
 
@@ -80,8 +80,12 @@ export class ForwardHandler {
80
80
 
81
81
  // Send back to receiver without including the original message to avoid spam
82
82
  return {
83
- from: email ?? Email.getWebmasterToEmail(),
84
- to: from,
83
+ from: email
84
+ ? {
85
+ email,
86
+ }
87
+ : Email.getWebmasterToEmail(),
88
+ to: [{ email: from }],
85
89
  subject: 'Ongeldig e-mailadres',
86
90
  text: 'Beste,\n\nDe vereniging die je probeert te bereiken via ' + email + ' is helaas niet bereikbaar via dit e-mailadres. Dit e-mailadres wordt enkel gebruikt voor het versturen van automatische e-mails in naam van een vereniging. Probeer de vereniging te contacteren via een ander e-mailadres.\n\nBedankt.',
87
91
  };
@@ -123,10 +127,18 @@ export class ForwardHandler {
123
127
  }
124
128
  }
125
129
 
126
- const options = {
127
- from: email ?? Email.getWebmasterToEmail(),
130
+ const options: EmailInterface = {
131
+ from: email
132
+ ? {
133
+ email,
134
+ }
135
+ : Email.getWebmasterToEmail(),
128
136
  to: defaultEmail,
129
- replyTo: parsed.from?.text,
137
+ replyTo: parsed.from?.text
138
+ ? {
139
+ email: parsed.from?.text,
140
+ }
141
+ : undefined,
130
142
  subject: parsed.subject ?? 'Doorgestuurd bericht',
131
143
  text: parsed.text ? extraDescription + '\n\n' + parsed.text : undefined,
132
144
  html: html,