@stamhoofd/backend 2.110.0 → 2.112.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 (51) hide show
  1. package/LICENSE.md +32 -0
  2. package/package.json +15 -12
  3. package/src/boot.ts +1 -0
  4. package/src/email-recipient-loaders/documents.ts +66 -0
  5. package/src/endpoints/auth/PatchUserEndpoint.test.ts +56 -0
  6. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +701 -4
  7. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +21 -10
  8. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +661 -4
  9. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +17 -6
  10. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +291 -8
  11. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +22 -0
  12. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.ts +16 -18
  13. package/src/endpoints/organization/dashboard/invoices/GetInvoicesCountEndpoint.ts +43 -0
  14. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +219 -0
  15. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +8 -8
  16. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +2 -2
  17. package/src/endpoints/organization/shared/GetUitpasNumberDetailsEndpoint.ts +72 -0
  18. package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +3 -2
  19. package/src/excel-loaders/members.ts +27 -27
  20. package/src/helpers/AdminPermissionChecker.ts +30 -10
  21. package/src/helpers/AuthenticatedStructures.ts +24 -5
  22. package/src/helpers/StripeHelper.ts +11 -1
  23. package/src/helpers/StripePayoutChecker.ts +7 -0
  24. package/src/helpers/UitpasTokenRepository.ts +7 -5
  25. package/src/helpers/passthroughFetch.ts +24 -0
  26. package/src/helpers/updateMemberDetailsUitpasNumber.ts +149 -0
  27. package/src/seeds/data/default-email-templates.sql +2 -1
  28. package/src/seeds/wip/1769088653-uitpas-status.ts +129 -0
  29. package/src/services/InvoiceService.ts +114 -0
  30. package/src/services/uitpas/PassholderEndpoints.ts +190 -0
  31. package/src/services/uitpas/UitpasService.ts +37 -12
  32. package/src/services/uitpas/checkUitpasNumbers.ts +16 -140
  33. package/src/services/uitpas/handleUitpasResponse.ts +89 -0
  34. package/src/sql-filters/invoiced-balance-items.ts +20 -0
  35. package/src/sql-filters/invoices.ts +122 -0
  36. package/src/sql-filters/payments.ts +11 -1
  37. package/src/sql-sorters/invoices.ts +83 -0
  38. package/src/sql-sorters/payments.ts +33 -0
  39. package/tests/e2e/bundle-discounts.test.ts +8 -8
  40. package/tests/e2e/tests-disable-net-connect.test.ts +5 -0
  41. package/tests/helpers/StripeMocker.ts +5 -5
  42. package/tests/helpers/UitpasApiMocker.ts +175 -0
  43. package/tests/helpers/index.ts +1 -0
  44. package/tests/helpers/resetNock.ts +7 -0
  45. package/tests/init/index.ts +1 -0
  46. package/tests/init/initPayconiq.ts +2 -2
  47. package/tests/init/initStripe.ts +1 -1
  48. package/tests/init/initUitpasApi.ts +14 -0
  49. package/tests/jest.global.setup.ts +6 -4
  50. package/tests/jest.setup.ts +12 -6
  51. package/LICENSE +0 -665
