@stamhoofd/backend 2.42.2 → 2.43.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.
- package/package.json +10 -10
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +17 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +104 -27
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -65
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/cached-outstanding-balance/GetCachedOutstandingBalanceCountEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/cached-outstanding-balance/GetCachedOutstandingBalanceEndpoint.ts +142 -0
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +1 -2
- package/src/excel-loaders/payments.ts +0 -15
- package/src/helpers/AdminPermissionChecker.ts +23 -118
- package/src/helpers/AuthenticatedStructures.ts +38 -2
- package/src/helpers/TemporaryMemberAccess.ts +40 -0
- package/src/helpers/ViesHelper.ts +17 -5
- package/src/sql-filters/cached-outstanding-balance.ts +13 -0
- package/src/sql-sorters/cached-outstanding-balance.ts +46 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.43.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"@simonbackx/simple-encoding": "2.15.1",
|
|
37
37
|
"@simonbackx/simple-endpoints": "1.14.0",
|
|
38
38
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
39
|
-
"@stamhoofd/backend-i18n": "2.
|
|
40
|
-
"@stamhoofd/backend-middleware": "2.
|
|
41
|
-
"@stamhoofd/email": "2.
|
|
42
|
-
"@stamhoofd/models": "2.
|
|
43
|
-
"@stamhoofd/queues": "2.
|
|
44
|
-
"@stamhoofd/sql": "2.
|
|
45
|
-
"@stamhoofd/structures": "2.
|
|
46
|
-
"@stamhoofd/utility": "2.
|
|
39
|
+
"@stamhoofd/backend-i18n": "2.43.0",
|
|
40
|
+
"@stamhoofd/backend-middleware": "2.43.0",
|
|
41
|
+
"@stamhoofd/email": "2.43.0",
|
|
42
|
+
"@stamhoofd/models": "2.43.0",
|
|
43
|
+
"@stamhoofd/queues": "2.43.0",
|
|
44
|
+
"@stamhoofd/sql": "2.43.0",
|
|
45
|
+
"@stamhoofd/structures": "2.43.0",
|
|
46
|
+
"@stamhoofd/utility": "2.43.0",
|
|
47
47
|
"archiver": "^7.0.1",
|
|
48
48
|
"aws-sdk": "^2.885.0",
|
|
49
49
|
"axios": "1.6.8",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"postmark": "^4.0.5",
|
|
61
61
|
"stripe": "^16.6.0"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "d5d4b4bedec089c5c6f945577695a410f0bd8a3d"
|
|
64
64
|
}
|
|
@@ -52,6 +52,15 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
52
52
|
const event = new Event();
|
|
53
53
|
event.organizationId = put.organizationId;
|
|
54
54
|
event.meta = put.meta;
|
|
55
|
+
|
|
56
|
+
if (event.meta.groups && event.meta.groups.length === 0) {
|
|
57
|
+
throw new SimpleError({
|
|
58
|
+
code: 'invalid_field',
|
|
59
|
+
message: 'Empty groups',
|
|
60
|
+
human: 'Kies minstens één leeftijdsgroep',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
const eventOrganization = await this.checkEventAccess(event);
|
|
56
65
|
event.id = put.id;
|
|
57
66
|
event.name = put.name;
|
|
@@ -122,6 +131,14 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
122
131
|
event.organizationId = patch.organizationId;
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
if (event.meta.groups && event.meta.groups.length === 0) {
|
|
135
|
+
throw new SimpleError({
|
|
136
|
+
code: 'invalid_field',
|
|
137
|
+
message: 'Empty groups',
|
|
138
|
+
human: 'Kies minstens één leeftijdsgroep',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
const eventOrganization = await this.checkEventAccess(event);
|
|
126
143
|
if (eventOrganization) {
|
|
127
144
|
event.meta.organizationCache = NamedObject.create({ id: eventOrganization.id, name: eventOrganization.name });
|
|
@@ -2,10 +2,11 @@ import { OneToManyRelation } from '@simonbackx/simple-database';
|
|
|
2
2
|
import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } 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, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Platform, Registration, SetupStepUpdater, User } from '@stamhoofd/models';
|
|
6
|
-
import { GroupType,
|
|
5
|
+
import { BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, mergeTwoMembers, Organization, Platform, RateLimiter, Registration, SetupStepUpdater, User } from '@stamhoofd/models';
|
|
6
|
+
import { GroupType, MembersBlob, MemberWithRegistrationsBlob, PermissionLevel } from '@stamhoofd/structures';
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
|
+
import { Email } from '@stamhoofd/email';
|
|
9
10
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
10
11
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
11
12
|
import { Context } from '../../../helpers/Context';
|
|
@@ -17,6 +18,16 @@ type Query = undefined;
|
|
|
17
18
|
type Body = PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
|
|
18
19
|
type ResponseBody = MembersBlob;
|
|
19
20
|
|
|
21
|
+
export const securityCodeLimiter = new RateLimiter({
|
|
22
|
+
limits: [
|
|
23
|
+
{
|
|
24
|
+
// Max 10 a day
|
|
25
|
+
limit: 10,
|
|
26
|
+
duration: 24 * 60 * 1000 * 60,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
20
31
|
/**
|
|
21
32
|
* One endpoint to create, patch and delete members and their registrations and payments
|
|
22
33
|
*/
|
|
@@ -92,33 +103,15 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
92
103
|
struct.details.cleanData();
|
|
93
104
|
member.details = struct.details;
|
|
94
105
|
|
|
95
|
-
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member);
|
|
106
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
|
|
96
107
|
if (duplicate) {
|
|
97
108
|
// Merge data
|
|
98
|
-
duplicate.details.merge(member.details);
|
|
99
109
|
member = duplicate;
|
|
100
|
-
|
|
101
|
-
// You need write permissions, because a user can potentially earn write permissions on a member
|
|
102
|
-
// by registering it
|
|
103
|
-
if (!await Context.auth.canAccessMember(duplicate, PermissionLevel.Write)) {
|
|
104
|
-
throw new SimpleError({
|
|
105
|
-
code: 'known_member_missing_rights',
|
|
106
|
-
message: 'Creating known member without sufficient access rights',
|
|
107
|
-
human: 'Dit lid is al bekend in het systeem, maar je hebt er geen toegang tot. Vraag iemand met de juiste toegangsrechten om dit lid voor jou toe te voegen, of vraag het lid om zelf in te schrijven via het ledenportaal.',
|
|
108
|
-
statusCode: 400,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
// We risk creating a new member without being able to access it manually afterwards
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
code: 'missing_group',
|
|
117
|
-
message: 'Missing group',
|
|
118
|
-
human: 'Je moet hoofdbeheerder zijn om een lid toe te voegen in het systeem',
|
|
119
|
-
statusCode: 400,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
113
|
+
// Cache access to this member temporarily in memory
|
|
114
|
+
await Context.auth.temporarilyGrantMemberAccess(member, PermissionLevel.Write);
|
|
122
115
|
|
|
123
116
|
if (STAMHOOFD.userMode !== 'platform' && !member.organizationId) {
|
|
124
117
|
throw new SimpleError({
|
|
@@ -132,8 +125,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
132
125
|
/**
|
|
133
126
|
* In development mode, we allow some secret usernames to create fake data
|
|
134
127
|
*/
|
|
135
|
-
if ((STAMHOOFD.environment
|
|
136
|
-
if (member.details.firstName.toLocaleLowerCase()
|
|
128
|
+
if ((STAMHOOFD.environment === 'development' || STAMHOOFD.environment === 'staging') && organization) {
|
|
129
|
+
if (member.details.firstName.toLocaleLowerCase() === 'create' && parseInt(member.details.lastName) > 0) {
|
|
137
130
|
const count = parseInt(member.details.lastName);
|
|
138
131
|
await this.createDummyMembers(organization, count);
|
|
139
132
|
|
|
@@ -155,9 +148,17 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
155
148
|
// Loop all members one by one
|
|
156
149
|
for (let patch of request.body.getPatches()) {
|
|
157
150
|
const member = members.find(m => m.id === patch.id) ?? await Member.getWithRegistrations(patch.id);
|
|
158
|
-
|
|
151
|
+
const securityCode = patch.details?.securityCode; // will get cleared after the filter
|
|
152
|
+
|
|
153
|
+
if (!member) {
|
|
159
154
|
throw Context.auth.notFoundOrNoAccess('Je hebt geen toegang tot dit lid of het bestaat niet');
|
|
160
155
|
}
|
|
156
|
+
|
|
157
|
+
if (!await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
|
|
158
|
+
// Still allowed if you provide a security code
|
|
159
|
+
await PatchOrganizationMembersEndpoint.checkSecurityCode(member, securityCode);
|
|
160
|
+
}
|
|
161
|
+
|
|
161
162
|
patch = await Context.auth.filterMemberPatch(member, patch);
|
|
162
163
|
|
|
163
164
|
if (patch.details) {
|
|
@@ -179,6 +180,19 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
|
|
184
|
+
if (duplicate) {
|
|
185
|
+
// Remove the member from the list
|
|
186
|
+
const iii = members.findIndex(m => m.id === member.id);
|
|
187
|
+
if (iii !== -1) {
|
|
188
|
+
members.splice(iii, 1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Add new
|
|
192
|
+
members.push(duplicate);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
182
196
|
await member.save();
|
|
183
197
|
|
|
184
198
|
// Update documents
|
|
@@ -561,7 +575,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
561
575
|
}
|
|
562
576
|
}
|
|
563
577
|
|
|
564
|
-
static async
|
|
578
|
+
static async findExistingMember(member: Member) {
|
|
565
579
|
if (!member.details.birthDay) {
|
|
566
580
|
return;
|
|
567
581
|
}
|
|
@@ -585,6 +599,69 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
585
599
|
}
|
|
586
600
|
}
|
|
587
601
|
|
|
602
|
+
static async checkSecurityCode(member: MemberWithRegistrations, securityCode: string | null | undefined) {
|
|
603
|
+
if (await member.isSafeToMergeDuplicateWithoutSecurityCode() || await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
|
|
604
|
+
console.log('checkSecurityCode: without security code: allowed for ' + member.id);
|
|
605
|
+
}
|
|
606
|
+
else if (securityCode) {
|
|
607
|
+
try {
|
|
608
|
+
securityCodeLimiter.track(member.details.name, 1);
|
|
609
|
+
}
|
|
610
|
+
catch (e) {
|
|
611
|
+
Email.sendWebmaster({
|
|
612
|
+
subject: '[Limiet] Limiet bereikt voor aantal beveiligingscodes',
|
|
613
|
+
text: 'Beste, \nDe limiet werd bereikt voor het aantal beveiligingscodes per dag. \nNaam lid: ' + member.details.name + ' (ID: ' + member.id + ')' + '\n\n' + e.message + '\n\nStamhoofd',
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
throw new SimpleError({
|
|
617
|
+
code: 'too_many_tries',
|
|
618
|
+
message: 'Too many securityCodes limited',
|
|
619
|
+
human: 'Oeps! Om spam te voorkomen limiteren we het aantal beveiligingscodes die je kan proberen. Probeer morgen opnieuw.',
|
|
620
|
+
field: 'details.securityCode',
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Entered the security code, so we can link the user to the member
|
|
625
|
+
if (STAMHOOFD.environment !== 'development') {
|
|
626
|
+
if (!member.details.securityCode || securityCode !== member.details.securityCode) {
|
|
627
|
+
throw new SimpleError({
|
|
628
|
+
code: 'invalid_field',
|
|
629
|
+
field: 'details.securityCode',
|
|
630
|
+
message: 'Invalid security code',
|
|
631
|
+
human: Context.i18n.$t('49753d6a-7ca4-4145-8024-0be05a9ab063'),
|
|
632
|
+
statusCode: 400,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
console.log('checkSecurityCode: security code is correct - for ' + member.id);
|
|
638
|
+
|
|
639
|
+
// Grant temporary access to this member without needing to enter the security code again
|
|
640
|
+
await Context.auth.temporarilyGrantMemberAccess(member, PermissionLevel.Write);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
throw new SimpleError({
|
|
644
|
+
code: 'known_member_missing_rights',
|
|
645
|
+
message: 'Creating known member without sufficient access rights',
|
|
646
|
+
human: `${member.details.firstName} is al gekend in ons systeem, maar jouw e-mailadres niet. Om toegang te krijgen heb je de beveiligingscode nodig.`,
|
|
647
|
+
statusCode: 400,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
static async checkDuplicate(member: Member, securityCode: string | null | undefined) {
|
|
653
|
+
// Check for duplicates and prevent creating a duplicate member by a user
|
|
654
|
+
const duplicate = await this.findExistingMember(member);
|
|
655
|
+
if (duplicate) {
|
|
656
|
+
await this.checkSecurityCode(duplicate, securityCode);
|
|
657
|
+
|
|
658
|
+
// Merge data
|
|
659
|
+
// NOTE: We use mergeTwoMembers instead of mergeMultipleMembers, because we should never safe 'member' , because that one does not exist in the database
|
|
660
|
+
await mergeTwoMembers(duplicate, member);
|
|
661
|
+
return duplicate;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
588
665
|
async createDummyMembers(organization: Organization, count: number) {
|
|
589
666
|
await new MemberFactory({
|
|
590
667
|
organization,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
-
import { Document, Member,
|
|
4
|
+
import { Document, Member, RateLimiter } from '@stamhoofd/models';
|
|
5
5
|
import { MemberDetails, MembersBlob, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
|
|
6
6
|
|
|
7
|
-
import { Email } from '@stamhoofd/email';
|
|
8
7
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
9
8
|
import { Context } from '../../../helpers/Context';
|
|
10
9
|
import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
|
|
@@ -61,7 +60,7 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
61
60
|
|
|
62
61
|
this.throwIfInvalidDetails(member.details);
|
|
63
62
|
|
|
64
|
-
const duplicate = await
|
|
63
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
|
|
65
64
|
if (duplicate) {
|
|
66
65
|
addedMembers.push(duplicate);
|
|
67
66
|
continue;
|
|
@@ -75,7 +74,7 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
75
74
|
let members = await Member.getMembersWithRegistrationForUser(user);
|
|
76
75
|
|
|
77
76
|
for (let struct of request.body.getPatches()) {
|
|
78
|
-
const member = members.find(m => m.id
|
|
77
|
+
const member = members.find(m => m.id === struct.id);
|
|
79
78
|
if (!member) {
|
|
80
79
|
throw new SimpleError({
|
|
81
80
|
code: 'invalid_member',
|
|
@@ -110,15 +109,15 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
110
109
|
});
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
|
|
112
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
|
|
114
113
|
if (duplicate) {
|
|
115
114
|
// Remove the member from the list
|
|
116
|
-
members.splice(members.findIndex(m => m.id === member.id), 1)
|
|
115
|
+
members.splice(members.findIndex(m => m.id === member.id), 1);
|
|
117
116
|
|
|
118
117
|
// Add new
|
|
119
|
-
addedMembers.push(duplicate)
|
|
120
|
-
continue
|
|
121
|
-
}
|
|
118
|
+
addedMembers.push(duplicate);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
122
121
|
|
|
123
122
|
await member.save();
|
|
124
123
|
await MemberUserSyncer.onChangeMember(member);
|
|
@@ -157,62 +156,6 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
157
156
|
);
|
|
158
157
|
}
|
|
159
158
|
|
|
160
|
-
async checkDuplicate(member: Member, securityCode: string | null | undefined) {
|
|
161
|
-
// Check for duplicates and prevent creating a duplicate member by a user
|
|
162
|
-
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member);
|
|
163
|
-
if (duplicate) {
|
|
164
|
-
if (await duplicate.isSafeToMergeDuplicateWithoutSecurityCode()) {
|
|
165
|
-
console.log('Merging duplicate without security code: allowed for ' + duplicate.id);
|
|
166
|
-
}
|
|
167
|
-
else if (securityCode) {
|
|
168
|
-
try {
|
|
169
|
-
securityCodeLimiter.track(member.details.name, 1);
|
|
170
|
-
}
|
|
171
|
-
catch (e) {
|
|
172
|
-
Email.sendWebmaster({
|
|
173
|
-
subject: '[Limiet] Limiet bereikt voor aantal beveiligingscodes',
|
|
174
|
-
text: 'Beste, \nDe limiet werd bereikt voor het aantal beveiligingscodes per dag. \nNaam lid: ' + member.details.name + ' (ID: ' + duplicate.id + ')' + '\n\n' + e.message + '\n\nStamhoofd',
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
throw new SimpleError({
|
|
178
|
-
code: 'too_many_tries',
|
|
179
|
-
message: 'Too many securityCodes limited',
|
|
180
|
-
human: 'Oeps! Om spam te voorkomen limiteren we het aantal beveiligingscodes die je kan proberen. Probeer morgen opnieuw.',
|
|
181
|
-
field: 'details.securityCode',
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Entered the security code, so we can link the user to the member
|
|
186
|
-
if (STAMHOOFD.environment !== 'development') {
|
|
187
|
-
if (!duplicate.details.securityCode || securityCode !== duplicate.details.securityCode) {
|
|
188
|
-
throw new SimpleError({
|
|
189
|
-
code: 'invalid_field',
|
|
190
|
-
field: 'details.securityCode',
|
|
191
|
-
message: 'Invalid security code',
|
|
192
|
-
human: Context.i18n.$t('49753d6a-7ca4-4145-8024-0be05a9ab063'),
|
|
193
|
-
statusCode: 400,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
console.log('Merging duplicate: security code is correct - for ' + duplicate.id);
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
throw new SimpleError({
|
|
202
|
-
code: 'known_member_missing_rights',
|
|
203
|
-
message: 'Creating known member without sufficient access rights',
|
|
204
|
-
human: `${member.details.firstName} is al gekend in ons systeem, maar jouw e-mailadres niet. Om toegang te krijgen heb je de beveiligingscode nodig.`,
|
|
205
|
-
statusCode: 400,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Merge data
|
|
210
|
-
// NOTE: We use mergeTwoMembers instead of mergeMultipleMembers, because we should never safe 'member' , because that one does not exist in the database
|
|
211
|
-
await mergeTwoMembers(duplicate, member);
|
|
212
|
-
return duplicate;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
159
|
private throwIfInvalidDetails(details: MemberDetails) {
|
|
217
160
|
if (details.firstName.length < 2) {
|
|
218
161
|
throw new SimpleError({
|
|
@@ -260,6 +260,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
260
260
|
registration.price = 0; // will get filled by balance items themselves
|
|
261
261
|
registration.groupPrice = item.groupPrice;
|
|
262
262
|
registration.options = item.options;
|
|
263
|
+
registration.recordAnswers = item.recordAnswers;
|
|
263
264
|
|
|
264
265
|
payRegistrations.push({
|
|
265
266
|
registration,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
import { Context } from '../../../../helpers/Context';
|
|
6
|
+
import { GetCachedOutstandingBalanceEndpoint } from './GetCachedOutstandingBalanceEndpoint';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, never>;
|
|
9
|
+
type Query = CountFilteredRequest;
|
|
10
|
+
type Body = undefined;
|
|
11
|
+
type ResponseBody = CountResponse;
|
|
12
|
+
|
|
13
|
+
export class GetCachedOutstandingBalanceCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
+
queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
|
|
15
|
+
|
|
16
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
17
|
+
if (request.method !== 'GET') {
|
|
18
|
+
return [false];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const params = Endpoint.parseParameters(request.url, '/cached-outstanding-balance/count', {});
|
|
22
|
+
|
|
23
|
+
if (params) {
|
|
24
|
+
return [true, params as Params];
|
|
25
|
+
}
|
|
26
|
+
return [false];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
|
+
await Context.setOrganizationScope();
|
|
31
|
+
await Context.authenticate();
|
|
32
|
+
const query = await GetCachedOutstandingBalanceEndpoint.buildQuery(request.query);
|
|
33
|
+
|
|
34
|
+
const count = await query
|
|
35
|
+
.count();
|
|
36
|
+
|
|
37
|
+
return new Response(
|
|
38
|
+
CountResponse.create({
|
|
39
|
+
count,
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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 { CachedOutstandingBalance } from '@stamhoofd/models';
|
|
5
|
+
import { compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
|
|
6
|
+
import { CachedOutstandingBalance as CachedOutstandingBalanceStruct, CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
7
|
+
|
|
8
|
+
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
9
|
+
import { Context } from '../../../../helpers/Context';
|
|
10
|
+
import { cachedOutstandingBalanceFilterCompilers } from '../../../../sql-filters/cached-outstanding-balance';
|
|
11
|
+
import { cachedOutstandingBalanceSorters } from '../../../../sql-sorters/cached-outstanding-balance';
|
|
12
|
+
|
|
13
|
+
type Params = Record<string, never>;
|
|
14
|
+
type Query = LimitedFilteredRequest;
|
|
15
|
+
type Body = undefined;
|
|
16
|
+
type ResponseBody = PaginatedResponse<CachedOutstandingBalanceStruct[], LimitedFilteredRequest>;
|
|
17
|
+
|
|
18
|
+
const sorters = cachedOutstandingBalanceSorters;
|
|
19
|
+
const filterCompilers = cachedOutstandingBalanceFilterCompilers;
|
|
20
|
+
|
|
21
|
+
export class GetCachedOutstandingBalanceEndpoint 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, '/cached-outstanding-balance', {});
|
|
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 = CachedOutstandingBalance
|
|
54
|
+
.select();
|
|
55
|
+
|
|
56
|
+
if (scopeFilter) {
|
|
57
|
+
query.where(compileToSQLFilter(scopeFilter, filterCompilers));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (q.filter) {
|
|
61
|
+
query.where(compileToSQLFilter(q.filter, filterCompilers));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (q.search) {
|
|
65
|
+
throw new SimpleError({
|
|
66
|
+
code: 'not_implemented',
|
|
67
|
+
message: 'Zoeken in openstaande bedragen is voorlopig nog niet mogelijk',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (q instanceof LimitedFilteredRequest) {
|
|
72
|
+
if (q.pageFilter) {
|
|
73
|
+
query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
q.sort = assertSort(q.sort, [{ key: 'id' }]);
|
|
77
|
+
query.orderBy(compileToSQLSorter(q.sort, sorters));
|
|
78
|
+
query.limit(q.limit);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return query;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static async buildData(requestQuery: LimitedFilteredRequest) {
|
|
85
|
+
const query = await GetCachedOutstandingBalanceEndpoint.buildQuery(requestQuery);
|
|
86
|
+
const data = await query.fetch();
|
|
87
|
+
|
|
88
|
+
// todo: Create objects from data
|
|
89
|
+
|
|
90
|
+
let next: LimitedFilteredRequest | undefined;
|
|
91
|
+
|
|
92
|
+
if (data.length >= requestQuery.limit) {
|
|
93
|
+
const lastObject = data[data.length - 1];
|
|
94
|
+
const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
|
|
95
|
+
|
|
96
|
+
next = new LimitedFilteredRequest({
|
|
97
|
+
filter: requestQuery.filter,
|
|
98
|
+
pageFilter: nextFilter,
|
|
99
|
+
sort: requestQuery.sort,
|
|
100
|
+
limit: requestQuery.limit,
|
|
101
|
+
search: requestQuery.search,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
|
|
105
|
+
console.error('Found infinite loading loop for', requestQuery);
|
|
106
|
+
next = undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return new PaginatedResponse<CachedOutstandingBalanceStruct[], LimitedFilteredRequest>({
|
|
111
|
+
results: await AuthenticatedStructures.cachedOutstandingBalances(data),
|
|
112
|
+
next,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
117
|
+
await Context.setOrganizationScope();
|
|
118
|
+
await Context.authenticate();
|
|
119
|
+
|
|
120
|
+
const maxLimit = Context.auth.hasSomePlatformAccess() ? 100 : 100;
|
|
121
|
+
|
|
122
|
+
if (request.query.limit > maxLimit) {
|
|
123
|
+
throw new SimpleError({
|
|
124
|
+
code: 'invalid_field',
|
|
125
|
+
field: 'limit',
|
|
126
|
+
message: 'Limit can not be more than ' + maxLimit,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (request.query.limit < 1) {
|
|
131
|
+
throw new SimpleError({
|
|
132
|
+
code: 'invalid_field',
|
|
133
|
+
field: 'limit',
|
|
134
|
+
message: 'Limit can not be less than 1',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return new Response(
|
|
139
|
+
await GetCachedOutstandingBalanceEndpoint.buildData(request.query),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import {
|
|
3
|
-
import { BalanceItem, Group, Member } from '@stamhoofd/models';
|
|
2
|
+
import { BalanceItem, Member } from '@stamhoofd/models';
|
|
4
3
|
import { BalanceItemWithPayments } from '@stamhoofd/structures';
|
|
5
4
|
|
|
6
5
|
import { Context } from '../../../../helpers/Context';
|
|
@@ -332,21 +332,6 @@ function getSettlementColumns(): XlsxTransformerColumn<PaymentGeneral>[] {
|
|
|
332
332
|
};
|
|
333
333
|
},
|
|
334
334
|
},
|
|
335
|
-
{
|
|
336
|
-
id: 'settlement.fee',
|
|
337
|
-
name: 'Uitbetalingstransactiekosten',
|
|
338
|
-
width: 25,
|
|
339
|
-
getValue: (object: PaymentGeneralWithStripeAccount) => {
|
|
340
|
-
return {
|
|
341
|
-
value: object.settlement?.fee !== undefined ? (object.settlement?.fee / 100) : null,
|
|
342
|
-
style: {
|
|
343
|
-
numberFormat: {
|
|
344
|
-
id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
};
|
|
348
|
-
},
|
|
349
|
-
},
|
|
350
335
|
];
|
|
351
336
|
}
|
|
352
337
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, PatchMap } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
3
|
import { BalanceItem, CachedOutstandingBalance, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
|
|
4
|
-
import { AccessRight, FinancialSupportSettings, GroupCategory, GroupStatus,
|
|
4
|
+
import { AccessRight, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from '@stamhoofd/structures';
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
|
+
import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* One class with all the responsabilities of checking permissions to each resource in the system by a given user, possibly in an organization context.
|
|
@@ -195,120 +196,11 @@ export class AdminPermissionChecker {
|
|
|
195
196
|
* @throws error if not allowed to write this event
|
|
196
197
|
*/
|
|
197
198
|
async checkEventAccess(event: Event): Promise<Organization | null> {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
let organizationPermissions: LoadedPermissions | null = null;
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
organization = await this.getOrganization(event.organizationId);
|
|
207
|
-
organizationPermissions = await this.getOrganizationPermissions(organization);
|
|
208
|
-
}
|
|
209
|
-
catch (error) {
|
|
210
|
-
console.error(error);
|
|
211
|
-
throw new SimpleError({
|
|
212
|
-
code: 'not_found',
|
|
213
|
-
message: 'Event not found',
|
|
214
|
-
human: 'De activiteit werd niet gevonden',
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (organizationPermissions === null) {
|
|
219
|
-
throw new SimpleError({
|
|
220
|
-
code: 'permission_denied',
|
|
221
|
-
message: 'Je hebt geen toegangsrechten om een activiteit te beheren voor deze organisatie.',
|
|
222
|
-
statusCode: 403,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (event.meta.groups === null) {
|
|
227
|
-
if (!organizationPermissions.hasResourceAccessRight(PermissionsResourceType.Groups, '', accessRight)) {
|
|
228
|
-
throw new SimpleError({
|
|
229
|
-
code: 'permission_denied',
|
|
230
|
-
message: 'Je hebt geen toegangsrechten om een activiteit te beheren voor deze organisatie.',
|
|
231
|
-
statusCode: 403,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
for (const group of event.meta.groups) {
|
|
237
|
-
if (!organizationPermissions.hasResourceAccessRight(PermissionsResourceType.Groups, group.id, accessRight)) {
|
|
238
|
-
throw new SimpleError({
|
|
239
|
-
code: 'permission_denied',
|
|
240
|
-
message: 'Je hebt geen toegangsrechten om een activiteit te beheren voor deze groep(en).',
|
|
241
|
-
statusCode: 403,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (event.meta.organizationTagIds !== null) {
|
|
248
|
-
// not supported currently
|
|
249
|
-
throw new SimpleError({
|
|
250
|
-
code: 'invalid_field',
|
|
251
|
-
message: 'Een activiteit voor een organisatie kan geen tags bevatten.',
|
|
252
|
-
statusCode: 403,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (event.meta.defaultAgeGroupIds !== null) {
|
|
257
|
-
// not supported currently
|
|
258
|
-
throw new SimpleError({
|
|
259
|
-
code: 'invalid_field',
|
|
260
|
-
message: 'Een activiteit voor een organisatie kan niet beperkt worden tot specifieke standaard leeftijdsgroepen.',
|
|
261
|
-
statusCode: 403,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return organization;
|
|
266
|
-
}
|
|
267
|
-
// #endregion
|
|
268
|
-
|
|
269
|
-
// #region platform
|
|
270
|
-
if (event.meta.groups !== null) {
|
|
271
|
-
// not supported currently
|
|
272
|
-
throw new SimpleError({
|
|
273
|
-
code: 'permission_denied',
|
|
274
|
-
message: 'Een nationale of regionale activiteit kan (momenteel) niet beperkt worden tot specifieke groepen.',
|
|
275
|
-
statusCode: 403,
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const platformPermissions = this.platformPermissions;
|
|
280
|
-
if (!platformPermissions) {
|
|
281
|
-
throw new SimpleError({
|
|
282
|
-
code: 'permission_denied',
|
|
283
|
-
message: 'Je hebt geen toegangsrechten om een nationale of regionale activiteit te beheren.',
|
|
284
|
-
statusCode: 403,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// organization tags
|
|
289
|
-
if (event.meta.organizationTagIds === null) {
|
|
290
|
-
if (!(platformPermissions.hasAccessRight(accessRight) || platformPermissions.hasResourceAccessRight(PermissionsResourceType.OrganizationTags, '', accessRight))) {
|
|
291
|
-
throw new SimpleError({
|
|
292
|
-
code: 'permission_denied',
|
|
293
|
-
message: 'Je kan geen nationale activiteiten beheren',
|
|
294
|
-
statusCode: 403,
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
for (const tagId of event.meta.organizationTagIds) {
|
|
300
|
-
if (!platformPermissions.hasResourceAccessRight(PermissionsResourceType.OrganizationTags, tagId, accessRight)) {
|
|
301
|
-
throw new SimpleError({
|
|
302
|
-
code: 'permission_denied',
|
|
303
|
-
message: "Je hebt geen toegangsrechten om een nationale of regionale activiteit te beheren voor deze regio('s).",
|
|
304
|
-
statusCode: 403,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
// #endregion
|
|
310
|
-
|
|
311
|
-
return null;
|
|
199
|
+
return await EventPermissionChecker.checkEventAccessAsync(event, {
|
|
200
|
+
userPermissions: this.user.permissions,
|
|
201
|
+
platform: this.platform,
|
|
202
|
+
getOrganization: async (id: string) => await this.getOrganization(id),
|
|
203
|
+
});
|
|
312
204
|
}
|
|
313
205
|
|
|
314
206
|
async canAccessArchivedGroups(organizationId: string) {
|
|
@@ -333,9 +225,9 @@ export class AdminPermissionChecker {
|
|
|
333
225
|
return true;
|
|
334
226
|
}
|
|
335
227
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
228
|
+
// Temporary access
|
|
229
|
+
if (hasTemporaryMemberAccess(this.user.id, member.id, permissionLevel)) {
|
|
230
|
+
console.log('User has temporary access to member', member.id, 'for user', this.user.id);
|
|
339
231
|
return true;
|
|
340
232
|
}
|
|
341
233
|
|
|
@@ -348,6 +240,15 @@ export class AdminPermissionChecker {
|
|
|
348
240
|
return false;
|
|
349
241
|
}
|
|
350
242
|
|
|
243
|
+
/**
|
|
244
|
+
* The server will temporarily grant the user access to this member, and store this in the server
|
|
245
|
+
* memory. This is required for adding new members to an organization (first add member -> then patch with registrations, which requires write access).
|
|
246
|
+
*/
|
|
247
|
+
async temporarilyGrantMemberAccess(member: MemberWithRegistrations, permissionLevel: PermissionLevel = PermissionLevel.Write) {
|
|
248
|
+
console.log('Temporarily granting access to member', member.id, 'for user', this.user.id);
|
|
249
|
+
addTemporaryMemberAccess(this.user.id, member.id, permissionLevel);
|
|
250
|
+
}
|
|
251
|
+
|
|
351
252
|
/**
|
|
352
253
|
* Only full admins can delete members permanently
|
|
353
254
|
*/
|
|
@@ -1249,6 +1150,10 @@ export class AdminPermissionChecker {
|
|
|
1249
1150
|
return !!this.platformPermissions && !!this.platformPermissions.hasAccessRight(AccessRight.PlatformLoginAs);
|
|
1250
1151
|
}
|
|
1251
1152
|
|
|
1153
|
+
canAccess(accessRight: AccessRight): boolean {
|
|
1154
|
+
return !!this.platformPermissions && !!this.platformPermissions.hasAccessRight(accessRight);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1252
1157
|
hasPlatformFullAccess(): boolean {
|
|
1253
1158
|
return !!this.platformPermissions && !!this.platformPermissions.hasFullAccess();
|
|
1254
1159
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
-
import { Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
|
|
3
|
-
import { Event as EventStruct, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
2
|
+
import { CachedOutstandingBalance, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
|
|
3
|
+
import { AccessRight, CachedOutstandingBalanceObject, CachedOutstandingBalanceObjectContact, CachedOutstandingBalance as CachedOutstandingBalanceStruct, CachedOutstandingBalanceType, Event as EventStruct, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { Context } from './Context';
|
|
@@ -422,4 +422,40 @@ export class AuthenticatedStructures {
|
|
|
422
422
|
|
|
423
423
|
return result;
|
|
424
424
|
}
|
|
425
|
+
|
|
426
|
+
static async cachedOutstandingBalances(balances: CachedOutstandingBalance[]): Promise<CachedOutstandingBalanceStruct[]> {
|
|
427
|
+
if (balances.length === 0) {
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const organizationIds = Formatter.uniqueArray(balances.filter(b => b.objectType === CachedOutstandingBalanceType.organization).map(b => b.objectId));
|
|
432
|
+
const organizations = organizationIds.length > 0 ? await Organization.getByIDs(...organizationIds) : [];
|
|
433
|
+
const admins = await User.getAdmins(organizationIds, { verified: true });
|
|
434
|
+
|
|
435
|
+
const organizationStructs = await this.organizations(organizations);
|
|
436
|
+
|
|
437
|
+
const result: CachedOutstandingBalanceStruct[] = [];
|
|
438
|
+
for (const balance of balances) {
|
|
439
|
+
const organization = organizationStructs.find(o => o.id == balance.objectId) ?? null;
|
|
440
|
+
let thisAdmins: User[] = [];
|
|
441
|
+
if (organization) {
|
|
442
|
+
thisAdmins = admins.filter(a => a.permissions && a.permissions.forOrganization(organization)?.hasAccessRight(AccessRight.OrganizationFinanceDirector));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const struct = CachedOutstandingBalanceStruct.create({
|
|
446
|
+
...balance,
|
|
447
|
+
object: CachedOutstandingBalanceObject.create({
|
|
448
|
+
name: organization?.name ?? 'Onbekend',
|
|
449
|
+
contacts: thisAdmins.map(a => CachedOutstandingBalanceObjectContact.create({
|
|
450
|
+
name: a.name ?? '',
|
|
451
|
+
emails: [a.email],
|
|
452
|
+
})),
|
|
453
|
+
}),
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
result.push(struct);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
425
461
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getPermissionLevelNumber, PermissionLevel } from '@stamhoofd/structures';
|
|
2
|
+
|
|
3
|
+
const userMemberTemporaryAccessCache = new Map<string, { memberId: string; level: PermissionLevel; validUntil: Date }[]>();
|
|
4
|
+
|
|
5
|
+
export function hasTemporaryMemberAccess(userId: string, memberId, level: PermissionLevel) {
|
|
6
|
+
const list = userMemberTemporaryAccessCache.get(userId);
|
|
7
|
+
if (!list) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const d = new Date();
|
|
11
|
+
return !!list.find(m =>
|
|
12
|
+
m.memberId === memberId
|
|
13
|
+
&& getPermissionLevelNumber(m.level) >= getPermissionLevelNumber(level)
|
|
14
|
+
&& m.validUntil > d,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function addTemporaryMemberAccess(userId: string, memberId, level: PermissionLevel, timeout: number = 1000 * 60 * 60 * 24) {
|
|
19
|
+
deleteExpiredTemporaryMemberAccess();
|
|
20
|
+
let list = userMemberTemporaryAccessCache.get(userId);
|
|
21
|
+
if (!list) {
|
|
22
|
+
list = [];
|
|
23
|
+
userMemberTemporaryAccessCache.set(userId, list);
|
|
24
|
+
}
|
|
25
|
+
list.push({ memberId, level, validUntil: new Date(Date.now() + timeout) });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function deleteExpiredTemporaryMemberAccess() {
|
|
29
|
+
const d = new Date();
|
|
30
|
+
for (const [userId, list] of userMemberTemporaryAccessCache) {
|
|
31
|
+
const filtered = list.filter(m => m.validUntil > d);
|
|
32
|
+
|
|
33
|
+
if (filtered.length === 0) {
|
|
34
|
+
userMemberTemporaryAccessCache.delete(userId);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
userMemberTemporaryAccessCache.set(userId, filtered);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -38,7 +38,7 @@ export class ViesHelperStatic {
|
|
|
38
38
|
patch.VATNumber = await ViesHelper.checkVATNumber(company.address.country, company.VATNumber);
|
|
39
39
|
|
|
40
40
|
if (company.address.country === Country.Belgium) {
|
|
41
|
-
patch.companyNumber = company.VATNumber;
|
|
41
|
+
patch.companyNumber = company.VATNumber.substring(2);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -66,8 +66,14 @@ export class ViesHelperStatic {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// In Belgium, the company number syntax is the same as VAT number
|
|
69
|
+
let correctedVATNumber = companyNumber;
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
if (companyNumber.length > 2 && companyNumber.substr(0, 2) !== country) {
|
|
72
|
+
// Add required country in VAT number
|
|
73
|
+
correctedVATNumber = country + companyNumber;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = jsvat.checkVAT(correctedVATNumber, [jsvat.belgium]);
|
|
71
77
|
|
|
72
78
|
if (!result.isValid) {
|
|
73
79
|
throw new SimpleError({
|
|
@@ -79,11 +85,11 @@ export class ViesHelperStatic {
|
|
|
79
85
|
|
|
80
86
|
// If this is a valid VAT number, we can assume it's a valid company number
|
|
81
87
|
try {
|
|
82
|
-
const corrected = await this.checkVATNumber(Country.Belgium,
|
|
88
|
+
const corrected = await this.checkVATNumber(Country.Belgium, correctedVATNumber);
|
|
83
89
|
|
|
84
90
|
// this is a VAT number, not a company number
|
|
85
91
|
return {
|
|
86
|
-
companyNumber: corrected,
|
|
92
|
+
companyNumber: corrected.substring(2),
|
|
87
93
|
VATNumber: corrected,
|
|
88
94
|
};
|
|
89
95
|
}
|
|
@@ -98,7 +104,7 @@ export class ViesHelperStatic {
|
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
return {
|
|
101
|
-
companyNumber: result.value ?? companyNumber,
|
|
107
|
+
companyNumber: result.value?.substring(2) ?? companyNumber,
|
|
102
108
|
|
|
103
109
|
// VATNumber should always be set to null if it is not a valid VAT number
|
|
104
110
|
VATNumber: null,
|
|
@@ -144,6 +150,12 @@ export class ViesHelperStatic {
|
|
|
144
150
|
}
|
|
145
151
|
// Unavailable: ignore for now
|
|
146
152
|
console.error('VIES error', e);
|
|
153
|
+
|
|
154
|
+
throw new SimpleError({
|
|
155
|
+
code: 'service_unavailable',
|
|
156
|
+
message: 'De BTW-nummer validatie service (VIES) is tijdelijk niet beschikbaar. Probeer het later opnieuw.',
|
|
157
|
+
field: 'VATNumber',
|
|
158
|
+
});
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
return formatted;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterCompiler } from '@stamhoofd/sql';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Defines how to filter cached balance items in the database from StamhoofdFilter objects
|
|
5
|
+
*/
|
|
6
|
+
export const cachedOutstandingBalanceFilterCompilers: SQLFilterDefinitions = {
|
|
7
|
+
...baseSQLFilterCompilers,
|
|
8
|
+
id: createSQLColumnFilterCompiler('id'),
|
|
9
|
+
organizationId: createSQLColumnFilterCompiler('organizationId'),
|
|
10
|
+
objectType: createSQLColumnFilterCompiler('objectType'),
|
|
11
|
+
amount: createSQLColumnFilterCompiler('amount'),
|
|
12
|
+
amountPending: createSQLColumnFilterCompiler('amountPending'),
|
|
13
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { CachedOutstandingBalance } from '@stamhoofd/models';
|
|
2
|
+
import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
3
|
+
|
|
4
|
+
export const cachedOutstandingBalanceSorters: SQLSortDefinitions<CachedOutstandingBalance> = {
|
|
5
|
+
// WARNING! TEST NEW SORTERS THOROUGHLY!
|
|
6
|
+
// Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
|
|
7
|
+
// An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
|
|
8
|
+
// You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
|
|
9
|
+
// Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
|
|
10
|
+
// And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
|
|
11
|
+
// What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
|
|
12
|
+
|
|
13
|
+
id: {
|
|
14
|
+
getValue(a) {
|
|
15
|
+
return a.id;
|
|
16
|
+
},
|
|
17
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
18
|
+
return new SQLOrderBy({
|
|
19
|
+
column: SQL.column('id'),
|
|
20
|
+
direction,
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
amount: {
|
|
25
|
+
getValue(a) {
|
|
26
|
+
return a.amount;
|
|
27
|
+
},
|
|
28
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
29
|
+
return new SQLOrderBy({
|
|
30
|
+
column: SQL.column('amount'),
|
|
31
|
+
direction,
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
amountPending: {
|
|
36
|
+
getValue(a) {
|
|
37
|
+
return a.amountPending;
|
|
38
|
+
},
|
|
39
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
40
|
+
return new SQLOrderBy({
|
|
41
|
+
column: SQL.column('amountPending'),
|
|
42
|
+
direction,
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|