@stamhoofd/backend 2.8.0 → 2.9.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/.env.template.json +3 -1
  2. package/package.json +3 -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 +14 -0
  7. package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
  8. package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
  9. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
  10. package/src/endpoints/global/members/GetMembersEndpoint.ts +0 -31
  11. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
  12. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
  13. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +114 -34
  14. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
  15. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
  16. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +3 -2
  17. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
  18. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
  19. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +3 -40
  20. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
  21. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
  22. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
  23. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
  24. package/src/helpers/AdminPermissionChecker.ts +17 -2
  25. package/src/helpers/AuthenticatedStructures.ts +16 -6
  26. package/src/helpers/Context.ts +21 -0
  27. package/src/helpers/EmailResumer.ts +22 -2
  28. package/src/seeds/1722344160-update-membership.ts +19 -22
  29. package/src/seeds/1722344161-sync-member-users.ts +60 -0
@@ -1,9 +1,10 @@
1
1
  import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
- import { EmailTemplate } from '@stamhoofd/models';
3
+ import { EmailTemplate, Group, Webshop } from '@stamhoofd/models';
4
4
  import { EmailTemplate as EmailTemplateStruct, PermissionLevel } from '@stamhoofd/structures';
5
5
 
6
6
  import { Context } from '../../../../helpers/Context';
7
+ import { SimpleError } from '@simonbackx/simple-errors';
7
8
 
8
9
  type Params = Record<string, never>;
9
10
  type Body = PatchableArrayAutoEncoder<EmailTemplateStruct>;
@@ -67,12 +68,32 @@ export class PatchEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, R
67
68
  throw Context.auth.error();
68
69
  }
69
70
 
71
+ if (!EmailTemplateStruct.allowPlatformLevel(struct.type) && !organization) {
72
+ throw Context.auth.error();
73
+ }
74
+
70
75
  const template = new EmailTemplate()
71
76
  template.id = struct.id
72
77
  template.organizationId = organization?.id ?? null
73
78
  template.webshopId = struct.webshopId
74
79
  template.groupId = struct.groupId
75
80
 
81
+ if (struct.groupId) {
82
+ const group = await Group.getByID(struct.groupId)
83
+ if (!group || !await Context.auth.canAccessGroup(group, PermissionLevel.Full)) {
84
+ throw Context.auth.error();
85
+ }
86
+ template.organizationId = group.organizationId
87
+ }
88
+
89
+ if (struct.webshopId) {
90
+ const webshop = await Webshop.getByID(struct.webshopId)
91
+ if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
92
+ throw Context.auth.error();
93
+ }
94
+ template.organizationId = webshop.organizationId
95
+ }
96
+
76
97
  template.html = struct.html
77
98
  template.subject = struct.subject
78
99
  template.text = struct.text
@@ -1,8 +1,8 @@
1
1
  import { AutoEncoderPatchType, Decoder, ObjectData, patchObject } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
3
  import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
4
- import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform, RegistrationPeriod, StripeAccount, User, Webshop } from '@stamhoofd/models';
5
- import { BuckarooSettings, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, UserPermissions } from "@stamhoofd/structures";
4
+ import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform, RegistrationPeriod, StripeAccount, Webshop } from '@stamhoofd/models';
5
+ import { BuckarooSettings, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel } from "@stamhoofd/structures";
6
6
  import { Formatter } from '@stamhoofd/utility';
7
7
 
8
8
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
@@ -98,6 +98,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
98
98
 