@@ -0,0 +1,219 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { SimpleError } from '@simonbackx/simple-errors';
4
+ import { Invoice } from '@stamhoofd/models';
5
+ import { applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
6
+ import { CountFilteredRequest, InvoiceStruct, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
7
+
8
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
9
+ import { Context } from '../../../../helpers/Context.js';
10
+ import { invoiceFilterCompilers } from '../../../../sql-filters/invoices.js';
11
+ import { invoiceSorters } from '../../../../sql-sorters/invoices.js';
12
+
13
+ type Params = Record<string, never>;
14
+ type Query = LimitedFilteredRequest;
15
+ type Body = undefined;
16
+ type ResponseBody = PaginatedResponse<InvoiceStruct[], LimitedFilteredRequest>;
17
+
18
+ const filterCompilers = invoiceFilterCompilers;
19
+ const sorters = invoiceSorters;
20
+
21
+ export class GetInvoicesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
23
+
24
+ protected doesMatch(request: Request): [true, Params] | [false] {
25
+ if (request.method !== 'GET') {
26
+ return [false];
27
+ }
28
+
29
+ const params = Endpoint.parseParameters(request.url, '/invoices', {});
30
+
31
+ if (params) {
32
+ return [true, params as Params];
33
+ }
34
+ return [false];
35
+ }
36
+
37
+ static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
38
+ const organization = Context.organization;
39
+ let scopeFilter: StamhoofdFilter | undefined = undefined;
40
+
41
+ if (!organization) {
42
+ throw Context.auth.error();
43
+ }
44
+
45
+ if (!await Context.auth.canManageFinances(organization.id)) {
46
+ throw Context.auth.error();
47
+ }
48
+
49
+ scopeFilter = {
50
+ organizationId: organization.id,
51
+ };
52
+
53
+ const query = Invoice
54
+ .select()
55
+ .setMaxExecutionTime(10 * 1000);
56
+
57
+ if (scopeFilter) {
58
+ query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
59
+ }
60
+
61
+ if (q.filter) {
62
+ query.where(await compileToSQLFilter(q.filter, filterCompilers));
63
+ }
64
+
65
+ if (q.search) {
66
+ // todo
67
+
68
+ let searchFilter: StamhoofdFilter | null = null;
69
+ searchFilter = {
70
+ $or: [
71
+ {
72
+ customer: {
73
+ name: {
74
+ $contains: q.search,
75
+ },
76
+ },
77
+ },
78
+ {
79
+ customer: {
80
+ company: {
81
+ name: {
82
+ $contains: q.search,
83
+ },
84
+ },
85
+ },
86
+ },
87
+ {
88
+ items: {
89
+ $elemMatch: {
90
+ $or: [
91
+ {
92
+ name: {
93
+ $contains: q.search,
94
+ },
95
+ },
96
+ {
97
+ description: {
98
+ $contains: q.search,
99
+ },
100
+ },
101
+ ],
102
+ },
103
+ },
104
+ },
105
+ ],
106
+ };
107
+
108
+ if (q.search.includes('@')) {
109
+ searchFilter = {
110
+ $or: [
111
+ {
112
+ customer: {
113
+ email: {
114
+ $contains: q.search,
115
+ },
116
+ },
117
+ },
118
+ {
119
+ customer: {
120
+ company: {
121
+ administrationEmail: {
122
+ $contains: q.search,
123
+ },
124
+ },
125
+ },
126
+ },
127
+ ],
128
+ };
129
+ }
130
+
131
+ if (searchFilter) {
132
+ query.where(await compileToSQLFilter(searchFilter, filterCompilers));
133
+ }
134
+ }
135
+
136
+ if (q instanceof LimitedFilteredRequest) {
137
+ if (q.pageFilter) {
138
+ query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
139
+ }
140
+
141
+ q.sort = assertSort(q.sort, [{ key: 'id' }]);
142
+ applySQLSorter(query, q.sort, sorters);
143
+ query.limit(q.limit);
144
+ }
145
+
146
+ return query;
147
+ }
148
+
149
+ static async buildData(requestQuery: LimitedFilteredRequest) {
150
+ const query = await this.buildQuery(requestQuery);
151
+ let invoices: Invoice[];
152
+
153
+ try {
154
+ invoices = await query.fetch();
155
+ }
156
+ catch (error) {
157
+ if (error.message.includes('ER_QUERY_TIMEOUT')) {
158
+ throw new SimpleError({
159
+ code: 'timeout',
160
+ message: 'Query took too long',
161
+ human: $t(`dce51638-6129-448b-8a15-e6d778f3a76a`),
162
+ });
163
+ }
164
+ throw error;
165
+ }
166
+
167
+ let next: LimitedFilteredRequest | undefined;
168
+
169
+ if (invoices.length >= requestQuery.limit) {
170
+ const lastObject = invoices[invoices.length - 1];
171
+ const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
172
+
173
+ next = new LimitedFilteredRequest({
174
+ filter: requestQuery.filter,
175
+ pageFilter: nextFilter,
176
+ sort: requestQuery.sort,
177
+ limit: requestQuery.limit,
178
+ search: requestQuery.search,
179
+ });
180
+
181
+ if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
182
+ console.error('Found infinite loading loop for', requestQuery);
183
+ next = undefined;
184
+ }
185
+ }
186
+
187
+ return new PaginatedResponse<InvoiceStruct[], LimitedFilteredRequest>({
188
+ results: await AuthenticatedStructures.invoices(invoices),
189
+ next,
190
+ });
191
+ }
192
+
193
+ async handle(request: DecodedRequest<Params, Query, Body>) {
194
+ await Context.setOrganizationScope();
195
+ await Context.authenticate();
196
+
197
+ const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
198
+
199
+ if (request.query.limit > maxLimit) {
200
+ throw new SimpleError({
201
+ code: 'invalid_field',
202
+ field: 'limit',
203
+ message: 'Limit can not be more than ' + maxLimit,
204
+ });
205
+ }
206
+
207
+ if (request.query.limit < 1) {
208
+ throw new SimpleError({
209
+ code: 'invalid_field',
210
+ field: 'limit',
211
+ message: 'Limit can not be less than 1',
212
+ });
213
+ }
214
+
215
+ return new Response(
216
+ await GetInvoicesEndpoint.buildData(request.query),
217
+ );
218
+ }
219
+ }
@@ -5,14 +5,14 @@ import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform
5
5
  import { BuckarooSettings, Company, MemberResponsibility, OrganizationMetaData, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, PermissionRoleDetailed, PermissionRoleForResponsibility, PermissionsResourceType, ResourcePermissions, UitpasClientCredentialsStatus } from '@stamhoofd/structures';
