@stamhoofd/backend 2.9.0 → 2.14.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 -2
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +6 -18
- package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -9
- package/src/endpoints/global/members/GetMembersEndpoint.ts +15 -31
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +52 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +54 -2
- package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +294 -134
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/payments/legacy/GetPaymentsEndpoint.ts +170 -0
- package/src/helpers/AdminPermissionChecker.ts +88 -68
- package/src/helpers/MemberUserSyncer.ts +8 -2
- package/src/helpers/ViesHelper.ts +151 -0
- package/.env.json +0 -65
|
@@ -814,7 +814,7 @@ export class AdminPermissionChecker {
|
|
|
814
814
|
async hasFinancialMemberAccess(member: MemberWithRegistrations, level: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
|
|
815
815
|
const isUserManager = this.isUserManager(member)
|
|
816
816
|
|
|
817
|
-
if (isUserManager) {
|
|
817
|
+
if (isUserManager && level === PermissionLevel.Read) {
|
|
818
818
|
return true;
|
|
819
819
|
}
|
|
820
820
|
|
|
@@ -846,6 +846,15 @@ export class AdminPermissionChecker {
|
|
|
846
846
|
continue;
|
|
847
847
|
}
|
|
848
848
|
|
|
849
|
+
if (isUserManager) {
|
|
850
|
+
// Requirements are higher: you need financial access to write your own financial
|
|
851
|
+
// data changes
|
|
852
|
+
if (permissions.hasAccessRight(AccessRight.OrganizationManagePayments)) {
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
|
|
849
858
|
if (permissions.hasAccessRight(level === PermissionLevel.Read ? AccessRight.MemberReadFinancialData : AccessRight.MemberWriteFinancialData)) {
|
|
850
859
|
return true;
|
|
851
860
|
}
|
|
@@ -854,8 +863,16 @@ export class AdminPermissionChecker {
|
|
|
854
863
|
// Platform data
|
|
855
864
|
const platformPermissions = this.platformPermissions
|
|
856
865
|
if (platformPermissions) {
|
|
857
|
-
if (
|
|
858
|
-
|
|
866
|
+
if (isUserManager) {
|
|
867
|
+
// Requirements are higher: you need financial access to write your own financial
|
|
868
|
+
// data changes
|
|
869
|
+
if (platformPermissions.hasAccessRight(AccessRight.OrganizationManagePayments)) {
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
} else {
|
|
873
|
+
if (platformPermissions.hasAccessRight(level === PermissionLevel.Read ? AccessRight.MemberReadFinancialData : AccessRight.MemberWriteFinancialData)) {
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
859
876
|
}
|
|
860
877
|
}
|
|
861
878
|
|
|
@@ -950,94 +967,97 @@ export class AdminPermissionChecker {
|
|
|
950
967
|
const hasRecordAnswers = !!data.details.recordAnswers;
|
|
951
968
|
const hasNotes = data.details.notes !== undefined;
|
|
952
969
|
const isSetFinancialSupportTrue = data.details.requiresFinancialSupport?.value === true;
|
|
970
|
+
const isUserManager = this.isUserManager(member);
|
|
953
971
|
|
|
954
|
-
if(hasRecordAnswers
|
|
955
|
-
|
|
972
|
+
if (hasRecordAnswers) {
|
|
973
|
+
if (!(data.details.recordAnswers instanceof PatchMap)) {
|
|
974
|
+
throw new SimpleError({
|
|
975
|
+
code: 'invalid_request',
|
|
976
|
+
message: 'Cannot PUT recordAnswers',
|
|
977
|
+
statusCode: 400
|
|
978
|
+
})
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
|
|
956
982
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
|
|
967
|
-
|
|
968
|
-
for (const [key, value] of data.details.recordAnswers.entries()) {
|
|
969
|
-
let name: string | undefined = undefined
|
|
970
|
-
if (value) {
|
|
971
|
-
if (value.isPatch()) {
|
|
972
|
-
throw new SimpleError({
|
|
973
|
-
code: 'invalid_request',
|
|
974
|
-
message: 'Cannot PATCH a record answer object',
|
|
975
|
-
statusCode: 400
|
|
976
|
-
})
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
const id = value.settings.id
|
|
980
|
-
|
|
981
|
-
if (id !== key) {
|
|
982
|
-
throw new SimpleError({
|
|
983
|
-
code: 'invalid_request',
|
|
984
|
-
message: 'Record answer key does not match record id',
|
|
985
|
-
statusCode: 400
|
|
986
|
-
})
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
name = value.settings.name
|
|
983
|
+
for (const [key, value] of data.details.recordAnswers.entries()) {
|
|
984
|
+
let name: string | undefined = undefined
|
|
985
|
+
if (value) {
|
|
986
|
+
if (value.isPatch()) {
|
|
987
|
+
throw new SimpleError({
|
|
988
|
+
code: 'invalid_request',
|
|
989
|
+
message: 'Cannot PATCH a record answer object',
|
|
990
|
+
statusCode: 400
|
|
991
|
+
})
|
|
990
992
|
}
|
|
991
993
|
|
|
992
|
-
|
|
994
|
+
const id = value.settings.id
|
|
995
|
+
|
|
996
|
+
if (id !== key) {
|
|
993
997
|
throw new SimpleError({
|
|
994
|
-
code: '
|
|
995
|
-
message:
|
|
998
|
+
code: 'invalid_request',
|
|
999
|
+
message: 'Record answer key does not match record id',
|
|
996
1000
|
statusCode: 400
|
|
997
1001
|
})
|
|
998
1002
|
}
|
|
1003
|
+
|
|
1004
|
+
name = value.settings.name
|
|
999
1005
|
}
|
|
1000
|
-
}
|
|
1001
1006
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1007
|
+
if (!isUserManager && !records.has(key)) {
|
|
1008
|
+
throw new SimpleError({
|
|
1009
|
+
code: 'permission_denied',
|
|
1010
|
+
message: `Je hebt geen toegangsrechten om het antwoord op ${name ?? 'deze vraag'} aan te passen voor dit lid`,
|
|
1011
|
+
statusCode: 400
|
|
1012
|
+
})
|
|
1013
|
+
}
|
|
1008
1014
|
}
|
|
1015
|
+
}
|
|
1009
1016
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1017
|
+
if (hasNotes && isUserManager) {
|
|
1018
|
+
throw new SimpleError({
|
|
1019
|
+
code: 'permission_denied',
|
|
1020
|
+
message: 'Cannot edit notes',
|
|
1021
|
+
statusCode: 400
|
|
1022
|
+
})
|
|
1023
|
+
}
|
|
1013
1024
|
|
|
1025
|
+
// Has financial write access?
|
|
1026
|
+
if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Write)) {
|
|
1027
|
+
if (isUserManager && isSetFinancialSupportTrue) {
|
|
1028
|
+
const financialSupportSettings = this.platform.config.financialSupport;
|
|
1029
|
+
const preventSelfAssignment = financialSupportSettings?.preventSelfAssignment === true;
|
|
1030
|
+
|
|
1014
1031
|
if(preventSelfAssignment) {
|
|
1015
1032
|
throw new SimpleError({
|
|
1016
1033
|
code: 'permission_denied',
|
|
1017
|
-
message: '
|
|
1018
|
-
human:
|
|
1034
|
+
message: 'No permissions to enable financial support for your own members',
|
|
1035
|
+
human: financialSupportSettings.preventSelfAssignmentText ?? FinancialSupportSettings.defaultPreventSelfAssignmentText,
|
|
1019
1036
|
statusCode: 400
|
|
1020
1037
|
});
|
|
1021
1038
|
}
|
|
1022
1039
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
// Has financial write access?
|
|
1026
|
-
if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Write)) {
|
|
1040
|
+
|
|
1027
1041
|
if (data.details.requiresFinancialSupport) {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1042
|
+
if (isUserManager) {
|
|
1043
|
+
// Already handled
|
|
1044
|
+
} else {
|
|
1045
|
+
throw new SimpleError({
|
|
1046
|
+
code: 'permission_denied',
|
|
1047
|
+
message: 'Je hebt geen toegangsrechten om de financiële status van dit lid aan te passen',
|
|
1048
|
+
statusCode: 400
|
|
1049
|
+
})
|
|
1050
|
+
}
|
|
1033
1051
|
}
|
|
1034
1052
|
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1053
|
+
if (!isUserManager) {
|
|
1054
|
+
if (data.details.uitpasNumber) {
|
|
1055
|
+
throw new SimpleError({
|
|
1056
|
+
code: 'permission_denied',
|
|
1057
|
+
message: 'Je hebt geen toegangsrechten om het UiTPAS-nummer van dit lid aan te passen',
|
|
1058
|
+
statusCode: 400
|
|
1059
|
+
})
|
|
1060
|
+
}
|
|
1041
1061
|
}
|
|
1042
1062
|
|
|
1043
1063
|
if (data.outstandingBalance) {
|
|
@@ -36,8 +36,14 @@ export class MemberUserSyncerStatic {
|
|
|
36
36
|
} else {
|
|
37
37
|
// Only auto unlink users that do not have an account
|
|
38
38
|
for (const user of member.users) {
|
|
39
|
-
if (!
|
|
40
|
-
|
|
39
|
+
if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
|
|
40
|
+
if (!user.hasAccount()) {
|
|
41
|
+
await this.unlinkUser(user, member)
|
|
42
|
+
} else {
|
|
43
|
+
// Make sure only linked as a parent, not as user self
|
|
44
|
+
// This makes sure we don't inherit permissions and aren't counted as 'begin' the member
|
|
45
|
+
await this.linkUser(user.email, member, true)
|
|
46
|
+
}
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
+
import { Company, Country } from '@stamhoofd/structures';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import * as jsvat from 'jsvat-next'; // has no default export, so we need the wildcard
|
|
6
|
+
|
|
7
|
+
export class ViesHelperStatic {
|
|
8
|
+
testMode = false;
|
|
9
|
+
|
|
10
|
+
async request(method: "GET" | "POST", url: string, content: any) {
|
|
11
|
+
|
|
12
|
+
const json = content ? JSON.stringify(content) : "";
|
|
13
|
+
|
|
14
|
+
console.log("[VIES REQUEST]", method, url, content ? "\n [VIES REQUEST] " : undefined, json)
|
|
15
|
+
|
|
16
|
+
const response = await axios.request({
|
|
17
|
+
method,
|
|
18
|
+
url,
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': json.length > 0 ? 'application/json' : "text/plain",
|
|
21
|
+
},
|
|
22
|
+
data: json
|
|
23
|
+
|
|
24
|
+
})
|
|
25
|
+
console.log("[VIES RESPONSE]", method, url, "\n[VIES RESPONSE]", JSON.stringify(response.data))
|
|
26
|
+
return response.data
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async checkCompany(company: Company, patch: AutoEncoderPatchType<Company>|Company) {
|
|
30
|
+
if (!company.address) {
|
|
31
|
+
// Not allowed to set
|
|
32
|
+
patch.companyNumber = null;
|
|
33
|
+
patch.VATNumber = null;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (company.VATNumber !== null) {
|
|
38
|
+
// Changed VAT number
|
|
39
|
+
patch.VATNumber = await ViesHelper.checkVATNumber(company.address.country, company.VATNumber)
|
|
40
|
+
|
|
41
|
+
if (company.address.country === Country.Belgium) {
|
|
42
|
+
patch.companyNumber = company.VATNumber
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (company.companyNumber) {
|
|
47
|
+
if (company.VATNumber !== null && company.address.country === Country.Belgium) {
|
|
48
|
+
// Already validated
|
|
49
|
+
} else {
|
|
50
|
+
// Need to validate
|
|
51
|
+
const result = await ViesHelper.checkCompanyNumber(company.address.country, company.companyNumber)
|
|
52
|
+
patch.companyNumber = result.companyNumber
|
|
53
|
+
if (result.VATNumber !== undefined) {
|
|
54
|
+
patch.VATNumber = result.VATNumber
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async checkCompanyNumber(country: Country, companyNumber: string): Promise<{companyNumber: string|null, VATNumber?: string|null}> {
|
|
61
|
+
if (country !== Country.Belgium) {
|
|
62
|
+
// Not supported
|
|
63
|
+
return {
|
|
64
|
+
companyNumber
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// In Belgium, the company number syntax is the same as VAT number
|
|
69
|
+
|
|
70
|
+
const result = jsvat.checkVAT(companyNumber, [jsvat.belgium]);
|
|
71
|
+
|
|
72
|
+
if (!result.isValid) {
|
|
73
|
+
throw new SimpleError({
|
|
74
|
+
"code": "invalid_field",
|
|
75
|
+
"message": "Ongeldig ondernemingsnummer: " + companyNumber,
|
|
76
|
+
"field": "companyNumber"
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If this is a valid VAT number, we can assume it's a valid company number
|
|
81
|
+
try {
|
|
82
|
+
const corrected = await this.checkVATNumber(Country.Belgium, companyNumber);
|
|
83
|
+
|
|
84
|
+
// this is a VAT number, not a company number
|
|
85
|
+
return {
|
|
86
|
+
companyNumber: corrected,
|
|
87
|
+
VATNumber: corrected
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (isSimpleError(e) || isSimpleErrors(e)) {
|
|
91
|
+
// Ignore: normal that it is not a valid VAT number
|
|
92
|
+
} else {
|
|
93
|
+
// Other errors should be thrown
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
companyNumber: result.value ?? companyNumber,
|
|
100
|
+
|
|
101
|
+
// VATNumber should always be set to null if it is not a valid VAT number
|
|
102
|
+
VATNumber: null
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async checkVATNumber(country: Country, vatNumber: string): Promise<string> {
|
|
107
|
+
const result = jsvat.checkVAT(vatNumber, country === Country.Belgium ? [jsvat.belgium] : [jsvat.netherlands]);
|
|
108
|
+
|
|
109
|
+
if (!result.isValid) {
|
|
110
|
+
throw new SimpleError({
|
|
111
|
+
"code": "invalid_field",
|
|
112
|
+
"message": "Ongeldig BTW-nummer: " + vatNumber,
|
|
113
|
+
"field": "VATNumber"
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const formatted = result.value ?? vatNumber;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const cleaned = formatted.substring(2).replace(/(\.\-\s)+/g, "")
|
|
121
|
+
const response = await this.request("POST", "https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number", {
|
|
122
|
+
countryCode: country,
|
|
123
|
+
vatNumber: cleaned
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (typeof response !== 'object' || response === null || typeof response.valid !== 'boolean') {
|
|
127
|
+
// APi error
|
|
128
|
+
throw new Error("Invalid response from VIES")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!response.valid) {
|
|
132
|
+
throw new SimpleError({
|
|
133
|
+
"code": "invalid_field",
|
|
134
|
+
"message": "Het opgegeven BTW-nummer is ongeldig of niet BTW-plichtig: " + formatted,
|
|
135
|
+
"field": "VATNumber"
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (isSimpleError(e) || isSimpleErrors(e)) {
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
142
|
+
// Unavailable: ignore for now
|
|
143
|
+
console.error('VIES error', e);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return formatted;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const ViesHelper = new ViesHelperStatic();
|
package/.env.json
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"environment": "development",
|
|
3
|
-
"domains": {
|
|
4
|
-
"dashboard": "dashboard.stamhoofd",
|
|
5
|
-
"registration": {
|
|
6
|
-
"": "be.stamhoofd",
|
|
7
|
-
"BE": "be.stamhoofd",
|
|
8
|
-
"NL": "nl.stamhoofd"
|
|
9
|
-
},
|
|
10
|
-
"marketing": {
|
|
11
|
-
"": "www.be.stamhoofd",
|
|
12
|
-
"BE": "www.be.stamhoofd",
|
|
13
|
-
"NL": "www.nl.stamhoofd"
|
|
14
|
-
},
|
|
15
|
-
"webshop": {
|
|
16
|
-
"": "shop.be.stamhoofd",
|
|
17
|
-
"BE": "shop.be.stamhoofd",
|
|
18
|
-
"NL": "shop.nl.stamhoofd"
|
|
19
|
-
},
|
|
20
|
-
"webshopPrefix": "shop",
|
|
21
|
-
"legacyWebshop": "shop.stamhoofd",
|
|
22
|
-
"api": "api.stamhoofd",
|
|
23
|
-
"demoApi": "api.stamhoofd",
|
|
24
|
-
"rendererApi": "renderer.stamhoofd"
|
|
25
|
-
},
|
|
26
|
-
"translationNamespace": "digit",
|
|
27
|
-
"userMode": "platform",
|
|
28
|
-
|
|
29
|
-
"PORT": 9091,
|
|
30
|
-
"DB_HOST": "127.0.0.1",
|
|
31
|
-
"DB_USER": "root",
|
|
32
|
-
"DB_PASS": "root",
|
|
33
|
-
"DB_DATABASE": "ksa-stamhoofd",
|
|
34
|
-
|
|
35
|
-
"SMTP_HOST": "0.0.0.0",
|
|
36
|
-
"SMTP_USERNAME": "username",
|
|
37
|
-
"SMTP_PASSWORD": "password",
|
|
38
|
-
"SMTP_PORT": 1025,
|
|
39
|
-
|
|
40
|
-
"TRANSACTIONAL_SMTP_HOST": "0.0.0.0",
|
|
41
|
-
"TRANSACTIONAL_SMTP_USERNAME": "username",
|
|
42
|
-
"TRANSACTIONAL_SMTP_PASSWORD": "password",
|
|
43
|
-
"TRANSACTIONAL_SMTP_PORT": 1025,
|
|
44
|
-
|
|
45
|
-
"AWS_ACCESS_KEY_ID": "",
|
|
46
|
-
"AWS_SECRET_ACCESS_KEY": "",
|
|
47
|
-
"AWS_REGION": "",
|
|
48
|
-
|
|
49
|
-
"SPACES_ENDPOINT": "",
|
|
50
|
-
"SPACES_BUCKET": "",
|
|
51
|
-
"SPACES_KEY": "",
|
|
52
|
-
"SPACES_SECRET": "",
|
|
53
|
-
|
|
54
|
-
"MOLLIE_CLIENT_ID": "",
|
|
55
|
-
"MOLLIE_SECRET": "",
|
|
56
|
-
"MOLLIE_API_KEY": "",
|
|
57
|
-
"MOLLIE_ORGANIZATION_TOKEN": "",
|
|
58
|
-
|
|
59
|
-
"LATEST_IOS_VERSION": 0,
|
|
60
|
-
"LATEST_ANDROID_VERSION": 0,
|
|
61
|
-
|
|
62
|
-
"NOLT_SSO_SECRET_KEY": "",
|
|
63
|
-
"INTERNAL_SECRET_KEY": "",
|
|
64
|
-
"CRONS_DISABLED": false
|
|
65
|
-
}
|