@stamhoofd/backend 2.18.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.
@@ -58,7 +58,7 @@
58
58
  "LATEST_IOS_VERSION": 0,
59
59
  "LATEST_ANDROID_VERSION": 0,
60
60
 
61
- "NOLT_SSO_SECRET_KEY": "",
61
+ "NOLT_SSO_SECRET_KEY": "optional",
62
62
  "INTERNAL_SECRET_KEY": "",
63
63
  "CRONS_DISABLED": false,
64
64
  "WHITELISTED_EMAIL_DESTINATIONS": ["*"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.18.0",
3
+ "version": "2.20.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -39,10 +39,10 @@
39
39
  "@stamhoofd/backend-i18n": "^2.17.0",
40
40
  "@stamhoofd/backend-middleware": "^2.17.0",
41
41
  "@stamhoofd/email": "^2.17.0",
42
- "@stamhoofd/models": "^2.18.0",
42
+ "@stamhoofd/models": "^2.20.0",
43
43
  "@stamhoofd/queues": "^2.17.3",
44
- "@stamhoofd/sql": "^2.18.0",
45
- "@stamhoofd/structures": "^2.18.0",
44
+ "@stamhoofd/sql": "^2.20.0",
45
+ "@stamhoofd/structures": "^2.20.0",
46
46
  "@stamhoofd/utility": "^2.17.0",
47
47
  "archiver": "^7.0.1",
48
48
  "aws-sdk": "^2.885.0",
@@ -60,5 +60,5 @@
60
60
  "postmark": "4.0.2",
61
61
  "stripe": "^16.6.0"
62
62
  },
63
- "gitHead": "a47aa5bb6ad71d1e6a2f88bc203caa5acf4f497f"
63
+ "gitHead": "946c0ddcaa7cdc9b769d4dc9083a517fe7ad19f8"
64
64
  }
package/src/crons.ts CHANGED
@@ -1,21 +1,14 @@
1
1
  import { Database } from '@simonbackx/simple-database';
2
2
  import { logger, StyledText } from "@simonbackx/simple-logging";
3
3
  import { I18n } from '@stamhoofd/backend-i18n';
4
- import { Email } from '@stamhoofd/email';
5
- import { EmailAddress } from '@stamhoofd/email';
6
- import { Group, STPackage, Webshop } from '@stamhoofd/models';
7
- import { Organization } from '@stamhoofd/models';
8
- import { Payment } from '@stamhoofd/models';
9
- import { Registration } from '@stamhoofd/models';
10
- import { STInvoice } from '@stamhoofd/models';
11
- import { STPendingInvoice } from '@stamhoofd/models';
4
+ import { Email, EmailAddress } from '@stamhoofd/email';
5
+ import { Group, Organization, Payment, Registration, STPackage, STPendingInvoice, Webshop } from '@stamhoofd/models';
12
6
  import { QueueHandler } from '@stamhoofd/queues';
13
7
  import { PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
14
8
  import { Formatter, sleep } from '@stamhoofd/utility';
15
9
  import AWS from 'aws-sdk';
16
10
  import { DateTime } from 'luxon';
17
11
 
18
- import { ExchangeSTPaymentEndpoint } from './endpoints/global/payments/ExchangeSTPaymentEndpoint';
19
12
  import { ExchangePaymentEndpoint } from './endpoints/organization/shared/ExchangePaymentEndpoint';
20
13
  import { checkSettlements } from './helpers/CheckSettlements';
21
14
  import { ForwardHandler } from './helpers/ForwardHandler';
@@ -471,12 +464,7 @@ async function checkPayments() {
471
464
  continue;
472
465
  }
473
466
  } else {
474
- // Try stamhoofd payment
475
- const invoices = await STInvoice.where({ paymentId: payment.id })
476
- if (invoices.length === 1) {
477
- await ExchangeSTPaymentEndpoint.pollStatus(payment, invoices[0])
478
- continue
479
- }
467
+ // deprecated
480
468
  }
481
469
 
482
470
  // Check expired
@@ -599,52 +587,6 @@ async function checkReservedUntil() {
599
587
  }
600
588
  }
601
589
 