6
6
  import { Formatter } from '@stamhoofd/utility';
7
7
 
8
- import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
9
- import { BuckarooHelper } from '../../../../helpers/BuckarooHelper';
10
- import { Context } from '../../../../helpers/Context';
11
- import { MemberUserSyncer } from '../../../../helpers/MemberUserSyncer';
12
- import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater';
13
- import { TagHelper } from '../../../../helpers/TagHelper';
14
- import { ViesHelper } from '../../../../helpers/ViesHelper';
15
- import { UitpasService } from '../../../../services/uitpas/UitpasService';
8
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
9
+ import { BuckarooHelper } from '../../../../helpers/BuckarooHelper.js';
10
+ import { Context } from '../../../../helpers/Context.js';
11
+ import { MemberUserSyncer } from '../../../../helpers/MemberUserSyncer.js';
12
+ import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater.js';
13
+ import { TagHelper } from '../../../../helpers/TagHelper.js';
14
+ import { ViesHelper } from '../../../../helpers/ViesHelper.js';
15
+ import { UitpasService } from '../../../../services/uitpas/UitpasService.js';
16
16
 
17
17
  type Params = Record<string, never>;
18
18
  type Query = undefined;
@@ -5,8 +5,8 @@ import { BalanceItem, Member, Order, User } from '@stamhoofd/models';
5
5
  import { QueueHandler } from '@stamhoofd/queues';
6
6
  import { BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, PermissionLevel } from '@stamhoofd/structures';
7
7
 
8
- import { Context } from '../../../../helpers/Context';
9
- import { BalanceItemService } from '../../../../services/BalanceItemService';
8
+ import { Context } from '../../../../helpers/Context.js';
9
+ import { BalanceItemService } from '../../../../services/BalanceItemService.js';
10
10
 
11
11
  type Params = Record<string, never>;
12
12
  type Query = undefined;
