@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.
Files changed (35) hide show
  1. package/.env.template.json +3 -1
  2. package/package.json +11 -3
  3. package/src/crons.ts +3 -3
  4. package/src/decoders/StringArrayDecoder.ts +24 -0
  5. package/src/decoders/StringNullableDecoder.ts +18 -0
  6. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +20 -18
  7. package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
  8. package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -9
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
  10. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
  11. package/src/endpoints/global/members/GetMembersEndpoint.ts +15 -62
  12. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
  13. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
  14. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +165 -35
  15. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
  16. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
  17. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +56 -3
  18. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
  19. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
  20. package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +43 -0
  21. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +292 -170
  22. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
  23. package/src/endpoints/organization/dashboard/payments/legacy/GetPaymentsEndpoint.ts +170 -0
  24. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
  25. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
  26. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
  27. package/src/helpers/AdminPermissionChecker.ts +95 -60
  28. package/src/helpers/AuthenticatedStructures.ts +16 -6
  29. package/src/helpers/Context.ts +21 -0
  30. package/src/helpers/EmailResumer.ts +22 -2
  31. package/src/helpers/MemberUserSyncer.ts +8 -2
  32. package/src/helpers/ViesHelper.ts +151 -0
  33. package/src/seeds/1722344160-update-membership.ts +19 -22
  34. package/src/seeds/1722344161-sync-member-users.ts +60 -0
  35. 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: 100, sort: ['id']});
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
- }