@stamhoofd/backend 2.8.0 → 2.13.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 (35) hide show
  1. package/.env.template.json +3 -1
  2. package/package.json +11 -3
  3. package/src/crons.ts +3 -3
  4. package/src/decoders/StringArrayDecoder.ts +24 -0
  5. package/src/decoders/StringNullableDecoder.ts +18 -0
  6. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +20 -18
  7. package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
  8. package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -9
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
  10. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
  11. package/src/endpoints/global/members/GetMembersEndpoint.ts +15 -62
  12. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
  13. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
  14. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +165 -35
  15. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
  16. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
  17. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +56 -3
  18. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
  19. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
  20. package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +43 -0
  21. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +292 -170
  22. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
  23. package/src/endpoints/organization/dashboard/payments/legacy/GetPaymentsEndpoint.ts +170 -0
  24. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
  25. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
  26. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
  27. package/src/helpers/AdminPermissionChecker.ts +95 -60
  28. package/src/helpers/AuthenticatedStructures.ts +16 -6
  29. package/src/helpers/Context.ts +21 -0
  30. package/src/helpers/EmailResumer.ts +22 -2
  31. package/src/helpers/MemberUserSyncer.ts +8 -2
  32. package/src/helpers/ViesHelper.ts +151 -0
  33. package/src/seeds/1722344160-update-membership.ts +19 -22
  34. package/src/seeds/1722344161-sync-member-users.ts +60 -0
  35. package/.env.json +0 -65
