@stamhoofd/backend 2.19.0 → 2.21.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 (22) hide show
  1. package/.env.template.json +1 -1
  2. package/package.json +6 -6
  3. package/src/crons.ts +3 -67
  4. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +33 -29
  5. package/src/endpoints/global/files/ExportToExcelEndpoint.ts +3 -8
  6. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +10 -1
  7. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +0 -47
  8. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +14 -16
  9. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +8 -0
  10. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +1 -1
  11. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +171 -36
  12. package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +2 -1
  13. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
  14. package/src/endpoints/organization/dashboard/registration-periods/SetupStepReviewEndpoint.ts +5 -2
  15. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +28 -32
  16. package/src/helpers/AdminPermissionChecker.ts +22 -5
  17. package/src/helpers/AuthenticatedStructures.ts +3 -2
  18. package/src/helpers/SetupStepsUpdater.ts +115 -13
  19. package/src/endpoints/global/payments/ExchangeSTPaymentEndpoint.ts +0 -153
  20. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.test.ts +0 -64
  21. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.ts +0 -84
  22. package/src/endpoints/organization/dashboard/organization/GetRegisterCodeEndpoint.ts +0 -65
@@ -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
-
@@ -1,65 +0,0 @@
1
- import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { Organization, RegisterCode, STCredit, UsedRegisterCode } from '@stamhoofd/models';
3
- import { RegisterCodeStatus, UsedRegisterCode as UsedRegisterCodeStruct } from '@stamhoofd/structures';
4
-
5
- import { Context } from '../../../../helpers/Context';
6
-
7
- type Params = Record<string, never>;
8
- type Query = undefined;
9
- type Body = undefined;
10
- type ResponseBody = RegisterCodeStatus;
11
-
12
- export class GetRegisterCodeEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
13
-
14
- protected doesMatch(request: Request): [true, Params] | [false] {
15
- if (request.method != "GET") {
16
- return [false];
17
- }
18
-
19
- const params = Endpoint.parseParameters(request.url, "/register-code", {});
20
-
21
- if (params) {
22
- return [true, params as Params];
23
- }
24
- return [false];
25
- }
26
-
27
- async handle(_: DecodedRequest<Params, Query, Body>) {
28
- const organization = await Context.setOrganizationScope();
29
- await Context.authenticate()
30
-
31
- if (!await Context.auth.hasSomeAccess(organization.id)) {
32
- throw Context.auth.error()
33
- }
34
-
35
- const codes = await RegisterCode.where({ organizationId: organization.id })
36
- let code = codes[0]
37
-
38
- if (codes.length == 0) {
39
- code = new RegisterCode()
40
- code.organizationId = organization.id
41
- code.description = "Doorverwezen door "+ organization.name
42
- code.value = 2500
43
- await code.generateCode()
44
- await code.save()
45
- }
46
-
47
- const usedCodes = await UsedRegisterCode.getAll(code.code)
48
- const allOrganizations = await Organization.getByIDs(...usedCodes.flatMap(u => u.organizationId ? [u.organizationId] : []))
49
- const allCredits = await STCredit.getByIDs(...usedCodes.flatMap(u => u.creditId ? [u.creditId] : []))
50
-
51
- return new Response(RegisterCodeStatus.create({
52
- code: code.code,
53
- value: code.value,
54
- invoiceValue: code.invoiceValue,
55
- usedCodes: usedCodes.map(c => {
56
- return UsedRegisterCodeStruct.create({
57
- id: c.id,
58
- organizationName: allOrganizations.find(o => o.id === c.organizationId)?.name ?? "Onbekend",
59
- createdAt: c.createdAt,
60
- creditValue: (c.creditId ? allCredits.find(credit => credit.id === c.creditId)?.change : null) ?? null
61
- })
62
- })
63
- }))
64
- }
65
- }