602
-
603
- // Wait for midnight before checking billing
604
- let lastBillingCheck: Date | null = new Date()
605
- let lastBillingId = ""
606
- async function checkBilling() {
607
- if (STAMHOOFD.environment === "development") {
608
- return
609
- }
610
-
611
- console.log("[BILLING] Checking billing...")
612
-
613
- // Wait for the next day before doing a new check
614
- if (lastBillingCheck && Formatter.dateIso(lastBillingCheck) === Formatter.dateIso(new Date())) {
615
- console.log("[BILLING] Billing check done for today")
616
- return
617
- }
618
-
619
- const organizations = await Organization.where({ id: { sign: '>', value: lastBillingId } }, {
620
- limit: 10,
621
- sort: ["id"]
622
- })
623
-
624
- if (organizations.length == 0) {
625
- // Wait again until next day
626
- lastBillingId = ""
627
- lastBillingCheck = new Date()
628
- return
629
- }
630
-
631
- for (const organization of organizations) {
632
- console.log("[BILLING] Checking billing for "+organization.name)
633
-
634
- try {
635
- await QueueHandler.schedule("billing/invoices-"+organization.id, async () => {
636
- await STPendingInvoice.addAutomaticItems(organization)
637
- });
638
- } catch (e) {
639
- console.error(e)
640
- }
641
-
642
- }
643
-
644
- lastBillingId = organizations[organizations.length - 1].id
645
-
646
- }
647
-
648
590
  let lastDripCheck: Date | null = null
649
591
  let lastDripId = ""
650
592
  async function checkDrips() {
@@ -723,12 +665,6 @@ registeredCronJobs.push({
723
665
  running: false
724
666
  });
725
667
 
726
- registeredCronJobs.push({
727
- name: 'checkBilling',
728
- method: checkBilling,
729
- running: false
730
- });
731
-
732
668
  registeredCronJobs.push({
733
669
  name: 'checkReservedUntil',
734
670
  method: checkReservedUntil,
@@ -0,0 +1,49 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { QueueHandler } from '@stamhoofd/queues';
3
+ import { sleep } from '@stamhoofd/utility';
4
+ import { Context } from '../../../helpers/Context';
5
+ import { MembershipCharger } from '../../../helpers/MembershipCharger';
6
+ import { SimpleError } from '@simonbackx/simple-errors';
7
+
8
+
9
+ type Params = Record<string, never>;
10
+ type Query = Record<string, never>;
11
+ type Body = undefined;
12
+ type ResponseBody = undefined;
13
+
14
+ export class ChargeMembershipsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
+ protected doesMatch(request: Request): [true, Params] | [false] {
16
+ if (request.method != "POST") {
17
+ return [false];
18
+ }
19
+
20
+ const params = Endpoint.parseParameters(request.url, "/admin/charge-memberships", {});
21
+
22
+ if (params) {
23
+ return [true, params as Params];
24
+ }
25
+ return [false];
26
+ }
27
+
28
+ async handle(request: DecodedRequest<Params, Query, Body>) {
29
+ await Context.authenticate()
30
+
31
+ if (!Context.auth.hasPlatformFullAccess()) {
32
+ throw Context.auth.error()
33
+ }
34
+
35
+ if (QueueHandler.isRunning('charge-memberships')) {
36
+ throw new SimpleError({
37
+ code: 'charge_pending',
38
+ message: 'Charge already pending',
39
+ human: 'Er is al een aanrekening bezig, even geduld.'
40
+ })
41
+ }
42
+
43
+ QueueHandler.schedule('charge-memberships', async () => {
44
+ await MembershipCharger.charge()
45
+ }).catch(console.error);
46
+
47
+ return new Response(undefined);
48
+ }
49
+ }
@@ -2,6 +2,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
2
2
  import { SQL, SQLAlias, SQLCount, SQLDistinct, SQLSelectAs, SQLSum } from '@stamhoofd/sql';
3
3
  import { ChargeMembershipsSummary, ChargeMembershipsTypeSummary } from '@stamhoofd/structures';
4
4
  import { Context } from '../../../helpers/Context';
5
+ import { QueueHandler } from '@stamhoofd/queues';
5
6
 
6
7
 
7
8
  type Params = Record<string, never>;
@@ -29,6 +30,14 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
29
30
  if (!Context.auth.hasPlatformFullAccess()) {
30
31
  throw Context.auth.error()
31
32
  }
33
+
34
+ if (QueueHandler.isRunning('charge-memberships')) {
35
+ return new Response(
36
+ ChargeMembershipsSummary.create({
37
+ running: true
38
+ })
39
+ );
40
+ }
32
41
 
33
42
  const query = SQL
34
43
  .select(
@@ -74,6 +83,7 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
74
83
 
75
84
  return new Response(
76
85
  ChargeMembershipsSummary.create({
86
+ running: false,
77
87
  memberships: memberships ?? 0,
78
88
  members: members ?? 0,
79
89
  price: price ?? 0,
@@ -9,6 +9,7 @@ import { Formatter } from '@stamhoofd/utility';
9
9
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
10
10
  import { Context } from '../../../helpers/Context';
11
11
  import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
12
+ import { SetupStepUpdater } from '../../../helpers/SetupStepsUpdater';
12
13
 
13
14
  type Params = Record<string, never>;
14
15
  type Query = undefined;
@@ -150,6 +151,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
150
151
  await MemberUserSyncer.onChangeMember(member)
151
152
  }
152
153
 
154
+ let shouldUpdateSetupSteps = false;
155
+
153
156
  // Loop all members one by one
154
157
  for (let patch of request.body.getPatches()) {
155
158
  const member = members.find(m => m.id === patch.id) ?? await Member.getWithRegistrations(patch.id)
@@ -223,6 +226,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
223
226
  }
224
227
 
225
228
  await responsibilityRecord.save()
229
+ shouldUpdateSetupSteps = true;
226
230
  }
227
231
 
228
232
  // Create responsibilities
@@ -323,6 +327,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
323
327
  model.startDate = put.startDate
324
328
 
325
329
  await model.save()
330
+ shouldUpdateSetupSteps = true;
326
331
  }
327
332
 
328
333
  // Auto link users based on data
@@ -443,7 +448,6 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
443
448
  updateMembershipMemberIds.add(member.id)
444
449
  }
445
450
 
446
-
447
451
  if (!members.find(m => m.id === member.id)) {
448
452
  members.push(member)
449
453
  }
@@ -460,6 +464,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
460
464
  await User.deleteForDeletedMember(member.id)
461
465
  await BalanceItem.deleteForDeletedMember(member.id)
462
466
  await member.delete()
467
+ shouldUpdateSetupSteps = true
463
468
 
464
469
  // Update occupancy of this member because we removed registrations
465
470
  const groupIds = member.registrations.flatMap(r => r.groupId)
@@ -498,6 +503,10 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
498
503
  }
499
504
  }