@@ -0,0 +1,170 @@
1
+ import { AutoEncoder, DateDecoder, EnumDecoder, field, IntegerDecoder, StringDecoder } from "@simonbackx/simple-encoding";
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { Organization, Payment } from "@stamhoofd/models";
4
+ import { PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus } from "@stamhoofd/structures";
5
+
6
+ import { StringArrayDecoder } from "../../../../../decoders/StringArrayDecoder";
7
+ import { AuthenticatedStructures } from "../../../../../helpers/AuthenticatedStructures";
8
+ import { Context } from "../../../../../helpers/Context";
9
+ import { StringNullableDecoder } from "../../../../../decoders/StringNullableDecoder";
10
+
11
+ type Params = Record<string, never>;
12
+ type Body = undefined
13
+ type ResponseBody = PaymentGeneral[]
14
+
15
+
16
+ class Query extends AutoEncoder {
17
+ /**
18
+ * Usage in combination with paidSince is special!
19
+ */
20
+ @field({ decoder: StringDecoder, optional: true })
21
+ afterId?: string
22
+
23
+ /**
24
+ * Return all payments that were paid after (and including) this date.
25
+ * Only returns orders **equal** to this date if afterId is not provided or if the id of those payments is also higher.
26
+ */
27
+ @field({ decoder: DateDecoder, optional: true })
28
+ paidSince?: Date
29
+
30
+ @field({ decoder: DateDecoder, optional: true })
31
+ paidBefore?: Date
32
+
33
+ @field({ decoder: IntegerDecoder, optional: true })
34
+ limit?: number
35
+
36
+ @field({ decoder: new StringArrayDecoder(new EnumDecoder(PaymentMethod)), optional: true })
37
+ methods?: PaymentMethod[]
38
+
39
+ @field({ decoder: new StringArrayDecoder(new StringNullableDecoder(new EnumDecoder(PaymentProvider))), optional: true })
40
+ providers?: (PaymentProvider|null)[]
41
+ }
42
+
43
+ export class GetPaymentsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
44
+ protected queryDecoder = Query;
45
+
46
+ protected doesMatch(request: Request): [true, Params] | [false] {
47
+ if (request.method != "GET") {
48
+ return [false];
49
+ }
50
+
51
+ const params = Endpoint.parseParameters(request.url, "/organization/payments", {});
52
+
53
+ if (params) {
54
+ return [true, params as Params];
55
+ }
56
+ return [false];
57
+ }
58
+
59
+ async handle(request: DecodedRequest<Params, Query, Body>) {
60
+ const organization = await Context.setOrganizationScope();
61
+ await Context.authenticate()
62
+
63
+ if (!await Context.auth.canManagePayments(organization.id)) {
64
+ throw Context.auth.error()
65
+ }
66
+
67
+ return new Response(
68
+ (await this.getPayments(organization, request.query))
69
+ );
70
+ }
71
+
72
+ async getPayments(organization: Organization, query: Query) {
73
+ const paidSince = query.paidSince ?? new Date(Date.now() - (24 * 60 * 60 * 1000 * 7 ))
74
+ paidSince.setMilliseconds(0)
75
+ const payments: Payment[] = []
76
+
77
+ if (query.afterId) {
78
+ // First return all payments with id > afterId and paidAt == paidSince
79
+ payments.push(...await Payment.where({
80
+ organizationId: organization.id,
81
+ paidAt: {
82
+ sign: '=',
83
+ value: paidSince
84
+ },
85
+ id: {
86
+ sign: '>',
87
+ value: query.afterId ?? ""
88
+ },
89
+ method: {
90
+ sign: 'IN',
91
+ value: query.methods ?? [PaymentMethod.Transfer]
92
+ },
93
+ provider: {
94
+ sign: 'IN',
95
+ value: query.providers ?? [null]
96
+ }
97
+ }, {
98
+ limit: query.limit ?? undefined,
99
+ sort: [{
100
+ column: "id",
101
+ direction: "ASC"
102
+ }]
103
+ }));
104
+ }
105
+
106
+ payments.push(...await Payment.where({
107
+ organizationId: organization.id,
108
+ paidAt: query.paidBefore ? [{
109
+ sign: query.afterId ? '>' : '>=',
110
+ value: paidSince
111
+ }, {
112
+ sign: '<=',
113
+ value: query.paidBefore
114
+ }] : {
115
+ sign: query.afterId ? '>' : '>=',
116
+ value: paidSince
117
+ },
118
+ method: {
119
+ sign: 'IN',
120
+ value: query.methods ?? [PaymentMethod.Transfer]
121
+ },
122
+ provider: {
123
+ sign: 'IN',
124
+ value: query.providers ?? [null]
125
+ }
126
+ }, {
127
+ limit: query.limit ? (query.limit - payments.length) : undefined,
128
+ sort: [{
129
+ column: "paidAt",
130
+ direction: "ASC"
131
+ },
132
+ {
133
+ column: "id",
134
+ direction: "ASC"
135
+ }]
136
+ }));
137
+
138
+
139
+ if (!query.paidSince && !query.methods && !query.providers) {
140
+ // Default behaviour is to return all not-paid transfer payments that are not yet paid
141
+
142
+ payments.push(...
143
+ await Payment.where({
144
+ organizationId: organization.id,
145
+ paidAt: null,
146
+ method: PaymentMethod.Transfer,
147
+ status: {
148
+ sign: '!=',
149
+ value: PaymentStatus.Failed
150
+ }
151
+ })
152
+ );
153
+
154
+ payments.push(...
155
+ await Payment.where({
156
+ organizationId: organization.id,
157
+ paidAt: null,
158
+ updatedAt: {
159
+ sign: '>',
160
+ value: new Date(Date.now() - (24 * 60 * 60 * 1000 * 7 ))
161
+ },
162
+ method: PaymentMethod.Transfer,
163
+ status: PaymentStatus.Failed
164
+ })
165
+ );
166
+ }
167
+
168
+ return await AuthenticatedStructures.paymentsGeneral(payments, true)
169
+ }
170
+ }
@@ -274,6 +274,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
274
274
  }
275
275
  }
276
276
  }
277
+
277
278
 
278
279
  await model.updateOccupancy()
279
280
  await model.save();
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-
3
3
  import { SimpleError } from "@simonbackx/simple-errors";
4
4
  import { BalanceItem, BalanceItemPayment, Order, Payment, Token, Webshop } from '@stamhoofd/models';
5
5
  import { QueueHandler } from '@stamhoofd/queues';
6
- import { BalanceItemStatus, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, PrivatePayment,Webshop as WebshopStruct } from "@stamhoofd/structures";
6
+ import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, PrivatePayment,Webshop as WebshopStruct } from "@stamhoofd/structures";
7
7
 
8
8
  import { Context } from '../../../../helpers/Context';
9
9
 
