@stamhoofd/backend 2.36.1 → 2.37.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/addresses/SearchRegionsEndpoint.ts +12 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -0
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +6 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +32 -21
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +2 -0
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +453 -0
- package/src/helpers/AddressValidator.ts +11 -0
- package/src/helpers/AdminPermissionChecker.ts +14 -1
- package/src/seeds/1726494420-update-cached-outstanding-balance-from-items.ts +40 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.37.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.37.0",
|
|
40
|
+
"@stamhoofd/backend-middleware": "2.37.0",
|
|
41
|
+
"@stamhoofd/email": "2.37.0",
|
|
42
|
+
"@stamhoofd/models": "2.37.0",
|
|
43
|
+
"@stamhoofd/queues": "2.37.0",
|
|
44
|
+
"@stamhoofd/sql": "2.37.0",
|
|
45
|
+
"@stamhoofd/structures": "2.37.0",
|
|
46
|
+
"@stamhoofd/utility": "2.37.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": "c01fc82d46188cc4e35a7edb90ee595ff7129e6f"
|
|
64
64
|
}
|
|
@@ -85,6 +85,18 @@ export class SearchRegionsEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
85
85
|
if (StringCompare.typoCount(request.query.query, "Nederland") < 3) {
|
|
86
86
|
countries.push(Country.Netherlands)
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
if (StringCompare.typoCount(request.query.query, "Luxemburg") < 3) {
|
|
90
|
+
countries.push(Country.Luxembourg)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (StringCompare.typoCount(request.query.query, "Duitsland") < 3) {
|
|
94
|
+
countries.push(Country.Germany)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (StringCompare.typoCount(request.query.query, "Frankrijk") < 3) {
|
|
98
|
+
countries.push(Country.France)
|
|
99
|
+
}
|
|
88
100
|
|
|
89
101
|
return new Response(SearchRegions.create({
|
|
90
102
|
cities: loadedCities.map(c => CityStruct.create(Object.assign({...c}, { province: ProvinceStruct.create(c.province) }))),
|
|
@@ -83,6 +83,8 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
83
83
|
model.fromAddress = request.body.fromAddress;
|
|
84
84
|
model.fromName = request.body.fromName;
|
|
85
85
|
|
|
86
|
+
model.validateAttachments()
|
|
87
|
+
|
|
86
88
|
// Check default
|
|
87
89
|
if (JSON.stringify(model.json).length < 3 && model.recipientFilter.filters[0].type && EmailTemplateStruct.getDefaultForRecipient(model.recipientFilter.filters[0].type)) {
|
|
88
90
|
const type = EmailTemplateStruct.getDefaultForRecipient(model.recipientFilter.filters[0].type)
|
|
@@ -89,6 +89,12 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
89
89
|
rebuild = true;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Attachments
|
|
93
|
+
if (request.body.attachments !== undefined) {
|
|
94
|
+
model.attachments = patchObject(model.attachments, request.body.attachments);
|
|
95
|
+
model.validateAttachments()
|
|
96
|
+
}
|
|
97
|
+
|
|
92
98
|
await model.save();
|
|
93
99
|
|
|
94
100
|
if (rebuild) {
|
|
@@ -71,8 +71,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
71
71
|
}
|
|
72
72
|
return null
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
const updateRegistrations = new Map<string, Registration>();
|
|
74
|
+
|
|
76
75
|
const updateMembershipMemberIds = new Set<string>()
|
|
77
76
|
|
|
78
77
|
// Loop all members one by one
|
|
@@ -489,8 +488,30 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
489
488
|
}
|
|
490
489
|
}
|
|
491
490
|
|
|
491
|
+
await PatchOrganizationMembersEndpoint.deleteMembers(request.body.getDeletes())
|
|
492
|
+
|
|
493
|
+
for (const member of members) {
|
|
494
|
+
if (updateMembershipMemberIds.has(member.id)) {
|
|
495
|
+
await member.updateMemberships()
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if(shouldUpdateSetupSteps && organization) {
|
|
500
|
+
SetupStepUpdater.updateForOrganization(organization).catch(console.error);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return new Response(
|
|
504
|
+
await AuthenticatedStructures.membersBlob(members)
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
static async deleteMembers(ids: string[]) {
|
|
509
|
+
const updateGroups = new Set<string>();
|
|
510
|
+
const updateRegistrations = new Map<string, Registration>();
|
|
511
|
+
const updateSteps = new Set<string>();
|
|
512
|
+
|
|
492
513
|
// Loop all members one by one
|
|
493
|
-
for (const id of
|
|
514
|
+
for (const id of ids) {
|
|
494
515
|
const member = await Member.getWithRegistrations(id)
|
|
495
516
|
if (!member || !await Context.auth.canDeleteMember(member)) {
|
|
496
517
|
throw Context.auth.error("Je hebt niet voldoende rechten om dit lid te verwijderen")
|
|
@@ -500,16 +521,12 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
500
521
|
await User.deleteForDeletedMember(member.id)
|
|
501
522
|
await BalanceItem.deleteForDeletedMember(member.id)
|
|
502
523
|
await member.delete()
|
|
503
|
-
shouldUpdateSetupSteps = true
|
|
504
524
|
|
|
505
525
|
for(const registration of member.registrations) {
|
|
506
526
|
const groupId = registration.groupId;
|
|
507
|
-
const group = await getGroup(groupId);
|
|
508
527
|
updateRegistrations.set(registration.id, registration);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
updateGroups.set(group.id, group)
|
|
512
|
-
}
|
|
528
|
+
updateGroups.add(groupId);
|
|
529
|
+
updateSteps.add(registration.organizationId);
|
|
513
530
|
}
|
|
514
531
|
}
|
|
515
532
|
|
|
@@ -517,25 +534,19 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
517
534
|
registration.scheduleStockUpdate();
|
|
518
535
|
}
|
|
519
536
|
|
|
537
|
+
const groups = await Group.getByIDs(...Array.from(updateGroups));
|
|
538
|
+
|
|
520
539
|
// Loop all groups and update occupancy if needed
|
|
521
|
-
for (const group of
|
|
540
|
+
for (const group of groups) {
|
|
522
541
|
await group.updateOccupancy()
|
|
523
542
|
await group.save()
|
|
524
543
|
}
|
|
525
|
-
|
|
526
|
-
for (const member of members) {
|
|
527
|
-
if (updateMembershipMemberIds.has(member.id)) {
|
|
528
|
-
await member.updateMemberships()
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
544
|
|
|
532
|
-
|
|
545
|
+
const organizations = await Organization.getByIDs(...Array.from(updateSteps));
|
|
546
|
+
|
|
547
|
+
for (const organization of organizations) {
|
|
533
548
|
SetupStepUpdater.updateForOrganization(organization).catch(console.error);
|
|
534
549
|
}
|
|
535
|
-
|
|
536
|
-
return new Response(
|
|
537
|
-
await AuthenticatedStructures.membersBlob(members)
|
|
538
|
-
);
|
|
539
550
|
}
|
|
540
551
|
|
|
541
552
|
static async checkDuplicate(member: Member) {
|
|
@@ -134,6 +134,8 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
134
134
|
// Give access to created members
|
|
135
135
|
await Member.users.reverse("members").link(user, addedMembers)
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
await PatchOrganizationMembersEndpoint.deleteMembers(request.body.getDeletes())
|
|
137
139
|
|
|
138
140
|
members = await Member.getMembersWithRegistrationForUser(user)
|
|
139
141
|
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { Request } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { Group, GroupFactory, MemberFactory, MemberWithRegistrations, Organization, OrganizationFactory, RegistrationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, User, UserFactory } from "@stamhoofd/models";
|
|
3
|
+
import { GroupPrice, IDRegisterCart, IDRegisterCheckout, IDRegisterItem, PayconiqAccount, PaymentMethod, PermissionLevel, Permissions, Version } from "@stamhoofd/structures";
|
|
4
|
+
import nock from "nock";
|
|
5
|
+
import { v4 as uuidv4 } from "uuid";
|
|
6
|
+
import { testServer } from "../../../../tests/helpers/TestServer";
|
|
7
|
+
import { RegisterMembersEndpoint } from "./RegisterMembersEndpoint";
|
|
8
|
+
|
|
9
|
+
const baseUrl = `/v${Version}/members/register`
|
|
10
|
+
|
|
11
|
+
describe("Endpoint.RegisterMembers", () => {
|
|
12
|
+
//#region global
|
|
13
|
+
const endpoint = new RegisterMembersEndpoint();
|
|
14
|
+
let period: RegistrationPeriod;
|
|
15
|
+
let organization: Organization;
|
|
16
|
+
let user: User;
|
|
17
|
+
let token: Token;
|
|
18
|
+
let member: MemberWithRegistrations;
|
|
19
|
+
let group1: Group;
|
|
20
|
+
let groupPrice1: GroupPrice;
|
|
21
|
+
let group2: Group;
|
|
22
|
+
let groupPrice2: GroupPrice;
|
|
23
|
+
|
|
24
|
+
//#region helpers
|
|
25
|
+
const post = async (body: IDRegisterCheckout) => {
|
|
26
|
+
const request = Request.buildJson("POST", baseUrl,organization.getApiHost(), body);
|
|
27
|
+
request.headers.authorization = "Bearer "+token.accessToken;
|
|
28
|
+
return await testServer.test(endpoint, request);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
period = await new RegistrationPeriodFactory({}).create();
|
|
36
|
+
organization = await new OrganizationFactory({ period }).create();
|
|
37
|
+
organization.meta.registrationPaymentConfiguration.paymentMethods = [PaymentMethod.PointOfSale, PaymentMethod.Payconiq];
|
|
38
|
+
|
|
39
|
+
organization.privateMeta.payconiqAccounts = [PayconiqAccount.create({
|
|
40
|
+
id: uuidv4(),
|
|
41
|
+
apiKey: 'test',
|
|
42
|
+
merchantId: 'test',
|
|
43
|
+
profileId: 'test',
|
|
44
|
+
name: 'test',
|
|
45
|
+
iban: 'BE56587127952688', // = random IBAN
|
|
46
|
+
callbackUrl: 'https://example.com'
|
|
47
|
+
})]
|
|
48
|
+
|
|
49
|
+
user = await new UserFactory({
|
|
50
|
+
organization,
|
|
51
|
+
permissions: Permissions.create({
|
|
52
|
+
level: PermissionLevel.Full
|
|
53
|
+
})
|
|
54
|
+
}).create();
|
|
55
|
+
token = await Token.createToken(user);
|
|
56
|
+
member = await new MemberFactory({ organization, user }).create();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
beforeEach(async () => {
|
|
60
|
+
//#region groups
|
|
61
|
+
group1 = await new GroupFactory({
|
|
62
|
+
organization,
|
|
63
|
+
price: 25,
|
|
64
|
+
stock: 5
|
|
65
|
+
}).create();
|
|
66
|
+
|
|
67
|
+
groupPrice1 = group1.settings.prices[0];
|
|
68
|
+
|
|
69
|
+
group2 = await new GroupFactory({
|
|
70
|
+
organization,
|
|
71
|
+
price: 15,
|
|
72
|
+
stock: 4,
|
|
73
|
+
maxMembers: 1
|
|
74
|
+
}).create();
|
|
75
|
+
|
|
76
|
+
groupPrice2 = group2.settings.prices[0];
|
|
77
|
+
//#endregion
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('Register member', () => {
|
|
81
|
+
|
|
82
|
+
test("Should update registered mmebers", async () => {
|
|
83
|
+
//#region arrange
|
|
84
|
+
const body = IDRegisterCheckout.create({
|
|
85
|
+
cart: IDRegisterCart.create({
|
|
86
|
+
items: [
|
|
87
|
+
IDRegisterItem.create({
|
|
88
|
+
id: uuidv4(),
|
|
89
|
+
replaceRegistrationIds: [],
|
|
90
|
+
options: [],
|
|
91
|
+
groupPrice: groupPrice1,
|
|
92
|
+
organizationId: organization.id,
|
|
93
|
+
groupId: group1.id,
|
|
94
|
+
memberId: member.id
|
|
95
|
+
})
|
|
96
|
+
],
|
|
97
|
+
balanceItems: [],
|
|
98
|
+
deleteRegistrationIds: []
|
|
99
|
+
}),
|
|
100
|
+
administrationFee: 0,
|
|
101
|
+
freeContribution: 0,
|
|
102
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
103
|
+
totalPrice: 25,
|
|
104
|
+
asOrganizationId: organization.id,
|
|
105
|
+
customer: null,
|
|
106
|
+
});
|
|
107
|
+
//#endregion
|
|
108
|
+
|
|
109
|
+
// act
|
|
110
|
+
const response = await post(body);
|
|
111
|
+
|
|
112
|
+
// assert
|
|
113
|
+
expect(response.body).toBeDefined();
|
|
114
|
+
expect(response.body.registrations.length).toBe(1);
|
|
115
|
+
|
|
116
|
+
const updatedGroup = await Group.getByID(group1.id);
|
|
117
|
+
expect(updatedGroup!.settings.registeredMembers).toBe(1);
|
|
118
|
+
expect(updatedGroup!.settings.reservedMembers).toBe(0);
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test("Should update reserved members", async () => {
|
|
122
|
+
//#region arrange
|
|
123
|
+
const body = IDRegisterCheckout.create({
|
|
124
|
+
cart: IDRegisterCart.create({
|
|
125
|
+
items: [
|
|
126
|
+
IDRegisterItem.create({
|
|
127
|
+
id: uuidv4(),
|
|
128
|
+
replaceRegistrationIds: [],
|
|
129
|
+
options: [],
|
|
130
|
+
groupPrice: groupPrice2,
|
|
131
|
+
organizationId: organization.id,
|
|
132
|
+
groupId: group2.id,
|
|
133
|
+
memberId: member.id
|
|
134
|
+
})
|
|
135
|
+
],
|
|
136
|
+
balanceItems: [],
|
|
137
|
+
deleteRegistrationIds: []
|
|
138
|
+
}),
|
|
139
|
+
administrationFee: 0,
|
|
140
|
+
freeContribution: 0,
|
|
141
|
+
paymentMethod: PaymentMethod.Payconiq,
|
|
142
|
+
redirectUrl: new URL("https://www.example.com"),
|
|
143
|
+
cancelUrl: new URL("https://www.example.com"),
|
|
144
|
+
totalPrice: 15,
|
|
145
|
+
customer: null,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
nock('https://api.ext.payconiq.com')
|
|
149
|
+
.post('/v3/payments')
|
|
150
|
+
.reply(200, {
|
|
151
|
+
paymentId: 'testPaymentId',
|
|
152
|
+
_links: {
|
|
153
|
+
checkout: {
|
|
154
|
+
href: 'https://www.example.com'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
//#endregion
|
|
159
|
+
|
|
160
|
+
// act
|
|
161
|
+
const response = await post(body);
|
|
162
|
+
|
|
163
|
+
// assert
|
|
164
|
+
expect(response.body).toBeDefined();
|
|
165
|
+
expect(response.body.registrations.length).toBe(1);
|
|
166
|
+
|
|
167
|
+
const updatedGroup = await Group.getByID(group2.id);
|
|
168
|
+
expect(updatedGroup!.settings.registeredMembers).toBe(0);
|
|
169
|
+
expect(updatedGroup!.settings.reservedMembers).toBe(1);
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe('Register member with replace registration', () => {
|
|
174
|
+
|
|
175
|
+
test("Should update registered members", async () => {
|
|
176
|
+
//#region arrange
|
|
177
|
+
const registration = await new RegistrationFactory({
|
|
178
|
+
member,
|
|
179
|
+
group: group1,
|
|
180
|
+
groupPrice: groupPrice1
|
|
181
|
+
})
|
|
182
|
+
.create();
|
|
183
|
+
|
|
184
|
+
const group = await new GroupFactory({
|
|
185
|
+
organization,
|
|
186
|
+
price: 30,
|
|
187
|
+
stock: 5
|
|
188
|
+
}).create();
|
|
189
|
+
|
|
190
|
+
const groupPrice = group.settings.prices[0];
|
|
191
|
+
|
|
192
|
+
const body = IDRegisterCheckout.create({
|
|
193
|
+
cart: IDRegisterCart.create({
|
|
194
|
+
items: [
|
|
195
|
+
IDRegisterItem.create({
|
|
196
|
+
id: uuidv4(),
|
|
197
|
+
replaceRegistrationIds: [registration.id],
|
|
198
|
+
options: [],
|
|
199
|
+
groupPrice,
|
|
200
|
+
organizationId: organization.id,
|
|
201
|
+
groupId: group.id,
|
|
202
|
+
memberId: member.id
|
|
203
|
+
})
|
|
204
|
+
],
|
|
205
|
+
balanceItems: [],
|
|
206
|
+
deleteRegistrationIds: []
|
|
207
|
+
}),
|
|
208
|
+
administrationFee: 0,
|
|
209
|
+
freeContribution: 0,
|
|
210
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
211
|
+
totalPrice: 5,
|
|
212
|
+
asOrganizationId: organization.id,
|
|
213
|
+
customer: null,
|
|
214
|
+
});
|
|
215
|
+
//#endregion
|
|
216
|
+
|
|
217
|
+
//#region act and assert
|
|
218
|
+
|
|
219
|
+
// update occupancy to be sure occupancy is 1
|
|
220
|
+
await group1.updateOccupancy();
|
|
221
|
+
expect(group1.settings.registeredMembers).toBe(1);
|
|
222
|
+
|
|
223
|
+
// send request and check occupancy
|
|
224
|
+
const response = await post(body);
|
|
225
|
+
|
|
226
|
+
expect(response.body).toBeDefined();
|
|
227
|
+
expect(response.body.registrations.length).toBe(1);
|
|
228
|
+
|
|
229
|
+
const updatedGroup = await Group.getByID(group.id);
|
|
230
|
+
expect(updatedGroup!.settings.registeredMembers).toBe(1);
|
|
231
|
+
expect(updatedGroup!.settings.reservedMembers).toBe(0);
|
|
232
|
+
|
|
233
|
+
const updatedGroup1After = await Group.getByID(group1.id);
|
|
234
|
+
// occupancy should go from 1 to 0 because the registration should be replaced
|
|
235
|
+
expect(updatedGroup1After!.settings.registeredMembers).toBe(0);
|
|
236
|
+
expect(updatedGroup1After!.settings.reservedMembers).toBe(0);
|
|
237
|
+
//#endregion
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test("Should throw error if with payment", async () => {
|
|
241
|
+
//#region arrange
|
|
242
|
+
const registration = await new RegistrationFactory({
|
|
243
|
+
member,
|
|
244
|
+
group: group1,
|
|
245
|
+
groupPrice: groupPrice1
|
|
246
|
+
})
|
|
247
|
+
.create();
|
|
248
|
+
|
|
249
|
+
const group = await new GroupFactory({
|
|
250
|
+
organization,
|
|
251
|
+
price: 30,
|
|
252
|
+
stock: 5,
|
|
253
|
+
maxMembers: 1
|
|
254
|
+
}).create();
|
|
255
|
+
|
|
256
|
+
const groupPrice = group.settings.prices[0];
|
|
257
|
+
|
|
258
|
+
const body = IDRegisterCheckout.create({
|
|
259
|
+
cart: IDRegisterCart.create({
|
|
260
|
+
items: [
|
|
261
|
+
IDRegisterItem.create({
|
|
262
|
+
id: uuidv4(),
|
|
263
|
+
replaceRegistrationIds: [registration.id],
|
|
264
|
+
options: [],
|
|
265
|
+
groupPrice,
|
|
266
|
+
organizationId: organization.id,
|
|
267
|
+
groupId: group.id,
|
|
268
|
+
memberId: member.id
|
|
269
|
+
})
|
|
270
|
+
],
|
|
271
|
+
balanceItems: [],
|
|
272
|
+
deleteRegistrationIds: []
|
|
273
|
+
}),
|
|
274
|
+
administrationFee: 0,
|
|
275
|
+
freeContribution: 0,
|
|
276
|
+
paymentMethod: PaymentMethod.Payconiq,
|
|
277
|
+
redirectUrl: new URL("https://www.example.com"),
|
|
278
|
+
cancelUrl: new URL("https://www.example.com"),
|
|
279
|
+
totalPrice: 5,
|
|
280
|
+
customer: null,
|
|
281
|
+
});
|
|
282
|
+
//#endregion
|
|
283
|
+
|
|
284
|
+
//#region act and assert
|
|
285
|
+
|
|
286
|
+
// update occupancy to be sure occupancy is 1
|
|
287
|
+
await group1.updateOccupancy();
|
|
288
|
+
expect(group1.settings.registeredMembers).toBe(1);
|
|
289
|
+
|
|
290
|
+
await expect(async () => await post(body)).rejects.toThrow("Not allowed to move registrations");
|
|
291
|
+
//#endregion
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe('Register member with delete registration', () => {
|
|
296
|
+
|
|
297
|
+
test("Should update registered members", async () => {
|
|
298
|
+
//#region arrange
|
|
299
|
+
const registration = await new RegistrationFactory({
|
|
300
|
+
member,
|
|
301
|
+
group: group1,
|
|
302
|
+
groupPrice: groupPrice1
|
|
303
|
+
})
|
|
304
|
+
.create();
|
|
305
|
+
|
|
306
|
+
const group = await new GroupFactory({
|
|
307
|
+
organization,
|
|
308
|
+
price: 30,
|
|
309
|
+
stock: 5
|
|
310
|
+
}).create();
|
|
311
|
+
|
|
312
|
+
const groupPrice = group.settings.prices[0];
|
|
313
|
+
|
|
314
|
+
const body = IDRegisterCheckout.create({
|
|
315
|
+
cart: IDRegisterCart.create({
|
|
316
|
+
items: [
|
|
317
|
+
IDRegisterItem.create({
|
|
318
|
+
id: uuidv4(),
|
|
319
|
+
replaceRegistrationIds: [],
|
|
320
|
+
options: [],
|
|
321
|
+
groupPrice,
|
|
322
|
+
organizationId: organization.id,
|
|
323
|
+
groupId: group.id,
|
|
324
|
+
memberId: member.id
|
|
325
|
+
})
|
|
326
|
+
],
|
|
327
|
+
balanceItems: [],
|
|
328
|
+
deleteRegistrationIds: [registration.id]
|
|
329
|
+
}),
|
|
330
|
+
administrationFee: 0,
|
|
331
|
+
freeContribution: 0,
|
|
332
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
333
|
+
totalPrice: 5,
|
|
334
|
+
asOrganizationId: organization.id,
|
|
335
|
+
customer: null,
|
|
336
|
+
});
|
|
337
|
+
//#endregion
|
|
338
|
+
|
|
339
|
+
//#region act and assert
|
|
340
|
+
|
|
341
|
+
// update occupancy to be sure occupancy is 1
|
|
342
|
+
await group1.updateOccupancy();
|
|
343
|
+
expect(group1.settings.registeredMembers).toBe(1);
|
|
344
|
+
|
|
345
|
+
// send request and check occupancy
|
|
346
|
+
const response = await post(body);
|
|
347
|
+
|
|
348
|
+
expect(response.body).toBeDefined();
|
|
349
|
+
expect(response.body.registrations.length).toBe(1);
|
|
350
|
+
|
|
351
|
+
const updatedGroup = await Group.getByID(group.id);
|
|
352
|
+
expect(updatedGroup!.settings.registeredMembers).toBe(1);
|
|
353
|
+
expect(updatedGroup!.settings.reservedMembers).toBe(0);
|
|
354
|
+
|
|
355
|
+
const updatedGroup1After = await Group.getByID(group1.id);
|
|
356
|
+
// occupancy should go from 1 to 0 because the registration should be deleted
|
|
357
|
+
expect(updatedGroup1After!.settings.registeredMembers).toBe(0);
|
|
358
|
+
expect(updatedGroup1After!.settings.reservedMembers).toBe(0);
|
|
359
|
+
//#endregion
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test("Should throw error if with payment", async () => {
|
|
363
|
+
//#region arrange
|
|
364
|
+
const registration = await new RegistrationFactory({
|
|
365
|
+
member,
|
|
366
|
+
group: group1,
|
|
367
|
+
groupPrice: groupPrice1
|
|
368
|
+
})
|
|
369
|
+
.create();
|
|
370
|
+
|
|
371
|
+
const group = await new GroupFactory({
|
|
372
|
+
organization,
|
|
373
|
+
price: 30,
|
|
374
|
+
stock: 5,
|
|
375
|
+
maxMembers: 1
|
|
376
|
+
}).create();
|
|
377
|
+
|
|
378
|
+
const groupPrice = group.settings.prices[0];
|
|
379
|
+
|
|
380
|
+
const body = IDRegisterCheckout.create({
|
|
381
|
+
cart: IDRegisterCart.create({
|
|
382
|
+
items: [
|
|
383
|
+
IDRegisterItem.create({
|
|
384
|
+
id: uuidv4(),
|
|
385
|
+
replaceRegistrationIds: [],
|
|
386
|
+
options: [],
|
|
387
|
+
groupPrice,
|
|
388
|
+
organizationId: organization.id,
|
|
389
|
+
groupId: group.id,
|
|
390
|
+
memberId: member.id
|
|
391
|
+
})
|
|
392
|
+
],
|
|
393
|
+
balanceItems: [],
|
|
394
|
+
deleteRegistrationIds: [registration.id]
|
|
395
|
+
}),
|
|
396
|
+
administrationFee: 0,
|
|
397
|
+
freeContribution: 0,
|
|
398
|
+
paymentMethod: PaymentMethod.Payconiq,
|
|
399
|
+
redirectUrl: new URL("https://www.example.com"),
|
|
400
|
+
cancelUrl: new URL("https://www.example.com"),
|
|
401
|
+
totalPrice: 5,
|
|
402
|
+
customer: null,
|
|
403
|
+
});
|
|
404
|
+
//#endregion
|
|
405
|
+
|
|
406
|
+
//#region act and assert
|
|
407
|
+
|
|
408
|
+
// update occupancy to be sure occupancy is 1
|
|
409
|
+
await group1.updateOccupancy();
|
|
410
|
+
expect(group1.settings.registeredMembers).toBe(1);
|
|
411
|
+
|
|
412
|
+
await expect(async () => await post(body)).rejects.toThrow("Permission denied: you are not allowed to delete registrations");
|
|
413
|
+
//#endregion
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('Register member that is already registered should throw error', async () => {
|
|
418
|
+
// create existing registration
|
|
419
|
+
await new RegistrationFactory({
|
|
420
|
+
member,
|
|
421
|
+
group: group1,
|
|
422
|
+
groupPrice: groupPrice1
|
|
423
|
+
})
|
|
424
|
+
.create();
|
|
425
|
+
|
|
426
|
+
// register again
|
|
427
|
+
const body = IDRegisterCheckout.create({
|
|
428
|
+
cart: IDRegisterCart.create({
|
|
429
|
+
items: [
|
|
430
|
+
IDRegisterItem.create({
|
|
431
|
+
id: uuidv4(),
|
|
432
|
+
replaceRegistrationIds: [],
|
|
433
|
+
options: [],
|
|
434
|
+
groupPrice: groupPrice1,
|
|
435
|
+
organizationId: organization.id,
|
|
436
|
+
groupId: group1.id,
|
|
437
|
+
memberId: member.id
|
|
438
|
+
})
|
|
439
|
+
],
|
|
440
|
+
balanceItems: [],
|
|
441
|
+
deleteRegistrationIds: []
|
|
442
|
+
}),
|
|
443
|
+
administrationFee: 0,
|
|
444
|
+
freeContribution: 0,
|
|
445
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
446
|
+
totalPrice: groupPrice1.price.price,
|
|
447
|
+
asOrganizationId: organization.id,
|
|
448
|
+
customer: null,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await expect(async () => await post(body)).rejects.toThrow("Already registered");
|
|
452
|
+
})
|
|
453
|
+
})
|
|
@@ -55,6 +55,17 @@ export class AddressValidatorStatic {
|
|
|
55
55
|
})
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
if (address.country !== Country.Belgium && address.country !== Country.Netherlands) {
|
|
60
|
+
// No validation for other countries
|
|
61
|
+
return ValidatedAddress.create(Object.assign({ ... address }, {
|
|
62
|
+
postalCode: address.postalCode,
|
|
63
|
+
city: address.city,
|
|
64
|
+
cityId: 'unknown',
|
|
65
|
+
parentCityId: null,
|
|
66
|
+
provinceId: 'unknown',
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
58
69
|
|
|
59
70
|
const city = await PostalCode.getCity(postalCode, address.city, address.country)
|
|
60
71
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, PatchMap } from "@simonbackx/simple-encoding"
|
|
2
2
|
import { SimpleError } from "@simonbackx/simple-errors"
|
|
3
|
-
import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from "@stamhoofd/models"
|
|
3
|
+
import { BalanceItem, CachedOutstandingBalance, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from "@stamhoofd/models"
|
|
4
4
|
import { AccessRight, FinancialSupportSettings, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from "@stamhoofd/structures"
|
|
5
5
|
import { Formatter } from "@stamhoofd/utility"
|
|
6
6
|
|
|
@@ -250,7 +250,20 @@ export class AdminPermissionChecker {
|
|
|
250
250
|
* Only full admins can delete members permanently
|
|
251
251
|
*/
|
|
252
252
|
async canDeleteMember(member: MemberWithRegistrations) {
|
|
253
|
+
if (member.registrations.length === 0 && this.isUserManager(member)) {
|
|
254
|
+
const platformMemberships = await MemberPlatformMembership.where({ memberId: member.id })
|
|
255
|
+
if (platformMemberships.length === 0) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const cachedBalance = await CachedOutstandingBalance.getForObjects([member.id])
|
|
260
|
+
if (cachedBalance.length === 0 || (cachedBalance[0].amount === 0 && cachedBalance[0].amountPending === 0)) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
253
265
|
if (member.organizationId) {
|
|
266
|
+
// Not a platform
|
|
254
267
|
return await this.hasFullAccess(member.organizationId)
|
|
255
268
|
}
|
|
256
269
|
return this.hasPlatformFullAccess()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { logger } from '@simonbackx/simple-logging';
|
|
3
|
+
import { BalanceItem } from '@stamhoofd/models';
|
|
4
|
+
|
|
5
|
+
export default new Migration(async () => {
|
|
6
|
+
if (STAMHOOFD.environment == "test") {
|
|
7
|
+
console.log("skipped in tests")
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
process.stdout.write('\n');
|
|
12
|
+
let c = 0;
|
|
13
|
+
let id: string = '';
|
|
14
|
+
|
|
15
|
+
await logger.setContext({tags: ['silent-seed', 'seed']}, async () => {
|
|
16
|
+
while(true) {
|
|
17
|
+
const items = await BalanceItem.where({
|
|
18
|
+
id: {
|
|
19
|
+
value: id,
|
|
20
|
+
sign: '>'
|
|
21
|
+
}
|
|
22
|
+
}, {limit: 1000, sort: ['id']});
|
|
23
|
+
|
|
24
|
+
await BalanceItem.updateOutstanding(items)
|
|
25
|
+
|
|
26
|
+
c += items.length;
|
|
27
|
+
process.stdout.write('.');
|
|
28
|
+
|
|
29
|
+
if (items.length < 1000) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
id = items[items.length - 1].id;
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
console.log("Updated outstanding balance for " + c + " items")
|
|
37
|
+
|
|
38
|
+
// Do something here
|
|
39
|
+
return Promise.resolve()
|
|
40
|
+
})
|