500
505
 
506
+ if(shouldUpdateSetupSteps && organization) {
507
+ SetupStepUpdater.updateForOrganization(organization).catch(console.error);
508
+ }
509
+
501
510
  return new Response(
502
511
  await AuthenticatedStructures.membersBlob(members)
503
512
  );
@@ -1,15 +1,10 @@
1
1
 
2
2
  import { Request } from "@simonbackx/simple-endpoints";
3
- import { Organization, OrganizationFactory, RegisterCodeFactory, STCredit } from "@stamhoofd/models";
4
3
  import { Address, Country, CreateOrganization, NewUser, Organization as OrganizationStruct, Version } from "@stamhoofd/structures";
5
4
 
6
5
  import { testServer } from "../../../../tests/helpers/TestServer";
7
6
  import { CreateOrganizationEndpoint } from "./CreateOrganizationEndpoint";
8
7
 
9
- function expect_toBeDefined<T>(arg: T): asserts arg is NonNullable<T> {
10
- expect(arg).toBeDefined();
11
- }
12
-
13
8
  describe("Endpoint.CreateOrganization", () => {
14
9
  // Test endpoint
15
10
  const endpoint = new CreateOrganizationEndpoint();
@@ -60,46 +55,4 @@ describe("Endpoint.CreateOrganization", () => {
60
55
 
61
56
  await expect(testServer.test(endpoint, r)).rejects.toThrow(/name/);
62
57
  });
63
-
64
- test("Can create an organization with a register code and apply the discount", async () => {
65
- const otherOrganization = await new OrganizationFactory({}).create();
66
- const code = await new RegisterCodeFactory({organization: otherOrganization}).create();
67
- const uri = 'my-organization-with-a-discount';
68
-
69
- const r = Request.buildJson(
70
- "POST",
71
- "/organizations",
72
- "todo-host.be",
73
- CreateOrganization.create({
74
- organization: OrganizationStruct.create({
75
- name: "My organization with a discount",
76
- uri,
77
- address: Address.create({
78
- street: "My street",
79
- number: "1",
80
- postalCode: "9000",
81
- city: "Gent",
82
- country: Country.Belgium
83
- }),
84
- }),
85
- user: NewUser.create({
86
- email: "voorbeeld@stamhoofd.be",
87
- password: "My user password",
88
- }),
89
- registerCode: code.code
90
- })
91
- );
92
-
93
- const response = await testServer.test(endpoint, r);
94
- expect(response.body.token).not.toBeEmpty();
95
-
96
- const organization = await Organization.getByURI(uri);
97
- expect_toBeDefined(organization);
98
-
99
- // Check if this organization has an open register code
100
- const credits = await STCredit.getForOrganization(organization.id);
101
- expect(credits.length).toBe(1);
102
- expect(credits[0].change).toBe(code.value);
103
- });
104
-
105
58
  });
