@stamhoofd/backend 2.19.0 → 2.20.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.
@@ -2,19 +2,15 @@ import { createMollieClient } from '@mollie/api-client';
2
2
  import { AutoEncoder, BooleanDecoder, Decoder, field } from '@simonbackx/simple-encoding';
3
3
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
4
4
  import { SimpleError } from "@simonbackx/simple-errors";
5
- import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, STPendingInvoice } from '@stamhoofd/models';
5
+ import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment } from '@stamhoofd/models';
6
6
  import { QueueHandler } from '@stamhoofd/queues';
7
- import { PaymentGeneral, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, STInvoiceItem } from "@stamhoofd/structures";
7
+ import { PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus } from "@stamhoofd/structures";
8
8
 
9
9
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
10
10
  import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
11
11
  import { Context } from '../../../helpers/Context';
12
12
  import { StripeHelper } from '../../../helpers/StripeHelper';
13
13
 
14
- function calculateFee(totalPrice: number, fixed: number, percentageTimes100: number) {
15
- return Math.round(fixed + Math.max(1, totalPrice * percentageTimes100 / 100 / 100)); // € 0,21 + 0,2%
16
- }
17
-
18
14
  type Params = {id: string};
19
15
  class Query extends AutoEncoder {
20
16
  @field({ decoder: BooleanDecoder, optional: true })
@@ -77,7 +73,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
77
73
  if (payment.status === status) {
78
74
  return;
79
75
  }
80
- const wasPaid = payment.paidAt !== null
76
+ // const wasPaid = payment.paidAt !== null
81
77
  if (status === PaymentStatus.Succeeded) {
82
78
  payment.status = PaymentStatus.Succeeded
83
79
  payment.paidAt = new Date()
@@ -97,31 +93,31 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
97
93
  await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
98
94
  })
99
95
 
100
- if (!wasPaid && payment.provider === PaymentProvider.Buckaroo && payment.method) {
101
- // Charge transaction fees
102
- let fee = 0
103
-
104
- if (payment.method === PaymentMethod.iDEAL) {
105
- fee = calculateFee(payment.price, 21, 20); // € 0,21 + 0,2%
106
- } else if (payment.method === PaymentMethod.Bancontact || payment.method === PaymentMethod.Payconiq) {
107
- fee = calculateFee(payment.price, 24, 20); // € 0,24 + 0,2%
108
- } else {
109
- fee = calculateFee(payment.price, 25, 150); // € 0,25 + 1,5%
110
- }
111
-
112
- const name = "Transactiekosten voor "+PaymentMethodHelper.getName(payment.method)
113
- const item = STInvoiceItem.create({
114
- name,
115
- description: "Via Buckaroo",
116
- amount: 1,
117
- unitPrice: fee,
118
- canUseCredits: false
119
- })
120
- console.log("Scheduling transaction fee charge for ", payment.id, item)
121
- await QueueHandler.schedule("billing/invoices-"+organization.id, async () => {
122
- await STPendingInvoice.addItems(organization, [item])
123
- });
124
- }
96
+ //if (!wasPaid && payment.provider === PaymentProvider.Buckaroo && payment.method) {
97
+ // // Charge transaction fees
98
+ // let fee = 0
99
+ //
100
+ // if (payment.method === PaymentMethod.iDEAL) {
101
+ // fee = calculateFee(payment.price, 21, 20); // € 0,21 + 0,2%
102
+ // } else if (payment.method === PaymentMethod.Bancontact || payment.method === PaymentMethod.Payconiq) {
103
+ // fee = calculateFee(payment.price, 24, 20); // € 0,24 + 0,2%
104
+ // } else {
105
+ // fee = calculateFee(payment.price, 25, 150); // € 0,25 + 1,5%
106
+ // }
107
+ //
108
+ // const name = "Transactiekosten voor "+PaymentMethodHelper.getName(payment.method)
109
+ // const item = STInvoiceItem.create({
110
+ // name,
111
+ // description: "Via Buckaroo",
112
+ // amount: 1,
113
+ // unitPrice: fee,
114
+ // canUseCredits: false
115
+ // })
116
+ // console.log("Scheduling transaction fee charge for ", payment.id, item)
117
+ // await QueueHandler.schedule("billing/invoices-"+organization.id, async () => {
118
+ // await STPendingInvoice.addItems(organization, [item])
119
+ // });
120
+ //}
125
121
  return;