@@ -0,0 +1,72 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { UitpasNumberDetails, UitpasNumbersGetDetailsRequest } from '@stamhoofd/structures';
3
+
4
+ import { Decoder } from '@simonbackx/simple-encoding';
5
+ import { isSimpleError, isSimpleErrors, SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
6
+ import { uitpasApiResponseToSocialTariff } from '../../../helpers/updateMemberDetailsUitpasNumber.js';
7
+ import { UitpasService } from '../../../services/uitpas/UitpasService.js';
8
+
9
+ type Params = Record<string, never>;
10
+ type Query = UitpasNumbersGetDetailsRequest;
11
+ type Body = undefined;
12
+ type ResponseBody = UitpasNumberDetails[];
13
+
14
+ /**
15
+ * Get the details such as the social tariff for a list of uitpas numbers.
16
+ */
17
+ export class GetUitpasNumberDetailsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
18
+ queryDecoder = UitpasNumbersGetDetailsRequest as Decoder<UitpasNumbersGetDetailsRequest>;
19
+
20
+ protected doesMatch(request: Request): [true, Params] | [false] {
21
+ if (request.method !== 'GET') {
22
+ return [false];
23
+ }
24
+
25
+ const params = Endpoint.parseParameters(request.url, '/uitpas/details', {});
26
+
27
+ if (params) {
28
+ return [true, params as Params];
29
+ }
30
+ return [false];
31
+ }
32
+
33
+ async handle(request: DecodedRequest<Params, Query, Body>) {
34
+ const uitpasNumbers: string[] = request.query.uitpasNumbers;
35
+ if (uitpasNumbers.length > 5) {
36
+ throw new SimpleError({
37
+ code: 'maximum_limit',
38
+ message: 'Please only request up to 5 numbers at the same time',
39
+ });
40
+ }
41
+
42
+ const results: UitpasNumberDetails[] = [];
43
+ const simpleErrors = new SimpleErrors();
44
+
45
+ for (let i = 0; i < uitpasNumbers.length; i++) {
46
+ const uitpasNumber = uitpasNumbers[i];
47
+
48
+ try {
49
+ const result = await UitpasService.getPassByUitpasNumber(uitpasNumber);
50
+ const socialTariff = uitpasApiResponseToSocialTariff(result);
51
+ results.push(UitpasNumberDetails.create({
52
+ uitpasNumber,
53
+ socialTariff,
54
+ }));
55
+ }
56
+ catch (error) {
57
+ if (isSimpleError(error) || isSimpleErrors(error)) {
58
+ error.addNamespace(i.toString());
59
+ error.addNamespace('uitpasNumbers');
60
+ simpleErrors.addError(error);
61
+ }
62
+ else {
63
+ throw error;
64
+ }
65
+ }
66
+ }
67
+
68
+ simpleErrors.throwIfNotEmpty();
69
+
70
+ return new Response(results);
71
+ }
72
+ }
@@ -3,8 +3,9 @@ import { SimpleError } from '@simonbackx/simple-errors';
3
3
  import { UitpasPriceCheckRequest, UitpasPriceCheckResponse } from '@stamhoofd/structures';
4
4
 
5
5
  import { Decoder } from '@simonbackx/simple-encoding';
6
- import { UitpasService } from '../../../services/uitpas/UitpasService';
7
- import { Context } from '../../../helpers/Context';
6
+ import { Context } from '../../../helpers/Context.js';
7
+ import { UitpasService } from '../../../services/uitpas/UitpasService.js';
8
+
8
9
  type Params = Record<string, never>;
9
10
  type Query = undefined;
10
11
  type Body = UitpasPriceCheckRequest;
@@ -122,7 +122,7 @@ export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
122
122
  name: $t(`87c1a48c-fef5-44c3-ae56-c83463fcfb84`),
123
123
  width: 20,
124
124
  getValue: ({ patchedMember: object }: PlatformMember) => ({
125
- value: object.details.uitpasNumber,
125
+ value: object.details.uitpasNumberDetails?.uitpasNumber ?? null,
126
126
  }),
127
127
  },
128
128
  {
@@ -160,6 +160,32 @@ export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
160
160
  };
161
161
  },
162
162
  },