@@ -1,9 +1,7 @@
1
- import { Model } from '@simonbackx/simple-database';
2
1
  import { Decoder } from '@simonbackx/simple-encoding';
3
2
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
4
3
  import { SimpleError } from '@simonbackx/simple-errors';
5
- import { Email, EmailInterfaceBase } from '@stamhoofd/email';
6
- import { EmailVerificationCode, Organization, RegisterCode, User } from '@stamhoofd/models';
4
+ import { EmailVerificationCode, Organization, User } from '@stamhoofd/models';
7
5
  import { CreateOrganization, PermissionLevel, Permissions, SignupResponse, UserPermissions } from "@stamhoofd/structures";
8
6
  import { Formatter } from "@stamhoofd/utility";
9
7
 
@@ -83,14 +81,14 @@ export class CreateOrganizationEndpoint extends Endpoint<Params, Query, Body, Re
83
81
  organization.name = request.body.organization.name;
84
82
 
85
83
  // Delay save until after organization is saved, but do validations before the organization is saved
86
- let registerCodeModels: Model[] = []
87
- let delayEmails: EmailInterfaceBase[] = []
84
+ // let registerCodeModels: Model[] = []
85
+ // let delayEmails: EmailInterfaceBase[] = []
88
86
 
89
- if (request.body.registerCode) {
90
- const applied = await RegisterCode.applyRegisterCode(organization, request.body.registerCode)
91
- registerCodeModels = applied.models
92
- delayEmails = applied.emails
93
- }
87
+ //if (request.body.registerCode) {
88
+ // const applied = await RegisterCode.applyRegisterCode(organization, request.body.registerCode)
89
+ // registerCodeModels = applied.models
90
+ // delayEmails = applied.emails
91
+ //}
94
92
 
95
93
  organization.uri = uri;
96
94
  organization.meta = request.body.organization.meta
@@ -126,16 +124,16 @@ export class CreateOrganizationEndpoint extends Endpoint<Params, Query, Body, Re
126
124
  user.permissions.organizationPermissions.set(organization.id, Permissions.create({ level: PermissionLevel.Full }))
127
125
  await user.save()
128
126
 
129
- for (const model of registerCodeModels) {
130
- await model.save()
131
- }
127
+ // for (const model of registerCodeModels) {
128
+ // await model.save()
129
+ // }
132
130
 
133
131
  const code = await EmailVerificationCode.createFor(user, user.email)
134
132
  code.send(user, organization, request.i18n)
135
133
 
136
- for (const email of delayEmails) {
137
- Email.sendInternal(email, organization.i18n)
138
- }
134
+ // for (const email of delayEmails) {
135
+ // Email.sendInternal(email, organization.i18n)
136
+ // }
139
137
 
140
138
  return new Response(SignupResponse.create({
141
139
  token: code.token
@@ -45,7 +45,7 @@ export class SearchOrganizationEndpoint extends Endpoint<Params, Query, Body, Re
45
45
  };
46
46
 
47
47
  // We had to add an order by in the query to fix the limit. MySQL doesn't want to limit the results correctly if we don't explicitly sort the results on their relevance
48
- const organizations = await Organization.where({ searchIndex: match }, {
48
+ const organizations = await Organization.where({ searchIndex: match, active: 1 }, {
49
49
  limit: 15,
50
50
  sort: [
51
51
  {