@stamhoofd/backend 2.78.1 → 2.78.3

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.
@@ -0,0 +1,155 @@
1
+ import { Model } from '@simonbackx/simple-database';
2
+ import { Organization, Platform } from '@stamhoofd/models';
3
+ import { RecordSettings } from '@stamhoofd/structures';
4
+
5
+ export type RecordCacheEntry = { record: RecordSettings; rootCategoryId: string; organizationId: string | null };
6
+
7
+ /**
8
+ * Service that caches all available member records in the system.
9
+ * - It verified whether ids are unique system wide
10
+ * - It allows fast retrieving of record settings by id (for permission checking)
11
+ */
12
+ export class MemberRecordStore {
13
+ private static cache = new Map<string, RecordCacheEntry>();
14
+
15
+ constructor() {
16
+
17
+ }
18
+
19
+ static init() {
20
+ // Load initial data
21
+ this.loadIfNeeded().catch(console.error);
22
+
23
+ // Create listeners to update data as organizations and platform is updated
24
+ this.listen();
25
+ }
26
+
27
+ private static listening = false;
28
+
29
+ private static listen() {
30
+ // Create listeners to update data as organizations and platform is updated
31
+ if (this.listening) {
32
+ return;
33
+ }
34
+
35
+ this.listening = true;
36
+ Model.modelEventBus.addListener(this, async (event) => {
37
+ if (event.type === 'deleted') {
38
+ if (event.model instanceof Platform || event.model instanceof Organization) {
39
+ // Reload
40
+ this._didLoadAll = false;
41
+ this._loadAllPromise = null;
42
+ }
43
+ return;
44
+ }
45
+
46
+ if (event.model instanceof Platform) {
47
+ // Delete all records with organizationId = null
48
+ for (const [id, entry] of this.cache) {
49
+ if (entry.organizationId === null) {
50
+ this.cache.delete(id);
51
+ }
52
+ }
53
+
54
+ // Readd of this model
55
+ for (const recordCategory of event.model.config.recordsConfiguration.recordCategories) {
56
+ for (const record of recordCategory.getAllRecords()) {
57
+ // Add to cache
58
+ this.cache.set(record.id, {
59
+ record: record.clone(),
60
+ organizationId: null,
61
+ rootCategoryId: recordCategory.id,
62
+ });
63
+ }
64
+ }
65
+ }
66
+
67
+ if (event.model instanceof Organization) {
68
+ // Delete all records with organizationId = null
69
+ for (const [id, entry] of this.cache) {
70
+ if (entry.organizationId === event.model.id) {
71
+ this.cache.delete(id);
72
+ }
73
+ }
74
+
75
+ // Readd of this model
76
+ for (const recordCategory of event.model.meta.recordsConfiguration.recordCategories) {
77
+ for (const record of recordCategory.getAllRecords()) {
78
+ // Add to cache
79
+ this.cache.set(record.id, {
80
+ record: record.clone(),
81
+ organizationId: event.model.id,
82
+ rootCategoryId: recordCategory.id,
83
+ });
84
+ }
85
+ }
86
+ }
87
+ });
88
+ }
89
+
90
+ static _loadAllPromise: Promise<void> | null = null;
91
+ static _didLoadAll = false;
92
+
93
+ static async loadIfNeeded() {
94
+ if (this._didLoadAll) {
95
+ return;
96
+ }
97
+
98
+ if (this._loadAllPromise) {
99
+ await this._loadAllPromise;
100
+ }
101
+ else {
102
+ this._loadAllPromise = (async () => {
103
+ try {
104
+ await this.loadAll();
105
+ }
106
+ catch (e) {
107
+ // Failed to load
108
+ this._loadAllPromise = null;
109
+ throw e;
110
+ }
111
+ this._didLoadAll = true;
112
+ this._loadAllPromise = null;
113
+ })();
114
+ await this._loadAllPromise;
115
+ }
116
+ }
117
+
118
+ static async loadAll() {
119
+ this.cache = new Map<string, RecordCacheEntry>();
120
+
121
+ const platform = await Platform.getShared();
122
+ for (const recordCategory of platform.config.recordsConfiguration.recordCategories) {
123
+ for (const record of recordCategory.getAllRecords()) {
124
+ // Add to cache
125
+ this.cache.set(record.id, {
126
+ record: record.clone(),
127
+ organizationId: null,
128
+ rootCategoryId: recordCategory.id,
129
+ });
130
+ }
131
+ }
132
+
133
+ for await (const organization of Organization.select().all()) {
134
+ for (const recordCategory of organization.meta.recordsConfiguration.recordCategories) {
135
+ for (const record of recordCategory.getAllRecords()) {
136
+ if (this.cache.has(record.id)) {
137
+ console.error(`Duplicate record id ${record.id} found in organization ${organization.id} for record ${record.name} (${record.id}) in ${recordCategory.name}`);
138
+ continue;
139
+ }
140
+ // Add to cache
141
+ this.cache.set(record.id, {
142
+ record: record.clone(),
143
+ organizationId: organization.id,
144
+ rootCategoryId: recordCategory.id,
145
+ });
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ static async getRecord(id: string): Promise<RecordCacheEntry | null> {
152
+ await this.loadIfNeeded();
153
+ return this.cache.get(id) ?? null;
154
+ }
155
+ }
@@ -138,6 +138,9 @@ export class PlatformMembershipService {
138
138
 
139
139
  static async updateMembershipsForId(id: string, silent = false) {
140
140
  if (STAMHOOFD.userMode === 'organization') {
141
+ if (!silent) {
142
+ console.warn('Skipping automatic membership for: ' + id, ' - organization mode');
143
+ }
141
144
  return;
142
145
  }
143
146
 
@@ -161,6 +164,12 @@ export class PlatformMembershipService {
161
164
  .where('endDate', SQLWhereSign.GreaterEqual, new Date()) // Avoid updating the price of past periods that were not yet locked
162
165
  .fetch();
163
166
 
167
+ if (periods.length === 0) {
168
+ if (!silent) {
169
+ console.warn('No periods found for member ' + me.id);
170
+ }
171
+ }
172
+
164
173
  // Every (not-locked) period can have a generated membership
165
174
  for (const period of periods) {
166
175
  const registrations = me.registrations.filter(r => r.group.periodId === period.id && r.registeredAt && !r.deactivatedAt);
@@ -252,16 +261,16 @@ export class PlatformMembershipService {
252
261
  const tagIdsA = a.organization?.meta.tags ?? [];
253
262
  const tagIdsB = b.organization?.meta.tags ?? [];
254
263
  const aPrice = a.membership.getPrice(
255
- period.id,
256
- a.registration.startDate ?? a.registration.registeredAt ?? a.registration.createdAt,
257
- tagIdsA,
258
- shouldApplyReducedPrice
264
+ period.id,
265
+ a.registration.startDate ?? a.registration.registeredAt ?? a.registration.createdAt,
266
+ tagIdsA,
267
+ shouldApplyReducedPrice,
259
268
  ) ?? 10000000;
260
269
  const bPrice = b.membership.getPrice(
261
- period.id,
262
- b.registration.startDate ?? b.registration.registeredAt ?? b.registration.createdAt,
263
- tagIdsB,
264
- shouldApplyReducedPrice
270
+ period.id,
271
+ b.registration.startDate ?? b.registration.registeredAt ?? b.registration.createdAt,
272
+ tagIdsB,
273
+ shouldApplyReducedPrice,
265
274
  ) ?? 10000000;
266
275
 
267
276
  const diff = aPrice - bPrice;
@@ -6,7 +6,9 @@ import { GetMemberFamilyEndpoint } from '../../src/endpoints/global/members/GetM
6
6
  import { RegisterMembersEndpoint } from '../../src/endpoints/global/registration/RegisterMembersEndpoint';
7
7
  import { GetMemberBalanceEndpoint } from '../../src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint';
8
8
  import { GetReceivableBalanceEndpoint } from '../../src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint';
9
+ import { PlatformMembershipService } from '../../src/services/PlatformMembershipService';
9
10
  import { testServer } from '../helpers/TestServer';
11
+ import { TestUtils } from '@stamhoofd/test-utils';
10
12
 
11
13
  describe('E2E.Register', () => {
12
14
  // #region global
@@ -45,6 +47,11 @@ describe('E2E.Register', () => {
45
47
 
46
48
  // #endregion
47
49
 
50
+ beforeEach(async () => {
51
+ // These tests should run in platform mode
52
+ TestUtils.setEnvironment('userMode', 'platform');
53
+ });
54
+
48
55
  beforeAll(async () => {
49
56
  const previousPeriod = await new RegistrationPeriodFactory({
50
57
  startDate: new Date(2022, 0, 1),
@@ -108,7 +115,7 @@ describe('E2E.Register', () => {
108
115
  beforeEach(async () => {
109
116
  });
110
117
 
111
- describe('Register', () => {
118
+ describe('Register prices and balances', () => {
112
119
  test('Register by member should create balance for member', async () => {
113
120
  // #region arrange
114
121
  const { organization, group, groupPrice, token, member } = await initData();
@@ -150,6 +157,7 @@ describe('E2E.Register', () => {
150
157
  expect(balance.body.length).toBe(1);
151
158
  expect(balance.body[0].price).toBe(25);
152
159
  expect(balance.body[0].pricePaid).toBe(0);
160
+ expect(balance.body[0].pricePending).toBe(25);
153
161
  // #endregion
154
162
  });
155
163
 
@@ -622,23 +630,29 @@ describe('E2E.Register', () => {
622
630
  const balance = await getBalance(member.id, organization, token);
623
631
  expect(balance).toBeDefined();
624
632
  expect(balance.body.length).toBe(3);
625
- expect.arrayContaining([
633
+ expect(balance.body).toEqual(expect.arrayContaining([
626
634
  expect.objectContaining({
627
- price: 6,
635
+ unitPrice: 3,
636
+ amount: 2,
637
+ pricePending: 6,
628
638
  pricePaid: 0,
629
639
  status: BalanceItemStatus.Due,
630
640
  }),
631
641
  expect.objectContaining({
632
- price: 5,
642
+ unitPrice: 21,
633
643
  pricePaid: 0,
644
+ pricePending: 21,
645
+ amount: 1,
634
646
  status: BalanceItemStatus.Due,
635
647
  }),
636
648
  expect.objectContaining({
637
- price: 25,
649
+ unitPrice: 1,
638
650
  pricePaid: 0,
651
+ pricePending: 5,
652
+ amount: 5,
639
653
  status: BalanceItemStatus.Due,
640
654
  }),
641
- ]);
655
+ ]));
642
656
  // #endregion
643
657
  });
644
658
  });
@@ -726,18 +740,18 @@ describe('E2E.Register', () => {
726
740
  expect(balance).toBeDefined();
727
741
  expect(balance.body.length).toBe(2);
728
742
 
729
- expect.arrayContaining([
743
+ expect(balance.body).toEqual(expect.arrayContaining([
730
744
  expect.objectContaining({
731
- price: 25,
745
+ unitPrice: 25,
732
746
  pricePaid: 0,
733
747
  status: BalanceItemStatus.Canceled,
734
748
  }),
735
749
  expect.objectContaining({
736
- price: 30,
750
+ unitPrice: 30,
737
751
  pricePaid: 0,
738
752
  status: BalanceItemStatus.Due,
739
753
  }),
740
- ]);
754
+ ]));
741
755
  // #endregion
742
756
  });
743
757
 
@@ -860,28 +874,32 @@ describe('E2E.Register', () => {
860
874
  expect(balance).toBeDefined();
861
875
  expect(balance.body.length).toBe(4);
862
876
 
863
- expect.arrayContaining([
877
+ expect(balance.body).toEqual(expect.arrayContaining([
864
878
  expect.objectContaining({
865
- price: 10,
879
+ unitPrice: 5,
880
+ amount: 2,
866
881
  pricePaid: 0,
867
882
  status: BalanceItemStatus.Canceled,
868
883
  }),
869
884
  expect.objectContaining({
870
- price: 15,
885
+ unitPrice: 3,
871
886
  pricePaid: 0,
887
+ amount: 5,
872
888
  status: BalanceItemStatus.Canceled,
873
889
  }),
874
890
  expect.objectContaining({
875
- price: 25,
891
+ unitPrice: 25,
876
892
  pricePaid: 0,
893
+ amount: 1,
877
894
  status: BalanceItemStatus.Canceled,
878
895
  }),
879
896
  expect.objectContaining({
880
- price: 30,
897
+ unitPrice: 30,
881
898
  pricePaid: 0,
899
+ amount: 1,
882
900
  status: BalanceItemStatus.Due,
883
901
  }),
884
- ]);
902
+ ]));
885
903
  // #endregion
886
904
  });
887
905
 
@@ -968,24 +986,32 @@ describe('E2E.Register', () => {
968
986
  expect(balance).toBeDefined();
969
987
  expect(balance.body.length).toBe(3);
970
988
 
971
- expect.arrayContaining([
989
+ expect(balance.body).toEqual(expect.arrayContaining([
972
990
  expect.objectContaining({
973
- price: 25,
991
+ unitPrice: 25,
974
992
  pricePaid: 0,
993
+ pricePending: 0,
994
+ amount: 1,
975
995
  status: BalanceItemStatus.Canceled,
996
+ type: BalanceItemType.Registration,
976
997
  }),
977
998
  expect.objectContaining({
978
- price: 30,
999
+ unitPrice: 30,
979
1000
  pricePaid: 0,
1001
+ pricePending: 0,
1002
+ amount: 1,
980
1003
  status: BalanceItemStatus.Due,
1004
+ type: BalanceItemType.Registration,
981
1005
  }),
982
1006
  expect.objectContaining({
983
- price: 5,
1007
+ unitPrice: 5,
984
1008
  pricePaid: 0,
1009
+ pricePending: 0,
1010
+ amount: 1,
985
1011
  type: BalanceItemType.CancellationFee,
986
1012
  status: BalanceItemStatus.Due,
987
1013
  }),
988
- ]);
1014
+ ]));
989
1015
  // #endregion
990
1016
  });
991
1017
  });
@@ -1076,6 +1102,8 @@ describe('E2E.Register', () => {
1076
1102
  expect(response.body).toBeDefined();
1077
1103
  expect(response.body.registrations.length).toBe(1);
1078
1104
 
1105
+ await PlatformMembershipService.updateMembershipsForId(member.id, false);
1106
+
1079
1107
  const familyAfter = await getMemberFamily(member.id, organization, token);
1080
1108
  expect(familyAfter).toBeDefined();
1081
1109
  expect(familyAfter.body.members.length).toBe(1);
@@ -24,6 +24,10 @@ export class StripeMocker {
24
24
  }
25
25
 
26
26
  start() {
27
+ if (!STAMHOOFD.STRIPE_SECRET_KEY || !STAMHOOFD.STRIPE_SECRET_KEY.startsWith('sk_test_')) {
28
+ throw new Error('Invalid STRIPE_SECRET_KEY. Even in test mode it should start with sk_test_');
29
+ }
30
+
27
31
  nock('https://api.stripe.com')
28
32
  .persist()
29
33
  .get(/v1\/.*/)
@@ -162,8 +166,8 @@ export class StripeMocker {
162
166
 
163
167
  const r = Request.buildJson('POST', `/stripe/webhooks`, undefined, payload);
164
168
  r.headers['stripe-signature'] = stripe.webhooks.generateTestHeaderString({
165
- payload: JSON.stringify(payload),
166
- secret: STAMHOOFD.STRIPE_ENDPOINT_SECRET,
169
+ payload: await r.body,
170
+ secret: payload.account ? STAMHOOFD.STRIPE_CONNECT_ENDPOINT_SECRET : STAMHOOFD.STRIPE_ENDPOINT_SECRET,
167
171
  });
168
172
  await testServer.test(endpoint, r);
169
173
  }
@@ -206,6 +210,7 @@ export class StripeMocker {
206
210
 
207
211
  stop() {
208
212
  nock.cleanAll();
213
+ nock.disableNetConnect();
209
214
  }
210
215
  }
211
216
 
@@ -2,10 +2,13 @@ import backendEnv from '@stamhoofd/backend-env';
2
2
  backendEnv.load({ path: __dirname + '/../../.env.test.json' });
3
3
 
4
4
  import { Database, Migration } from '@simonbackx/simple-database';
5
+ import * as jose from 'jose';
6
+ import nock from 'nock';
5
7
  import path from 'path';
8
+ import { GlobalHelper } from '../src/helpers/GlobalHelper';
6
9
  const emailPath = require.resolve('@stamhoofd/email');
7
10
  const modelsPath = require.resolve('@stamhoofd/models');
8
- import nock from 'nock';
11
+ import { TestUtils } from '@stamhoofd/test-utils';
9
12
 
10
13
  // Disable network requests
11
14
  nock.disableNetConnect();
@@ -27,4 +30,6 @@ export default async () => {
27
30
  await Migration.runAll(__dirname + '/src/migrations');
28
31
 
29
32
  await Database.end();
33
+
34
+ await TestUtils.globalSetup();
30
35
  };
@@ -9,6 +9,7 @@ import { sleep } from '@stamhoofd/utility';
9
9
  import nock from 'nock';
10
10
  import { GlobalHelper } from '../src/helpers/GlobalHelper';
11
11
  import * as jose from 'jose';
12
+ import { TestUtils } from '@stamhoofd/test-utils';
12
13
 
13
14
  // Set version of saved structures
14
15
  Column.setJSONVersion(Version);
@@ -30,6 +31,9 @@ if (new Date().getTimezoneOffset() !== 0) {
30
31
  console.log = jest.fn();
31
32
 
32
33
  beforeAll(async () => {
34
+ nock.cleanAll();
35
+ nock.disableNetConnect();
36
+
33
37
  await Database.delete('DELETE FROM `tokens`');
34
38
  await Database.delete('DELETE FROM `users`');
35
39
  await Database.delete('DELETE FROM `registrations`');
@@ -37,6 +41,8 @@ beforeAll(async () => {
37
41
  await Database.delete('DELETE FROM `postal_codes`');
38
42
  await Database.delete('DELETE FROM `cities`');
39
43
  await Database.delete('DELETE FROM `provinces`');
44
+ await Database.delete('DELETE FROM `email_recipients`');
45
+ await Database.delete('DELETE FROM `emails`');
40
46
 
41
47
  await Database.delete('DELETE FROM `webshop_orders`');
42
48
  await Database.delete('DELETE FROM `webshops`');
@@ -57,8 +63,8 @@ beforeAll(async () => {
57
63
  const exportedPublicKey = await jose.exportJWK(publicKey);
58
64
  const exportedPrivateKey = await jose.exportJWK(privateKey);
59
65
 
60
- (STAMHOOFD as any).FILE_SIGNING_PUBLIC_KEY = exportedPublicKey;
61
- (STAMHOOFD as any).FILE_SIGNING_PRIVATE_KEY = exportedPrivateKey;
66
+ TestUtils.setPermanentEnvironment('FILE_SIGNING_PUBLIC_KEY', exportedPublicKey);
67
+ TestUtils.setPermanentEnvironment('FILE_SIGNING_PRIVATE_KEY', exportedPrivateKey);
62
68
 
63
69
  await GlobalHelper.load();
64
70
  });
@@ -71,3 +77,5 @@ afterAll(async () => {
71
77
  }
72
78
  await Database.end();
73
79
  });
80
+
81
+ TestUtils.setup();