@@ -157,11 +157,21 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
157
157
  // Create balance item
158
158
  const balanceItem = new BalanceItem();
159
159
  balanceItem.orderId = order.id;
160
- balanceItem.price = totalPrice
160
+ balanceItem.type = BalanceItemType.Order;
161
+ balanceItem.unitPrice = totalPrice
161
162
  balanceItem.description = webshop.meta.name
162
163
  balanceItem.pricePaid = 0
163
164
  balanceItem.organizationId = organization.id;
164
165
  balanceItem.status = BalanceItemStatus.Pending;
166
+ balanceItem.relations = new Map([
167
+ [
168
+ BalanceItemRelationType.Webshop,
169
+ BalanceItemRelation.create({
170
+ id: webshop.id,
171
+ name: webshop.meta.name,
172
+ })
173
+ ]
174
+ ])
165
175
  await balanceItem.save();
166
176
 
167
177
  // Create one balance item payment to pay it in one payment
@@ -245,7 +255,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
245
255
  const items = await BalanceItem.where({ orderId: model.id })
246
256
  if (items.length >= 1) {
247
257
  model.markUpdated()
248
- items[0].price = model.totalToPay
258
+ items[0].unitPrice = model.totalToPay
249
259
  items[0].description = model.generateBalanceDescription(webshop)
250
260
  items[0].updateStatus();
251
261
  await items[0].save()
@@ -258,7 +268,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
258
268
  model.markUpdated()
259
269
  const balanceItem = new BalanceItem();
260
270
  balanceItem.orderId = model.id;
261
- balanceItem.price = model.totalToPay
271
+ balanceItem.unitPrice = model.totalToPay
262
272
  balanceItem.description = model.generateBalanceDescription(webshop)
263
273
  balanceItem.pricePaid = 0
264
274
  balanceItem.organizationId = organization.id;
@@ -6,7 +6,7 @@ import { I18n } from '@stamhoofd/backend-i18n';
6
6
  import { Email } from '@stamhoofd/email';
7
7
  import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
8
8
  import { QueueHandler } from '@stamhoofd/queues';
9
- import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct,WebshopAuthType } from "@stamhoofd/structures";
9
+ import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct,WebshopAuthType, BalanceItemType, BalanceItemRelationType, BalanceItemRelation } from "@stamhoofd/structures";
10
10
  import { Formatter } from '@stamhoofd/utility';
11
11
 
12
12
  import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
@@ -182,12 +182,22 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
182
182
 
183
183
  // Create balance item
184
184
  const balanceItem = new BalanceItem();
185
+ balanceItem.type = BalanceItemType.Order;
185
186
  balanceItem.orderId = order.id;
186
- balanceItem.price = totalPrice
187
+ balanceItem.unitPrice = totalPrice
187
188
  balanceItem.description = webshop.meta.name
188
189
  balanceItem.pricePaid = 0
189
190
  balanceItem.organizationId = organization.id;
190
191
  balanceItem.status = BalanceItemStatus.Hidden;
192
+ balanceItem.relations = new Map([
193
+ [
194
+ BalanceItemRelationType.Webshop,
195
+ BalanceItemRelation.create({
196
+ id: webshop.id,
197
+ name: webshop.meta.name,
198
+ })
199
+ ]
200
+ ])
191
201
  await balanceItem.save();
192
202
 
193
203
  // Create one balance item payment to pay it in one payment
@@ -1,7 +1,7 @@
1
1
  import { AutoEncoderPatchType, PatchMap } from "@simonbackx/simple-encoding"
2
2
  import { SimpleError } from "@simonbackx/simple-errors"
3
3
  import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, Payment, Registration, User, Webshop } from "@stamhoofd/models"