163
+ {
164
+ id: 'organization',
165
+ name: $t(`afd7843d-f355-445b-a158-ddacf469a5b1`),
166
+ width: 40,
167
+ getValue: (member: PlatformMember) => {
168
+ const organizations = member.filterOrganizations({ currentPeriod: true, types: [GroupType.Membership] });
169
+ const str = Formatter.joinLast(organizations.map(o => o.name).sort(), ', ', ' ' + $t(`c1843768-2bf4-42f2-baa4-42f49028463d`) + ' ') || Context.i18n.$t('1a16a32a-7ee4-455d-af3d-6073821efa8f');
170
+
171
+ return {
172
+ value: str,
173
+ };
174
+ },
175
+ },
176
+ {
177
+ id: 'uri',
178
+ name: $t(`27cfaf26-6b88-4ebc-a50a-627a9f0f9e64`),
179
+ width: 30,
180
+ getValue: (member: PlatformMember) => {
181
+ const organizations = member.filterOrganizations({ currentPeriod: true, types: [GroupType.Membership] });
182
+ const str = Formatter.joinLast(organizations.map(o => o.uri).sort(), ', ', ' ' + $t(`c1843768-2bf4-42f2-baa4-42f49028463d`) + ' ') || Context.i18n.$t('1a16a32a-7ee4-455d-af3d-6073821efa8f');
183
+
184
+ return {
185
+ value: str,
186
+ };
187
+ },
188
+ },
163
189
 
164
190
  ...XlsxTransformerColumnHelper.creatColumnsForParents(),
165
191
 
@@ -216,32 +242,6 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
216
242
  name: $t(`fb35c140-e936-4e91-aa92-ef4dfc59fb51`),
