@stamhoofd/backend 2.8.0 → 2.13.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/.env.template.json +3 -1
- package/package.json +11 -3
- package/src/crons.ts +3 -3
- package/src/decoders/StringArrayDecoder.ts +24 -0
- package/src/decoders/StringNullableDecoder.ts +18 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +20 -18
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
- package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -9
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +15 -62
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
- package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +165 -35
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +56 -3
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +292 -170
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
- package/src/endpoints/organization/dashboard/payments/legacy/GetPaymentsEndpoint.ts +170 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
- package/src/helpers/AdminPermissionChecker.ts +95 -60
- package/src/helpers/AuthenticatedStructures.ts +16 -6
- package/src/helpers/Context.ts +21 -0
- package/src/helpers/EmailResumer.ts +22 -2
- package/src/helpers/MemberUserSyncer.ts +8 -2
- package/src/helpers/ViesHelper.ts +151 -0
- package/src/seeds/1722344160-update-membership.ts +19 -22
- package/src/seeds/1722344161-sync-member-users.ts +60 -0
- package/.env.json +0 -65
|
@@ -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();
|
|
@@ -22,33 +22,30 @@ export default new Migration(async () => {
|
|
|
22
22
|
value: id,
|
|
23
23
|
sign: '>'
|
|
24
24
|
}
|
|
25
|
-
}, {limit:
|
|
26
|
-
|
|
27
|
-
// const members = await Member.getByIDs(...rawMembers.map(m => m.id));
|
|
28
|
-
|
|
29
|
-
for (const member of rawMembers) {
|
|
30
|
-
const memberWithRegistrations = await Member.getWithRegistrations(member.id);
|
|
31
|
-
if(memberWithRegistrations) {
|
|
32
|
-
await memberWithRegistrations.updateMemberships();
|
|
33
|
-
await memberWithRegistrations.save();
|
|
34
|
-
} else {
|
|
35
|
-
throw new Error("Member with registrations not found: " + member.id);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
c++;
|
|
39
|
-
|
|
40
|
-
if (c%1000 === 0) {
|
|
41
|
-
process.stdout.write('.');
|
|
42
|
-
}
|
|
43
|
-
if (c%10000 === 0) {
|
|
44
|
-
process.stdout.write('\n');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
25
|
+
}, {limit: 500, sort: ['id']});
|
|
47
26
|
|
|
48
27
|
if (rawMembers.length === 0) {
|
|
49
28
|
break;
|
|
50
29
|
}
|
|
51
30
|
|
|
31
|
+
const promises: Promise<any>[] = [];
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
for (const member of rawMembers) {
|
|
35
|
+
promises.push((async () => {
|
|
36
|
+
await Member.updateMembershipsForId(member.id, true);
|
|
37
|
+
c++;
|
|
38
|
+
|
|
39
|
+
if (c%1000 === 0) {
|
|
40
|
+
process.stdout.write('.');
|
|
41
|
+
}
|
|
42
|
+
if (c%10000 === 0) {
|
|
43
|
+
process.stdout.write('\n');
|
|
44
|
+
}
|
|
45
|
+
})())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await Promise.all(promises);
|
|
52
49
|
id = rawMembers[rawMembers.length - 1].id;
|
|
53
50
|
}
|
|
54
51
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Member } from '@stamhoofd/models';
|
|
3
|
+
import { MemberUserSyncer } from '../helpers/MemberUserSyncer';
|
|
4
|
+
import { logger } from '@simonbackx/simple-logging';
|
|
5
|
+
|
|
6
|
+
export default new Migration(async () => {
|
|
7
|
+
if (STAMHOOFD.environment == "test") {
|
|
8
|
+
console.log("skipped in tests")
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if(STAMHOOFD.userMode !== "platform") {
|
|
13
|
+
console.log("skipped seed update-membership because usermode not platform")
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
process.stdout.write('\n');
|
|
18
|
+
let c = 0;
|
|
19
|
+
let id: string = '';
|
|
20
|
+
|
|
21
|
+
await logger.setContext({tags: ['silent-seed', 'seed']}, async () => {
|
|
22
|
+
while(true) {
|
|
23
|
+
const rawMembers = await Member.where({
|
|
24
|
+
id: {
|
|
25
|
+
value: id,
|
|
26
|
+
sign: '>'
|
|
27
|
+
}
|
|
28
|
+
}, {limit: 500, sort: ['id']});
|
|
29
|
+
|
|
30
|
+
if (rawMembers.length === 0) {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const membersWithRegistrations = await Member.getBlobByIds(...rawMembers.map(m => m.id));
|
|
35
|
+
|
|
36
|
+
const promises: Promise<any>[] = [];
|
|
37
|
+
|
|
38
|
+
for (const memberWithRegistrations of membersWithRegistrations) {
|
|
39
|
+
promises.push((async () => {
|
|
40
|
+
await MemberUserSyncer.onChangeMember(memberWithRegistrations);
|
|
41
|
+
c++;
|
|
42
|
+
|
|
43
|
+
if (c%1000 === 0) {
|
|
44
|
+
process.stdout.write('.');
|
|
45
|
+
}
|
|
46
|
+
if (c%10000 === 0) {
|
|
47
|
+
process.stdout.write('\n');
|
|
48
|
+
}
|
|
49
|
+
})());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await Promise.all(promises);
|
|
53
|
+
id = rawMembers[rawMembers.length - 1].id;
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// Do something here
|
|
59
|
+
return Promise.resolve()
|
|
60
|
+
})
|
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
|
-
}
|