99
99
  if (request.body.privateMeta && request.body.privateMeta.isPatch()) {
100
100
  organization.privateMeta.emails = request.body.privateMeta.emails.applyTo(organization.privateMeta.emails)
101
+ organization.privateMeta.premises = patchObject(organization.privateMeta.premises, request.body.privateMeta.premises);
101
102
  organization.privateMeta.roles = request.body.privateMeta.roles.applyTo(organization.privateMeta.roles)
102
103
  organization.privateMeta.responsibilities = request.body.privateMeta.responsibilities.applyTo(organization.privateMeta.responsibilities)
103
104
  organization.privateMeta.inheritedResponsibilityRoles = request.body.privateMeta.inheritedResponsibilityRoles.applyTo(organization.privateMeta.inheritedResponsibilityRoles)
@@ -6,6 +6,7 @@ import NodeRSA from 'node-rsa';
6
6
 
7
7
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
8
8
  import { Context } from '../../../../helpers/Context';
9
+ import { Formatter } from '@stamhoofd/utility';
9
10
 
10
11
  type Params = Record<string, never>;
11
12
  type Query = undefined;
@@ -98,7 +99,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
98
99
  organization.privateMeta.dnsRecords = []
99
100
 
100
101
  if (organization.privateMeta.pendingMailDomain !== null) {
101
- const defaultFromDomain = "stamhoofd." + organization.privateMeta.pendingMailDomain;
102
+ const defaultFromDomain = Formatter.slug(STAMHOOFD.platformName) + "." + organization.privateMeta.pendingMailDomain;
102
103
  if (organization.privateMeta.pendingRegisterDomain === null || !organization.privateMeta.pendingRegisterDomain.endsWith('.' + organization.privateMeta.pendingMailDomain)) {
103
104
  // We set a custom domainname for webshops already
104
105
  // This is not used at this moment
@@ -109,8 +110,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
109
110
  }
110
111
 
111
112
  if (organization.privateMeta.mailFromDomain !== organization.privateMeta.pendingRegisterDomain) {
112
-
113
- organization.privateMeta.dnsRecords.push(DNSRecord.create({
113
+ organization.privateMeta.dnsRecords.push(DNSRecord.create({
114
114
  type: DNSRecordType.CNAME,
115
115
  name: organization.privateMeta.mailFromDomain + ".",
116
116
  // Use shops for mail domain, to allow reuse
@@ -1,14 +1,14 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
2
  import { SimpleError } from "@simonbackx/simple-errors";
3
3
  import { BalanceItem, Group, Member } from "@stamhoofd/models";
4
- import { MemberBalanceItem } from "@stamhoofd/structures";
4
+ import { BalanceItemWithPayments } from "@stamhoofd/structures";
5
5
 
6
6
  import { Context } from "../../../../helpers/Context";
7
7
 
8
8
  type Params = { id: string };
9
9
  type Query = undefined
10
10
  type Body = undefined
11
- type ResponseBody = MemberBalanceItem[]
11
+ type ResponseBody = BalanceItemWithPayments[]
12
12
 
13
13
  export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
14
  protected doesMatch(request: Request): [true, Params] | [false] {
@@ -42,7 +42,7 @@ export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, Resp
42
42
  const balanceItems = await BalanceItem.balanceItemsForUsersAndMembers(organization.id, member.users.map(u => u.id), [member.id])
43
43
 
44
44
  return new Response(
45
- await BalanceItem.getMemberStructure(balanceItems)
45
+ await BalanceItem.getStructureWithPayments(balanceItems)
46
46
  );
47
47
  }
48
48
  }
@@ -1,54 +1,17 @@
1
- import { AutoEncoder, Data, DateDecoder, Decoder, EnumDecoder, field, IntegerDecoder, StringDecoder } from "@simonbackx/simple-encoding";
1
+ import { AutoEncoder, DateDecoder, EnumDecoder, field, IntegerDecoder, StringDecoder } from "@simonbackx/simple-encoding";
2
2
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
3
  import { Organization, Payment } from "@stamhoofd/models";
4
4
  import { PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus } from "@stamhoofd/structures";
5
5
 
6
+ import { StringArrayDecoder } from "../../../../decoders/StringArrayDecoder";
6
7
  import { AuthenticatedStructures } from "../../../../helpers/AuthenticatedStructures";
7
8
  import { Context } from "../../../../helpers/Context";
9
+ import { StringNullableDecoder } from "../../../../decoders/StringNullableDecoder";
8
10
 
9
11
  type Params = Record<string, never>;
10
12
  type Body = undefined
11
13
  type ResponseBody = PaymentGeneral[]
12
14
 
13
- export class StringArrayDecoder<T> implements Decoder<T[]> {
14
- decoder: Decoder<T>;
15
-
16
- constructor(decoder: Decoder<T>) {
17
- this.decoder = decoder;
18
- }
19
-
20
- decode(data: Data): T[] {
21
- const strValue = data.string;
22
-
23
- // Split on comma
24
- const parts = strValue.split(",");
25
- return parts
26
- .map((v, index) => {
27
- return data.clone({
28
- data: v,
29
- context: data.context,
30
- field: data.addToCurrentField(index)
31
- }).decode(this.decoder)
32
- });
33
- }
34
- }
35
-
36
- export class StringNullableDecoder<T> implements Decoder<T | null> {
37
- decoder: Decoder<T>;
38
-
39
- constructor(decoder: Decoder<T>) {
40
- this.decoder = decoder;
41
- }
42
-
43
- decode(data: Data): T | null {
44
- if (data.value === 'null') {
45
- return null;
46
- }
47
-
48
- return data.decode(this.decoder);
49
- }
50
- }
51
-
52
15
 
53
16
  class Query extends AutoEncoder {
54
17
  /**
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-
3
3
  import { SimpleError } from "@simonbackx/simple-errors";
4
4
  import { BalanceItem, Member, Order, Registration, User } from '@stamhoofd/models';
5
5
  import { QueueHandler } from '@stamhoofd/queues';
6
- import { BalanceItemStatus, MemberBalanceItem, PermissionLevel } from "@stamhoofd/structures";
6
+ import { BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, PermissionLevel } from "@stamhoofd/structures";
7
7
  import { Formatter } from '@stamhoofd/utility';
8
8
 
9
9
  import { Context } from '../../../../helpers/Context';
@@ -11,11 +11,11 @@ import { Context } from '../../../../helpers/Context';
11
11
 
12
12
  type Params = Record<string, never>;
13
13
  type Query = undefined;
14
- type Body = PatchableArrayAutoEncoder<MemberBalanceItem>
15
- type ResponseBody = MemberBalanceItem[]
14
+ type Body = PatchableArrayAutoEncoder<BalanceItemWithPayments>
15
+ type ResponseBody = BalanceItemWithPayments[]
16
16
 
17
17
  export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
18
- bodyDecoder = new PatchableArrayDecoder(MemberBalanceItem as Decoder<MemberBalanceItem>, MemberBalanceItem.patchType() as Decoder<AutoEncoderPatchType<MemberBalanceItem>>, StringDecoder)
18
+ bodyDecoder = new PatchableArrayDecoder(BalanceItemWithPayments as Decoder<BalanceItemWithPayments>, BalanceItemWithPayments.patchType() as Decoder<AutoEncoderPatchType<BalanceItemWithPayments>>, StringDecoder)
19
19
 
20
20
  protected doesMatch(request: Request): [true, Params] | [false] {
21
21
  if (request.method != "PATCH") {
@@ -53,7 +53,10 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
53
53
  // Create a new balance item
54
54
  const model = new BalanceItem();
55
55
  model.description = put.description;
56
- model.price = put.price;
56
+ model.amount = put.amount;
57
+ model.type = BalanceItemType.Other
58
+ model.unitPrice = put.unitPrice;
59
+ model.amount = put.amount;
57
60
  model.organizationId = organization.id;
58
61
  model.createdAt = put.createdAt;
59
62
  model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Pending;
@@ -75,19 +78,6 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
75
78
  })
76
79
  }
77
80
 
78
- if (put.registration) {
79
- const registration = await Registration.getByID(put.registration.id)
80
- if (!registration || registration.memberId !== model.memberId || registration.organizationId !== organization.id) {
81
- throw new SimpleError({
82
- code: 'invalid_field',
83
- message: 'Registration not found',
84
- field: 'registration'
85
- })
86
- }
87
- model.registrationId = registration.id
88
- registrationIds.push(registration.id)
89
- }
90
-
91
81
  await model.save();
92
82
  returnedModels.push(model);
93
83
  }
@@ -101,6 +91,15 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
101
91
  message: 'BalanceItem not found'
102
92
  })
103
93
  }
94
+
95
+ if (patch.unitPrice !== undefined) {
96
+ throw new SimpleError({
97
+ code: 'invalid_field',
98
+ message: 'You cannot change the unit price of a balance item',
99
+ human: 'Het is niet mogelijk om de eenheidsprijs van een openstaande schuld te wijzigen. Je kan de openstaande schuld verwijderen en opnieuw aanmaken indien noodzakelijk.'
100
+ })
101
+ }
102
+
104
103
  // Check permissions
105
104
  if (model.memberId) {
106
105
  // Update old
@@ -123,30 +122,16 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
123
122
  model.createdAt = patch.createdAt
124
123
  }
125
124
 
126
- if (patch.registration) {
127
- const registration = await Registration.getByID(patch.registration.id)
128
- if (!registration || registration.memberId !== model.memberId || registration.organizationId !== organization.id) {
129
- throw new SimpleError({
130
- code: 'invalid_field',
131
- message: 'Registration not found',
132
- field: 'registration'
133
- })
134
- }
135
- model.registrationId = registration.id
136
-
137
- // Update new
138
- registrationIds.push(model.registrationId)
139
- } else if (patch.registration === null) {
140
- model.registrationId = null
141
- }
142
125
  model.description = patch.description ?? model.description;
143
- model.price = patch.price ?? model.price;
126
+ model.unitPrice = patch.unitPrice ?? model.unitPrice;
127
+ model.amount = patch.amount ?? model.amount;
144
128
 
145
129
  if (model.orderId) {
146
130
  // Not allowed to change this
147
131
  const order = await Order.getByID(model.orderId)
148
132
  if (order) {
149
- model.price = order.totalToPay
133
+ model.unitPrice = order.totalToPay
134
+ model.amount = 1
150
135
  }
151
136
  }
152
137
 
@@ -167,7 +152,7 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
167
152
  await Registration.updateOutstandingBalance(Formatter.uniqueArray(registrationIds), organization.id)
168
153
 
169
154
  return new Response(
170
- await BalanceItem.getMemberStructure(returnedModels)
155
+ await BalanceItem.getStructureWithPayments(returnedModels)
171
156
  );
172
157
  }
173
158
 
@@ -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
  /**
@@ -949,8 +949,9 @@ export class AdminPermissionChecker {
949
949
 
950
950
  const hasRecordAnswers = !!data.details.recordAnswers;
951
951
  const hasNotes = data.details.notes !== undefined;
952
+ const isSetFinancialSupportTrue = data.details.requiresFinancialSupport?.value === true;
952
953
 
953
- if(hasRecordAnswers || hasNotes) {
954
+ if(hasRecordAnswers || hasNotes || isSetFinancialSupportTrue) {
954
955
  const isUserManager = this.isUserManager(member);
955
956
 
956
957
  if (hasRecordAnswers) {
@@ -1005,6 +1006,20 @@ export class AdminPermissionChecker {
1005
1006
  statusCode: 400
1006
1007
  })
1007
1008
  }
1009
+
1010
+ if(isSetFinancialSupportTrue) {
1011
+ const financialSupport = this.platform.config.recordsConfiguration.financialSupport;
1012
+ const preventSelfAssignment = financialSupport?.preventSelfAssignment === true;
1013
+
1014
+ if(preventSelfAssignment) {
1015
+ throw new SimpleError({
1016
+ code: 'permission_denied',
1017
+ message: 'Je hebt geen toegangsrechten om de financiële status van dit lid aan te passen',
1018
+ human: financialSupport.preventSelfAssignmentText ?? FinancialSupportSettings.defaultPreventSelfAssignmentText,
1019
+ statusCode: 400
1020
+ });
1021
+ }
1022
+ }
1008
1023
  }
1009
1024
 
1010
1025
  // Has financial write access?
@@ -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
  }
@@ -22,33 +22,30 @@ export default new Migration(async () => {
22
22
  value: id,
23
23
  sign: '>'
24
24
  }
25
- }, {limit: 100, sort: ['id']});
26
-
27
- // const members = await Member.getByIDs(...rawMembers.map(m => m.id));
28
-
29
- for (const member of rawMembers) {
30
- const memberWithRegistrations = await Member.getWithRegistrations(member.id);
31
- if(memberWithRegistrations) {
32
- await memberWithRegistrations.updateMemberships();
33
- await memberWithRegistrations.save();
34
- } else {
35
- throw new Error("Member with registrations not found: " + member.id);
36
- }
37
-
38
- c++;
39
-
40
- if (c%1000 === 0) {
41
- process.stdout.write('.');
42
- }
43
- if (c%10000 === 0) {
44
- process.stdout.write('\n');
45
- }
46
- }
25
+ }, {limit: 500, sort: ['id']});
47
26
 
48
27
  if (rawMembers.length === 0) {
49
28
  break;
50
29
  }
51
30
 
31
+ const promises: Promise<any>[] = [];
32
+
33
+
34
+ for (const member of rawMembers) {
35
+ promises.push((async () => {
36
+ await Member.updateMembershipsForId(member.id, true);
37
+ c++;
38
+
39
+ if (c%1000 === 0) {
40
+ process.stdout.write('.');
41
+ }
42
+ if (c%10000 === 0) {
43
+ process.stdout.write('\n');
44
+ }
45
+ })())
46
+ }
47
+
48
+ await Promise.all(promises);
52
49
  id = rawMembers[rawMembers.length - 1].id;
53
50
  }
54
51
 
@@ -0,0 +1,60 @@
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 membersWithRegistrations = await Member.getBlobByIds(...rawMembers.map(m => m.id));
35
+
36
+ const promises: Promise<any>[] = [];
37
+
38
+ for (const memberWithRegistrations of membersWithRegistrations) {
39
+ promises.push((async () => {
40
+ await MemberUserSyncer.onChangeMember(memberWithRegistrations);
41
+ c++;
42
+
43
+ if (c%1000 === 0) {
44
+ process.stdout.write('.');
45
+ }
46
+ if (c%10000 === 0) {
47
+ process.stdout.write('\n');
48
+ }
49
+ })());
50
+ }
51
+
52
+ await Promise.all(promises);
53
+ id = rawMembers[rawMembers.length - 1].id;
54
+ }
55
+ })
56
+
57
+
58
+ // Do something here
59
+ return Promise.resolve()
60
+ })