4
- import { AccessRight, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from "@stamhoofd/structures"
4
+ import { AccessRight, FinancialSupportSettings, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from "@stamhoofd/structures"
5
5
  import { Formatter } from "@stamhoofd/utility"
6
6
 
7
7
  /**
@@ -814,7 +814,7 @@ export class AdminPermissionChecker {
814
814
  async hasFinancialMemberAccess(member: MemberWithRegistrations, level: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
815
815
  const isUserManager = this.isUserManager(member)
816
816
 
817
- if (isUserManager) {
817
+ if (isUserManager && level === PermissionLevel.Read) {
818
818
  return true;
819
819
  }
820
820
 
@@ -846,6 +846,15 @@ export class AdminPermissionChecker {
846
846
  continue;
847
847
  }
848
848
 
849
+ if (isUserManager) {
850
+ // Requirements are higher: you need financial access to write your own financial
851
+ // data changes
852
+ if (permissions.hasAccessRight(AccessRight.OrganizationManagePayments)) {
853
+ return true;
854
+ }
855
+ continue;
856
+ }
857
+
849
858
  if (permissions.hasAccessRight(level === PermissionLevel.Read ? AccessRight.MemberReadFinancialData : AccessRight.MemberWriteFinancialData)) {
850
859
  return true;
851
860
  }
@@ -854,8 +863,16 @@ export class AdminPermissionChecker {
854
863
  // Platform data
855
864
  const platformPermissions = this.platformPermissions
856
865
  if (platformPermissions) {
857
- if (platformPermissions.hasAccessRight(level === PermissionLevel.Read ? AccessRight.MemberReadFinancialData : AccessRight.MemberWriteFinancialData)) {
858
- return true;
866
+ if (isUserManager) {
867
+ // Requirements are higher: you need financial access to write your own financial
868
+ // data changes
869
+ if (platformPermissions.hasAccessRight(AccessRight.OrganizationManagePayments)) {
870
+ return true;
871
+ }
872
+ } else {
873
+ if (platformPermissions.hasAccessRight(level === PermissionLevel.Read ? AccessRight.MemberReadFinancialData : AccessRight.MemberWriteFinancialData)) {
874
+ return true;
875
+ }
859
876
  }
860
877
  }
861
878
 
@@ -949,80 +966,98 @@ export class AdminPermissionChecker {
949
966
 
950
967
  const hasRecordAnswers = !!data.details.recordAnswers;
951
968
  const hasNotes = data.details.notes !== undefined;
969
+ const isSetFinancialSupportTrue = data.details.requiresFinancialSupport?.value === true;
970
+ const isUserManager = this.isUserManager(member);
952
971
 
953
- if(hasRecordAnswers || hasNotes) {
954
- const isUserManager = this.isUserManager(member);
972
+ if (hasRecordAnswers) {
973
+ if (!(data.details.recordAnswers instanceof PatchMap)) {
974
+ throw new SimpleError({
975
+ code: 'invalid_request',
976
+ message: 'Cannot PUT recordAnswers',
977
+ statusCode: 400
978
+ })
979
+ }
980
+
981
+ const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
955
982
 
956
- if (hasRecordAnswers) {
957
- if (!(data.details.recordAnswers instanceof PatchMap)) {
958
- throw new SimpleError({
959
- code: 'invalid_request',
960
- message: 'Cannot PUT recordAnswers',
961
- statusCode: 400
962
- })
963
- }
964
-
965
- const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
966
-
967
- for (const [key, value] of data.details.recordAnswers.entries()) {
968
- let name: string | undefined = undefined
969
- if (value) {
970
- if (value.isPatch()) {
971
- throw new SimpleError({
972
- code: 'invalid_request',
973
- message: 'Cannot PATCH a record answer object',
974
- statusCode: 400
975
- })
976
- }
977
-
978
- const id = value.settings.id
979
-
980
- if (id !== key) {
981
- throw new SimpleError({
982
- code: 'invalid_request',
983
- message: 'Record answer key does not match record id',
984
- statusCode: 400
985
- })
986
- }
987
-
988
- name = value.settings.name
983
+ for (const [key, value] of data.details.recordAnswers.entries()) {
984
+ let name: string | undefined = undefined
985
+ if (value) {
986
+ if (value.isPatch()) {
987
+ throw new SimpleError({
988
+ code: 'invalid_request',
989
+ message: 'Cannot PATCH a record answer object',
990
+ statusCode: 400
991
+ })
989
992
  }
990
993
 
991
- if (!isUserManager && !records.has(key)) {
994
+ const id = value.settings.id
995
+
996
+ if (id !== key) {
992
997
  throw new SimpleError({
993
- code: 'permission_denied',
994
- message: `Je hebt geen toegangsrechten om het antwoord op ${name ?? 'deze vraag'} aan te passen voor dit lid`,
998
+ code: 'invalid_request',
999
+ message: 'Record answer key does not match record id',
995
1000
  statusCode: 400
996
1001
  })
997
1002
  }
1003
+
1004
+ name = value.settings.name
998
1005
  }
999
- }
1000
1006
 
1001
- if(hasNotes && isUserManager) {
1002
- throw new SimpleError({
1003
- code: 'permission_denied',
1004
- message: 'Cannot edit notes',
1005
- statusCode: 400
1006
- })
1007
+ if (!isUserManager && !records.has(key)) {
1008
+ throw new SimpleError({
1009
+ code: 'permission_denied',
1010
+ message: `Je hebt geen toegangsrechten om het antwoord op ${name ?? 'deze vraag'} aan te passen voor dit lid`,
1011
+ statusCode: 400
1012
+ })
1013
+ }
1007
1014
  }
1008
1015
  }
1009
1016
 
1017
+ if (hasNotes && isUserManager) {
1018
+ throw new SimpleError({
1019
+ code: 'permission_denied',
1020
+ message: 'Cannot edit notes',
1021
+ statusCode: 400
1022
+ })
1023
+ }
1024
+
1010
1025
  // Has financial write access?
1011
1026
  if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Write)) {
1027
+ if (isUserManager && isSetFinancialSupportTrue) {
1028
+ const financialSupportSettings = this.platform.config.financialSupport;
1029
+ const preventSelfAssignment = financialSupportSettings?.preventSelfAssignment === true;
1030
+
1031
+ if(preventSelfAssignment) {
1032
+ throw new SimpleError({
1033
+ code: 'permission_denied',
1034
+ message: 'No permissions to enable financial support for your own members',
1035
+ human: financialSupportSettings.preventSelfAssignmentText ?? FinancialSupportSettings.defaultPreventSelfAssignmentText,
1036
+ statusCode: 400
1037
+ });
1038
+ }
1039
+ }
1040
+
1012
1041
  if (data.details.requiresFinancialSupport) {
1013
- throw new SimpleError({
1014
- code: 'permission_denied',
1015
- message: 'Je hebt geen toegangsrechten om de financiële status van dit lid aan te passen',
1016
- statusCode: 400
1017
- })
1042
+ if (isUserManager) {
1043
+ // Already handled
1044
+ } else {
1045
+ throw new SimpleError({
1046
+ code: 'permission_denied',
1047
+ message: 'Je hebt geen toegangsrechten om de financiële status van dit lid aan te passen',
1048
+ statusCode: 400
1049
+ })
1050
+ }
1018
1051
  }
1019
1052
 
1020
- if (data.details.uitpasNumber) {
1021
- throw new SimpleError({
1022
- code: 'permission_denied',
1023
- message: 'Je hebt geen toegangsrechten om het UiTPAS-nummer van dit lid aan te passen',
1024
- statusCode: 400
1025
- })
1053
+ if (!isUserManager) {
1054
+ if (data.details.uitpasNumber) {
1055
+ throw new SimpleError({
1056
+ code: 'permission_denied',
1057
+ message: 'Je hebt geen toegangsrechten om het UiTPAS-nummer van dit lid aan te passen',
1058
+ statusCode: 400
1059
+ })
1060
+ }
1026
1061
  }
1027
1062
 
1028
1063
  if (data.outstandingBalance) {
@@ -26,7 +26,7 @@ export class AuthenticatedStructures {
26
26
  }
27
27
 
28
28
  const {balanceItemPayments, balanceItems} = await Payment.loadBalanceItems(payments)
29
- const {registrations, orders, groups} = await Payment.loadBalanceItemRelations(balanceItems);
29
+ const {registrations, orders} = await Payment.loadBalanceItemRelations(balanceItems);
30
30
 
31
31
  if (checkPermissions) {
32
32
  // Note: permission checking is moved here for performacne to avoid loading the data multiple times
@@ -41,13 +41,10 @@ export class AuthenticatedStructures {
41
41
 
42
42
  const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions
43
43
 
44
- return await Payment.getGeneralStructureFromRelations({
44
+ return Payment.getGeneralStructureFromRelations({
45
45
  payments,
46
46
  balanceItemPayments,
47
- balanceItems,
48
- registrations,
49
- orders,
50
- groups
47
+ balanceItems
51
48
  }, includeSettlements)
52
49
  }
53
50
 
@@ -154,6 +151,19 @@ export class AuthenticatedStructures {
154
151
  })
155
152
  }
156
153
 
154
+ static async organizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
155
+ // for now simple loop
156
+ if (organizations.length > 10) {
157
+ console.warn('Trying to load too many organizations at once: ' + organizations.length)
158
+ }
159
+
160
+ const structs: OrganizationStruct[] = [];
161
+ for (const organization of organizations) {
162
+ structs.push(await this.organization(organization))
163
+ }
164
+ return structs
165
+ }
166
+
157
167
  static async adminOrganizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
158
168
  const structs: OrganizationStruct[] = [];
159
169
 
@@ -60,6 +60,27 @@ export class ContextInstance {
60
60
  return c;
61
61
  }
62
62
 
63
+ static async startForUser<T>(user: User, organization: Organization|null, handler: () => Promise<T>): Promise<T> {
64
+ const request = new Request({
65
+ method: 'GET',
66
+ url: '/',
67
+ host: ''
68
+ })
69
+ const context = new ContextInstance(request);
70
+
71
+ if (organization) {
72
+ context.organization = organization
73
+ context.i18n.switchToLocale({ country: organization.address.country })
74
+ }
75
+
76
+ context.user = user
77
+ context.#auth = new AdminPermissionChecker(user, await Platform.getSharedPrivateStruct(), context.organization);
78
+
79
+ return await this.asyncLocalStorage.run(context, async () => {
80
+ return await handler()
81
+ });
82
+ }
83
+
63
84
  static async start<T>(request: Request, handler: () => Promise<T>): Promise<T> {
64
85
  const context = new ContextInstance(request);
65
86
 
@@ -1,6 +1,7 @@
1
- import { Email } from "@stamhoofd/models";
1
+ import { Email, Organization, User } from "@stamhoofd/models";
2
2
  import { SQL } from "@stamhoofd/sql";
3
3
  import { EmailStatus } from "@stamhoofd/structures";
4
+ import { ContextInstance } from "./Context";
4
5
 
5
6
  export async function resumeEmails() {
6
7
  const query = SQL.select()
@@ -11,7 +12,26 @@ export async function resumeEmails() {
11
12
  const emails = Email.fromRows(result, Email.table);
12
13
 
13
14
  for (const email of emails) {
15
+ if (!email.userId) {
16
+ console.warn('Cannot retry sending email because userId is not set - which is required for setting the scope', email.id)
17
+ continue;
18
+ }
14
19
  console.log('Resuming email that has sending status on boot', email.id);
15
- await email.send()
20
+
21
+ const user = await User.getByID(email.userId);
22
+ if (!user) {
23
+ console.warn('Cannot retry sending email because user not found', email.id)
24
+ continue;
25
+ }
26
+
27
+ const organization = email.organizationId ? (await Organization.getByID(email.organizationId)) : null;
28
+ if (organization === undefined) {
29
+ console.warn('Cannot retry sending email because organization not found', email.id)
30
+ continue;
31
+ }
32
+
33
+ await ContextInstance.startForUser(user, organization, async () => {
34
+ await email.send()
35
+ })
16
36
  }
17
37
  }
@@ -36,8 +36,14 @@ export class MemberUserSyncerStatic {
36
36
  } else {
37
37
  // Only auto unlink users that do not have an account
38
38
  for (const user of member.users) {
39
- if (!user.hasAccount() && !userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
40
- await this.unlinkUser(user, member)
39
+ if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
40
+ if (!user.hasAccount()) {
41
+ await this.unlinkUser(user, member)
42
+ } else {
43
+ // Make sure only linked as a parent, not as user self
44
+ // This makes sure we don't inherit permissions and aren't counted as 'begin' the member
45
+ await this.linkUser(user.email, member, true)
46
+ }
41
47
  }
42
48
  }
43
49
  }