217
243
  columns: [
218
244
  ...baseMemberColumns,
219
- {
220
- id: 'organization',
221
- name: $t(`afd7843d-f355-445b-a158-ddacf469a5b1`),
222
- width: 40,
223
- getValue: (member: PlatformMember) => {
224
- const organizations = member.filterOrganizations({ currentPeriod: true, types: [GroupType.Membership] });
225
- const str = Formatter.joinLast(organizations.map(o => o.name).sort(), ', ', ' ' + $t(`c1843768-2bf4-42f2-baa4-42f49028463d`) + ' ') || Context.i18n.$t('1a16a32a-7ee4-455d-af3d-6073821efa8f');
226
-
227
- return {
228
- value: str,
229
- };
230
- },
231
- },
232
- {
233
- id: 'uri',
234
- name: $t(`27cfaf26-6b88-4ebc-a50a-627a9f0f9e64`),
235
- width: 30,
236
- getValue: (member: PlatformMember) => {
237
- const organizations = member.filterOrganizations({ currentPeriod: true, types: [GroupType.Membership] });
238
- const str = Formatter.joinLast(organizations.map(o => o.uri).sort(), ', ', ' ' + $t(`c1843768-2bf4-42f2-baa4-42f49028463d`) + ' ') || Context.i18n.$t('1a16a32a-7ee4-455d-af3d-6073821efa8f');
239
-
240
- return {
241
- value: str,
242
- };
243
- },
244
- },
245
245
  {
246
246
  id: 'group',
247
247
  name: $t(`0c230001-c3be-4a8e-8eab-23dc3fd96e52`),
@@ -1,10 +1,10 @@
1
1
  import { AutoEncoderPatchType, PatchMap } from '@simonbackx/simple-encoding';
2
2
  import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
3
3
  import { BalanceItem, CachedBalance, Document, Email, EmailTemplate, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
4
- import { AccessRight, EmailTemplate as EmailTemplateStruct, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, ReceivableBalanceType, RecordSettings, ResourcePermissions } from '@stamhoofd/structures';
4
+ import { AccessRight, EmailTemplate as EmailTemplateStruct, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, ReceivableBalanceType, RecordSettings, ResourcePermissions, UitpasNumberDetails, UitpasSocialTariff, UitpasSocialTariffStatus } from '@stamhoofd/structures';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
- import { MemberRecordStore } from '../services/MemberRecordStore';
7
- import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
6
+ import { MemberRecordStore } from '../services/MemberRecordStore.js';
7
+ import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess.js';
8
8
 
9
9
  /**
10
10
  * One class with all the responsabilities of checking permissions to each resource in the system by a given user, possibly in an organization context.
@@ -1478,7 +1478,7 @@ export class AdminPermissionChecker {
1478
1478
  // Has financial read access?
1479
1479
  if (!options?.forAdminCartCalculation && !await this.hasFinancialMemberAccess(member, PermissionLevel.Read)) {
1480
1480
  cloned.details.requiresFinancialSupport = null;
1481
- cloned.details.uitpasNumber = null;
1481
+ cloned.details.uitpasNumberDetails = null;
1482
1482
  cloned.outstandingBalance = 0;
1483
1483
 
1484
1484
  for (const registration of cloned.registrations) {
@@ -1507,7 +1507,7 @@ export class AdminPermissionChecker {
1507
1507
  /**
1508
1508
  * Only for creating new members
1509
1509
  */
1510
- filterMemberPut(member: MemberWithRegistrations, data: MemberWithRegistrationsBlob, options: {asUserManager: boolean}) {
1510
+ filterMemberPut(member: MemberWithRegistrations, data: MemberWithRegistrationsBlob, options: { asUserManager: boolean }) {
1511
1511
  if (options.asUserManager || STAMHOOFD.userMode === 'platform') {
1512
1512
  // A user manager cannot choose the member number + in platform mode, nobody can choose the member number
1513
1513
  data.details.memberNumber = null;
@@ -1515,7 +1515,12 @@ export class AdminPermissionChecker {
1515
1515
 
1516
1516
  // Do not allow setting the security code
1517
1517
  data.details.securityCode = null;
1518
- }
1518
+ if (data.details.uitpasNumberDetails) {
1519
+ data.details.uitpasNumberDetails.socialTariff = UitpasSocialTariff.create({
1520
+ status: UitpasSocialTariffStatus.Unknown,
1521
+ });
1522
+ }
1523
+ }
1519
1524
 
1520
1525
  async filterMemberPatch(member: MemberWithRegistrations, data: AutoEncoderPatchType<MemberWithRegistrationsBlob>): Promise<AutoEncoderPatchType<MemberWithRegistrationsBlob>> {
1521
1526
  if (!data.details) {
@@ -1537,10 +1542,9 @@ export class AdminPermissionChecker {
1537
1542
  });
1538
1543
  }
1539
1544
 
1540
-
1541
1545
  const hasRecordAnswers = !!data.details.recordAnswers;
1542
1546
  const hasNotes = data.details.notes !== undefined;
1543
- const isSetFinancialSupportTrue = data.details.shouldApplyReducedPrice;
1547
+ const isSetFinancialSupportTrue = data.details.didSetManualFinancialSupport;
1544
1548
 
1545
1549
  if (data.details.securityCode !== undefined || data.details.trackingYear !== undefined) {
1546
1550
  const hasFullAccess = await this.canAccessMember(member, PermissionLevel.Full);
@@ -1561,6 +1565,22 @@ export class AdminPermissionChecker {
1561
1565
  }
1562
1566
  }
1563
1567
 
1568
+ if (data.details.uitpasNumberDetails && data.details.uitpasNumberDetails.socialTariff !== undefined) {
1569
+ if (data.details.uitpasNumberDetails.uitpasNumber === undefined) {
1570
+ data.details.uitpasNumberDetails = undefined;
1571
+ }
1572
+ else if (data.details.uitpasNumberDetails.uitpasNumber !== member.details.uitpasNumberDetails?.uitpasNumber) {
1573
+ // if uitpas number did change -> status should be reset
1574
+ data.details.uitpasNumberDetails = UitpasNumberDetails.create({
1575
+ uitpasNumber: data.details.uitpasNumberDetails.uitpasNumber,
1576
+ });
1577
+ }
1578
+ else {
1579
+ // if uitpas number did not change
1580
+ data.details.uitpasNumberDetails.socialTariff = member.details.uitpasNumberDetails?.socialTariff;
1581
+ }
1582
+ }
1583
+
1564
1584
  if (hasRecordAnswers) {
1565
1585
  if (!(data.details.recordAnswers instanceof PatchMap)) {
1566
1586
  throw new SimpleError({
@@ -1594,7 +1614,7 @@ export class AdminPermissionChecker {
1594
1614
  // A user manager cannot choose the member number + in platform mode, nobody can choose the member number
1595
1615
  delete data.details.memberNumber;
1596
1616
  }
1597
-
1617
+
1598
1618
  if (hasNotes && isUserManager && !(await this.canAccessMember(member, PermissionLevel.Full))) {
1599
1619
  throw new SimpleError({
1600
1620
  code: 'permission_denied',
@@ -1633,7 +1653,7 @@ export class AdminPermissionChecker {
1633
1653
  }
1634
1654
 
1635
1655
  if (!isUserManager) {
1636
- if (data.details.uitpasNumber) {
1656
+ if (data.details.uitpasNumberDetails) {
1637
1657
  throw new SimpleError({
1638
1658
  code: 'permission_denied',
1639
1659
  message: 'Je hebt geen toegangsrechten om het UiTPAS-nummer van dit lid aan te passen',
@@ -1,12 +1,12 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
- import { AuditLog, BalanceItem, CachedBalance, Document, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
3
- import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, GroupType, MemberPlatformMembership as MemberPlatformMembershipStruct, MembersBlob, MemberWithRegistrationsBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, RegistrationsBlob, RegistrationWithMemberBlob, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
2
+ import { AuditLog, BalanceItem, CachedBalance, Document, Event, EventNotification, Group, Invoice, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
3
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, GroupType, InvoicedBalanceItem, InvoiceStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MembersBlob, MemberWithRegistrationsBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, RegistrationsBlob, RegistrationWithMemberBlob, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
4
  import { Sorter } from '@stamhoofd/utility';
5
5
 
6
6
  import { SQL } from '@stamhoofd/sql';
7
7
  import { Formatter } from '@stamhoofd/utility';
8
- import { BalanceItemService } from '../services/BalanceItemService';
9
- import { Context } from './Context';
8
+ import { BalanceItemService } from '../services/BalanceItemService.js';
9
+ import { Context } from './Context.js';
10
10
 
11
11
  /**
12
12
  * Builds authenticated structures for the current user
@@ -28,9 +28,10 @@ export class AuthenticatedStructures {
28
28
  }
29
29
 
30
30
  const { balanceItemPayments, balanceItems } = await Payment.loadBalanceItems(payments);
31
- const { registrations, orders } = await Payment.loadBalanceItemRelations(balanceItems);
32
31
 
33
32
  if (checkPermissions) {
33
+ const { registrations, orders } = await Payment.loadBalanceItemRelations(balanceItems);
34
+
34
35
  // Note: permission checking is moved here for performacne to avoid loading the data multiple times
35
36
  if (!(await Context.auth.canAccessBalanceItems(balanceItems, PermissionLevel.Read, { registrations, orders }))) {
36
37
  throw new SimpleError({
@@ -53,6 +54,24 @@ export class AuthenticatedStructures {
53
54
  }, includeSettlements);
54
55
  }
55
56
 
57
+ static async invoices(invoices: Invoice[]): Promise<InvoiceStruct[]> {
58
+ if (invoices.length === 0) {
59
+ return [];
60
+ }
61
+
62
+ const { invoicedBalanceItems } = await Invoice.loadBalanceItems(invoices);
63
+
64
+ return invoices.map((invoice) => {
65
+ const items = invoicedBalanceItems.filter(i => i.invoiceId === invoice.id);
66
+ return InvoiceStruct.create({
67
+ ...invoice,
68
+ items: items.map((item) => {
69
+ return InvoicedBalanceItem.create(item);
70
+ }),
71
+ });
72
+ });
73
+ }
74
+
56
75
  static async group(group: Group) {
57
76
  return (await this.groups([group]))[0];
58
77
  }
@@ -4,6 +4,7 @@ import { BalanceItem, BalanceItemPayment, Organization, Payment, StripeAccount,
4
4
  import { calculateVATPercentage, PaymentMethod, PaymentMethodHelper, PaymentStatus } from '@stamhoofd/structures';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import Stripe from 'stripe';
7
+ import { passthroughFetch } from './passthroughFetch.js';
7
8
 
8
9
  export class StripeHelper {
9
10
  static get notConfiguredError() {
@@ -18,7 +19,16 @@ export class StripeHelper {
18
19
  if (!STAMHOOFD.STRIPE_SECRET_KEY) {
19
20
  throw this.notConfiguredError;
20
21
  }
21
- return new Stripe(STAMHOOFD.STRIPE_SECRET_KEY, { apiVersion: '2024-06-20', typescript: true, maxNetworkRetries: 0, timeout: 10000, stripeAccount: accountId ?? undefined });
22
+ return new Stripe(STAMHOOFD.STRIPE_SECRET_KEY, {
23
+ apiVersion: '2024-06-20',
24
+ typescript: true,
25
+ maxNetworkRetries: 0,
26
+ timeout: 10000,
27
+ stripeAccount: accountId ?? undefined,
28
+ httpClient: STAMHOOFD.environment === 'test'
29
+ ? Stripe.createFetchHttpClient(passthroughFetch)
30
+ : undefined,
31
+ });
22
32
  }
23
33
 
24
34
  static async saveChargeInfo(model: StripePaymentIntent | StripeCheckoutSession, charge: Stripe.Charge, payment: Payment) {
@@ -1,6 +1,7 @@
1
1
  import { Order, Payment, StripeCheckoutSession, StripePaymentIntent } from '@stamhoofd/models';
2
2
  import { Settlement } from '@stamhoofd/structures';
3
3
  import Stripe from 'stripe';
4
+ import { passthroughFetch } from './passthroughFetch.js';
4
5
 
5
6
  export class StripePayoutChecker {
6
7
  private stripe: Stripe;
@@ -14,6 +15,9 @@ export class StripePayoutChecker {
14
15
  maxNetworkRetries: 1,
15
16
  timeout: 10000,
16
17
  stripeAccount,
18
+ httpClient: STAMHOOFD.environment === 'test'
19
+ ? Stripe.createFetchHttpClient(passthroughFetch)
20
+ : undefined,
17
21
  });
18
22
 
19
23
  this.stripePlatform = new Stripe(
@@ -22,6 +26,9 @@ export class StripePayoutChecker {
22
26
  typescript: true,
23
27
  maxNetworkRetries: 1,
24
28
  timeout: 10000,
29
+ httpClient: STAMHOOFD.environment === 'test'
30
+ ? Stripe.createFetchHttpClient(passthroughFetch)
31
+ : undefined,
25
32
  });
26
33
  }
27
34
 
@@ -73,10 +73,6 @@ export class UitpasTokenRepository {
73
73
  }
74
74
 
75
75
  private static async getModelFromDb(organizationId: string | null) {
76
- let model = await UitpasClientCredential.select().where('organizationId', organizationId).first(false);
77
- if (model) {
78
- return model; // found in database
79
- }
80
76
  if (organizationId === null) {
81
77
  // platform client id and secret are not yet in the database, but should be configured in the environment variables
82
78
  if (!STAMHOOFD.UITPAS_API_CLIENT_ID || !STAMHOOFD.UITPAS_API_CLIENT_SECRET) {
@@ -86,12 +82,18 @@ export class UitpasTokenRepository {
86
82
  human: $t('71a8218b-c58e-4e95-9626-551b80eb8367'),
87
83
  });
88
84
  }
89
- model = new UitpasClientCredential();
85
+ const model = new UitpasClientCredential();
90
86
  model.clientId = STAMHOOFD.UITPAS_API_CLIENT_ID;
91
87
  model.clientSecret = STAMHOOFD.UITPAS_API_CLIENT_SECRET;
92
88
  model.organizationId = null; // null means platform
93
89
  return model;
94
90
  }
91
+
92
+ const model = await UitpasClientCredential.select().where('organizationId', organizationId).first(false);
93
+ if (model) {
94
+ return model; // found in database
95
+ }
96
+
95
97
  return null; // not found in database
96
98
  }
97
99