126
122
  }
127
123
 
@@ -1,6 +1,6 @@
1
1
  import { AutoEncoderPatchType, PatchMap } from "@simonbackx/simple-encoding"
2
2
  import { SimpleError } from "@simonbackx/simple-errors"
3
- import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, Payment, Registration, User, Webshop } from "@stamhoofd/models"
3
+ import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from "@stamhoofd/models"
4
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
 
@@ -75,6 +75,11 @@ export class AdminPermissionChecker {
75
75
  return result;
76
76
  }
77
77
 
78
+ async getOrganizationCurrentPeriod(id: string|Organization): Promise<OrganizationRegistrationPeriod> {
79
+ const organization = await this.getOrganization(id);
80
+ return await organization.getPeriod()
81
+ }
82
+
78
83
  error(message?: string): SimpleError {
79
84
  return new SimpleError({
80
85
  code: "permission_denied",
@@ -171,7 +176,8 @@ export class AdminPermissionChecker {
171
176
  }
172
177
 
173
178
  // Check parent categories
174
- const parentCategories = group.getParentCategories(organization.meta.categories)
179
+ const organizationPeriod = await this.getOrganizationCurrentPeriod(organization)
180
+ const parentCategories = group.getParentCategories(organizationPeriod.settings.categories)
175
181
  for (const category of parentCategories) {
176
182
  if (organizationPermissions.hasResourceAccess(PermissionsResourceType.GroupCategories, category.id, permissionLevel)) {
177
183
  return true
@@ -677,11 +683,22 @@ export class AdminPermissionChecker {
677
683
  return false;
678
684
  }
679
685
 
680
- if (!organizationPermissions.hasResourceAccessRight(PermissionsResourceType.GroupCategories, category.id, AccessRight.OrganizationCreateGroups)) {
681
- return false;
686
+ if (organizationPermissions.hasResourceAccessRight(PermissionsResourceType.GroupCategories, category.id, AccessRight.OrganizationCreateGroups)) {
687
+ return true;
682
688
  }
683
689
 
684
- return true;
690
+ // Check parents
691
+ const organization = await this.getOrganization(organizationId)
692
+ const organizationPeriod = await this.getOrganizationCurrentPeriod(organization)
693
+ const parentCategories = category.getParentCategories(organizationPeriod.settings.categories)
694
+
695
+ for (const parentCategory of parentCategories) {
696
+ if (organizationPermissions.hasResourceAccessRight(PermissionsResourceType.GroupCategories, parentCategory.id, AccessRight.OrganizationCreateGroups)) {
697
+ return true;
698
+ }
699
+ }
700
+
701
+ return false;
685
702
  }
686
703
 
687
704
  canUpload() {
@@ -238,11 +238,12 @@ export class AuthenticatedStructures {
238
238
  }
239
239
  }
240
240
  }
241
-
241
+ member.registrations = member.registrations.filter(r => (Context.auth.organization && Context.auth.organization.active && r.organizationId === Context.auth.organization.id) || (organizations.get(r.organizationId)?.active ?? false))
242
242
  const blob = member.getStructureWithRegistrations()
243
243
  memberBlobs.push(
244
244
  await Context.auth.filterMemberData(member, blob)
245
245
  )
246
+
246
247
  }
247
248
 
248
249
  // Load responsibilities
@@ -268,7 +269,7 @@ export class AuthenticatedStructures {
268
269
 
269
270
  return MembersBlob.create({
270
271
  members: memberBlobs,
271
- organizations: await Promise.all([...organizations.values()].map(o => this.organization(o)))
272
+ organizations: await Promise.all([...organizations.values()].filter(o => o.active).map(o => this.organization(o)))
272
273
  })
273
274
  }
274
275
 
@@ -1,38 +1,41 @@
1
1
  import {
2
+ Group,
3
+ MemberResponsibilityRecord,
2
4
  Organization,
3
5
  OrganizationRegistrationPeriod,
4
- Platform,
6
+ Platform
5
7
  } from "@stamhoofd/models";
6
8
  import { QueueHandler } from "@stamhoofd/queues";
9
+ import { SQL, SQLWhereSign } from "@stamhoofd/sql";
7
10
  import {
8
- PlatformPremiseType,
11
+ MemberResponsibility,
9
12
  Platform as PlatformStruct,
10
13
  SetupStepType,
11
- SetupSteps,
14
+ SetupSteps
12
15
  } from "@stamhoofd/structures";
13
16
 
14
- type SetupStepOperation = (setupSteps: SetupSteps, organization: Organization, platform: PlatformStruct) => void;
17
+ type SetupStepOperation = (setupSteps: SetupSteps, organization: Organization, platform: PlatformStruct) => void | Promise<void>;
15
18
 
16
19
  export class SetupStepUpdater {
17
20
  private static readonly STEP_TYPE_OPERATIONS: Record<
18
21
  SetupStepType,
19
22
  SetupStepOperation
20
23
  > = {
24
+ [SetupStepType.Functions]: this.updateStepFunctions,
25
+ [SetupStepType.Companies]: this.updateStepCompanies,
21
26
  [SetupStepType.Groups]: this.updateStepGroups,
22
27
  [SetupStepType.Premises]: this.updateStepPremises,
23
28
  };
24
29
 
25
30
  static async updateSetupStepsForAllOrganizationsInCurrentPeriod({
26
- batchSize, premiseTypes
27
- }: { batchSize?: number, premiseTypes?: PlatformPremiseType[] } = {}) {
31
+ batchSize
32
+ }: { batchSize?: number } = {}) {
28
33
  const tag = "updateSetupStepsForAllOrganizationsInCurrentPeriod";
29
34
  QueueHandler.cancel(tag);
30
35
 
31
36
  await QueueHandler.schedule(tag, async () => {
32
37
  const platform = (await Platform.getSharedPrivateStruct()).clone();
33
- if(premiseTypes) {
34
- platform.config.premiseTypes = premiseTypes;
35
- }
38
+
36
39
  const periodId = platform.period.id;
37
40
 
38
41
  let lastId = "";
@@ -136,7 +139,7 @@ export class SetupStepUpdater {
136
139
  );
137
140
  }
138
141
 
139
- static async updateFor(
142
+ private static async updateFor(
140
143
  organizationRegistrationPeriod: OrganizationRegistrationPeriod,
141
144
  platform: PlatformStruct,
142
145
  organization: Organization
@@ -147,13 +150,13 @@ export class SetupStepUpdater {
147
150
  for (const stepType of Object.values(SetupStepType)) {
148
151
  console.log(`[STEP TYPE] ${stepType}`);
149
152
  const operation = this.STEP_TYPE_OPERATIONS[stepType];
150
- operation(setupSteps, organization, platform);
153
+ await operation(setupSteps, organization, platform);
151
154
  }
152
155
 
153
156
  await organizationRegistrationPeriod.save();
154
157
  }
155
158
 
156
- static updateStepPremises(
159
+ private static updateStepPremises(
157
160
  setupSteps: SetupSteps,
158
161
  organization: Organization,
159
162
  platform: PlatformStruct
@@ -165,6 +168,8 @@ export class SetupStepUpdater {
165
168
 
166
169
  for (const premiseType of premiseTypes) {
167
170
  const { min, max } = premiseType;
171
+
172
+ // only add step if premise type has restrictions
168
173
  if (min === null && max === null) {
169
174
  continue;
170
175
  }
@@ -197,7 +202,7 @@ export class SetupStepUpdater {
197
202
  });
198
203
  }
199
204
 
200
- static updateStepGroups(
205
+ private static updateStepGroups(
201
206
  setupSteps: SetupSteps,
202
207
  _organization: Organization,
203
208
  _platform: PlatformStruct
@@ -207,4 +212,101 @@ export class SetupStepUpdater {
207
212
  finishedSteps: 0,
208
213
  });
209
214
  }
215
+
216
+ private static updateStepCompanies(
217
+ setupSteps: SetupSteps,
218
+ _organization: Organization,
219
+ _platform: PlatformStruct
220
+ ) {
221
+ setupSteps.update(SetupStepType.Companies, {
222
+ totalSteps: 0,
223
+ finishedSteps: 0,
224
+ });
225
+ }
226
+
227
+ private static async updateStepFunctions(
228
+ setupSteps: SetupSteps,
229
+ organization: Organization,
230
+ platform: PlatformStruct
231
+ ) {
232
+ const now = new Date();
233
+ const organizationBasedResponsibilitiesWithRestriction = platform.config.responsibilities
234
+ .filter(r => r.organizationBased && (r.minimumMembers || r.maximumMembers));
235
+
236
+ const responsibilityIds = organizationBasedResponsibilitiesWithRestriction.map(r => r.id);
237
+
238
+ const records = await MemberResponsibilityRecord.select()
239
+ .where('responsibilityId', responsibilityIds)
240
+ .where('organizationId', organization.id)
241
+ .where(SQL.where('endDate', SQLWhereSign.Greater, now).or('endDate', null))
242
+ .fetch();
243
+
244
+ let totalSteps = 0;
245
+ let finishedSteps = 0;
246
+
247
+ const groups = await Group.getAll(organization.id, organization.periodId);
248
+
249
+ const flatResponsibilities: {responsibility: MemberResponsibility, group: Group | null}[] = organizationBasedResponsibilitiesWithRestriction
250
+ .flatMap(responsibility => {
251
+ const defaultAgeGroupIds = responsibility.defaultAgeGroupIds;
252
+ if(defaultAgeGroupIds === null) {
253
+ const item: {responsibility: MemberResponsibility, group: Group | null} = {
254
+ responsibility,
255
+ group: null
256
+ }
257
+ return [item];
258
+ }
259
+
260
+ return groups
261
+ .filter(g => g.defaultAgeGroupId !== null && defaultAgeGroupIds.includes(g.defaultAgeGroupId))
262
+ .map(group => {
263
+ return {
264
+ responsibility,
265
+ group
266
+ }
267
+ });
268
+ });
269
+
270
+ for(const {responsibility, group} of flatResponsibilities) {
271
+ const { minimumMembers: min, maximumMembers: max } = responsibility;
272
+
273
+ if (min === null && max === null) {
274
+ continue;
275
+ }
276
+
277
+ totalSteps++;
278
+
279
+ const responsibilityId = responsibility.id;
280
+ let totalRecordsWithThisResponsibility = 0;
281
+
282
+ if(group === null) {
283
+ for (const record of records) {
284
+ if (record.responsibilityId === responsibilityId) {
285
+ totalRecordsWithThisResponsibility++;
286
+ }
287
+ }
288
+ } else {
289
+ for (const record of records) {
290
+ if (record.responsibilityId === responsibilityId && record.groupId === group.id) {
291
+ totalRecordsWithThisResponsibility++;
292
+ }
293
+ }
294
+ }
295
+
296
+ if (max !== null && totalRecordsWithThisResponsibility > max) {
297
+ continue;
298
+ }
299
+
300
+ if (min !== null && totalRecordsWithThisResponsibility < min) {
301
+ continue;
302
+ }
303
+
304
+ finishedSteps++;
305
+ }
306
+
307
+ setupSteps.update(SetupStepType.Functions, {
308
+ totalSteps,
309
+ finishedSteps,
310
+ });
311
+ }
210
312
  }
@@ -1,153 +0,0 @@
1
- import { createMollieClient } from '@mollie/api-client';
2
- import { AutoEncoder, BooleanDecoder,Decoder,field } from '@simonbackx/simple-encoding';
3
- import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
4
- import { SimpleError } from "@simonbackx/simple-errors";
5
- import { MolliePayment, Organization } from "@stamhoofd/models";
6
- import { Payment } from "@stamhoofd/models";
7
- import { STInvoice } from "@stamhoofd/models";
8
- import { QueueHandler } from '@stamhoofd/queues';
9
- import { PaymentMethod,PaymentProvider,PaymentStatus, STInvoice as STInvoiceStruct } from "@stamhoofd/structures";
10
- type Params = {id: string};
11
- class Query extends AutoEncoder {
12
- @field({ decoder: BooleanDecoder, optional: true })
13
- exchange = false
14
- }
15
- type Body = undefined
16
- type ResponseBody = STInvoiceStruct | undefined;
17
-
18
- /**
19
- * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
20
- */
21
-
22
- export class ExchangeSTPaymentEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
23
- queryDecoder = Query as Decoder<Query>
24
-
25
- protected doesMatch(request: Request): [true, Params] | [false] {
26
- if (request.method != "POST") {
27
- return [false];
28
- }
29
-
30
- const params = Endpoint.parseParameters(request.url, "/billing/payments/@id", {id: String});
31
-
32
- if (params) {
33
- return [true, params as Params];
34
- }
35
- return [false];
36
- }
37
-
38
- async handle(request: DecodedRequest<Params, Query, Body>) {
39
- const payment = await Payment.getByID(request.params.id)
40
- if (!payment) {
41
- throw new SimpleError({
42
- code: "",
43
- message: "Deze link is ongeldig"
44
- })
45
- }
46
-
47
- const invoices = await STInvoice.where({ paymentId: payment.id })
48
- if (invoices.length > 1) {
49
- console.error("Received more than 1 invoices for the same payment. Danger zone!")
50
- throw new Error("Unexpected error")
51
- }
52
-
53
- if (invoices.length == 0) {
54
- console.error("Didn't found and invoice for a given payment!")
55
- throw new Error("Unexpected error")
56
- }
57
-
58
- // Not method on payment because circular references (not supprted in ts)
59
- const invoice = invoices[0]
60
-
61
- if (request.query.exchange) {
62
- // Don't wait for exchanges
63
- ExchangeSTPaymentEndpoint.pollStatus(payment, invoice).catch(e => {
64
- console.error(e)
65
- })
66
- return new Response(undefined);
67
- }
68
-
69
- const updatedInvoice = await ExchangeSTPaymentEndpoint.pollStatus(payment, invoice)
70
-
71
- if (!updatedInvoice) {
72
- return new Response(undefined);
73
- }
74
-
75
- return new Response(
76
- await updatedInvoice.getStructure()
77
- );
78
- }
79
-
80
- static async pollStatus(payment: Payment, _invoice: STInvoice): Promise<STInvoice | undefined> {
81
- // All invoice related logic needs to happen after each ather, not concurrently
82
- return await QueueHandler.schedule("billing/invoices-"+_invoice.organizationId, async () => {
83
-
84
- // Get a new copy of the invoice (is required to prevent concurrenty bugs)
85
- const invoice = await STInvoice.getByID(_invoice.id)
86
- if (!invoice || invoice.paidAt !== null) {
87
- return invoice
88
- }
89
-
90
- if ((payment.provider === PaymentProvider.Mollie || (payment.provider === null && payment.method == PaymentMethod.DirectDebit)) && (payment.status == PaymentStatus.Pending || payment.status == PaymentStatus.Created || payment.status == PaymentStatus.Failed)) {
91
- if (payment.method == PaymentMethod.Bancontact || payment.method == PaymentMethod.iDEAL || payment.method == PaymentMethod.CreditCard || payment.method == PaymentMethod.DirectDebit || payment.method == PaymentMethod.Transfer) {
92
- // check status via mollie
93
- const molliePayments = await MolliePayment.where({ paymentId: payment.id}, { limit: 1 })
94
- if (molliePayments.length == 1) {
95
- const molliePayment = molliePayments[0]
96
- // check status
97
- const apiKey = STAMHOOFD.MOLLIE_API_KEY
98
- if (apiKey) {
99
- const mollieClient = createMollieClient({ apiKey });
100
- const mollieData = await mollieClient.payments.get(molliePayment.mollieId)
101
-
102
- console.log(mollieData) // log to log files to check issues
103
-
104
- const details = (mollieData.details as any)
105
- if (details?.cardNumber) {
106
- payment.iban = "xxxx xxxx xxxx "+details.cardNumber
107
- }
108
- if (details?.cardHolder) {
109
- payment.ibanName = details.cardHolder
110
- }
111
- if (details?.consumerAccount) {
112
- payment.iban = details.consumerAccount
113
- }
114
- if (details?.consumerName) {
115
- payment.ibanName = details.consumerName
116
- }
117
-
118
- if (mollieData.status == "paid") {
119
- payment.status = PaymentStatus.Succeeded
120
- payment.paidAt = new Date()
121
- await payment.save();
122
-
123
- await invoice.markPaid()
124
-
125
- // Save customer id
126
- if (mollieData.customerId && _invoice.organizationId) {
127
- const organization = await Organization.getByID(_invoice.organizationId)
128
- if (organization) {
129
- organization.serverMeta.mollieCustomerId = mollieData.customerId
130
- console.log("Saving mollie customer", mollieData.customerId, "for organization", organization.id)
131
- await organization.save()
132
- }
133
- }
134
- } else if (mollieData.status == "failed" || mollieData.status == "expired" || mollieData.status == "canceled") {
135
- payment.status = PaymentStatus.Failed
136
- await payment.save();
137
- await invoice.markFailed(payment)
138
- }
139
- } else {
140
- console.error("Mollie api key is missing for Stamhoofd payments! "+payment.id)
141
- }
142
- } else {
143
- console.error("Couldn't find mollie payment for payment "+payment.id)
144
- }
145
- } else {
146
- console.error("Payment method not supported for invoice "+invoice.id+" and payment "+payment.id)
147
- throw new Error("Unsupported payment method for invoices")
148
- }
149
- }
150
- return invoice
151
- });
152
- }
153
- }
@@ -1,64 +0,0 @@
1
-
2
- import { Request } from "@simonbackx/simple-endpoints";
3
- import { OrganizationFactory, RegisterCodeFactory, STCredit, Token, UserFactory } from "@stamhoofd/models";
4
- import { PermissionLevel, Permissions } from "@stamhoofd/structures";
5
-
6
- import { testServer } from "../../../../../tests/helpers/TestServer";
7
- import { ApplyRegisterCodeEndpoint } from "./ApplyRegisterCodeEndpoint";
8
-
9
- describe("Endpoint.ApplyRegisterCodeEndpoint", () => {
10
- // Test endpoint
11
- const endpoint = new ApplyRegisterCodeEndpoint();
12
-
13
- test("Cannot apply a register code if not platform admin", async () => {
14
- const otherOrganization = await new OrganizationFactory({}).create();
15
- const code = await new RegisterCodeFactory({organization: otherOrganization}).create();
16
-
17
- const organization = await new OrganizationFactory({}).create();
18
- const user = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create()
19
- const token = await Token.createToken(user)
20
-
21
- const r = Request.buildJson(
22
- "POST",
23
- "/organization/register-code",
24
- organization.getApiHost(),
25
- {
26
- registerCode: code.code,
27
- }
28
- );
29
- r.headers.authorization = "Bearer "+token.accessToken
30
-
31
- await expect(testServer.test(endpoint, r)).rejects.toThrow("You do not have permissions for this action");
32
- });
33
-
34
- test("Can apply a register code and apply the discount", async () => {
35
- const otherOrganization = await new OrganizationFactory({}).create();
36
- const code = await new RegisterCodeFactory({organization: otherOrganization}).create();
37
-
38
- const organization = await new OrganizationFactory({}).create();
39
- const user = await new UserFactory({
40
- organization,
41
- globalPermissions: Permissions.create({ level: PermissionLevel.Full }),
42
- email: 'admin@stamhoofd.be'
43
- }).create()
44
- const token = await Token.createToken(user)
45
-
46
- const r = Request.buildJson(
47
- "POST",
48
- "/organization/register-code",
49
- organization.getApiHost(),
50
- {
51
- registerCode: code.code,
52
- }
53
- );
54
- r.headers.authorization = "Bearer "+token.accessToken
55
-
56
- const response = await testServer.test(endpoint, r);
57
- expect(response.body).toBeUndefined();
58
-
59
- // Check if this organization has an open register code
60
- const credits = await STCredit.getForOrganization(organization.id);
61
- expect(credits.length).toBe(1);
62
- expect(credits[0].change).toBe(code.value);
63
- });
64
- });
@@ -1,84 +0,0 @@
1
- import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
2
- import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
- import { Email } from '@stamhoofd/email';
4
- import { RegisterCode, UsedRegisterCode } from '@stamhoofd/models';
5
-
6
- import { Context } from '../../../../helpers/Context';
7
-
8
- type Params = Record<string, never>;
9
- type Query = undefined;
10
- type ResponseBody = undefined;
11
-
12
- class Body extends AutoEncoder {
13
- @field({ decoder: StringDecoder })
14
- registerCode: string
15
- }
16
-
17
- /**
18
- * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
19
- */
20
-
21
- export class ApplyRegisterCodeEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
- bodyDecoder = Body as Decoder<Body>
23
-
24
- protected doesMatch(request: Request): [true, Params] | [false] {
25
- if (request.method != "POST") {
26
- return [false];
27
- }
28
-
29
- const params = Endpoint.parseParameters(request.url, "/organization/register-code", {});
30
-
31
- if (params) {
32
- return [true, params as Params];
33
- }
34
- return [false];
35
- }
36
-
37
- async handle(request: DecodedRequest<Params, Query, Body>) {
38
- const organization = await Context.setOrganizationScope();
39
- await Context.authenticate()
40
-
41
- if (!Context.auth.hasPlatformFullAccess()) {
42
- throw Context.auth.error()
43
- }
44
-
45
- let code = request.body.registerCode;
46
-
47
- if (code.startsWith('https:')) {
48
- try {
49
- const url = new URL(code);
50
- const codeParam = url.searchParams.get('code');
51
- if (codeParam) {
52
- console.log('Parsed code from URL', codeParam)
53
- code = codeParam;
54
- }
55
- } catch (e) {
56
- console.error('Tried parsing code as URL but failed', code)
57
- }
58
- }
59
-
60
- const {models, emails} = await RegisterCode.applyRegisterCode(organization, code)
61
-
62
- for (const model of models) {
63
- await model.save();
64
- }
65
-
66
- for (const email of emails) {
67
- Email.sendInternal(email, organization.i18n)
68
- }
69
-
70
- if (organization.meta.packages.isPaid) {
71
- // Already bought something: apply credit to other organization immediately
72
- const code = await UsedRegisterCode.getFor(organization.id)
73
- if (code && !code.creditId) {
74
- console.log("Rewarding code "+code.id+" for payment")
75
-
76
- // Deze code werd nog niet beloond
77
- await code.reward()
78
- }
79
- }
80
-
81
- return new Response(undefined);
82
- }
83
- }
84
-