@stamhoofd/backend 2.83.5 → 2.84.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/index.ts +19 -4
- package/package.json +18 -14
- package/src/crons/amazon-ses.ts +26 -5
- package/src/crons/balance-emails.ts +18 -17
- package/src/email-recipient-loaders/registrations.ts +87 -0
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +5 -2
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +40 -40
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +28 -22
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +81 -49
- package/src/endpoints/global/files/UploadFile.ts +11 -16
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +234 -0
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +117 -43
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +1054 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +163 -141
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +6 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +0 -16
- package/src/endpoints/global/members/helpers/validateGroupFilter.ts +73 -0
- package/src/endpoints/global/registration/GetPaymentRegistrations.ts +1 -2
- package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +43 -0
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1016 -0
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +234 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +5 -5
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +474 -554
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +191 -52
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +107 -9
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +89 -0
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +9 -6
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +88 -0
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +0 -6
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -6
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +10 -25
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +0 -5
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +0 -5
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +4 -0
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +44 -19
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +140 -25
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +40 -10
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +2 -2
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -2
- package/src/excel-loaders/members.ts +233 -232
- package/src/excel-loaders/payments.ts +1 -1
- package/src/excel-loaders/receivable-balances.ts +1 -1
- package/src/excel-loaders/registrations.ts +153 -0
- package/src/helpers/AdminPermissionChecker.ts +65 -37
- package/src/helpers/AuthenticatedStructures.ts +43 -3
- package/src/helpers/Context.ts +29 -1
- package/src/helpers/GlobalHelper.ts +3 -1
- package/src/helpers/GroupedThrottledQueue.test.ts +219 -0
- package/src/helpers/GroupedThrottledQueue.ts +108 -0
- package/src/helpers/LimitedFilteredRequestHelper.ts +26 -1
- package/src/helpers/MemberCharger.ts +0 -5
- package/src/helpers/MembershipCharger.ts +3 -9
- package/src/helpers/OrganizationCharger.ts +0 -5
- package/src/helpers/ThrottledQueue.test.ts +194 -0
- package/src/helpers/ThrottledQueue.ts +145 -0
- package/src/helpers/XlsxTransformerColumnHelper.ts +44 -1
- package/src/middleware/ContextMiddleware.ts +1 -1
- package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +2 -1
- package/src/seeds/1735577912-update-cached-outstanding-balance-from-items.ts +2 -1
- package/src/services/BalanceItemPaymentService.ts +1 -33
- package/src/services/BalanceItemService.ts +167 -48
- package/src/services/FileSignService.ts +18 -13
- package/src/services/MemberRecordStore.ts +28 -19
- package/src/services/PaymentReallocationService.test.ts +25 -14
- package/src/services/PaymentReallocationService.ts +29 -10
- package/src/services/PaymentService.ts +4 -16
- package/src/services/PlatformMembershipService.ts +8 -4
- package/src/services/RegistrationService.ts +66 -2
- package/src/sql-filters/base-registration-filter-compilers.ts +43 -0
- package/src/sql-filters/groups.ts +67 -0
- package/src/sql-filters/members.ts +33 -58
- package/src/sql-filters/organization-registration-periods.ts +8 -0
- package/src/sql-filters/registration-periods.ts +8 -0
- package/src/sql-filters/registrations.ts +11 -22
- package/src/sql-sorters/groups.ts +24 -0
- package/src/sql-sorters/organization-registration-periods.ts +24 -0
- package/src/sql-sorters/registration-periods.ts +47 -0
- package/src/sql-sorters/registrations.ts +77 -0
- package/tests/actions/patchOrganizationMember.ts +27 -0
- package/tests/actions/patchPaymentStatus.ts +45 -0
- package/tests/actions/patchUserMember.ts +27 -0
- package/tests/assertions/assertBalances.ts +49 -0
- package/tests/e2e/api-rate-limits.test.ts +5 -5
- package/tests/e2e/bundle-discounts.test.ts +4060 -0
- package/tests/e2e/charge-members.test.ts +27 -24
- package/tests/e2e/documents.test.ts +398 -0
- package/tests/e2e/register.test.ts +292 -312
- package/tests/helpers/PayconiqMocker.ts +55 -0
- package/tests/init/index.ts +5 -0
- package/tests/init/initAdmin.ts +14 -0
- package/tests/init/initBundleDiscount.ts +47 -0
- package/tests/init/initPayconiq.ts +9 -0
- package/tests/init/initPlatformAdmin.ts +13 -0
- package/tests/init/initStripe.ts +21 -0
- package/tests/jest.setup.ts +29 -0
- package/src/seeds-temporary/1736266448-recall-balance-item-price-paid.ts +0 -70
|
@@ -0,0 +1,4060 @@
|
|
|
1
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { BalanceItem, BalanceItemFactory, GroupFactory, MemberFactory, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, Registration, RegistrationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
3
|
+
import { AppliedRegistrationDiscount, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, BooleanStatus, GroupPriceDiscount, GroupPriceDiscountType, IDRegisterCart, IDRegisterCheckout, IDRegisterItem, PaymentMethod, PermissionLevel, Permissions, ReduceablePrice } from '@stamhoofd/structures';
|
|
4
|
+
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
5
|
+
import { RegisterMembersEndpoint } from '../../src/endpoints/global/registration/RegisterMembersEndpoint';
|
|
6
|
+
import { assertBalances } from '../assertions/assertBalances';
|
|
7
|
+
import { testServer } from '../helpers/TestServer';
|
|
8
|
+
import { initBundleDiscount } from '../init/initBundleDiscount';
|
|
9
|
+
import { initStripe } from '../init/initStripe';
|
|
10
|
+
import { initAdmin } from '../init/initAdmin';
|
|
11
|
+
import { BalanceItemService } from '../../src/services/BalanceItemService';
|
|
12
|
+
|
|
13
|
+
const baseUrl = `/members/register`;
|
|
14
|
+
|
|
15
|
+
describe('E2E.Bundle Discounts', () => {
|
|
16
|
+
const endpoint = new RegisterMembersEndpoint();
|
|
17
|
+
let period: RegistrationPeriod;
|
|
18
|
+
const post = async (body: IDRegisterCheckout, organization: Organization, token: Token) => {
|
|
19
|
+
const request = Request.post({
|
|
20
|
+
path: baseUrl,
|
|
21
|
+
host: organization.getApiHost(),
|
|
22
|
+
body,
|
|
23
|
+
headers: {
|
|
24
|
+
authorization: 'Bearer ' + token.accessToken,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return await testServer.test(endpoint, request);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
const previousPeriod = await new RegistrationPeriodFactory({
|
|
32
|
+
startDate: new Date(2022, 0, 1),
|
|
33
|
+
endDate: new Date(2022, 11, 31),
|
|
34
|
+
}).create();
|
|
35
|
+
|
|
36
|
+
period = await new RegistrationPeriodFactory({
|
|
37
|
+
startDate: new Date(2023, 0, 1),
|
|
38
|
+
endDate: new Date(2030, 11, 31),
|
|
39
|
+
previousPeriodId: previousPeriod.id,
|
|
40
|
+
}).create();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
TestUtils.setEnvironment('userMode', 'platform');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
jest.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
async function initData() {
|
|
52
|
+
const organization = await new OrganizationFactory({ period })
|
|
53
|
+
.create();
|
|
54
|
+
|
|
55
|
+
const organizationRegistrationPeriod = await new OrganizationRegistrationPeriodFactory({ organization, period }).create();
|
|
56
|
+
|
|
57
|
+
const user = await new UserFactory({
|
|
58
|
+
organization,
|
|
59
|
+
permissions: null,
|
|
60
|
+
}).create();
|
|
61
|
+
|
|
62
|
+
const token = await Token.createToken(user);
|
|
63
|
+
|
|
64
|
+
const member = await new MemberFactory({ organization, user })
|
|
65
|
+
.create();
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
organization,
|
|
69
|
+
organizationRegistrationPeriod,
|
|
70
|
+
user,
|
|
71
|
+
token,
|
|
72
|
+
member,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('Registering as member', () => {
|
|
77
|
+
test('PointOfSale: The first registration has no discount applied', async () => {
|
|
78
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
79
|
+
const bundleDiscount = await initBundleDiscount({
|
|
80
|
+
organizationRegistrationPeriod,
|
|
81
|
+
discount: {
|
|
82
|
+
discounts: [
|
|
83
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const group = await new GroupFactory({
|
|
89
|
+
organization,
|
|
90
|
+
price: 25_00,
|
|
91
|
+
bundleDiscount,
|
|
92
|
+
})
|
|
93
|
+
.create();
|
|
94
|
+
|
|
95
|
+
const groupPrice = group.settings.prices[0];
|
|
96
|
+
|
|
97
|
+
// First register the member for group 1. No discount should be applied yet
|
|
98
|
+
const checkout1 = IDRegisterCheckout.create({
|
|
99
|
+
cart: IDRegisterCart.create({
|
|
100
|
+
items: [
|
|
101
|
+
IDRegisterItem.create({
|
|
102
|
+
groupPrice,
|
|
103
|
+
groupId: group.id,
|
|
104
|
+
organizationId: organization.id,
|
|
105
|
+
memberId: member.id,
|
|
106
|
+
}),
|
|
107
|
+
],
|
|
108
|
+
}),
|
|
109
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
110
|
+
totalPrice: 25_00,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const response1 = await post(checkout1, organization, token);
|
|
114
|
+
expect(response1.body.registrations).toEqual([
|
|
115
|
+
expect.objectContaining({
|
|
116
|
+
registeredAt: expect.any(Date),
|
|
117
|
+
discounts: new Map(),
|
|
118
|
+
}),
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
await assertBalances({ user }, [
|
|
122
|
+
{
|
|
123
|
+
type: BalanceItemType.Registration,
|
|
124
|
+
amount: 1,
|
|
125
|
+
price: 25_00,
|
|
126
|
+
status: BalanceItemStatus.Due,
|
|
127
|
+
priceOpen: 0,
|
|
128
|
+
pricePending: 25_00,
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('Unrelated registrations do not get counted for bundle discounts', async () => {
|
|
134
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
135
|
+
const bundleDiscount = await initBundleDiscount({
|
|
136
|
+
organizationRegistrationPeriod,
|
|
137
|
+
discount: {
|
|
138
|
+
discounts: [
|
|
139
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const group = await new GroupFactory({
|
|
145
|
+
organization,
|
|
146
|
+
price: 25_00,
|
|
147
|
+
}).create();
|
|
148
|
+
|
|
149
|
+
const groupPrice = group.settings.prices[0];
|
|
150
|
+
|
|
151
|
+
const group2 = await new GroupFactory({
|
|
152
|
+
organization,
|
|
153
|
+
price: 35_00,
|
|
154
|
+
bundleDiscount,
|
|
155
|
+
}).create();
|
|
156
|
+
|
|
157
|
+
const groupPrice2 = group2.settings.prices[0];
|
|
158
|
+
|
|
159
|
+
// First register the member for group 1. No discount should be applied yet
|
|
160
|
+
const checkout1 = IDRegisterCheckout.create({
|
|
161
|
+
cart: IDRegisterCart.create({
|
|
162
|
+
items: [
|
|
163
|
+
IDRegisterItem.create({
|
|
164
|
+
groupPrice,
|
|
165
|
+
groupId: group.id,
|
|
166
|
+
organizationId: organization.id,
|
|
167
|
+
memberId: member.id,
|
|
168
|
+
}),
|
|
169
|
+
],
|
|
170
|
+
}),
|
|
171
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
172
|
+
totalPrice: 25_00,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const response1 = await post(checkout1, organization, token);
|
|
176
|
+
expect(response1.body.registrations.length).toBe(1);
|
|
177
|
+
const registration1 = response1.body.registrations[0];
|
|
178
|
+
expect(registration1.registeredAt).not.toBeNull();
|
|
179
|
+
expect(registration1.discounts).toMatchMap(new Map());
|
|
180
|
+
|
|
181
|
+
const checkout2 = IDRegisterCheckout.create({
|
|
182
|
+
cart: IDRegisterCart.create({
|
|
183
|
+
items: [
|
|
184
|
+
IDRegisterItem.create({
|
|
185
|
+
groupPrice: groupPrice2,
|
|
186
|
+
groupId: group2.id,
|
|
187
|
+
organizationId: organization.id,
|
|
188
|
+
memberId: member.id,
|
|
189
|
+
}),
|
|
190
|
+
],
|
|
191
|
+
}),
|
|
192
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
193
|
+
totalPrice: 35_00,
|
|
194
|
+
});
|
|
195
|
+
const response2 = await post(checkout2, organization, token);
|
|
196
|
+
expect(response2.body.registrations.length).toBe(1);
|
|
197
|
+
|
|
198
|
+
const registration2 = response2.body.registrations[0];
|
|
199
|
+
expect(registration2.registeredAt).not.toBeNull();
|
|
200
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
201
|
+
|
|
202
|
+
const updatedRegistration1 = (await Registration.getByID(registration1.id))!;
|
|
203
|
+
expect(updatedRegistration1).toBeDefined();
|
|
204
|
+
expect(updatedRegistration1.discounts).toMatchMap(new Map());
|
|
205
|
+
|
|
206
|
+
await assertBalances({ user }, [
|
|
207
|
+
{
|
|
208
|
+
type: BalanceItemType.Registration,
|
|
209
|
+
registrationId: registration1.id,
|
|
210
|
+
amount: 1,
|
|
211
|
+
price: 25_00,
|
|
212
|
+
status: BalanceItemStatus.Due,
|
|
213
|
+
priceOpen: 0,
|
|
214
|
+
pricePending: 25_00,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
type: BalanceItemType.Registration,
|
|
218
|
+
registrationId: registration2.id,
|
|
219
|
+
amount: 1,
|
|
220
|
+
price: 35_00,
|
|
221
|
+
status: BalanceItemStatus.Due,
|
|
222
|
+
priceOpen: 0,
|
|
223
|
+
pricePending: 35_00,
|
|
224
|
+
},
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('PointOfSale: A bundle discount can be applied to a previous registration', async () => {
|
|
229
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
230
|
+
const bundleDiscount = await initBundleDiscount({
|
|
231
|
+
organizationRegistrationPeriod,
|
|
232
|
+
discount: {
|
|
233
|
+
discounts: [
|
|
234
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const group = await new GroupFactory({
|
|
240
|
+
organization,
|
|
241
|
+
price: 25_00,
|
|
242
|
+
bundleDiscount,
|
|
243
|
+
}).create();
|
|
244
|
+
|
|
245
|
+
const groupPrice = group.settings.prices[0];
|
|
246
|
+
|
|
247
|
+
const group2 = await new GroupFactory({
|
|
248
|
+
organization,
|
|
249
|
+
price: 15_00, // Lower price so discount is applied preferably on the first group
|
|
250
|
+
bundleDiscount,
|
|
251
|
+
}).create();
|
|
252
|
+
|
|
253
|
+
const groupPrice2 = group2.settings.prices[0];
|
|
254
|
+
|
|
255
|
+
// First register the member for group 1. No discount should be applied yet
|
|
256
|
+
const checkout1 = IDRegisterCheckout.create({
|
|
257
|
+
cart: IDRegisterCart.create({
|
|
258
|
+
items: [
|
|
259
|
+
IDRegisterItem.create({
|
|
260
|
+
groupPrice,
|
|
261
|
+
groupId: group.id,
|
|
262
|
+
organizationId: organization.id,
|
|
263
|
+
memberId: member.id,
|
|
264
|
+
}),
|
|
265
|
+
],
|
|
266
|
+
}),
|
|
267
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
268
|
+
totalPrice: groupPrice.price.price,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const response1 = await post(checkout1, organization, token);
|
|
272
|
+
expect(response1.body.registrations.length).toBe(1);
|
|
273
|
+
const registration1 = response1.body.registrations[0];
|
|
274
|
+
expect(registration1.registeredAt).not.toBeNull();
|
|
275
|
+
expect(registration1.discounts).toMatchMap(new Map());
|
|
276
|
+
|
|
277
|
+
const checkout2 = IDRegisterCheckout.create({
|
|
278
|
+
cart: IDRegisterCart.create({
|
|
279
|
+
items: [
|
|
280
|
+
IDRegisterItem.create({
|
|
281
|
+
groupPrice: groupPrice2,
|
|
282
|
+
groupId: group2.id,
|
|
283
|
+
organizationId: organization.id,
|
|
284
|
+
memberId: member.id,
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
287
|
+
}),
|
|
288
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
289
|
+
totalPrice: 15_00 - 5_00, // 20% discount on first group
|
|
290
|
+
});
|
|
291
|
+
const response2 = await post(checkout2, organization, token);
|
|
292
|
+
expect(response2.body.registrations.length).toBe(1);
|
|
293
|
+
|
|
294
|
+
const registration2 = response2.body.registrations[0];
|
|
295
|
+
expect(registration2.registeredAt).not.toBeNull();
|
|
296
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
297
|
+
|
|
298
|
+
// Get registration 1 again, it should now have the bundle discount applied
|
|
299
|
+
const updatedRegistration1 = (await Registration.getByID(registration1.id))!;
|
|
300
|
+
expect(updatedRegistration1).toBeDefined();
|
|
301
|
+
expect(updatedRegistration1.discounts).toMatchMap(new Map([
|
|
302
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
303
|
+
name: bundleDiscount.name,
|
|
304
|
+
amount: 5_00,
|
|
305
|
+
})],
|
|
306
|
+
]));
|
|
307
|
+
|
|
308
|
+
await assertBalances({ user }, [
|
|
309
|
+
{
|
|
310
|
+
type: BalanceItemType.Registration,
|
|
311
|
+
registrationId: registration1.id,
|
|
312
|
+
amount: 1,
|
|
313
|
+
price: 25_00,
|
|
314
|
+
status: BalanceItemStatus.Due,
|
|
315
|
+
priceOpen: 0,
|
|
316
|
+
pricePending: 25_00,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
320
|
+
registrationId: registration1.id,
|
|
321
|
+
amount: 1,
|
|
322
|
+
price: -5_00,
|
|
323
|
+
status: BalanceItemStatus.Due,
|
|
324
|
+
priceOpen: 0,
|
|
325
|
+
pricePending: -5_00,
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
type: BalanceItemType.Registration,
|
|
329
|
+
registrationId: registration2.id,
|
|
330
|
+
amount: 1,
|
|
331
|
+
price: 15_00,
|
|
332
|
+
status: BalanceItemStatus.Due,
|
|
333
|
+
priceOpen: 0,
|
|
334
|
+
pricePending: 15_00,
|
|
335
|
+
},
|
|
336
|
+
]);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('PointOfSale: A bundle discount can be applied to an added registration', async () => {
|
|
340
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
341
|
+
const bundleDiscount = await initBundleDiscount({
|
|
342
|
+
organizationRegistrationPeriod,
|
|
343
|
+
discount: {
|
|
344
|
+
discounts: [
|
|
345
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const group = await new GroupFactory({
|
|
351
|
+
organization,
|
|
352
|
+
price: 25_00,
|
|
353
|
+
bundleDiscount,
|
|
354
|
+
})
|
|
355
|
+
.create();
|
|
356
|
+
|
|
357
|
+
const groupPrice = group.settings.prices[0];
|
|
358
|
+
|
|
359
|
+
const group2 = await new GroupFactory({
|
|
360
|
+
organization,
|
|
361
|
+
price: 35_00, // Higher price so discount is applied preferably here
|
|
362
|
+
bundleDiscount,
|
|
363
|
+
}).create();
|
|
364
|
+
|
|
365
|
+
const groupPrice2 = group2.settings.prices[0];
|
|
366
|
+
|
|
367
|
+
// First register the member for group 1. No discount should be applied yet
|
|
368
|
+
const checkout1 = IDRegisterCheckout.create({
|
|
369
|
+
cart: IDRegisterCart.create({
|
|
370
|
+
items: [
|
|
371
|
+
IDRegisterItem.create({
|
|
372
|
+
groupPrice,
|
|
373
|
+
groupId: group.id,
|
|
374
|
+
organizationId: organization.id,
|
|
375
|
+
memberId: member.id,
|
|
376
|
+
}),
|
|
377
|
+
],
|
|
378
|
+
}),
|
|
379
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
380
|
+
totalPrice: 25_00,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const response1 = await post(checkout1, organization, token);
|
|
384
|
+
expect(response1.body.registrations.length).toBe(1);
|
|
385
|
+
const registration1 = response1.body.registrations[0];
|
|
386
|
+
expect(registration1.registeredAt).not.toBeNull();
|
|
387
|
+
expect(registration1.discounts).toMatchMap(new Map());
|
|
388
|
+
|
|
389
|
+
const checkout2 = IDRegisterCheckout.create({
|
|
390
|
+
cart: IDRegisterCart.create({
|
|
391
|
+
items: [
|
|
392
|
+
IDRegisterItem.create({
|
|
393
|
+
groupPrice: groupPrice2,
|
|
394
|
+
groupId: group2.id,
|
|
395
|
+
organizationId: organization.id,
|
|
396
|
+
memberId: member.id,
|
|
397
|
+
}),
|
|
398
|
+
],
|
|
399
|
+
}),
|
|
400
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
401
|
+
totalPrice: 35_00 - 7_00, // 20% discount on 35_00 = 7_00
|
|
402
|
+
});
|
|
403
|
+
const response2 = await post(checkout2, organization, token);
|
|
404
|
+
expect(response2.body.registrations.length).toBe(1);
|
|
405
|
+
|
|
406
|
+
const registration2 = response2.body.registrations[0];
|
|
407
|
+
expect(registration2.registeredAt).not.toBeNull();
|
|
408
|
+
expect(registration2.discounts).toMatchMap(new Map([
|
|
409
|
+
[
|
|
410
|
+
bundleDiscount.id,
|
|
411
|
+
AppliedRegistrationDiscount.create({
|
|
412
|
+
name: bundleDiscount.name,
|
|
413
|
+
amount: 7_00,
|
|
414
|
+
}),
|
|
415
|
+
],
|
|
416
|
+
]));
|
|
417
|
+
|
|
418
|
+
// Get registration 1 again, it should now have the bundle discount applied
|
|
419
|
+
const updatedRegistration1 = (await Registration.getByID(registration1.id))!;
|
|
420
|
+
expect(updatedRegistration1).toBeDefined();
|
|
421
|
+
expect(updatedRegistration1.discounts).toMatchMap(new Map());
|
|
422
|
+
|
|
423
|
+
await assertBalances({ user }, [
|
|
424
|
+
{
|
|
425
|
+
type: BalanceItemType.Registration,
|
|
426
|
+
registrationId: registration1.id,
|
|
427
|
+
amount: 1,
|
|
428
|
+
price: 25_00,
|
|
429
|
+
status: BalanceItemStatus.Due,
|
|
430
|
+
priceOpen: 0,
|
|
431
|
+
pricePending: 25_00,
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
435
|
+
registrationId: registration2.id,
|
|
436
|
+
amount: 1,
|
|
437
|
+
price: -7_00,
|
|
438
|
+
status: BalanceItemStatus.Due,
|
|
439
|
+
priceOpen: 0,
|
|
440
|
+
pricePending: -7_00,
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
type: BalanceItemType.Registration,
|
|
444
|
+
registrationId: registration2.id,
|
|
445
|
+
amount: 1,
|
|
446
|
+
price: 35_00,
|
|
447
|
+
status: BalanceItemStatus.Due,
|
|
448
|
+
priceOpen: 0,
|
|
449
|
+
pricePending: 35_00,
|
|
450
|
+
},
|
|
451
|
+
]);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('PointOfSale: A bundle discount can be applied to a new and previous registration at the same time', async () => {
|
|
455
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
456
|
+
const bundleDiscount = await initBundleDiscount({
|
|
457
|
+
organizationRegistrationPeriod,
|
|
458
|
+
discount: {
|
|
459
|
+
discounts: [
|
|
460
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
461
|
+
{ value: 40_00, type: GroupPriceDiscountType.Percentage },
|
|
462
|
+
],
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const groups = [
|
|
467
|
+
await new GroupFactory({
|
|
468
|
+
organization,
|
|
469
|
+
price: 35_00,
|
|
470
|
+
bundleDiscount,
|
|
471
|
+
}).create(),
|
|
472
|
+
await new GroupFactory({
|
|
473
|
+
organization,
|
|
474
|
+
price: 25_00,
|
|
475
|
+
bundleDiscount,
|
|
476
|
+
}).create(),
|
|
477
|
+
await new GroupFactory({
|
|
478
|
+
organization,
|
|
479
|
+
price: 45_00,
|
|
480
|
+
bundleDiscount,
|
|
481
|
+
}).create(),
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
// Create existing registration for group 1
|
|
485
|
+
const registration1 = await new RegistrationFactory({
|
|
486
|
+
organization,
|
|
487
|
+
member,
|
|
488
|
+
group: groups[0],
|
|
489
|
+
}).create();
|
|
490
|
+
|
|
491
|
+
// Create balance item for existing registration
|
|
492
|
+
await new BalanceItemFactory({
|
|
493
|
+
userId: user.id,
|
|
494
|
+
memberId: member.id,
|
|
495
|
+
organizationId: organization.id,
|
|
496
|
+
registrationId: registration1.id,
|
|
497
|
+
type: BalanceItemType.Registration,
|
|
498
|
+
amount: 1,
|
|
499
|
+
unitPrice: 35_00,
|
|
500
|
+
status: BalanceItemStatus.Due,
|
|
501
|
+
}).create();
|
|
502
|
+
|
|
503
|
+
// Now register the member for group 2 & 3 at the same time
|
|
504
|
+
const checkout = IDRegisterCheckout.create({
|
|
505
|
+
cart: IDRegisterCart.create({
|
|
506
|
+
items: [
|
|
507
|
+
IDRegisterItem.create({
|
|
508
|
+
options: [],
|
|
509
|
+
groupPrice: groups[1].settings.prices[0],
|
|
510
|
+
groupId: groups[1].id,
|
|
511
|
+
organizationId: organization.id,
|
|
512
|
+
memberId: member.id,
|
|
513
|
+
}),
|
|
514
|
+
IDRegisterItem.create({
|
|
515
|
+
options: [],
|
|
516
|
+
groupPrice: groups[2].settings.prices[0],
|
|
517
|
+
groupId: groups[2].id,
|
|
518
|
+
organizationId: organization.id,
|
|
519
|
+
memberId: member.id,
|
|
520
|
+
}),
|
|
521
|
+
],
|
|
522
|
+
}),
|
|
523
|
+
administrationFee: 0,
|
|
524
|
+
freeContribution: 0,
|
|
525
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
526
|
+
totalPrice: 25_00 + 45_00 - 7_00 - 18_00, // 20% off 35_00 = 7_00, 40% off 45_00 = 18_00
|
|
527
|
+
});
|
|
528
|
+
const response = await post(checkout, organization, token);
|
|
529
|
+
expect(response.body.registrations.length).toBe(2);
|
|
530
|
+
const registration2 = response.body.registrations.find(r => r.groupId === groups[1].id)!;
|
|
531
|
+
const registration3 = response.body.registrations.find(r => r.groupId === groups[2].id)!;
|
|
532
|
+
|
|
533
|
+
expect(registration2).toMatchObject({
|
|
534
|
+
registeredAt: expect.any(Date),
|
|
535
|
+
discounts: new Map(),
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
expect(registration3).toMatchObject({
|
|
539
|
+
registeredAt: expect.any(Date),
|
|
540
|
+
discounts: new Map([
|
|
541
|
+
[
|
|
542
|
+
bundleDiscount.id,
|
|
543
|
+
AppliedRegistrationDiscount.create({
|
|
544
|
+
name: bundleDiscount.name,
|
|
545
|
+
amount: 18_00,
|
|
546
|
+
}),
|
|
547
|
+
],
|
|
548
|
+
]),
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
await assertBalances({ user }, [
|
|
552
|
+
{
|
|
553
|
+
registrationId: registration1.id,
|
|
554
|
+
unitPrice: 35_00,
|
|
555
|
+
amount: 1,
|
|
556
|
+
type: BalanceItemType.Registration,
|
|
557
|
+
priceOpen: 35_00,
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
registrationId: registration2.id,
|
|
561
|
+
unitPrice: 25_00,
|
|
562
|
+
amount: 1,
|
|
563
|
+
type: BalanceItemType.Registration,
|
|
564
|
+
pricePending: 25_00,
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
registrationId: registration3.id,
|
|
568
|
+
unitPrice: 45_00,
|
|
569
|
+
amount: 1,
|
|
570
|
+
type: BalanceItemType.Registration,
|
|
571
|
+
pricePending: 45_00,
|
|
572
|
+
},
|
|
573
|
+
// Discounts
|
|
574
|
+
{
|
|
575
|
+
registrationId: registration1.id,
|
|
576
|
+
unitPrice: -7_00,
|
|
577
|
+
amount: 1,
|
|
578
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
579
|
+
pricePending: -7_00,
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
registrationId: registration3.id,
|
|
583
|
+
unitPrice: -18_00,
|
|
584
|
+
amount: 1,
|
|
585
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
586
|
+
pricePending: -18_00,
|
|
587
|
+
},
|
|
588
|
+
]);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* When we calculate the discounts, we calculate the discount based on the group price at the time of the registration.
|
|
593
|
+
* This also mainly applies to corrections that happen later
|
|
594
|
+
*/
|
|
595
|
+
test('Discounts are calculated based on the price at time of registration', async () => {
|
|
596
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
597
|
+
const bundleDiscount = await initBundleDiscount({
|
|
598
|
+
organizationRegistrationPeriod,
|
|
599
|
+
discount: {
|
|
600
|
+
discounts: [
|
|
601
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
602
|
+
{ value: 40_00, type: GroupPriceDiscountType.Percentage },
|
|
603
|
+
],
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const groups = [
|
|
608
|
+
await new GroupFactory({
|
|
609
|
+
organization,
|
|
610
|
+
price: 35_00,
|
|
611
|
+
bundleDiscount,
|
|
612
|
+
}).create(),
|
|
613
|
+
await new GroupFactory({
|
|
614
|
+
organization,
|
|
615
|
+
price: 25_00,
|
|
616
|
+
bundleDiscount,
|
|
617
|
+
}).create(),
|
|
618
|
+
];
|
|
619
|
+
|
|
620
|
+
// Create existing registration for group 1
|
|
621
|
+
const registration1 = await new RegistrationFactory({
|
|
622
|
+
organization,
|
|
623
|
+
member,
|
|
624
|
+
group: groups[0],
|
|
625
|
+
groupPrice: groups[0].settings.prices[0].patch({
|
|
626
|
+
price: ReduceablePrice.create({
|
|
627
|
+
price: 30_00, // This has changed, the group normally 'costs' 35_00
|
|
628
|
+
}),
|
|
629
|
+
}),
|
|
630
|
+
}).create();
|
|
631
|
+
|
|
632
|
+
// Create balance item for existing registration
|
|
633
|
+
await new BalanceItemFactory({
|
|
634
|
+
userId: user.id,
|
|
635
|
+
memberId: member.id,
|
|
636
|
+
organizationId: organization.id,
|
|
637
|
+
registrationId: registration1.id,
|
|
638
|
+
type: BalanceItemType.Registration,
|
|
639
|
+
amount: 1,
|
|
640
|
+
unitPrice: 30_00, // This has changed, the group normally 'costs' 35_00
|
|
641
|
+
status: BalanceItemStatus.Due,
|
|
642
|
+
}).create();
|
|
643
|
+
|
|
644
|
+
// Now register the member for group 2 & 3 at the same time
|
|
645
|
+
const checkout = IDRegisterCheckout.create({
|
|
646
|
+
cart: IDRegisterCart.create({
|
|
647
|
+
items: [
|
|
648
|
+
IDRegisterItem.create({
|
|
649
|
+
options: [],
|
|
650
|
+
groupPrice: groups[1].settings.prices[0],
|
|
651
|
+
groupId: groups[1].id,
|
|
652
|
+
organizationId: organization.id,
|
|
653
|
+
memberId: member.id,
|
|
654
|
+
}),
|
|
655
|
+
],
|
|
656
|
+
}),
|
|
657
|
+
administrationFee: 0,
|
|
658
|
+
freeContribution: 0,
|
|
659
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
660
|
+
totalPrice: 25_00 - 6_00, // 20% of 30_00 = 6_00
|
|
661
|
+
});
|
|
662
|
+
const response = await post(checkout, organization, token);
|
|
663
|
+
expect(response.body.registrations.length).toBe(1);
|
|
664
|
+
const registration2 = response.body.registrations[0];
|
|
665
|
+
|
|
666
|
+
expect(registration2).toMatchObject({
|
|
667
|
+
registeredAt: expect.any(Date),
|
|
668
|
+
discounts: new Map(),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
await assertBalances({ user }, [
|
|
672
|
+
{
|
|
673
|
+
registrationId: registration1.id,
|
|
674
|
+
unitPrice: 30_00,
|
|
675
|
+
amount: 1,
|
|
676
|
+
type: BalanceItemType.Registration,
|
|
677
|
+
priceOpen: 30_00,
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
registrationId: registration2.id,
|
|
681
|
+
unitPrice: 25_00,
|
|
682
|
+
amount: 1,
|
|
683
|
+
type: BalanceItemType.Registration,
|
|
684
|
+
pricePending: 25_00,
|
|
685
|
+
},
|
|
686
|
+
// Discounts
|
|
687
|
+
{
|
|
688
|
+
registrationId: registration1.id,
|
|
689
|
+
unitPrice: -6_00,
|
|
690
|
+
amount: 1,
|
|
691
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
692
|
+
pricePending: -6_00,
|
|
693
|
+
},
|
|
694
|
+
]);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* If you cancel a registration with 100% cancellation fee, we'll keep the balances items 'Due'.
|
|
699
|
+
* This tests that we don't substract the discounts on those registration on new registrations.
|
|
700
|
+
*/
|
|
701
|
+
test('A previous discount on a cancelled registration that has a 100% cancellation fee is not taken into account', async () => {
|
|
702
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
703
|
+
const bundleDiscount = await initBundleDiscount({
|
|
704
|
+
organizationRegistrationPeriod,
|
|
705
|
+
discount: {
|
|
706
|
+
discounts: [
|
|
707
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
708
|
+
{ value: 100_00, type: GroupPriceDiscountType.Percentage },
|
|
709
|
+
],
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
const groups = [
|
|
714
|
+
await new GroupFactory({
|
|
715
|
+
organization,
|
|
716
|
+
price: 25_00,
|
|
717
|
+
bundleDiscount,
|
|
718
|
+
}).create(),
|
|
719
|
+
|
|
720
|
+
await new GroupFactory({
|
|
721
|
+
organization,
|
|
722
|
+
price: 15_00,
|
|
723
|
+
bundleDiscount,
|
|
724
|
+
}).create(),
|
|
725
|
+
|
|
726
|
+
await new GroupFactory({
|
|
727
|
+
organization,
|
|
728
|
+
price: 35_00,
|
|
729
|
+
bundleDiscount,
|
|
730
|
+
}).create(),
|
|
731
|
+
];
|
|
732
|
+
|
|
733
|
+
const registrations = [
|
|
734
|
+
await new RegistrationFactory({
|
|
735
|
+
organization,
|
|
736
|
+
member,
|
|
737
|
+
group: groups[0],
|
|
738
|
+
deactivatedAt: new Date(), // had a discount in the past - but was cancelled
|
|
739
|
+
}).create(),
|
|
740
|
+
await new RegistrationFactory({
|
|
741
|
+
organization,
|
|
742
|
+
member,
|
|
743
|
+
group: groups[1],
|
|
744
|
+
}).create(),
|
|
745
|
+
];
|
|
746
|
+
|
|
747
|
+
// Create initial balances
|
|
748
|
+
await new BalanceItemFactory({
|
|
749
|
+
userId: user.id,
|
|
750
|
+
memberId: member.id,
|
|
751
|
+
organizationId: organization.id,
|
|
752
|
+
registrationId: registrations[0].id,
|
|
753
|
+
type: BalanceItemType.Registration,
|
|
754
|
+
amount: 1,
|
|
755
|
+
unitPrice: 25_00,
|
|
756
|
+
status: BalanceItemStatus.Due, // Still due, because we had a 100% cancellation fee
|
|
757
|
+
}).create();
|
|
758
|
+
|
|
759
|
+
// Create applied discount
|
|
760
|
+
await new BalanceItemFactory({
|
|
761
|
+
userId: user.id,
|
|
762
|
+
memberId: member.id,
|
|
763
|
+
organizationId: organization.id,
|
|
764
|
+
registrationId: registrations[0].id,
|
|
765
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
766
|
+
amount: 1,
|
|
767
|
+
unitPrice: -5_00,
|
|
768
|
+
status: BalanceItemStatus.Due, // Still due, because we had a 100% cancellation fee
|
|
769
|
+
relations: new Map([
|
|
770
|
+
[
|
|
771
|
+
BalanceItemRelationType.Discount,
|
|
772
|
+
BalanceItemRelation.create({
|
|
773
|
+
// We need the ID of the discount saved here
|
|
774
|
+
id: bundleDiscount.id,
|
|
775
|
+
name: bundleDiscount.name,
|
|
776
|
+
}),
|
|
777
|
+
],
|
|
778
|
+
]),
|
|
779
|
+
}).create();
|
|
780
|
+
|
|
781
|
+
await new BalanceItemFactory({
|
|
782
|
+
userId: user.id,
|
|
783
|
+
memberId: member.id,
|
|
784
|
+
organizationId: organization.id,
|
|
785
|
+
registrationId: registrations[1].id,
|
|
786
|
+
type: BalanceItemType.Registration,
|
|
787
|
+
amount: 1,
|
|
788
|
+
unitPrice: 15_00,
|
|
789
|
+
status: BalanceItemStatus.Due,
|
|
790
|
+
}).create();
|
|
791
|
+
|
|
792
|
+
// Register for group 3
|
|
793
|
+
const checkout = IDRegisterCheckout.create({
|
|
794
|
+
cart: IDRegisterCart.create({
|
|
795
|
+
items: [
|
|
796
|
+
IDRegisterItem.create({
|
|
797
|
+
options: [],
|
|
798
|
+
groupPrice: groups[2].settings.prices[0],
|
|
799
|
+
groupId: groups[2].id,
|
|
800
|
+
organizationId: organization.id,
|
|
801
|
+
memberId: member.id,
|
|
802
|
+
}),
|
|
803
|
+
],
|
|
804
|
+
}),
|
|
805
|
+
administrationFee: 0,
|
|
806
|
+
freeContribution: 0,
|
|
807
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
808
|
+
totalPrice: 35_00 - 7_00, // 20% of 35 = 7_00
|
|
809
|
+
});
|
|
810
|
+
const response = await post(checkout, organization, token);
|
|
811
|
+
const registration3 = response.body.registrations[0];
|
|
812
|
+
|
|
813
|
+
// Check did not reuse id
|
|
814
|
+
expect(registration3.id).not.toEqual(registrations[0].id);
|
|
815
|
+
|
|
816
|
+
expect(registration3).toMatchObject({
|
|
817
|
+
registeredAt: expect.any(Date),
|
|
818
|
+
discounts: new Map([
|
|
819
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
820
|
+
name: bundleDiscount.name,
|
|
821
|
+
amount: 7_00,
|
|
822
|
+
})],
|
|
823
|
+
]),
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// Check registration 1 still has the discount applied
|
|
827
|
+
await registrations[0].refresh();
|
|
828
|
+
expect(registrations[0].discounts).toMatchMap(new Map([
|
|
829
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
830
|
+
name: bundleDiscount.name,
|
|
831
|
+
amount: 5_00,
|
|
832
|
+
})],
|
|
833
|
+
]));
|
|
834
|
+
|
|
835
|
+
// Check balances as expected
|
|
836
|
+
await assertBalances({ user }, [
|
|
837
|
+
{
|
|
838
|
+
type: BalanceItemType.Registration,
|
|
839
|
+
registrationId: registrations[0].id,
|
|
840
|
+
amount: 1,
|
|
841
|
+
price: 25_00,
|
|
842
|
+
status: BalanceItemStatus.Due,
|
|
843
|
+
priceOpen: 25_00,
|
|
844
|
+
pricePending: 0,
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
848
|
+
registrationId: registrations[0].id,
|
|
849
|
+
amount: 1,
|
|
850
|
+
price: -5_00,
|
|
851
|
+
status: BalanceItemStatus.Due,
|
|
852
|
+
priceOpen: -5_00,
|
|
853
|
+
pricePending: 0,
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
type: BalanceItemType.Registration,
|
|
857
|
+
registrationId: registrations[1].id,
|
|
858
|
+
amount: 1,
|
|
859
|
+
price: 15_00,
|
|
860
|
+
status: BalanceItemStatus.Due,
|
|
861
|
+
priceOpen: 15_00,
|
|
862
|
+
pricePending: 0,
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
866
|
+
registrationId: registration3.id,
|
|
867
|
+
amount: 1,
|
|
868
|
+
price: -7_00,
|
|
869
|
+
status: BalanceItemStatus.Due,
|
|
870
|
+
priceOpen: 0,
|
|
871
|
+
pricePending: -7_00,
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
type: BalanceItemType.Registration,
|
|
875
|
+
registrationId: registration3.id,
|
|
876
|
+
amount: 1,
|
|
877
|
+
price: 35_00,
|
|
878
|
+
status: BalanceItemStatus.Due,
|
|
879
|
+
priceOpen: 0,
|
|
880
|
+
pricePending: 35_00,
|
|
881
|
+
},
|
|
882
|
+
]);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
test('A wrong discount on a previous registration is automatically corrected', async () => {
|
|
886
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
887
|
+
const bundleDiscount = await initBundleDiscount({
|
|
888
|
+
organizationRegistrationPeriod,
|
|
889
|
+
discount: {
|
|
890
|
+
discounts: [
|
|
891
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
892
|
+
{ value: 100_00, type: GroupPriceDiscountType.Percentage },
|
|
893
|
+
],
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
const groups = [
|
|
898
|
+
await new GroupFactory({
|
|
899
|
+
organization,
|
|
900
|
+
price: 25_00,
|
|
901
|
+
bundleDiscount,
|
|
902
|
+
}).create(),
|
|
903
|
+
|
|
904
|
+
await new GroupFactory({
|
|
905
|
+
organization,
|
|
906
|
+
price: 35_00,
|
|
907
|
+
bundleDiscount,
|
|
908
|
+
}).create(),
|
|
909
|
+
];
|
|
910
|
+
|
|
911
|
+
// Due to a fictive bug, we have a registration that already has a discount applied
|
|
912
|
+
// The system should auto recover from this automatically by removing it on the next registration
|
|
913
|
+
const registrations = [
|
|
914
|
+
await new RegistrationFactory({
|
|
915
|
+
organization,
|
|
916
|
+
member,
|
|
917
|
+
group: groups[0],
|
|
918
|
+
}).create(),
|
|
919
|
+
];
|
|
920
|
+
|
|
921
|
+
// Create initial balances
|
|
922
|
+
await new BalanceItemFactory({
|
|
923
|
+
userId: user.id,
|
|
924
|
+
memberId: member.id,
|
|
925
|
+
organizationId: organization.id,
|
|
926
|
+
registrationId: registrations[0].id,
|
|
927
|
+
type: BalanceItemType.Registration,
|
|
928
|
+
amount: 1,
|
|
929
|
+
unitPrice: 25_00,
|
|
930
|
+
status: BalanceItemStatus.Due,
|
|
931
|
+
}).create();
|
|
932
|
+
|
|
933
|
+
// Create applied discount
|
|
934
|
+
await new BalanceItemFactory({
|
|
935
|
+
userId: user.id,
|
|
936
|
+
memberId: member.id,
|
|
937
|
+
organizationId: organization.id,
|
|
938
|
+
registrationId: registrations[0].id,
|
|
939
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
940
|
+
amount: 1,
|
|
941
|
+
unitPrice: -15_00, // not correct, this is what we'll correct
|
|
942
|
+
status: BalanceItemStatus.Due,
|
|
943
|
+
relations: new Map([
|
|
944
|
+
[
|
|
945
|
+
BalanceItemRelationType.Discount,
|
|
946
|
+
BalanceItemRelation.create({
|
|
947
|
+
// We need the ID of the discount saved here
|
|
948
|
+
id: bundleDiscount.id,
|
|
949
|
+
name: bundleDiscount.name,
|
|
950
|
+
}),
|
|
951
|
+
],
|
|
952
|
+
]),
|
|
953
|
+
}).create();
|
|
954
|
+
|
|
955
|
+
// Register for group 2
|
|
956
|
+
const checkout = IDRegisterCheckout.create({
|
|
957
|
+
cart: IDRegisterCart.create({
|
|
958
|
+
items: [
|
|
959
|
+
IDRegisterItem.create({
|
|
960
|
+
options: [],
|
|
961
|
+
groupPrice: groups[1].settings.prices[0],
|
|
962
|
+
groupId: groups[1].id,
|
|
963
|
+
organizationId: organization.id,
|
|
964
|
+
memberId: member.id,
|
|
965
|
+
}),
|
|
966
|
+
],
|
|
967
|
+
}),
|
|
968
|
+
administrationFee: 0,
|
|
969
|
+
freeContribution: 0,
|
|
970
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
971
|
+
totalPrice: 35_00 - 7_00 + 15_00, // - normal discount + wrong discount
|
|
972
|
+
});
|
|
973
|
+
const response = await post(checkout, organization, token);
|
|
974
|
+
const registration2 = response.body.registrations[0];
|
|
975
|
+
|
|
976
|
+
expect(registration2).toMatchObject({
|
|
977
|
+
registeredAt: expect.any(Date),
|
|
978
|
+
discounts: new Map([
|
|
979
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
980
|
+
name: bundleDiscount.name,
|
|
981
|
+
amount: 7_00,
|
|
982
|
+
})],
|
|
983
|
+
]),
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
// Check registration 1 has removed the wrong discount
|
|
987
|
+
await registrations[0].refresh();
|
|
988
|
+
expect(registrations[0].discounts).toMatchMap(new Map());
|
|
989
|
+
|
|
990
|
+
// Check balances as expected
|
|
991
|
+
await assertBalances({ user }, [
|
|
992
|
+
{
|
|
993
|
+
type: BalanceItemType.Registration,
|
|
994
|
+
registrationId: registrations[0].id,
|
|
995
|
+
amount: 1,
|
|
996
|
+
price: 25_00,
|
|
997
|
+
status: BalanceItemStatus.Due,
|
|
998
|
+
priceOpen: 25_00,
|
|
999
|
+
pricePending: 0,
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1003
|
+
registrationId: registrations[0].id,
|
|
1004
|
+
amount: 1,
|
|
1005
|
+
price: -15_00,
|
|
1006
|
+
status: BalanceItemStatus.Due,
|
|
1007
|
+
priceOpen: -15_00,
|
|
1008
|
+
pricePending: 0,
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1012
|
+
registrationId: registration2.id,
|
|
1013
|
+
amount: 1,
|
|
1014
|
+
price: -7_00,
|
|
1015
|
+
status: BalanceItemStatus.Due,
|
|
1016
|
+
priceOpen: 0,
|
|
1017
|
+
pricePending: -7_00,
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
type: BalanceItemType.Registration,
|
|
1021
|
+
registrationId: registration2.id,
|
|
1022
|
+
amount: 1,
|
|
1023
|
+
price: 35_00,
|
|
1024
|
+
status: BalanceItemStatus.Due,
|
|
1025
|
+
priceOpen: 0,
|
|
1026
|
+
pricePending: 35_00,
|
|
1027
|
+
},
|
|
1028
|
+
// Corrected discount:
|
|
1029
|
+
{
|
|
1030
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1031
|
+
registrationId: registrations[0].id,
|
|
1032
|
+
amount: 1,
|
|
1033
|
+
price: 15_00,
|
|
1034
|
+
status: BalanceItemStatus.Due,
|
|
1035
|
+
priceOpen: 0,
|
|
1036
|
+
pricePending: 15_00,
|
|
1037
|
+
},
|
|
1038
|
+
]);
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
test('New registrations do not autocorrect if they are not eligible for the discount', async () => {
|
|
1042
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
1043
|
+
const bundleDiscount = await initBundleDiscount({
|
|
1044
|
+
organizationRegistrationPeriod,
|
|
1045
|
+
discount: {
|
|
1046
|
+
discounts: [
|
|
1047
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
1048
|
+
{ value: 100_00, type: GroupPriceDiscountType.Percentage },
|
|
1049
|
+
],
|
|
1050
|
+
},
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
const groups = [
|
|
1054
|
+
await new GroupFactory({
|
|
1055
|
+
organization,
|
|
1056
|
+
price: 25_00,
|
|
1057
|
+
bundleDiscount,
|
|
1058
|
+
}).create(),
|
|
1059
|
+
|
|
1060
|
+
await new GroupFactory({
|
|
1061
|
+
organization,
|
|
1062
|
+
price: 35_00,
|
|
1063
|
+
}).create(),
|
|
1064
|
+
];
|
|
1065
|
+
|
|
1066
|
+
// Due to a fictive bug, we have a registration that already has a discount applied
|
|
1067
|
+
// The system should auto recover from this automatically by removing it on the next registration
|
|
1068
|
+
const registrations = [
|
|
1069
|
+
await new RegistrationFactory({
|
|
1070
|
+
organization,
|
|
1071
|
+
member,
|
|
1072
|
+
group: groups[0],
|
|
1073
|
+
}).create(),
|
|
1074
|
+
];
|
|
1075
|
+
|
|
1076
|
+
// Create initial balances
|
|
1077
|
+
await new BalanceItemFactory({
|
|
1078
|
+
userId: user.id,
|
|
1079
|
+
memberId: member.id,
|
|
1080
|
+
organizationId: organization.id,
|
|
1081
|
+
registrationId: registrations[0].id,
|
|
1082
|
+
type: BalanceItemType.Registration,
|
|
1083
|
+
amount: 1,
|
|
1084
|
+
unitPrice: 25_00,
|
|
1085
|
+
status: BalanceItemStatus.Due,
|
|
1086
|
+
}).create();
|
|
1087
|
+
|
|
1088
|
+
// Create applied discount
|
|
1089
|
+
await new BalanceItemFactory({
|
|
1090
|
+
userId: user.id,
|
|
1091
|
+
memberId: member.id,
|
|
1092
|
+
organizationId: organization.id,
|
|
1093
|
+
registrationId: registrations[0].id,
|
|
1094
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1095
|
+
amount: 1,
|
|
1096
|
+
unitPrice: -15_00, // not correct, this is what we'll correct
|
|
1097
|
+
status: BalanceItemStatus.Due,
|
|
1098
|
+
relations: new Map([
|
|
1099
|
+
[
|
|
1100
|
+
BalanceItemRelationType.Discount,
|
|
1101
|
+
BalanceItemRelation.create({
|
|
1102
|
+
// We need the ID of the discount saved here
|
|
1103
|
+
id: bundleDiscount.id,
|
|
1104
|
+
name: bundleDiscount.name,
|
|
1105
|
+
}),
|
|
1106
|
+
],
|
|
1107
|
+
]),
|
|
1108
|
+
}).create();
|
|
1109
|
+
|
|
1110
|
+
// Register for group 2
|
|
1111
|
+
const checkout = IDRegisterCheckout.create({
|
|
1112
|
+
cart: IDRegisterCart.create({
|
|
1113
|
+
items: [
|
|
1114
|
+
IDRegisterItem.create({
|
|
1115
|
+
options: [],
|
|
1116
|
+
groupPrice: groups[1].settings.prices[0],
|
|
1117
|
+
groupId: groups[1].id,
|
|
1118
|
+
organizationId: organization.id,
|
|
1119
|
+
memberId: member.id,
|
|
1120
|
+
}),
|
|
1121
|
+
],
|
|
1122
|
+
}),
|
|
1123
|
+
administrationFee: 0,
|
|
1124
|
+
freeContribution: 0,
|
|
1125
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1126
|
+
totalPrice: 35_00,
|
|
1127
|
+
});
|
|
1128
|
+
const response = await post(checkout, organization, token);
|
|
1129
|
+
const registration2 = response.body.registrations[0];
|
|
1130
|
+
|
|
1131
|
+
expect(registration2).toMatchObject({
|
|
1132
|
+
registeredAt: expect.any(Date),
|
|
1133
|
+
discounts: new Map([]),
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
// Check registration 1 has NOT removed the wrong discount
|
|
1137
|
+
await registrations[0].refresh();
|
|
1138
|
+
expect(registrations[0].discounts).toMatchMap(new Map([
|
|
1139
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
1140
|
+
name: bundleDiscount.name,
|
|
1141
|
+
amount: 15_00,
|
|
1142
|
+
})],
|
|
1143
|
+
]));
|
|
1144
|
+
|
|
1145
|
+
// Check balances as expected
|
|
1146
|
+
await assertBalances({ user }, [
|
|
1147
|
+
{
|
|
1148
|
+
type: BalanceItemType.Registration,
|
|
1149
|
+
registrationId: registrations[0].id,
|
|
1150
|
+
amount: 1,
|
|
1151
|
+
price: 25_00,
|
|
1152
|
+
status: BalanceItemStatus.Due,
|
|
1153
|
+
priceOpen: 25_00,
|
|
1154
|
+
pricePending: 0,
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1158
|
+
registrationId: registrations[0].id,
|
|
1159
|
+
amount: 1,
|
|
1160
|
+
price: -15_00,
|
|
1161
|
+
status: BalanceItemStatus.Due,
|
|
1162
|
+
priceOpen: -15_00,
|
|
1163
|
+
pricePending: 0,
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
type: BalanceItemType.Registration,
|
|
1167
|
+
registrationId: registration2.id,
|
|
1168
|
+
amount: 1,
|
|
1169
|
+
price: 35_00,
|
|
1170
|
+
status: BalanceItemStatus.Due,
|
|
1171
|
+
priceOpen: 0,
|
|
1172
|
+
pricePending: 35_00,
|
|
1173
|
+
},
|
|
1174
|
+
]);
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
test('PointOfSale: A bundle discount is applied when registering for two groups at once', async () => {
|
|
1178
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
1179
|
+
const bundleDiscount = await initBundleDiscount({
|
|
1180
|
+
organizationRegistrationPeriod,
|
|
1181
|
+
discount: {
|
|
1182
|
+
discounts: [
|
|
1183
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
1184
|
+
],
|
|
1185
|
+
},
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
const groups = [
|
|
1189
|
+
await new GroupFactory({
|
|
1190
|
+
organization,
|
|
1191
|
+
price: 25_00,
|
|
1192
|
+
bundleDiscount,
|
|
1193
|
+
}).create(),
|
|
1194
|
+
await new GroupFactory({
|
|
1195
|
+
organization,
|
|
1196
|
+
price: 15_00,
|
|
1197
|
+
bundleDiscount,
|
|
1198
|
+
}).create(),
|
|
1199
|
+
];
|
|
1200
|
+
|
|
1201
|
+
// First register the member for group 1 & 2
|
|
1202
|
+
const checkout = IDRegisterCheckout.create({
|
|
1203
|
+
cart: IDRegisterCart.create({
|
|
1204
|
+
items: [
|
|
1205
|
+
IDRegisterItem.create({
|
|
1206
|
+
options: [],
|
|
1207
|
+
groupPrice: groups[0].settings.prices[0],
|
|
1208
|
+
groupId: groups[0].id,
|
|
1209
|
+
organizationId: organization.id,
|
|
1210
|
+
memberId: member.id,
|
|
1211
|
+
}),
|
|
1212
|
+
IDRegisterItem.create({
|
|
1213
|
+
options: [],
|
|
1214
|
+
groupPrice: groups[1].settings.prices[0],
|
|
1215
|
+
groupId: groups[1].id,
|
|
1216
|
+
organizationId: organization.id,
|
|
1217
|
+
memberId: member.id,
|
|
1218
|
+
}),
|
|
1219
|
+
],
|
|
1220
|
+
}),
|
|
1221
|
+
administrationFee: 0,
|
|
1222
|
+
freeContribution: 0,
|
|
1223
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1224
|
+
totalPrice: 25_00 + 15_00 - 5_00, // 20% discount on highest price (25_00 * 0.2 = 5_00)
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
const response1 = await post(checkout, organization, token);
|
|
1228
|
+
expect(response1.body.registrations.length).toBe(2);
|
|
1229
|
+
|
|
1230
|
+
expect(response1.body.registrations).toIncludeAllMembers([
|
|
1231
|
+
expect.objectContaining({
|
|
1232
|
+
registeredAt: expect.any(Date),
|
|
1233
|
+
groupId: groups[0].id,
|
|
1234
|
+
discounts: new Map([
|
|
1235
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
1236
|
+
name: bundleDiscount.name,
|
|
1237
|
+
amount: 5_00,
|
|
1238
|
+
})],
|
|
1239
|
+
]),
|
|
1240
|
+
}),
|
|
1241
|
+
expect.objectContaining({
|
|
1242
|
+
registeredAt: expect.any(Date),
|
|
1243
|
+
groupId: groups[1].id,
|
|
1244
|
+
discounts: new Map(),
|
|
1245
|
+
}),
|
|
1246
|
+
]);
|
|
1247
|
+
|
|
1248
|
+
await assertBalances({ user }, [
|
|
1249
|
+
{
|
|
1250
|
+
type: BalanceItemType.Registration,
|
|
1251
|
+
registrationId: expect.any(String),
|
|
1252
|
+
amount: 1,
|
|
1253
|
+
price: 25_00,
|
|
1254
|
+
status: BalanceItemStatus.Due,
|
|
1255
|
+
priceOpen: 0,
|
|
1256
|
+
pricePending: 25_00,
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1260
|
+
registrationId: expect.any(String),
|
|
1261
|
+
amount: 1,
|
|
1262
|
+
price: -5_00,
|
|
1263
|
+
status: BalanceItemStatus.Due,
|
|
1264
|
+
priceOpen: 0,
|
|
1265
|
+
pricePending: -5_00,
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
type: BalanceItemType.Registration,
|
|
1269
|
+
registrationId: expect.any(String),
|
|
1270
|
+
amount: 1,
|
|
1271
|
+
price: 15_00,
|
|
1272
|
+
status: BalanceItemStatus.Due,
|
|
1273
|
+
priceOpen: 0,
|
|
1274
|
+
pricePending: 15_00,
|
|
1275
|
+
},
|
|
1276
|
+
]);
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
test('Apply a discount on a previous registration with online payment (2 tries)', async () => {
|
|
1280
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
1281
|
+
const { stripeMocker } = await initStripe({ organization });
|
|
1282
|
+
|
|
1283
|
+
const bundleDiscount = await initBundleDiscount({
|
|
1284
|
+
organizationRegistrationPeriod,
|
|
1285
|
+
discount: {
|
|
1286
|
+
discounts: [
|
|
1287
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
1288
|
+
],
|
|
1289
|
+
},
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
const group = await new GroupFactory({
|
|
1293
|
+
organization,
|
|
1294
|
+
price: 25_00,
|
|
1295
|
+
stock: 500,
|
|
1296
|
+
bundleDiscount,
|
|
1297
|
+
})
|
|
1298
|
+
.create();
|
|
1299
|
+
const groupPrice = group.settings.prices[0];
|
|
1300
|
+
|
|
1301
|
+
const group2 = await new GroupFactory({
|
|
1302
|
+
organization,
|
|
1303
|
+
price: 15_00, // Lower price so discount is applied preferably on the first group
|
|
1304
|
+
bundleDiscount,
|
|
1305
|
+
}).create();
|
|
1306
|
+
|
|
1307
|
+
const groupPrice2 = group2.settings.prices[0];
|
|
1308
|
+
|
|
1309
|
+
// First register the member for group 1. No discount should be applied yet
|
|
1310
|
+
const checkout1 = IDRegisterCheckout.create({
|
|
1311
|
+
cart: IDRegisterCart.create({
|
|
1312
|
+
items: [
|
|
1313
|
+
IDRegisterItem.create({
|
|
1314
|
+
options: [],
|
|
1315
|
+
groupPrice,
|
|
1316
|
+
groupId: group.id,
|
|
1317
|
+
organizationId: organization.id,
|
|
1318
|
+
memberId: member.id,
|
|
1319
|
+
}),
|
|
1320
|
+
],
|
|
1321
|
+
}),
|
|
1322
|
+
administrationFee: 0,
|
|
1323
|
+
freeContribution: 0,
|
|
1324
|
+
paymentMethod: PaymentMethod.Bancontact,
|
|
1325
|
+
totalPrice: groupPrice.price.price,
|
|
1326
|
+
redirectUrl: new URL('https://www.example.com/success'),
|
|
1327
|
+
cancelUrl: new URL('https://www.example.com/cancel'),
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
const response1 = await post(checkout1, organization, token);
|
|
1331
|
+
expect(response1.body.registrations.length).toBe(1);
|
|
1332
|
+
const registration1 = response1.body.registrations[0];
|
|
1333
|
+
expect(registration1.registeredAt).toBeNull();
|
|
1334
|
+
expect(registration1.discounts).toMatchMap(new Map());
|
|
1335
|
+
|
|
1336
|
+
// Check state of balances
|
|
1337
|
+
await assertBalances({ user }, [
|
|
1338
|
+
{
|
|
1339
|
+
type: BalanceItemType.Registration,
|
|
1340
|
+
registrationId: registration1.id,
|
|
1341
|
+
amount: 1,
|
|
1342
|
+
unitPrice: 25_00,
|
|
1343
|
+
status: BalanceItemStatus.Hidden,
|
|
1344
|
+
priceOpen: -25_00, // hidden, so no payment expected yet
|
|
1345
|
+
pricePending: 25_00,
|
|
1346
|
+
pricePaid: 0,
|
|
1347
|
+
},
|
|
1348
|
+
]);
|
|
1349
|
+
|
|
1350
|
+
await stripeMocker.succeedPayment(stripeMocker.getLastIntent());
|
|
1351
|
+
|
|
1352
|
+
// Check registration became valid
|
|
1353
|
+
const updatedRegistration1 = (await Registration.getByID(registration1.id))!;
|
|
1354
|
+
expect(updatedRegistration1).toBeDefined();
|
|
1355
|
+
expect(updatedRegistration1.registeredAt).not.toBeNull();
|
|
1356
|
+
|
|
1357
|
+
// Check state of balances
|
|
1358
|
+
const expectedBalances: Partial<BalanceItem>[] = [
|
|
1359
|
+
{
|
|
1360
|
+
type: BalanceItemType.Registration,
|
|
1361
|
+
registrationId: registration1.id,
|
|
1362
|
+
amount: 1,
|
|
1363
|
+
unitPrice: 25_00,
|
|
1364
|
+
status: BalanceItemStatus.Due,
|
|
1365
|
+
priceOpen: 0,
|
|
1366
|
+
pricePaid: 25_00,
|
|
1367
|
+
pricePending: 0,
|
|
1368
|
+
paidAt: expect.any(Date),
|
|
1369
|
+
},
|
|
1370
|
+
];
|
|
1371
|
+
|
|
1372
|
+
await assertBalances({ user }, expectedBalances);
|
|
1373
|
+
|
|
1374
|
+
const checkout2 = IDRegisterCheckout.create({
|
|
1375
|
+
cart: IDRegisterCart.create({
|
|
1376
|
+
items: [
|
|
1377
|
+
IDRegisterItem.create({
|
|
1378
|
+
options: [],
|
|
1379
|
+
groupPrice: groupPrice2,
|
|
1380
|
+
groupId: group2.id,
|
|
1381
|
+
organizationId: organization.id,
|
|
1382
|
+
memberId: member.id,
|
|
1383
|
+
}),
|
|
1384
|
+
],
|
|
1385
|
+
}),
|
|
1386
|
+
administrationFee: 0,
|
|
1387
|
+
freeContribution: 0,
|
|
1388
|
+
paymentMethod: PaymentMethod.Bancontact,
|
|
1389
|
+
totalPrice: 15_00 - 5_00, // 20% discount on first group
|
|
1390
|
+
redirectUrl: new URL('https://www.example.com/success'),
|
|
1391
|
+
cancelUrl: new URL('https://www.example.com/cancel'),
|
|
1392
|
+
});
|
|
1393
|
+
const response2 = await post(checkout2, organization, token);
|
|
1394
|
+
expect(response2.body.registrations.length).toBe(1);
|
|
1395
|
+
|
|
1396
|
+
const registration2 = response2.body.registrations[0];
|
|
1397
|
+
expect(registration2.registeredAt).toBeNull(); // not yet valid
|
|
1398
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
1399
|
+
|
|
1400
|
+
// Get registration 1 again, it should not yet have any bundle discount applied
|
|
1401
|
+
await updatedRegistration1.refresh();
|
|
1402
|
+
expect(updatedRegistration1.discounts).toMatchMap(new Map());
|
|
1403
|
+
|
|
1404
|
+
// Check state of balances
|
|
1405
|
+
await assertBalances({ user }, [
|
|
1406
|
+
...expectedBalances,
|
|
1407
|
+
{
|
|
1408
|
+
type: BalanceItemType.Registration,
|
|
1409
|
+
registrationId: registration2.id,
|
|
1410
|
+
amount: 1,
|
|
1411
|
+
unitPrice: 15_00,
|
|
1412
|
+
status: BalanceItemStatus.Hidden, // Pending
|
|
1413
|
+
priceOpen: -15_00, // hidden, so no payment expected yet
|
|
1414
|
+
pricePending: 15_00,
|
|
1415
|
+
pricePaid: 0,
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1419
|
+
registrationId: registration1.id,
|
|
1420
|
+
amount: 1,
|
|
1421
|
+
unitPrice: -5_00,
|
|
1422
|
+
status: BalanceItemStatus.Hidden, // Pending
|
|
1423
|
+
priceOpen: 5_00,
|
|
1424
|
+
pricePending: -5_00,
|
|
1425
|
+
pricePaid: 0,
|
|
1426
|
+
},
|
|
1427
|
+
]);
|
|
1428
|
+
|
|
1429
|
+
// Fail the payment...
|
|
1430
|
+
await stripeMocker.failPayment(stripeMocker.getLastIntent());
|
|
1431
|
+
|
|
1432
|
+
// Only price pending and open has changed now
|
|
1433
|
+
expectedBalances.push(
|
|
1434
|
+
{
|
|
1435
|
+
type: BalanceItemType.Registration,
|
|
1436
|
+
registrationId: registration2.id,
|
|
1437
|
+
amount: 1,
|
|
1438
|
+
unitPrice: 15_00,
|
|
1439
|
+
status: BalanceItemStatus.Hidden,
|
|
1440
|
+
priceOpen: 0,
|
|
1441
|
+
pricePending: 0,
|
|
1442
|
+
pricePaid: 0,
|
|
1443
|
+
paidAt: null,
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1447
|
+
registrationId: registration1.id,
|
|
1448
|
+
amount: 1,
|
|
1449
|
+
unitPrice: -5_00,
|
|
1450
|
+
status: BalanceItemStatus.Hidden,
|
|
1451
|
+
priceOpen: 0,
|
|
1452
|
+
pricePending: 0,
|
|
1453
|
+
pricePaid: 0,
|
|
1454
|
+
paidAt: null,
|
|
1455
|
+
},
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
await assertBalances({ user }, expectedBalances);
|
|
1459
|
+
|
|
1460
|
+
// Try the payment again
|
|
1461
|
+
const response3 = await post(checkout2, organization, token);
|
|
1462
|
+
expect(response3.body.registrations.length).toBe(1);
|
|
1463
|
+
|
|
1464
|
+
const registration3 = response3.body.registrations[0];
|
|
1465
|
+
expect(registration3.id).not.toEqual(registration2.id);
|
|
1466
|
+
expect(registration3.registeredAt).toBeNull(); // not yet valid
|
|
1467
|
+
expect(registration3.discounts).toMatchMap(new Map());
|
|
1468
|
+
|
|
1469
|
+
// Get registration 1 again, it should not yet have any bundle discount applied
|
|
1470
|
+
await updatedRegistration1.refresh();
|
|
1471
|
+
expect(updatedRegistration1.discounts).toMatchMap(new Map());
|
|
1472
|
+
|
|
1473
|
+
await assertBalances({ user }, [
|
|
1474
|
+
...expectedBalances,
|
|
1475
|
+
{
|
|
1476
|
+
type: BalanceItemType.Registration,
|
|
1477
|
+
registrationId: registration3.id,
|
|
1478
|
+
amount: 1,
|
|
1479
|
+
unitPrice: 15_00,
|
|
1480
|
+
status: BalanceItemStatus.Hidden, // Pending
|
|
1481
|
+
priceOpen: -15_00, // hidden, so no payment expected yet
|
|
1482
|
+
pricePending: 15_00,
|
|
1483
|
+
pricePaid: 0,
|
|
1484
|
+
paidAt: null,
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1488
|
+
registrationId: registration1.id,
|
|
1489
|
+
amount: 1,
|
|
1490
|
+
unitPrice: -5_00,
|
|
1491
|
+
status: BalanceItemStatus.Hidden, // Pending
|
|
1492
|
+
priceOpen: 5_00,
|
|
1493
|
+
pricePending: -5_00,
|
|
1494
|
+
pricePaid: 0,
|
|
1495
|
+
paidAt: null,
|
|
1496
|
+
},
|
|
1497
|
+
]);
|
|
1498
|
+
|
|
1499
|
+
// Success the payment
|
|
1500
|
+
await stripeMocker.succeedPayment(stripeMocker.getLastIntent());
|
|
1501
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
1502
|
+
|
|
1503
|
+
// Check registration 3 became valid
|
|
1504
|
+
const updatedRegistration3 = (await Registration.getByID(registration3.id))!;
|
|
1505
|
+
expect(updatedRegistration3).toBeDefined();
|
|
1506
|
+
expect(updatedRegistration3.registeredAt).not.toBeNull();
|
|
1507
|
+
|
|
1508
|
+
// Check registration 1 now has the discounts saved
|
|
1509
|
+
await updatedRegistration1.refresh();
|
|
1510
|
+
expect(updatedRegistration1.discounts).toMatchMap(new Map([
|
|
1511
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
1512
|
+
name: bundleDiscount.name,
|
|
1513
|
+
amount: 5_00,
|
|
1514
|
+
})],
|
|
1515
|
+
]));
|
|
1516
|
+
|
|
1517
|
+
// Only price pending and open has changed now
|
|
1518
|
+
expectedBalances.push(
|
|
1519
|
+
{
|
|
1520
|
+
type: BalanceItemType.Registration,
|
|
1521
|
+
registrationId: registration3.id,
|
|
1522
|
+
amount: 1,
|
|
1523
|
+
unitPrice: 15_00,
|
|
1524
|
+
status: BalanceItemStatus.Due,
|
|
1525
|
+
priceOpen: 0,
|
|
1526
|
+
pricePending: 0,
|
|
1527
|
+
pricePaid: 15_00,
|
|
1528
|
+
paidAt: expect.any(Date),
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1532
|
+
registrationId: registration1.id,
|
|
1533
|
+
amount: 1,
|
|
1534
|
+
unitPrice: -5_00,
|
|
1535
|
+
status: BalanceItemStatus.Due,
|
|
1536
|
+
priceOpen: 0,
|
|
1537
|
+
pricePending: 0,
|
|
1538
|
+
pricePaid: -5_00,
|
|
1539
|
+
paidAt: expect.any(Date),
|
|
1540
|
+
},
|
|
1541
|
+
);
|
|
1542
|
+
|
|
1543
|
+
await assertBalances({ user }, expectedBalances);
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
test('Multiple discounts can be applied to a single registration', async () => {
|
|
1547
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
1548
|
+
|
|
1549
|
+
const bundleDiscounts = [
|
|
1550
|
+
await initBundleDiscount({
|
|
1551
|
+
organizationRegistrationPeriod,
|
|
1552
|
+
discount: {
|
|
1553
|
+
discounts: [
|
|
1554
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
1555
|
+
{ value: 40_00, type: GroupPriceDiscountType.Percentage },
|
|
1556
|
+
],
|
|
1557
|
+
},
|
|
1558
|
+
}),
|
|
1559
|
+
await initBundleDiscount({
|
|
1560
|
+
organizationRegistrationPeriod,
|
|
1561
|
+
discount: {
|
|
1562
|
+
discounts: [
|
|
1563
|
+
{ value: 50, type: GroupPriceDiscountType.Fixed },
|
|
1564
|
+
{ value: 5_00, type: GroupPriceDiscountType.Fixed },
|
|
1565
|
+
],
|
|
1566
|
+
},
|
|
1567
|
+
}),
|
|
1568
|
+
];
|
|
1569
|
+
|
|
1570
|
+
const groups = [
|
|
1571
|
+
await new GroupFactory({
|
|
1572
|
+
organization,
|
|
1573
|
+
price: 35_00,
|
|
1574
|
+
bundleDiscounts,
|
|
1575
|
+
}).create(),
|
|
1576
|
+
await new GroupFactory({
|
|
1577
|
+
organization,
|
|
1578
|
+
price: 25_00,
|
|
1579
|
+
bundleDiscounts,
|
|
1580
|
+
}).create(),
|
|
1581
|
+
await new GroupFactory({
|
|
1582
|
+
organization,
|
|
1583
|
+
price: 45_00,
|
|
1584
|
+
bundleDiscounts,
|
|
1585
|
+
}).create(),
|
|
1586
|
+
];
|
|
1587
|
+
|
|
1588
|
+
// Create existing registration for group 1
|
|
1589
|
+
const registration1 = await new RegistrationFactory({
|
|
1590
|
+
organization,
|
|
1591
|
+
member,
|
|
1592
|
+
group: groups[0],
|
|
1593
|
+
}).create();
|
|
1594
|
+
|
|
1595
|
+
// Create balance item for existing registration
|
|
1596
|
+
await new BalanceItemFactory({
|
|
1597
|
+
userId: user.id,
|
|
1598
|
+
memberId: member.id,
|
|
1599
|
+
organizationId: organization.id,
|
|
1600
|
+
registrationId: registration1.id,
|
|
1601
|
+
type: BalanceItemType.Registration,
|
|
1602
|
+
amount: 1,
|
|
1603
|
+
unitPrice: 35_00,
|
|
1604
|
+
status: BalanceItemStatus.Due,
|
|
1605
|
+
}).create();
|
|
1606
|
+
|
|
1607
|
+
// Now register the member for group 2 & 3 at the same time
|
|
1608
|
+
const checkout = IDRegisterCheckout.create({
|
|
1609
|
+
cart: IDRegisterCart.create({
|
|
1610
|
+
items: [
|
|
1611
|
+
IDRegisterItem.create({
|
|
1612
|
+
groupPrice: groups[1].settings.prices[0],
|
|
1613
|
+
groupId: groups[1].id,
|
|
1614
|
+
organizationId: organization.id,
|
|
1615
|
+
memberId: member.id,
|
|
1616
|
+
}),
|
|
1617
|
+
IDRegisterItem.create({
|
|
1618
|
+
groupPrice: groups[2].settings.prices[0],
|
|
1619
|
+
groupId: groups[2].id,
|
|
1620
|
+
organizationId: organization.id,
|
|
1621
|
+
memberId: member.id,
|
|
1622
|
+
}),
|
|
1623
|
+
],
|
|
1624
|
+
}),
|
|
1625
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1626
|
+
totalPrice: 25_00 + 45_00 - 7_00 - 18_00 - 50 - 5_00,
|
|
1627
|
+
});
|
|
1628
|
+
const response = await post(checkout, organization, token);
|
|
1629
|
+
expect(response.body.registrations.length).toBe(2);
|
|
1630
|
+
const registration2 = response.body.registrations.find(r => r.groupId === groups[1].id)!;
|
|
1631
|
+
const registration3 = response.body.registrations.find(r => r.groupId === groups[2].id)!;
|
|
1632
|
+
|
|
1633
|
+
expect(registration2).toMatchObject({
|
|
1634
|
+
registeredAt: expect.any(Date),
|
|
1635
|
+
discounts: new Map([
|
|
1636
|
+
[
|
|
1637
|
+
bundleDiscounts[1].id,
|
|
1638
|
+
AppliedRegistrationDiscount.create({
|
|
1639
|
+
name: bundleDiscounts[1].name,
|
|
1640
|
+
amount: 50,
|
|
1641
|
+
}),
|
|
1642
|
+
],
|
|
1643
|
+
]),
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
expect(registration3).toMatchObject({
|
|
1647
|
+
registeredAt: expect.any(Date),
|
|
1648
|
+
discounts: new Map([
|
|
1649
|
+
[
|
|
1650
|
+
bundleDiscounts[0].id,
|
|
1651
|
+
AppliedRegistrationDiscount.create({
|
|
1652
|
+
name: bundleDiscounts[0].name,
|
|
1653
|
+
amount: 18_00,
|
|
1654
|
+
}),
|
|
1655
|
+
],
|
|
1656
|
+
[
|
|
1657
|
+
bundleDiscounts[1].id,
|
|
1658
|
+
AppliedRegistrationDiscount.create({
|
|
1659
|
+
name: bundleDiscounts[1].name,
|
|
1660
|
+
amount: 5_00,
|
|
1661
|
+
}),
|
|
1662
|
+
],
|
|
1663
|
+
]),
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
await assertBalances({ user }, [
|
|
1667
|
+
{
|
|
1668
|
+
type: BalanceItemType.Registration,
|
|
1669
|
+
registrationId: registration1.id,
|
|
1670
|
+
amount: 1,
|
|
1671
|
+
unitPrice: 35_00,
|
|
1672
|
+
status: BalanceItemStatus.Due,
|
|
1673
|
+
priceOpen: 35_00,
|
|
1674
|
+
pricePending: 0,
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
type: BalanceItemType.Registration,
|
|
1678
|
+
registrationId: registration2.id,
|
|
1679
|
+
amount: 1,
|
|
1680
|
+
unitPrice: 25_00,
|
|
1681
|
+
status: BalanceItemStatus.Due,
|
|
1682
|
+
priceOpen: 0,
|
|
1683
|
+
pricePending: 25_00,
|
|
1684
|
+
},
|
|
1685
|
+
{
|
|
1686
|
+
type: BalanceItemType.Registration,
|
|
1687
|
+
registrationId: registration3.id,
|
|
1688
|
+
amount: 1,
|
|
1689
|
+
unitPrice: 45_00,
|
|
1690
|
+
status: BalanceItemStatus.Due,
|
|
1691
|
+
priceOpen: 0,
|
|
1692
|
+
pricePending: 45_00,
|
|
1693
|
+
},
|
|
1694
|
+
|
|
1695
|
+
// Discounts (4)
|
|
1696
|
+
{
|
|
1697
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1698
|
+
registrationId: registration2.id,
|
|
1699
|
+
amount: 1,
|
|
1700
|
+
unitPrice: -50,
|
|
1701
|
+
status: BalanceItemStatus.Due,
|
|
1702
|
+
priceOpen: 0,
|
|
1703
|
+
pricePending: -50,
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1707
|
+
registrationId: registration3.id,
|
|
1708
|
+
amount: 1,
|
|
1709
|
+
unitPrice: -18_00,
|
|
1710
|
+
status: BalanceItemStatus.Due,
|
|
1711
|
+
priceOpen: 0,
|
|
1712
|
+
pricePending: -18_00,
|
|
1713
|
+
},
|
|
1714
|
+
{
|
|
1715
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1716
|
+
registrationId: registration3.id,
|
|
1717
|
+
amount: 1,
|
|
1718
|
+
unitPrice: -5_00,
|
|
1719
|
+
status: BalanceItemStatus.Due,
|
|
1720
|
+
priceOpen: 0,
|
|
1721
|
+
pricePending: -5_00,
|
|
1722
|
+
},
|
|
1723
|
+
{
|
|
1724
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1725
|
+
registrationId: registration1.id,
|
|
1726
|
+
amount: 1,
|
|
1727
|
+
unitPrice: -7_00,
|
|
1728
|
+
status: BalanceItemStatus.Due,
|
|
1729
|
+
priceOpen: 0,
|
|
1730
|
+
pricePending: -7_00,
|
|
1731
|
+
},
|
|
1732
|
+
]);
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
// We run this test 5 times because it should be stable
|
|
1736
|
+
test.each([1, 2, 3, 4, 5])('If discounts are the same, they are not moved around after new registrations are added (%i th try)', async () => {
|
|
1737
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
1738
|
+
const bundleDiscount = await initBundleDiscount({
|
|
1739
|
+
organizationRegistrationPeriod,
|
|
1740
|
+
discount: {
|
|
1741
|
+
discounts: [
|
|
1742
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
1743
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
1744
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
1745
|
+
],
|
|
1746
|
+
},
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
const groups = [
|
|
1750
|
+
await new GroupFactory({
|
|
1751
|
+
organization,
|
|
1752
|
+
price: 25_00, // 20% discount = 5_00
|
|
1753
|
+
bundleDiscount,
|
|
1754
|
+
}).create(),
|
|
1755
|
+
await new GroupFactory({
|
|
1756
|
+
organization,
|
|
1757
|
+
price: 25_00, // 20% discount = 5_00 (same as group 1)
|
|
1758
|
+
bundleDiscount,
|
|
1759
|
+
}).create(),
|
|
1760
|
+
await new GroupFactory({
|
|
1761
|
+
organization,
|
|
1762
|
+
price: 25_00, // 20% discount = 5_00 (same as groups 1 & 2)
|
|
1763
|
+
bundleDiscount,
|
|
1764
|
+
}).create(),
|
|
1765
|
+
];
|
|
1766
|
+
|
|
1767
|
+
// Create existing registrations for group 1 & 2 with discounts already applied
|
|
1768
|
+
const registration1 = await new RegistrationFactory({
|
|
1769
|
+
organization,
|
|
1770
|
+
member,
|
|
1771
|
+
group: groups[0],
|
|
1772
|
+
}).create();
|
|
1773
|
+
|
|
1774
|
+
const registration2 = await new RegistrationFactory({
|
|
1775
|
+
organization,
|
|
1776
|
+
member,
|
|
1777
|
+
group: groups[1],
|
|
1778
|
+
}).create();
|
|
1779
|
+
|
|
1780
|
+
// Create balance items for existing registrations
|
|
1781
|
+
await new BalanceItemFactory({
|
|
1782
|
+
userId: user.id,
|
|
1783
|
+
memberId: member.id,
|
|
1784
|
+
organizationId: organization.id,
|
|
1785
|
+
registrationId: registration1.id,
|
|
1786
|
+
type: BalanceItemType.Registration,
|
|
1787
|
+
amount: 1,
|
|
1788
|
+
unitPrice: 25_00,
|
|
1789
|
+
status: BalanceItemStatus.Due,
|
|
1790
|
+
}).create();
|
|
1791
|
+
|
|
1792
|
+
await new BalanceItemFactory({
|
|
1793
|
+
userId: user.id,
|
|
1794
|
+
memberId: member.id,
|
|
1795
|
+
organizationId: organization.id,
|
|
1796
|
+
registrationId: registration1.id,
|
|
1797
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1798
|
+
amount: 1,
|
|
1799
|
+
unitPrice: -5_00,
|
|
1800
|
+
status: BalanceItemStatus.Due,
|
|
1801
|
+
relations: new Map([
|
|
1802
|
+
[
|
|
1803
|
+
BalanceItemRelationType.Discount,
|
|
1804
|
+
BalanceItemRelation.create({
|
|
1805
|
+
id: bundleDiscount.id,
|
|
1806
|
+
name: bundleDiscount.name,
|
|
1807
|
+
}),
|
|
1808
|
+
],
|
|
1809
|
+
]),
|
|
1810
|
+
}).create();
|
|
1811
|
+
|
|
1812
|
+
await new BalanceItemFactory({
|
|
1813
|
+
userId: user.id,
|
|
1814
|
+
memberId: member.id,
|
|
1815
|
+
organizationId: organization.id,
|
|
1816
|
+
registrationId: registration2.id,
|
|
1817
|
+
type: BalanceItemType.Registration,
|
|
1818
|
+
amount: 1,
|
|
1819
|
+
unitPrice: 25_00,
|
|
1820
|
+
status: BalanceItemStatus.Due,
|
|
1821
|
+
}).create();
|
|
1822
|
+
|
|
1823
|
+
// No discount on 2 ( = first)
|
|
1824
|
+
|
|
1825
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
1826
|
+
await registration1.refresh();
|
|
1827
|
+
await registration2.refresh();
|
|
1828
|
+
|
|
1829
|
+
// Check this is what we expect
|
|
1830
|
+
expect(registration2.discounts).toMatchMap(new Map([]));
|
|
1831
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
1832
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
1833
|
+
name: bundleDiscount.name,
|
|
1834
|
+
amount: 5_00,
|
|
1835
|
+
})],
|
|
1836
|
+
]));
|
|
1837
|
+
|
|
1838
|
+
// Now register the member for group 3
|
|
1839
|
+
const checkout = IDRegisterCheckout.create({
|
|
1840
|
+
cart: IDRegisterCart.create({
|
|
1841
|
+
items: [
|
|
1842
|
+
IDRegisterItem.create({
|
|
1843
|
+
groupPrice: groups[2].settings.prices[0],
|
|
1844
|
+
groupId: groups[2].id,
|
|
1845
|
+
organizationId: organization.id,
|
|
1846
|
+
memberId: member.id,
|
|
1847
|
+
}),
|
|
1848
|
+
],
|
|
1849
|
+
}),
|
|
1850
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1851
|
+
totalPrice: 25_00 - 5_00, // 20% discount on new registration
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
const response = await post(checkout, organization, token);
|
|
1855
|
+
expect(response.body.registrations.length).toBe(1);
|
|
1856
|
+
const registration3 = response.body.registrations[0];
|
|
1857
|
+
|
|
1858
|
+
expect(registration3).toMatchObject({
|
|
1859
|
+
registeredAt: expect.any(Date),
|
|
1860
|
+
discounts: new Map([
|
|
1861
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
1862
|
+
name: bundleDiscount.name,
|
|
1863
|
+
amount: 5_00,
|
|
1864
|
+
})],
|
|
1865
|
+
]),
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
// Verify that existing registrations still have their original discounts
|
|
1869
|
+
await registration1.refresh();
|
|
1870
|
+
await registration2.refresh();
|
|
1871
|
+
|
|
1872
|
+
expect(registration2.discounts).toMatchMap(new Map([]));
|
|
1873
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
1874
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
1875
|
+
name: bundleDiscount.name,
|
|
1876
|
+
amount: 5_00,
|
|
1877
|
+
})],
|
|
1878
|
+
]));
|
|
1879
|
+
|
|
1880
|
+
await assertBalances({ user }, [
|
|
1881
|
+
{
|
|
1882
|
+
type: BalanceItemType.Registration,
|
|
1883
|
+
registrationId: registration1.id,
|
|
1884
|
+
amount: 1,
|
|
1885
|
+
price: 25_00,
|
|
1886
|
+
status: BalanceItemStatus.Due,
|
|
1887
|
+
priceOpen: 25_00,
|
|
1888
|
+
pricePending: 0,
|
|
1889
|
+
},
|
|
1890
|
+
{
|
|
1891
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1892
|
+
registrationId: registration1.id,
|
|
1893
|
+
amount: 1,
|
|
1894
|
+
price: -5_00,
|
|
1895
|
+
status: BalanceItemStatus.Due,
|
|
1896
|
+
priceOpen: -5_00,
|
|
1897
|
+
pricePending: 0,
|
|
1898
|
+
},
|
|
1899
|
+
{
|
|
1900
|
+
type: BalanceItemType.Registration,
|
|
1901
|
+
registrationId: registration2.id,
|
|
1902
|
+
amount: 1,
|
|
1903
|
+
price: 25_00,
|
|
1904
|
+
status: BalanceItemStatus.Due,
|
|
1905
|
+
priceOpen: 25_00,
|
|
1906
|
+
pricePending: 0,
|
|
1907
|
+
},
|
|
1908
|
+
{
|
|
1909
|
+
type: BalanceItemType.Registration,
|
|
1910
|
+
registrationId: registration3.id,
|
|
1911
|
+
amount: 1,
|
|
1912
|
+
price: 25_00,
|
|
1913
|
+
status: BalanceItemStatus.Due,
|
|
1914
|
+
priceOpen: 0,
|
|
1915
|
+
pricePending: 25_00,
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1919
|
+
registrationId: registration3.id,
|
|
1920
|
+
amount: 1,
|
|
1921
|
+
price: -5_00,
|
|
1922
|
+
status: BalanceItemStatus.Due,
|
|
1923
|
+
priceOpen: 0,
|
|
1924
|
+
pricePending: -5_00,
|
|
1925
|
+
},
|
|
1926
|
+
]);
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
test('Wrong discounts are not corrected for unrelated registrations', async () => {
|
|
1930
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
1931
|
+
const bundleDiscount = await initBundleDiscount({
|
|
1932
|
+
organizationRegistrationPeriod,
|
|
1933
|
+
discount: {
|
|
1934
|
+
discounts: [
|
|
1935
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
1936
|
+
],
|
|
1937
|
+
},
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
const groups = [
|
|
1941
|
+
await new GroupFactory({
|
|
1942
|
+
organization,
|
|
1943
|
+
price: 25_00, // 20% discount = 5_00
|
|
1944
|
+
bundleDiscount,
|
|
1945
|
+
}).create(),
|
|
1946
|
+
|
|
1947
|
+
// Does not have discount:
|
|
1948
|
+
await new GroupFactory({
|
|
1949
|
+
organization,
|
|
1950
|
+
price: 25_00,
|
|
1951
|
+
}).create(),
|
|
1952
|
+
];
|
|
1953
|
+
|
|
1954
|
+
// Create existing registrations for group 1
|
|
1955
|
+
const registration1 = await new RegistrationFactory({
|
|
1956
|
+
organization,
|
|
1957
|
+
member,
|
|
1958
|
+
group: groups[0],
|
|
1959
|
+
}).create();
|
|
1960
|
+
|
|
1961
|
+
// Create balance items for existing registrations
|
|
1962
|
+
await new BalanceItemFactory({
|
|
1963
|
+
userId: user.id,
|
|
1964
|
+
memberId: member.id,
|
|
1965
|
+
organizationId: organization.id,
|
|
1966
|
+
registrationId: registration1.id,
|
|
1967
|
+
type: BalanceItemType.Registration,
|
|
1968
|
+
amount: 1,
|
|
1969
|
+
unitPrice: 25_00,
|
|
1970
|
+
status: BalanceItemStatus.Due,
|
|
1971
|
+
}).create();
|
|
1972
|
+
|
|
1973
|
+
// This registration should not have a discount applied
|
|
1974
|
+
// but for some reason it has
|
|
1975
|
+
// (e.g the discount was altered in some way)
|
|
1976
|
+
// we test that we won't touch this discount
|
|
1977
|
+
await new BalanceItemFactory({
|
|
1978
|
+
userId: user.id,
|
|
1979
|
+
memberId: member.id,
|
|
1980
|
+
organizationId: organization.id,
|
|
1981
|
+
registrationId: registration1.id,
|
|
1982
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
1983
|
+
amount: 1,
|
|
1984
|
+
unitPrice: -5_00,
|
|
1985
|
+
status: BalanceItemStatus.Due,
|
|
1986
|
+
relations: new Map([
|
|
1987
|
+
[
|
|
1988
|
+
BalanceItemRelationType.Discount,
|
|
1989
|
+
BalanceItemRelation.create({
|
|
1990
|
+
id: bundleDiscount.id,
|
|
1991
|
+
name: bundleDiscount.name,
|
|
1992
|
+
}),
|
|
1993
|
+
],
|
|
1994
|
+
]),
|
|
1995
|
+
}).create();
|
|
1996
|
+
|
|
1997
|
+
// No discount on 2 ( = first)
|
|
1998
|
+
|
|
1999
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
2000
|
+
await registration1.refresh();
|
|
2001
|
+
|
|
2002
|
+
// Check this is what we expect
|
|
2003
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
2004
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
2005
|
+
name: bundleDiscount.name,
|
|
2006
|
+
amount: 5_00,
|
|
2007
|
+
})],
|
|
2008
|
+
]));
|
|
2009
|
+
|
|
2010
|
+
// Now register the member for group 2
|
|
2011
|
+
const checkout = IDRegisterCheckout.create({
|
|
2012
|
+
cart: IDRegisterCart.create({
|
|
2013
|
+
items: [
|
|
2014
|
+
IDRegisterItem.create({
|
|
2015
|
+
groupPrice: groups[1].settings.prices[0],
|
|
2016
|
+
groupId: groups[1].id,
|
|
2017
|
+
organizationId: organization.id,
|
|
2018
|
+
memberId: member.id,
|
|
2019
|
+
}),
|
|
2020
|
+
],
|
|
2021
|
+
}),
|
|
2022
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
2023
|
+
totalPrice: 25_00,
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
const response = await post(checkout, organization, token);
|
|
2027
|
+
expect(response.body.registrations.length).toBe(1);
|
|
2028
|
+
const registration2 = response.body.registrations[0];
|
|
2029
|
+
|
|
2030
|
+
expect(registration2).toMatchObject({
|
|
2031
|
+
registeredAt: expect.any(Date),
|
|
2032
|
+
discounts: new Map([]),
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
// Verify that existing registrations still have their original discounts
|
|
2036
|
+
await registration1.refresh();
|
|
2037
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
2038
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
2039
|
+
name: bundleDiscount.name,
|
|
2040
|
+
amount: 5_00,
|
|
2041
|
+
})],
|
|
2042
|
+
]));
|
|
2043
|
+
|
|
2044
|
+
await assertBalances({ user }, [
|
|
2045
|
+
{
|
|
2046
|
+
type: BalanceItemType.Registration,
|
|
2047
|
+
registrationId: registration1.id,
|
|
2048
|
+
amount: 1,
|
|
2049
|
+
price: 25_00,
|
|
2050
|
+
status: BalanceItemStatus.Due,
|
|
2051
|
+
priceOpen: 25_00,
|
|
2052
|
+
pricePending: 0,
|
|
2053
|
+
},
|
|
2054
|
+
{
|
|
2055
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
2056
|
+
registrationId: registration1.id,
|
|
2057
|
+
amount: 1,
|
|
2058
|
+
price: -5_00,
|
|
2059
|
+
status: BalanceItemStatus.Due,
|
|
2060
|
+
priceOpen: -5_00,
|
|
2061
|
+
pricePending: 0,
|
|
2062
|
+
},
|
|
2063
|
+
{
|
|
2064
|
+
type: BalanceItemType.Registration,
|
|
2065
|
+
registrationId: registration2.id,
|
|
2066
|
+
amount: 1,
|
|
2067
|
+
price: 25_00,
|
|
2068
|
+
status: BalanceItemStatus.Due,
|
|
2069
|
+
priceOpen: 0,
|
|
2070
|
+
pricePending: 25_00,
|
|
2071
|
+
},
|
|
2072
|
+
]);
|
|
2073
|
+
});
|
|
2074
|
+
|
|
2075
|
+
// We run this test 5 times because it should be stable
|
|
2076
|
+
test.each([1, 2, 3, 4, 5])('If discounts are the same, they are preferrably added to the new registration, not the existing registrations (%i th try)', async () => {
|
|
2077
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
2078
|
+
const bundleDiscount = await initBundleDiscount({
|
|
2079
|
+
organizationRegistrationPeriod,
|
|
2080
|
+
discount: {
|
|
2081
|
+
discounts: [
|
|
2082
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
2083
|
+
],
|
|
2084
|
+
},
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
const groups = [
|
|
2088
|
+
await new GroupFactory({
|
|
2089
|
+
organization,
|
|
2090
|
+
price: 25_00, // 20% discount = 5_00
|
|
2091
|
+
bundleDiscount,
|
|
2092
|
+
}).create(),
|
|
2093
|
+
await new GroupFactory({
|
|
2094
|
+
organization,
|
|
2095
|
+
price: 25_00, // 20% discount = 5_00 (same as group 1)
|
|
2096
|
+
bundleDiscount,
|
|
2097
|
+
}).create(),
|
|
2098
|
+
];
|
|
2099
|
+
|
|
2100
|
+
// Create existing registrations for group 1
|
|
2101
|
+
const registration1 = await new RegistrationFactory({
|
|
2102
|
+
organization,
|
|
2103
|
+
member,
|
|
2104
|
+
group: groups[0],
|
|
2105
|
+
}).create();
|
|
2106
|
+
|
|
2107
|
+
// Create balance items for existing registrations
|
|
2108
|
+
await new BalanceItemFactory({
|
|
2109
|
+
userId: user.id,
|
|
2110
|
+
memberId: member.id,
|
|
2111
|
+
organizationId: organization.id,
|
|
2112
|
+
registrationId: registration1.id,
|
|
2113
|
+
type: BalanceItemType.Registration,
|
|
2114
|
+
amount: 1,
|
|
2115
|
+
unitPrice: 25_00,
|
|
2116
|
+
status: BalanceItemStatus.Due,
|
|
2117
|
+
}).create();
|
|
2118
|
+
|
|
2119
|
+
// No discount on 2 ( = first)
|
|
2120
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
2121
|
+
await registration1.refresh();
|
|
2122
|
+
|
|
2123
|
+
// Check this is what we expect
|
|
2124
|
+
expect(registration1.discounts).toMatchMap(new Map([]));
|
|
2125
|
+
|
|
2126
|
+
// Now register the member for group 3
|
|
2127
|
+
const checkout = IDRegisterCheckout.create({
|
|
2128
|
+
cart: IDRegisterCart.create({
|
|
2129
|
+
items: [
|
|
2130
|
+
IDRegisterItem.create({
|
|
2131
|
+
groupPrice: groups[1].settings.prices[0],
|
|
2132
|
+
groupId: groups[1].id,
|
|
2133
|
+
organizationId: organization.id,
|
|
2134
|
+
memberId: member.id,
|
|
2135
|
+
}),
|
|
2136
|
+
],
|
|
2137
|
+
}),
|
|
2138
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
2139
|
+
totalPrice: 25_00 - 5_00, // 20% discount on new registration
|
|
2140
|
+
});
|
|
2141
|
+
|
|
2142
|
+
const response = await post(checkout, organization, token);
|
|
2143
|
+
expect(response.body.registrations.length).toBe(1);
|
|
2144
|
+
const registration2 = response.body.registrations[0];
|
|
2145
|
+
|
|
2146
|
+
expect(registration2).toMatchObject({
|
|
2147
|
+
registeredAt: expect.any(Date),
|
|
2148
|
+
discounts: new Map([
|
|
2149
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
2150
|
+
name: bundleDiscount.name,
|
|
2151
|
+
amount: 5_00,
|
|
2152
|
+
})],
|
|
2153
|
+
]),
|
|
2154
|
+
});
|
|
2155
|
+
|
|
2156
|
+
// Verify that existing registrations still have their original discounts
|
|
2157
|
+
await registration1.refresh();
|
|
2158
|
+
expect(registration1.discounts).toMatchMap(new Map([]));
|
|
2159
|
+
|
|
2160
|
+
await assertBalances({ user }, [
|
|
2161
|
+
{
|
|
2162
|
+
type: BalanceItemType.Registration,
|
|
2163
|
+
registrationId: registration1.id,
|
|
2164
|
+
amount: 1,
|
|
2165
|
+
price: 25_00,
|
|
2166
|
+
status: BalanceItemStatus.Due,
|
|
2167
|
+
priceOpen: 25_00,
|
|
2168
|
+
pricePending: 0,
|
|
2169
|
+
},
|
|
2170
|
+
{
|
|
2171
|
+
type: BalanceItemType.Registration,
|
|
2172
|
+
registrationId: registration2.id,
|
|
2173
|
+
amount: 1,
|
|
2174
|
+
price: 25_00,
|
|
2175
|
+
status: BalanceItemStatus.Due,
|
|
2176
|
+
priceOpen: 0,
|
|
2177
|
+
pricePending: 25_00,
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
2181
|
+
registrationId: registration2.id,
|
|
2182
|
+
amount: 1,
|
|
2183
|
+
price: -5_00,
|
|
2184
|
+
status: BalanceItemStatus.Due,
|
|
2185
|
+
priceOpen: 0,
|
|
2186
|
+
pricePending: -5_00,
|
|
2187
|
+
},
|
|
2188
|
+
]);
|
|
2189
|
+
});
|
|
2190
|
+
|
|
2191
|
+
test('Negative prices are not possible for combination of discounts', async () => {
|
|
2192
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
2193
|
+
const bundleDiscounts = [
|
|
2194
|
+
await initBundleDiscount({
|
|
2195
|
+
organizationRegistrationPeriod,
|
|
2196
|
+
discount: {
|
|
2197
|
+
discounts: [
|
|
2198
|
+
{ value: 60_00, type: GroupPriceDiscountType.Percentage },
|
|
2199
|
+
{ value: 40_00, type: GroupPriceDiscountType.Percentage },
|
|
2200
|
+
],
|
|
2201
|
+
},
|
|
2202
|
+
}),
|
|
2203
|
+
await initBundleDiscount({
|
|
2204
|
+
organizationRegistrationPeriod,
|
|
2205
|
+
discount: {
|
|
2206
|
+
discounts: [
|
|
2207
|
+
{ value: 60_00, type: GroupPriceDiscountType.Percentage },
|
|
2208
|
+
{ value: 40_00, type: GroupPriceDiscountType.Percentage },
|
|
2209
|
+
],
|
|
2210
|
+
},
|
|
2211
|
+
}),
|
|
2212
|
+
];
|
|
2213
|
+
|
|
2214
|
+
const groups = [
|
|
2215
|
+
await new GroupFactory({
|
|
2216
|
+
organization,
|
|
2217
|
+
price: 25_00,
|
|
2218
|
+
bundleDiscounts,
|
|
2219
|
+
}).create(),
|
|
2220
|
+
await new GroupFactory({
|
|
2221
|
+
organization,
|
|
2222
|
+
price: 35_00, // Higher price so discount is applied preferably here
|
|
2223
|
+
bundleDiscounts,
|
|
2224
|
+
}).create(),
|
|
2225
|
+
];
|
|
2226
|
+
|
|
2227
|
+
// Create existing registration for group 1
|
|
2228
|
+
const registration1 = await new RegistrationFactory({
|
|
2229
|
+
organization,
|
|
2230
|
+
member,
|
|
2231
|
+
group: groups[0],
|
|
2232
|
+
}).create();
|
|
2233
|
+
|
|
2234
|
+
// Create balance item for existing registration
|
|
2235
|
+
await new BalanceItemFactory({
|
|
2236
|
+
userId: user.id,
|
|
2237
|
+
memberId: member.id,
|
|
2238
|
+
organizationId: organization.id,
|
|
2239
|
+
registrationId: registration1.id,
|
|
2240
|
+
type: BalanceItemType.Registration,
|
|
2241
|
+
amount: 1,
|
|
2242
|
+
unitPrice: 25_00,
|
|
2243
|
+
status: BalanceItemStatus.Due,
|
|
2244
|
+
}).create();
|
|
2245
|
+
|
|
2246
|
+
const checkout = IDRegisterCheckout.create({
|
|
2247
|
+
cart: IDRegisterCart.create({
|
|
2248
|
+
items: [
|
|
2249
|
+
IDRegisterItem.create({
|
|
2250
|
+
groupPrice: groups[1].settings.prices[0],
|
|
2251
|
+
groupId: groups[1].id,
|
|
2252
|
+
organizationId: organization.id,
|
|
2253
|
+
memberId: member.id,
|
|
2254
|
+
}),
|
|
2255
|
+
],
|
|
2256
|
+
}),
|
|
2257
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
2258
|
+
totalPrice: 35_00 - 42_00, // 120% discount on 35_00
|
|
2259
|
+
});
|
|
2260
|
+
await expect(post(checkout, organization, token)).rejects
|
|
2261
|
+
.toThrow(STExpect.simpleError({
|
|
2262
|
+
code: 'negative_price',
|
|
2263
|
+
}));
|
|
2264
|
+
|
|
2265
|
+
// The backend should have thrown the error early enough, so no balances should have been created
|
|
2266
|
+
await assertBalances({ user }, [
|
|
2267
|
+
{
|
|
2268
|
+
type: BalanceItemType.Registration,
|
|
2269
|
+
registrationId: registration1.id,
|
|
2270
|
+
amount: 1,
|
|
2271
|
+
unitPrice: 25_00,
|
|
2272
|
+
status: BalanceItemStatus.Due,
|
|
2273
|
+
priceOpen: 25_00,
|
|
2274
|
+
pricePending: 0,
|
|
2275
|
+
},
|
|
2276
|
+
]);
|
|
2277
|
+
});
|
|
2278
|
+
|
|
2279
|
+
test('Reduced prices and discounts are used', async () => {
|
|
2280
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
2281
|
+
member.details.requiresFinancialSupport = BooleanStatus.create({ value: true });
|
|
2282
|
+
await member.save();
|
|
2283
|
+
|
|
2284
|
+
const bundleDiscount = await initBundleDiscount({
|
|
2285
|
+
organizationRegistrationPeriod,
|
|
2286
|
+
discount: {
|
|
2287
|
+
discounts: [
|
|
2288
|
+
{
|
|
2289
|
+
value: 20_00,
|
|
2290
|
+
type: GroupPriceDiscountType.Percentage,
|
|
2291
|
+
reducedValue: 10_00,
|
|
2292
|
+
},
|
|
2293
|
+
],
|
|
2294
|
+
},
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
const groups = [
|
|
2298
|
+
await new GroupFactory({
|
|
2299
|
+
organization,
|
|
2300
|
+
price: 25_00,
|
|
2301
|
+
reducedPrice: 20_00,
|
|
2302
|
+
bundleDiscount,
|
|
2303
|
+
}).create(),
|
|
2304
|
+
await new GroupFactory({
|
|
2305
|
+
organization,
|
|
2306
|
+
price: 15_00,
|
|
2307
|
+
reducedPrice: 10_00,
|
|
2308
|
+
bundleDiscount,
|
|
2309
|
+
}).create(),
|
|
2310
|
+
];
|
|
2311
|
+
|
|
2312
|
+
// First register the member for group 1. No discount should be applied yet
|
|
2313
|
+
const registration1 = await new RegistrationFactory({
|
|
2314
|
+
organization,
|
|
2315
|
+
member,
|
|
2316
|
+
group: groups[0],
|
|
2317
|
+
}).create();
|
|
2318
|
+
|
|
2319
|
+
await new BalanceItemFactory({
|
|
2320
|
+
userId: user.id,
|
|
2321
|
+
memberId: member.id,
|
|
2322
|
+
organizationId: organization.id,
|
|
2323
|
+
type: BalanceItemType.Registration,
|
|
2324
|
+
amount: 1,
|
|
2325
|
+
unitPrice: 20_00, // Reduced price
|
|
2326
|
+
status: BalanceItemStatus.Due,
|
|
2327
|
+
registrationId: registration1.id,
|
|
2328
|
+
}).create();
|
|
2329
|
+
|
|
2330
|
+
const checkout = IDRegisterCheckout.create({
|
|
2331
|
+
cart: IDRegisterCart.create({
|
|
2332
|
+
items: [
|
|
2333
|
+
IDRegisterItem.create({
|
|
2334
|
+
groupPrice: groups[1].settings.prices[0],
|
|
2335
|
+
groupId: groups[1].id,
|
|
2336
|
+
organizationId: organization.id,
|
|
2337
|
+
memberId: member.id,
|
|
2338
|
+
}),
|
|
2339
|
+
],
|
|
2340
|
+
}),
|
|
2341
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
2342
|
+
totalPrice: 10_00 - 2_00, // 10% discount on first group
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
const response2 = await post(checkout, organization, token);
|
|
2346
|
+
expect(response2.body.registrations.length).toBe(1);
|
|
2347
|
+
|
|
2348
|
+
const registration2 = response2.body.registrations[0];
|
|
2349
|
+
expect(registration2.registeredAt).not.toBeNull();
|
|
2350
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
2351
|
+
|
|
2352
|
+
// Get registration 1 again, it should now have the bundle discount applied
|
|
2353
|
+
await registration1.refresh();
|
|
2354
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
2355
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
2356
|
+
name: bundleDiscount.name,
|
|
2357
|
+
amount: 2_00,
|
|
2358
|
+
})],
|
|
2359
|
+
]));
|
|
2360
|
+
|
|
2361
|
+
await assertBalances({ user }, [
|
|
2362
|
+
{
|
|
2363
|
+
type: BalanceItemType.Registration,
|
|
2364
|
+
registrationId: registration1.id,
|
|
2365
|
+
amount: 1,
|
|
2366
|
+
price: 20_00,
|
|
2367
|
+
status: BalanceItemStatus.Due,
|
|
2368
|
+
priceOpen: 20_00,
|
|
2369
|
+
pricePending: 0,
|
|
2370
|
+
},
|
|
2371
|
+
{
|
|
2372
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
2373
|
+
registrationId: registration1.id,
|
|
2374
|
+
amount: 1,
|
|
2375
|
+
price: -2_00,
|
|
2376
|
+
status: BalanceItemStatus.Due,
|
|
2377
|
+
priceOpen: 0,
|
|
2378
|
+
pricePending: -2_00,
|
|
2379
|
+
},
|
|
2380
|
+
{
|
|
2381
|
+
type: BalanceItemType.Registration,
|
|
2382
|
+
registrationId: registration2.id,
|
|
2383
|
+
amount: 1,
|
|
2384
|
+
price: 10_00,
|
|
2385
|
+
status: BalanceItemStatus.Due,
|
|
2386
|
+
priceOpen: 0,
|
|
2387
|
+
pricePending: 10_00,
|
|
2388
|
+
},
|
|
2389
|
+
]);
|
|
2390
|
+
});
|
|
2391
|
+
|
|
2392
|
+
test('Custom discounts are used for certain groups', async () => {
|
|
2393
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
2394
|
+
member.details.requiresFinancialSupport = BooleanStatus.create({ value: true });
|
|
2395
|
+
await member.save();
|
|
2396
|
+
|
|
2397
|
+
const bundleDiscount = await initBundleDiscount({
|
|
2398
|
+
organizationRegistrationPeriod,
|
|
2399
|
+
discount: {
|
|
2400
|
+
discounts: [
|
|
2401
|
+
{
|
|
2402
|
+
value: 20_00,
|
|
2403
|
+
type: GroupPriceDiscountType.Percentage,
|
|
2404
|
+
},
|
|
2405
|
+
],
|
|
2406
|
+
},
|
|
2407
|
+
});
|
|
2408
|
+
|
|
2409
|
+
const groups = [
|
|
2410
|
+
await new GroupFactory({
|
|
2411
|
+
organization,
|
|
2412
|
+
price: 25_00,
|
|
2413
|
+
bundleDiscount,
|
|
2414
|
+
}).create(),
|
|
2415
|
+
await new GroupFactory({
|
|
2416
|
+
organization,
|
|
2417
|
+
price: 15_00,
|
|
2418
|
+
bundleDiscounts: new Map([
|
|
2419
|
+
[
|
|
2420
|
+
bundleDiscount,
|
|
2421
|
+
[
|
|
2422
|
+
GroupPriceDiscount.create({
|
|
2423
|
+
value: ReduceablePrice.create({ price: 50_00 }),
|
|
2424
|
+
type: GroupPriceDiscountType.Percentage,
|
|
2425
|
+
}),
|
|
2426
|
+
],
|
|
2427
|
+
],
|
|
2428
|
+
]),
|
|
2429
|
+
}).create(),
|
|
2430
|
+
];
|
|
2431
|
+
|
|
2432
|
+
// First register the member for group 1. No discount should be applied yet
|
|
2433
|
+
const registration1 = await new RegistrationFactory({
|
|
2434
|
+
organization,
|
|
2435
|
+
member,
|
|
2436
|
+
group: groups[0],
|
|
2437
|
+
}).create();
|
|
2438
|
+
|
|
2439
|
+
await new BalanceItemFactory({
|
|
2440
|
+
userId: user.id,
|
|
2441
|
+
memberId: member.id,
|
|
2442
|
+
organizationId: organization.id,
|
|
2443
|
+
type: BalanceItemType.Registration,
|
|
2444
|
+
amount: 1,
|
|
2445
|
+
unitPrice: 25_00,
|
|
2446
|
+
status: BalanceItemStatus.Due,
|
|
2447
|
+
registrationId: registration1.id,
|
|
2448
|
+
}).create();
|
|
2449
|
+
|
|
2450
|
+
const checkout = IDRegisterCheckout.create({
|
|
2451
|
+
cart: IDRegisterCart.create({
|
|
2452
|
+
items: [
|
|
2453
|
+
IDRegisterItem.create({
|
|
2454
|
+
groupPrice: groups[1].settings.prices[0],
|
|
2455
|
+
groupId: groups[1].id,
|
|
2456
|
+
organizationId: organization.id,
|
|
2457
|
+
memberId: member.id,
|
|
2458
|
+
}),
|
|
2459
|
+
],
|
|
2460
|
+
}),
|
|
2461
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
2462
|
+
totalPrice: 15_00 - 7_50, // 50% discount on last group
|
|
2463
|
+
});
|
|
2464
|
+
|
|
2465
|
+
const response2 = await post(checkout, organization, token);
|
|
2466
|
+
expect(response2.body.registrations.length).toBe(1);
|
|
2467
|
+
|
|
2468
|
+
const registration2 = response2.body.registrations[0];
|
|
2469
|
+
expect(registration2.registeredAt).not.toBeNull();
|
|
2470
|
+
expect(registration2.discounts).toMatchMap(new Map([
|
|
2471
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
2472
|
+
name: bundleDiscount.name,
|
|
2473
|
+
amount: 7_50,
|
|
2474
|
+
})],
|
|
2475
|
+
]));
|
|
2476
|
+
|
|
2477
|
+
// Get registration 1 again, it should now have the bundle discount applied
|
|
2478
|
+
await registration1.refresh();
|
|
2479
|
+
expect(registration1.discounts).toMatchMap(new Map());
|
|
2480
|
+
|
|
2481
|
+
await assertBalances({ user }, [
|
|
2482
|
+
{
|
|
2483
|
+
type: BalanceItemType.Registration,
|
|
2484
|
+
registrationId: registration1.id,
|
|
2485
|
+
amount: 1,
|
|
2486
|
+
price: 25_00,
|
|
2487
|
+
status: BalanceItemStatus.Due,
|
|
2488
|
+
priceOpen: 25_00,
|
|
2489
|
+
pricePending: 0,
|
|
2490
|
+
},
|
|
2491
|
+
{
|
|
2492
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
2493
|
+
registrationId: registration2.id,
|
|
2494
|
+
amount: 1,
|
|
2495
|
+
price: -7_50,
|
|
2496
|
+
status: BalanceItemStatus.Due,
|
|
2497
|
+
priceOpen: 0,
|
|
2498
|
+
pricePending: -7_50,
|
|
2499
|
+
},
|
|
2500
|
+
{
|
|
2501
|
+
type: BalanceItemType.Registration,
|
|
2502
|
+
registrationId: registration2.id,
|
|
2503
|
+
amount: 1,
|
|
2504
|
+
price: 15_00,
|
|
2505
|
+
status: BalanceItemStatus.Due,
|
|
2506
|
+
priceOpen: 0,
|
|
2507
|
+
pricePending: 15_00,
|
|
2508
|
+
},
|
|
2509
|
+
]);
|
|
2510
|
+
});
|
|
2511
|
+
|
|
2512
|
+
test('Discounts work across family members', async () => {
|
|
2513
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
2514
|
+
|
|
2515
|
+
const otherMember = await new MemberFactory({
|
|
2516
|
+
organization,
|
|
2517
|
+
user,
|
|
2518
|
+
}).create();
|
|
2519
|
+
|
|
2520
|
+
const bundleDiscount = await initBundleDiscount({
|
|
2521
|
+
organizationRegistrationPeriod,
|
|
2522
|
+
discount: {
|
|
2523
|
+
countWholeFamily: true,
|
|
2524
|
+
discounts: [
|
|
2525
|
+
{
|
|
2526
|
+
value: 20_00,
|
|
2527
|
+
type: GroupPriceDiscountType.Percentage,
|
|
2528
|
+
},
|
|
2529
|
+
],
|
|
2530
|
+
},
|
|
2531
|
+
});
|
|
2532
|
+
|
|
2533
|
+
const groups = [
|
|
2534
|
+
await new GroupFactory({
|
|
2535
|
+
organization,
|
|
2536
|
+
price: 25_00,
|
|
2537
|
+
bundleDiscount,
|
|
2538
|
+
}).create(),
|
|
2539
|
+
await new GroupFactory({
|
|
2540
|
+
organization,
|
|
2541
|
+
price: 15_00,
|
|
2542
|
+
bundleDiscount,
|
|
2543
|
+
}).create(),
|
|
2544
|
+
];
|
|
2545
|
+
|
|
2546
|
+
// First register the otherMember for group 1. No discount should be applied yet
|
|
2547
|
+
const registration1 = await new RegistrationFactory({
|
|
2548
|
+
organization,
|
|
2549
|
+
member: otherMember,
|
|
2550
|
+
group: groups[0],
|
|
2551
|
+
}).create();
|
|
2552
|
+
|
|
2553
|
+
await new BalanceItemFactory({
|
|
2554
|
+
userId: user.id,
|
|
2555
|
+
memberId: otherMember.id,
|
|
2556
|
+
organizationId: organization.id,
|
|
2557
|
+
type: BalanceItemType.Registration,
|
|
2558
|
+
amount: 1,
|
|
2559
|
+
unitPrice: 25_00,
|
|
2560
|
+
status: BalanceItemStatus.Due,
|
|
2561
|
+
registrationId: registration1.id,
|
|
2562
|
+
}).create();
|
|
2563
|
+
|
|
2564
|
+
const checkout = IDRegisterCheckout.create({
|
|
2565
|
+
cart: IDRegisterCart.create({
|
|
2566
|
+
items: [
|
|
2567
|
+
IDRegisterItem.create({
|
|
2568
|
+
groupPrice: groups[1].settings.prices[0],
|
|
2569
|
+
groupId: groups[1].id,
|
|
2570
|
+
organizationId: organization.id,
|
|
2571
|
+
memberId: member.id,
|
|
2572
|
+
}),
|
|
2573
|
+
],
|
|
2574
|
+
}),
|
|
2575
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
2576
|
+
totalPrice: 15_00 - 5_00, // 20% discount on first group
|
|
2577
|
+
});
|
|
2578
|
+
|
|
2579
|
+
const response2 = await post(checkout, organization, token);
|
|
2580
|
+
expect(response2.body.registrations.length).toBe(1);
|
|
2581
|
+
|
|
2582
|
+
const registration2 = response2.body.registrations[0];
|
|
2583
|
+
expect(registration2.registeredAt).not.toBeNull();
|
|
2584
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
2585
|
+
|
|
2586
|
+
// Get registration 1 again, it should now have the bundle discount applied
|
|
2587
|
+
await registration1.refresh();
|
|
2588
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
2589
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
2590
|
+
name: bundleDiscount.name,
|
|
2591
|
+
amount: 5_00,
|
|
2592
|
+
})],
|
|
2593
|
+
]));
|
|
2594
|
+
|
|
2595
|
+
await assertBalances({ user }, [
|
|
2596
|
+
{
|
|
2597
|
+
type: BalanceItemType.Registration,
|
|
2598
|
+
registrationId: registration1.id,
|
|
2599
|
+
amount: 1,
|
|
2600
|
+
price: 25_00,
|
|
2601
|
+
status: BalanceItemStatus.Due,
|
|
2602
|
+
priceOpen: 25_00,
|
|
2603
|
+
pricePending: 0,
|
|
2604
|
+
},
|
|
2605
|
+
{
|
|
2606
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
2607
|
+
registrationId: registration1.id,
|
|
2608
|
+
amount: 1,
|
|
2609
|
+
price: -5_00,
|
|
2610
|
+
status: BalanceItemStatus.Due,
|
|
2611
|
+
priceOpen: 0,
|
|
2612
|
+
pricePending: -5_00,
|
|
2613
|
+
},
|
|
2614
|
+
{
|
|
2615
|
+
type: BalanceItemType.Registration,
|
|
2616
|
+
registrationId: registration2.id,
|
|
2617
|
+
amount: 1,
|
|
2618
|
+
price: 15_00,
|
|
2619
|
+
status: BalanceItemStatus.Due,
|
|
2620
|
+
priceOpen: 0,
|
|
2621
|
+
pricePending: 15_00,
|
|
2622
|
+
},
|
|
2623
|
+
]);
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
test('Discounts can be disabled across family members', async () => {
|
|
2627
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
2628
|
+
|
|
2629
|
+
const otherMember = await new MemberFactory({
|
|
2630
|
+
organization,
|
|
2631
|
+
user,
|
|
2632
|
+
}).create();
|
|
2633
|
+
|
|
2634
|
+
const bundleDiscount = await initBundleDiscount({
|
|
2635
|
+
organizationRegistrationPeriod,
|
|
2636
|
+
discount: {
|
|
2637
|
+
countWholeFamily: false,
|
|
2638
|
+
discounts: [
|
|
2639
|
+
{
|
|
2640
|
+
value: 20_00,
|
|
2641
|
+
type: GroupPriceDiscountType.Percentage,
|
|
2642
|
+
},
|
|
2643
|
+
],
|
|
2644
|
+
},
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
const groups = [
|
|
2648
|
+
await new GroupFactory({
|
|
2649
|
+
organization,
|
|
2650
|
+
price: 25_00,
|
|
2651
|
+
bundleDiscount,
|
|
2652
|
+
}).create(),
|
|
2653
|
+
await new GroupFactory({
|
|
2654
|
+
organization,
|
|
2655
|
+
price: 15_00,
|
|
2656
|
+
bundleDiscount,
|
|
2657
|
+
}).create(),
|
|
2658
|
+
];
|
|
2659
|
+
|
|
2660
|
+
// First register the otherMember for group 1. No discount should be applied yet
|
|
2661
|
+
const registration1 = await new RegistrationFactory({
|
|
2662
|
+
organization,
|
|
2663
|
+
member: otherMember,
|
|
2664
|
+
group: groups[0],
|
|
2665
|
+
}).create();
|
|
2666
|
+
|
|
2667
|
+
await new BalanceItemFactory({
|
|
2668
|
+
userId: user.id,
|
|
2669
|
+
memberId: otherMember.id,
|
|
2670
|
+
organizationId: organization.id,
|
|
2671
|
+
type: BalanceItemType.Registration,
|
|
2672
|
+
amount: 1,
|
|
2673
|
+
unitPrice: 25_00,
|
|
2674
|
+
status: BalanceItemStatus.Due,
|
|
2675
|
+
registrationId: registration1.id,
|
|
2676
|
+
}).create();
|
|
2677
|
+
|
|
2678
|
+
const checkout = IDRegisterCheckout.create({
|
|
2679
|
+
cart: IDRegisterCart.create({
|
|
2680
|
+
items: [
|
|
2681
|
+
IDRegisterItem.create({
|
|
2682
|
+
groupPrice: groups[1].settings.prices[0],
|
|
2683
|
+
groupId: groups[1].id,
|
|
2684
|
+
organizationId: organization.id,
|
|
2685
|
+
memberId: member.id,
|
|
2686
|
+
}),
|
|
2687
|
+
],
|
|
2688
|
+
}),
|
|
2689
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
2690
|
+
totalPrice: 15_00,
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
const response2 = await post(checkout, organization, token);
|
|
2694
|
+
expect(response2.body.registrations.length).toBe(1);
|
|
2695
|
+
|
|
2696
|
+
const registration2 = response2.body.registrations[0];
|
|
2697
|
+
expect(registration2.registeredAt).not.toBeNull();
|
|
2698
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
2699
|
+
|
|
2700
|
+
await registration1.refresh();
|
|
2701
|
+
expect(registration1.discounts).toMatchMap(new Map());
|
|
2702
|
+
|
|
2703
|
+
await assertBalances({ user }, [
|
|
2704
|
+
{
|
|
2705
|
+
type: BalanceItemType.Registration,
|
|
2706
|
+
registrationId: registration1.id,
|
|
2707
|
+
amount: 1,
|
|
2708
|
+
price: 25_00,
|
|
2709
|
+
status: BalanceItemStatus.Due,
|
|
2710
|
+
priceOpen: 25_00,
|
|
2711
|
+
pricePending: 0,
|
|
2712
|
+
},
|
|
2713
|
+
{
|
|
2714
|
+
type: BalanceItemType.Registration,
|
|
2715
|
+
registrationId: registration2.id,
|
|
2716
|
+
amount: 1,
|
|
2717
|
+
price: 15_00,
|
|
2718
|
+
status: BalanceItemStatus.Due,
|
|
2719
|
+
priceOpen: 0,
|
|
2720
|
+
pricePending: 15_00,
|
|
2721
|
+
},
|
|
2722
|
+
]);
|
|
2723
|
+
});
|
|
2724
|
+
});
|
|
2725
|
+
|
|
2726
|
+
describe('Changing registrations as admin', () => {
|
|
2727
|
+
async function initDataWithRegistrations() {
|
|
2728
|
+
const { organizationRegistrationPeriod, organization, member, user } = await initData();
|
|
2729
|
+
|
|
2730
|
+
const bundleDiscount = await initBundleDiscount({
|
|
2731
|
+
organizationRegistrationPeriod,
|
|
2732
|
+
discount: {
|
|
2733
|
+
discounts: [
|
|
2734
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
2735
|
+
],
|
|
2736
|
+
},
|
|
2737
|
+
});
|
|
2738
|
+
|
|
2739
|
+
const groups = [
|
|
2740
|
+
await new GroupFactory({
|
|
2741
|
+
organization,
|
|
2742
|
+
price: 25_00,
|
|
2743
|
+
bundleDiscount,
|
|
2744
|
+
}).create(),
|
|
2745
|
+
await new GroupFactory({
|
|
2746
|
+
organization,
|
|
2747
|
+
price: 15_00,
|
|
2748
|
+
bundleDiscount,
|
|
2749
|
+
}).create(),
|
|
2750
|
+
await new GroupFactory({
|
|
2751
|
+
organization,
|
|
2752
|
+
price: 40_00,
|
|
2753
|
+
bundleDiscount,
|
|
2754
|
+
}).create(),
|
|
2755
|
+
];
|
|
2756
|
+
|
|
2757
|
+
// First register the member for group 1 & 2
|
|
2758
|
+
const registration1 = await new RegistrationFactory({
|
|
2759
|
+
organization,
|
|
2760
|
+
member,
|
|
2761
|
+
group: groups[0],
|
|
2762
|
+
}).create();
|
|
2763
|
+
|
|
2764
|
+
const registration2 = await new RegistrationFactory({
|
|
2765
|
+
organization,
|
|
2766
|
+
member,
|
|
2767
|
+
group: groups[1],
|
|
2768
|
+
}).create();
|
|
2769
|
+
|
|
2770
|
+
// Create initial balances
|
|
2771
|
+
await new BalanceItemFactory({
|
|
2772
|
+
memberId: member.id,
|
|
2773
|
+
organizationId: organization.id,
|
|
2774
|
+
registrationId: registration1.id,
|
|
2775
|
+
type: BalanceItemType.Registration,
|
|
2776
|
+
amount: 1,
|
|
2777
|
+
unitPrice: 25_00,
|
|
2778
|
+
status: BalanceItemStatus.Due,
|
|
2779
|
+
}).create();
|
|
2780
|
+
|
|
2781
|
+
// Create applied discount
|
|
2782
|
+
await new BalanceItemFactory({
|
|
2783
|
+
memberId: member.id,
|
|
2784
|
+
organizationId: organization.id,
|
|
2785
|
+
registrationId: registration1.id,
|
|
2786
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
2787
|
+
amount: 1,
|
|
2788
|
+
unitPrice: -5_00,
|
|
2789
|
+
status: BalanceItemStatus.Due,
|
|
2790
|
+
relations: new Map([
|
|
2791
|
+
[
|
|
2792
|
+
BalanceItemRelationType.Discount,
|
|
2793
|
+
BalanceItemRelation.create({
|
|
2794
|
+
// We need the ID of the discount saved here
|
|
2795
|
+
id: bundleDiscount.id,
|
|
2796
|
+
name: bundleDiscount.name,
|
|
2797
|
+
}),
|
|
2798
|
+
],
|
|
2799
|
+
]),
|
|
2800
|
+
}).create();
|
|
2801
|
+
|
|
2802
|
+
await new BalanceItemFactory({
|
|
2803
|
+
memberId: member.id,
|
|
2804
|
+
organizationId: organization.id,
|
|
2805
|
+
registrationId: registration2.id,
|
|
2806
|
+
type: BalanceItemType.Registration,
|
|
2807
|
+
amount: 1,
|
|
2808
|
+
unitPrice: 15_00,
|
|
2809
|
+
status: BalanceItemStatus.Due,
|
|
2810
|
+
}).create();
|
|
2811
|
+
|
|
2812
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
2813
|
+
|
|
2814
|
+
await registration1.refresh();
|
|
2815
|
+
expect(registration1).toMatchObject({
|
|
2816
|
+
groupId: groups[0].id,
|
|
2817
|
+
registeredAt: expect.any(Date),
|
|
2818
|
+
discounts: new Map([
|
|
2819
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
2820
|
+
name: bundleDiscount.name,
|
|
2821
|
+
amount: 5_00,
|
|
2822
|
+
})],
|
|
2823
|
+
]),
|
|
2824
|
+
});
|
|
2825
|
+
|
|
2826
|
+
return {
|
|
2827
|
+
organization,
|
|
2828
|
+
user,
|
|
2829
|
+
groups,
|
|
2830
|
+
registration1,
|
|
2831
|
+
registration2,
|
|
2832
|
+
member,
|
|
2833
|
+
bundleDiscount,
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
/**
|
|
2838
|
+
* If you replace a registration, it is possible that a bundle discount of a different registration is not optimal anymore and
|
|
2839
|
+
* a more optimal discount can be applied. In that case, the bundle discount can be moved from the unaltered registration to the newly created registration.
|
|
2840
|
+
*/
|
|
2841
|
+
test('Replacing a registration can move the bundle discount of an unaltered registration', async () => {
|
|
2842
|
+
const { organization, bundleDiscount, groups, registration1, registration2, member } = await initDataWithRegistrations();
|
|
2843
|
+
const { adminToken } = await initAdmin({ organization });
|
|
2844
|
+
|
|
2845
|
+
// Now replace registration 2 with group 3, which is more expensive and should give more discount
|
|
2846
|
+
const checkout = IDRegisterCheckout.create({
|
|
2847
|
+
cart: IDRegisterCart.create({
|
|
2848
|
+
items: [
|
|
2849
|
+
IDRegisterItem.create({
|
|
2850
|
+
groupPrice: groups[2].settings.prices[0],
|
|
2851
|
+
groupId: groups[2].id,
|
|
2852
|
+
organizationId: organization.id,
|
|
2853
|
+
memberId: member.id,
|
|
2854
|
+
replaceRegistrationIds: [registration2.id],
|
|
2855
|
+
}),
|
|
2856
|
+
],
|
|
2857
|
+
}),
|
|
2858
|
+
asOrganizationId: organization.id,
|
|
2859
|
+
totalPrice: 40_00 - 15_00 + 5_00 - 8_00, // group 3 - group 2 + reverted discount - new discount
|
|
2860
|
+
});
|
|
2861
|
+
|
|
2862
|
+
const response = await post(checkout, organization, adminToken);
|
|
2863
|
+
expect(response.body.registrations.length).toBe(1);
|
|
2864
|
+
|
|
2865
|
+
const registration3 = response.body.registrations[0];
|
|
2866
|
+
expect(registration3).toMatchObject({
|
|
2867
|
+
groupId: groups[2].id,
|
|
2868
|
+
registeredAt: expect.any(Date),
|
|
2869
|
+
discounts: new Map([
|
|
2870
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
2871
|
+
name: bundleDiscount.name,
|
|
2872
|
+
amount: 8_00,
|
|
2873
|
+
})],
|
|
2874
|
+
]),
|
|
2875
|
+
});
|
|
2876
|
+
|
|
2877
|
+
await registration2.refresh();
|
|
2878
|
+
await registration1.refresh();
|
|
2879
|
+
|
|
2880
|
+
// Check discount has been removed in registration 1
|
|
2881
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
2882
|
+
expect(registration1.discounts).toMatchMap(new Map());
|
|
2883
|
+
|
|
2884
|
+
// Check balances: no deletions happened, only additions or status changes
|
|
2885
|
+
await assertBalances({ member }, [
|
|
2886
|
+
{
|
|
2887
|
+
type: BalanceItemType.Registration,
|
|
2888
|
+
registrationId: registration1.id,
|
|
2889
|
+
amount: 1,
|
|
2890
|
+
unitPrice: 25_00,
|
|
2891
|
+
status: BalanceItemStatus.Due,
|
|
2892
|
+
priceOpen: 25_00,
|
|
2893
|
+
pricePending: 0,
|
|
2894
|
+
},
|
|
2895
|
+
{
|
|
2896
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
2897
|
+
registrationId: registration1.id,
|
|
2898
|
+
amount: 1,
|
|
2899
|
+
unitPrice: -5_00,
|
|
2900
|
+
status: BalanceItemStatus.Due,
|
|
2901
|
+
pricePending: 0,
|
|
2902
|
+
priceOpen: -5_00,
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
type: BalanceItemType.Registration,
|
|
2906
|
+
registrationId: registration2.id,
|
|
2907
|
+
amount: 1,
|
|
2908
|
+
unitPrice: 15_00,
|
|
2909
|
+
status: BalanceItemStatus.Canceled,
|
|
2910
|
+
pricePending: 0,
|
|
2911
|
+
priceOpen: 0,
|
|
2912
|
+
},
|
|
2913
|
+
{
|
|
2914
|
+
type: BalanceItemType.Registration,
|
|
2915
|
+
userId: null,
|
|
2916
|
+
memberId: member.id,
|
|
2917
|
+
registrationId: registration3.id,
|
|
2918
|
+
amount: 1,
|
|
2919
|
+
unitPrice: 40_00,
|
|
2920
|
+
status: BalanceItemStatus.Due,
|
|
2921
|
+
priceOpen: 40_00,
|
|
2922
|
+
|
|
2923
|
+
// Not pending because created by admin
|
|
2924
|
+
pricePending: 0,
|
|
2925
|
+
},
|
|
2926
|
+
// Revert first discount
|
|
2927
|
+
{
|
|
2928
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
2929
|
+
userId: null,
|
|
2930
|
+
memberId: member.id,
|
|
2931
|
+
registrationId: registration1.id,
|
|
2932
|
+
amount: 1,
|
|
2933
|
+
unitPrice: 5_00,
|
|
2934
|
+
status: BalanceItemStatus.Due,
|
|
2935
|
+
priceOpen: 5_00,
|
|
2936
|
+
|
|
2937
|
+
// Not pending because created by admin
|
|
2938
|
+
pricePending: 0,
|
|
2939
|
+
},
|
|
2940
|
+
// Add new discount
|
|
2941
|
+
{
|
|
2942
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
2943
|
+
userId: null,
|
|
2944
|
+
memberId: member.id,
|
|
2945
|
+
registrationId: registration3.id,
|
|
2946
|
+
amount: 1,
|
|
2947
|
+
unitPrice: -8_00,
|
|
2948
|
+
status: BalanceItemStatus.Due,
|
|
2949
|
+
priceOpen: -8_00,
|
|
2950
|
+
|
|
2951
|
+
// Not pending because created by admin
|
|
2952
|
+
pricePending: 0,
|
|
2953
|
+
},
|
|
2954
|
+
]);
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2957
|
+
test('Replacing a registration with discount', async () => {
|
|
2958
|
+
const { organization, bundleDiscount, groups, registration1, registration2, member } = await initDataWithRegistrations();
|
|
2959
|
+
const { adminToken } = await initAdmin({ organization });
|
|
2960
|
+
|
|
2961
|
+
// Now replace registration 2 with group 3, which is more expensive and should give more discount
|
|
2962
|
+
const checkout = IDRegisterCheckout.create({
|
|
2963
|
+
cart: IDRegisterCart.create({
|
|
2964
|
+
items: [
|
|
2965
|
+
IDRegisterItem.create({
|
|
2966
|
+
groupPrice: groups[2].settings.prices[0],
|
|
2967
|
+
groupId: groups[2].id,
|
|
2968
|
+
organizationId: organization.id,
|
|
2969
|
+
memberId: member.id,
|
|
2970
|
+
replaceRegistrationIds: [registration1.id],
|
|
2971
|
+
}),
|
|
2972
|
+
],
|
|
2973
|
+
}),
|
|
2974
|
+
asOrganizationId: organization.id,
|
|
2975
|
+
totalPrice: 40_00 - 25_00 + 5_00 - 8_00, // group 3 - group 1 + reverted discount - new discount
|
|
2976
|
+
});
|
|
2977
|
+
|
|
2978
|
+
const response = await post(checkout, organization, adminToken);
|
|
2979
|
+
expect(response.body.registrations.length).toBe(1);
|
|
2980
|
+
|
|
2981
|
+
const registration3 = response.body.registrations[0];
|
|
2982
|
+
expect(registration3).toMatchObject({
|
|
2983
|
+
groupId: groups[2].id,
|
|
2984
|
+
registeredAt: expect.any(Date),
|
|
2985
|
+
discounts: new Map([
|
|
2986
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
2987
|
+
name: bundleDiscount.name,
|
|
2988
|
+
amount: 8_00,
|
|
2989
|
+
})],
|
|
2990
|
+
]),
|
|
2991
|
+
});
|
|
2992
|
+
|
|
2993
|
+
await registration2.refresh();
|
|
2994
|
+
await registration1.refresh();
|
|
2995
|
+
|
|
2996
|
+
// Check discount has been removed in registration 1
|
|
2997
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
2998
|
+
expect(registration1.discounts).toMatchMap(new Map());
|
|
2999
|
+
|
|
3000
|
+
// Check balances
|
|
3001
|
+
await assertBalances({ member }, [
|
|
3002
|
+
{
|
|
3003
|
+
type: BalanceItemType.Registration,
|
|
3004
|
+
registrationId: registration1.id,
|
|
3005
|
+
amount: 1,
|
|
3006
|
+
unitPrice: 25_00,
|
|
3007
|
+
status: BalanceItemStatus.Canceled, // has been cancelled
|
|
3008
|
+
priceOpen: 0,
|
|
3009
|
+
pricePending: 0,
|
|
3010
|
+
},
|
|
3011
|
+
{
|
|
3012
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3013
|
+
registrationId: registration1.id,
|
|
3014
|
+
amount: 1,
|
|
3015
|
+
unitPrice: -5_00,
|
|
3016
|
+
status: BalanceItemStatus.Canceled, // has been cancelled
|
|
3017
|
+
pricePending: 0,
|
|
3018
|
+
priceOpen: 0,
|
|
3019
|
+
},
|
|
3020
|
+
{
|
|
3021
|
+
type: BalanceItemType.Registration,
|
|
3022
|
+
registrationId: registration2.id,
|
|
3023
|
+
amount: 1,
|
|
3024
|
+
unitPrice: 15_00,
|
|
3025
|
+
status: BalanceItemStatus.Due,
|
|
3026
|
+
pricePending: 0,
|
|
3027
|
+
priceOpen: 15_00,
|
|
3028
|
+
},
|
|
3029
|
+
{
|
|
3030
|
+
type: BalanceItemType.Registration,
|
|
3031
|
+
userId: null,
|
|
3032
|
+
memberId: member.id,
|
|
3033
|
+
registrationId: registration3.id,
|
|
3034
|
+
amount: 1,
|
|
3035
|
+
unitPrice: 40_00,
|
|
3036
|
+
status: BalanceItemStatus.Due,
|
|
3037
|
+
priceOpen: 40_00,
|
|
3038
|
+
|
|
3039
|
+
// Not pending because created by admin
|
|
3040
|
+
pricePending: 0,
|
|
3041
|
+
},
|
|
3042
|
+
// Add new discount
|
|
3043
|
+
{
|
|
3044
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3045
|
+
userId: null,
|
|
3046
|
+
memberId: member.id,
|
|
3047
|
+
registrationId: registration3.id,
|
|
3048
|
+
amount: 1,
|
|
3049
|
+
unitPrice: -8_00,
|
|
3050
|
+
status: BalanceItemStatus.Due,
|
|
3051
|
+
priceOpen: -8_00,
|
|
3052
|
+
|
|
3053
|
+
// Not pending because created by admin
|
|
3054
|
+
pricePending: 0,
|
|
3055
|
+
},
|
|
3056
|
+
]);
|
|
3057
|
+
});
|
|
3058
|
+
|
|
3059
|
+
test('Deleting a registration with discount, without cancellation fee', async () => {
|
|
3060
|
+
const { organization, registration1, registration2, member } = await initDataWithRegistrations();
|
|
3061
|
+
const { adminToken } = await initAdmin({ organization });
|
|
3062
|
+
|
|
3063
|
+
// Now delete registration 1, which has discount
|
|
3064
|
+
const checkout = IDRegisterCheckout.create({
|
|
3065
|
+
cart: IDRegisterCart.create({
|
|
3066
|
+
deleteRegistrationIds: [registration1.id],
|
|
3067
|
+
}),
|
|
3068
|
+
asOrganizationId: organization.id,
|
|
3069
|
+
totalPrice: -25_00 + 5_00,
|
|
3070
|
+
cancellationFeePercentage: 0,
|
|
3071
|
+
});
|
|
3072
|
+
|
|
3073
|
+
await post(checkout, organization, adminToken);
|
|
3074
|
+
await registration2.refresh();
|
|
3075
|
+
await registration1.refresh();
|
|
3076
|
+
|
|
3077
|
+
// Check discount has been removed in registration 1
|
|
3078
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
3079
|
+
expect(registration1.discounts).toMatchMap(new Map());
|
|
3080
|
+
expect(registration1.deactivatedAt).not.toBeNull(); // should be cancelled
|
|
3081
|
+
|
|
3082
|
+
// Check balances
|
|
3083
|
+
await assertBalances({ member }, [
|
|
3084
|
+
{
|
|
3085
|
+
type: BalanceItemType.Registration,
|
|
3086
|
+
registrationId: registration1.id,
|
|
3087
|
+
amount: 1,
|
|
3088
|
+
unitPrice: 25_00,
|
|
3089
|
+
status: BalanceItemStatus.Canceled, // has been cancelled
|
|
3090
|
+
priceOpen: 0,
|
|
3091
|
+
pricePending: 0,
|
|
3092
|
+
},
|
|
3093
|
+
{
|
|
3094
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3095
|
+
registrationId: registration1.id,
|
|
3096
|
+
amount: 1,
|
|
3097
|
+
unitPrice: -5_00,
|
|
3098
|
+
status: BalanceItemStatus.Canceled, // has been cancelled
|
|
3099
|
+
pricePending: 0,
|
|
3100
|
+
priceOpen: 0,
|
|
3101
|
+
},
|
|
3102
|
+
{
|
|
3103
|
+
type: BalanceItemType.Registration,
|
|
3104
|
+
registrationId: registration2.id,
|
|
3105
|
+
amount: 1,
|
|
3106
|
+
unitPrice: 15_00,
|
|
3107
|
+
status: BalanceItemStatus.Due,
|
|
3108
|
+
pricePending: 0,
|
|
3109
|
+
priceOpen: 15_00,
|
|
3110
|
+
},
|
|
3111
|
+
]);
|
|
3112
|
+
});
|
|
3113
|
+
|
|
3114
|
+
test('Deleting a registration with discount, with cancellation fee', async () => {
|
|
3115
|
+
const { organization, bundleDiscount, registration1, registration2, member } = await initDataWithRegistrations();
|
|
3116
|
+
const { adminToken } = await initAdmin({ organization });
|
|
3117
|
+
|
|
3118
|
+
// Now delete registration 1, which has discount
|
|
3119
|
+
const checkout = IDRegisterCheckout.create({
|
|
3120
|
+
cart: IDRegisterCart.create({
|
|
3121
|
+
deleteRegistrationIds: [registration1.id],
|
|
3122
|
+
}),
|
|
3123
|
+
asOrganizationId: organization.id,
|
|
3124
|
+
totalPrice: 0, // no change in outstanding balance, because we charge a cancellation fee
|
|
3125
|
+
cancellationFeePercentage: 100_00,
|
|
3126
|
+
});
|
|
3127
|
+
|
|
3128
|
+
await post(checkout, organization, adminToken);
|
|
3129
|
+
await registration2.refresh();
|
|
3130
|
+
await registration1.refresh();
|
|
3131
|
+
|
|
3132
|
+
// Check discount has NOT been removed in registration 1
|
|
3133
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
3134
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
3135
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3136
|
+
name: bundleDiscount.name,
|
|
3137
|
+
amount: 5_00,
|
|
3138
|
+
})],
|
|
3139
|
+
]));
|
|
3140
|
+
expect(registration1.deactivatedAt).not.toBeNull(); // should be cancelled
|
|
3141
|
+
|
|
3142
|
+
// Check balances
|
|
3143
|
+
await assertBalances({ member }, [
|
|
3144
|
+
{
|
|
3145
|
+
type: BalanceItemType.Registration,
|
|
3146
|
+
registrationId: registration1.id,
|
|
3147
|
+
amount: 1,
|
|
3148
|
+
unitPrice: 25_00,
|
|
3149
|
+
status: BalanceItemStatus.Due, // NOT cancelled, because we charge a cancellation fee
|
|
3150
|
+
priceOpen: 25_00, // cancellation fee
|
|
3151
|
+
pricePending: 0,
|
|
3152
|
+
},
|
|
3153
|
+
{
|
|
3154
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3155
|
+
registrationId: registration1.id,
|
|
3156
|
+
amount: 1,
|
|
3157
|
+
unitPrice: -5_00,
|
|
3158
|
+
status: BalanceItemStatus.Due, // NOT cancelled, because we charge a cancellation fee
|
|
3159
|
+
priceOpen: -5_00,
|
|
3160
|
+
pricePending: 0,
|
|
3161
|
+
},
|
|
3162
|
+
{
|
|
3163
|
+
type: BalanceItemType.Registration,
|
|
3164
|
+
registrationId: registration2.id,
|
|
3165
|
+
amount: 1,
|
|
3166
|
+
unitPrice: 15_00,
|
|
3167
|
+
status: BalanceItemStatus.Due,
|
|
3168
|
+
pricePending: 0,
|
|
3169
|
+
priceOpen: 15_00,
|
|
3170
|
+
},
|
|
3171
|
+
]);
|
|
3172
|
+
});
|
|
3173
|
+
|
|
3174
|
+
test('Deleting a registration can alter a discount on a different registration, without cancellation fee', async () => {
|
|
3175
|
+
const { organization, registration1, registration2, member } = await initDataWithRegistrations();
|
|
3176
|
+
const { adminToken } = await initAdmin({ organization });
|
|
3177
|
+
|
|
3178
|
+
// Now delete registration 2, which does not have a discount
|
|
3179
|
+
const checkout = IDRegisterCheckout.create({
|
|
3180
|
+
cart: IDRegisterCart.create({
|
|
3181
|
+
deleteRegistrationIds: [registration2.id],
|
|
3182
|
+
}),
|
|
3183
|
+
asOrganizationId: organization.id,
|
|
3184
|
+
totalPrice: -15_00 + 5_00, // -15 back, but also lose discount on registration 1, so add 5
|
|
3185
|
+
cancellationFeePercentage: 0,
|
|
3186
|
+
});
|
|
3187
|
+
|
|
3188
|
+
await post(checkout, organization, adminToken);
|
|
3189
|
+
await registration2.refresh();
|
|
3190
|
+
await registration1.refresh();
|
|
3191
|
+
|
|
3192
|
+
// Check discount has been removed in registration 1
|
|
3193
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
3194
|
+
expect(registration1.discounts).toMatchMap(new Map());
|
|
3195
|
+
|
|
3196
|
+
// Registration 2 is canceled:
|
|
3197
|
+
expect(registration2.deactivatedAt).not.toBeNull();
|
|
3198
|
+
|
|
3199
|
+
// Check balances
|
|
3200
|
+
await assertBalances({ member }, [
|
|
3201
|
+
{
|
|
3202
|
+
type: BalanceItemType.Registration,
|
|
3203
|
+
registrationId: registration1.id,
|
|
3204
|
+
amount: 1,
|
|
3205
|
+
unitPrice: 25_00,
|
|
3206
|
+
status: BalanceItemStatus.Due,
|
|
3207
|
+
priceOpen: 25_00,
|
|
3208
|
+
pricePending: 0,
|
|
3209
|
+
},
|
|
3210
|
+
{
|
|
3211
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3212
|
+
registrationId: registration1.id,
|
|
3213
|
+
amount: 1,
|
|
3214
|
+
unitPrice: -5_00,
|
|
3215
|
+
status: BalanceItemStatus.Due, // has NOT been cancelled
|
|
3216
|
+
pricePending: 0,
|
|
3217
|
+
priceOpen: -5_00,
|
|
3218
|
+
},
|
|
3219
|
+
{
|
|
3220
|
+
type: BalanceItemType.Registration,
|
|
3221
|
+
registrationId: registration2.id,
|
|
3222
|
+
amount: 1,
|
|
3223
|
+
unitPrice: 15_00,
|
|
3224
|
+
status: BalanceItemStatus.Canceled,
|
|
3225
|
+
pricePending: 0,
|
|
3226
|
+
priceOpen: 0,
|
|
3227
|
+
},
|
|
3228
|
+
// A new bundle discount balance item has been created to offset the difference
|
|
3229
|
+
{
|
|
3230
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3231
|
+
registrationId: registration1.id,
|
|
3232
|
+
amount: 1,
|
|
3233
|
+
unitPrice: 5_00,
|
|
3234
|
+
status: BalanceItemStatus.Due,
|
|
3235
|
+
pricePending: 0,
|
|
3236
|
+
priceOpen: 5_00,
|
|
3237
|
+
},
|
|
3238
|
+
]);
|
|
3239
|
+
});
|
|
3240
|
+
|
|
3241
|
+
test('Deleting a registration can alter a discount on a different registration, with cancellation fee', async () => {
|
|
3242
|
+
const { organization, registration1, registration2, member } = await initDataWithRegistrations();
|
|
3243
|
+
const { adminToken } = await initAdmin({ organization });
|
|
3244
|
+
|
|
3245
|
+
// Now delete registration 2, which does not have a discount
|
|
3246
|
+
const checkout = IDRegisterCheckout.create({
|
|
3247
|
+
cart: IDRegisterCart.create({
|
|
3248
|
+
deleteRegistrationIds: [registration2.id],
|
|
3249
|
+
}),
|
|
3250
|
+
asOrganizationId: organization.id,
|
|
3251
|
+
totalPrice: 5_00, // Positive 5 because we lose the discount on registration 1
|
|
3252
|
+
cancellationFeePercentage: 100_00, // we charge a cancellation fee
|
|
3253
|
+
});
|
|
3254
|
+
|
|
3255
|
+
await post(checkout, organization, adminToken);
|
|
3256
|
+
await registration2.refresh();
|
|
3257
|
+
await registration1.refresh();
|
|
3258
|
+
|
|
3259
|
+
// Check discount has been removed in registration 1
|
|
3260
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
3261
|
+
expect(registration1.discounts).toMatchMap(new Map());
|
|
3262
|
+
|
|
3263
|
+
// Registration 2 is canceled:
|
|
3264
|
+
expect(registration2.deactivatedAt).not.toBeNull();
|
|
3265
|
+
|
|
3266
|
+
// Check balances
|
|
3267
|
+
await assertBalances({ member }, [
|
|
3268
|
+
{
|
|
3269
|
+
type: BalanceItemType.Registration,
|
|
3270
|
+
registrationId: registration1.id,
|
|
3271
|
+
amount: 1,
|
|
3272
|
+
unitPrice: 25_00,
|
|
3273
|
+
status: BalanceItemStatus.Due,
|
|
3274
|
+
priceOpen: 25_00,
|
|
3275
|
+
pricePending: 0,
|
|
3276
|
+
},
|
|
3277
|
+
{
|
|
3278
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3279
|
+
registrationId: registration1.id,
|
|
3280
|
+
amount: 1,
|
|
3281
|
+
unitPrice: -5_00,
|
|
3282
|
+
status: BalanceItemStatus.Due, // has NOT been cancelled
|
|
3283
|
+
pricePending: 0,
|
|
3284
|
+
priceOpen: -5_00,
|
|
3285
|
+
},
|
|
3286
|
+
{
|
|
3287
|
+
type: BalanceItemType.Registration,
|
|
3288
|
+
registrationId: registration2.id,
|
|
3289
|
+
amount: 1,
|
|
3290
|
+
unitPrice: 15_00,
|
|
3291
|
+
status: BalanceItemStatus.Due, // still due because of cancellation fee
|
|
3292
|
+
pricePending: 0,
|
|
3293
|
+
priceOpen: 15_00,
|
|
3294
|
+
},
|
|
3295
|
+
// A new bundle discount balance item has been created to offset the difference
|
|
3296
|
+
{
|
|
3297
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3298
|
+
registrationId: registration1.id,
|
|
3299
|
+
amount: 1,
|
|
3300
|
+
unitPrice: 5_00,
|
|
3301
|
+
status: BalanceItemStatus.Due,
|
|
3302
|
+
pricePending: 0,
|
|
3303
|
+
priceOpen: 5_00,
|
|
3304
|
+
},
|
|
3305
|
+
]);
|
|
3306
|
+
});
|
|
3307
|
+
|
|
3308
|
+
test('Discounts work across family members when admins register members', async () => {
|
|
3309
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
3310
|
+
const { adminToken } = await initAdmin({ organization });
|
|
3311
|
+
|
|
3312
|
+
const otherMember = await new MemberFactory({
|
|
3313
|
+
organization,
|
|
3314
|
+
user,
|
|
3315
|
+
}).create();
|
|
3316
|
+
|
|
3317
|
+
const bundleDiscount = await initBundleDiscount({
|
|
3318
|
+
organizationRegistrationPeriod,
|
|
3319
|
+
discount: {
|
|
3320
|
+
countWholeFamily: true,
|
|
3321
|
+
discounts: [
|
|
3322
|
+
{
|
|
3323
|
+
value: 20_00,
|
|
3324
|
+
type: GroupPriceDiscountType.Percentage,
|
|
3325
|
+
},
|
|
3326
|
+
],
|
|
3327
|
+
},
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
// Create an unrelated group and registration so admin has access to the member
|
|
3331
|
+
const randomGroup = await new GroupFactory({
|
|
3332
|
+
organization,
|
|
3333
|
+
price: 0,
|
|
3334
|
+
}).create();
|
|
3335
|
+
|
|
3336
|
+
await new RegistrationFactory({
|
|
3337
|
+
organization,
|
|
3338
|
+
member: member,
|
|
3339
|
+
group: randomGroup,
|
|
3340
|
+
}).create();
|
|
3341
|
+
|
|
3342
|
+
const groups = [
|
|
3343
|
+
await new GroupFactory({
|
|
3344
|
+
organization,
|
|
3345
|
+
price: 25_00,
|
|
3346
|
+
bundleDiscount,
|
|
3347
|
+
}).create(),
|
|
3348
|
+
await new GroupFactory({
|
|
3349
|
+
organization,
|
|
3350
|
+
price: 15_00,
|
|
3351
|
+
bundleDiscount,
|
|
3352
|
+
}).create(),
|
|
3353
|
+
];
|
|
3354
|
+
|
|
3355
|
+
// First register the otherMember for group 1. No discount should be applied yet
|
|
3356
|
+
const registration1 = await new RegistrationFactory({
|
|
3357
|
+
organization,
|
|
3358
|
+
member: otherMember,
|
|
3359
|
+
group: groups[0],
|
|
3360
|
+
}).create();
|
|
3361
|
+
|
|
3362
|
+
await new BalanceItemFactory({
|
|
3363
|
+
userId: user.id,
|
|
3364
|
+
memberId: otherMember.id,
|
|
3365
|
+
organizationId: organization.id,
|
|
3366
|
+
type: BalanceItemType.Registration,
|
|
3367
|
+
amount: 1,
|
|
3368
|
+
unitPrice: 25_00,
|
|
3369
|
+
status: BalanceItemStatus.Due,
|
|
3370
|
+
registrationId: registration1.id,
|
|
3371
|
+
}).create();
|
|
3372
|
+
|
|
3373
|
+
const checkout = IDRegisterCheckout.create({
|
|
3374
|
+
cart: IDRegisterCart.create({
|
|
3375
|
+
items: [
|
|
3376
|
+
IDRegisterItem.create({
|
|
3377
|
+
groupPrice: groups[1].settings.prices[0],
|
|
3378
|
+
groupId: groups[1].id,
|
|
3379
|
+
organizationId: organization.id,
|
|
3380
|
+
memberId: member.id,
|
|
3381
|
+
}),
|
|
3382
|
+
],
|
|
3383
|
+
}),
|
|
3384
|
+
totalPrice: 15_00 - 5_00, // 20% discount on first group
|
|
3385
|
+
asOrganizationId: organization.id,
|
|
3386
|
+
});
|
|
3387
|
+
|
|
3388
|
+
const response2 = await post(checkout, organization, adminToken);
|
|
3389
|
+
expect(response2.body.registrations.length).toBe(1);
|
|
3390
|
+
|
|
3391
|
+
const registration2 = response2.body.registrations[0];
|
|
3392
|
+
expect(registration2.registeredAt).not.toBeNull();
|
|
3393
|
+
expect(registration2.discounts).toMatchMap(new Map());
|
|
3394
|
+
|
|
3395
|
+
// Get registration 1 again, it should now have the bundle discount applied
|
|
3396
|
+
await registration1.refresh();
|
|
3397
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
3398
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3399
|
+
name: bundleDiscount.name,
|
|
3400
|
+
amount: 5_00,
|
|
3401
|
+
})],
|
|
3402
|
+
]));
|
|
3403
|
+
|
|
3404
|
+
await assertBalances({ member: otherMember }, [
|
|
3405
|
+
{
|
|
3406
|
+
type: BalanceItemType.Registration,
|
|
3407
|
+
registrationId: registration1.id,
|
|
3408
|
+
amount: 1,
|
|
3409
|
+
price: 25_00,
|
|
3410
|
+
status: BalanceItemStatus.Due,
|
|
3411
|
+
priceOpen: 25_00,
|
|
3412
|
+
pricePending: 0,
|
|
3413
|
+
userId: user.id,
|
|
3414
|
+
},
|
|
3415
|
+
{
|
|
3416
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3417
|
+
registrationId: registration1.id,
|
|
3418
|
+
amount: 1,
|
|
3419
|
+
price: -5_00,
|
|
3420
|
+
status: BalanceItemStatus.Due,
|
|
3421
|
+
priceOpen: -5_00,
|
|
3422
|
+
pricePending: 0,
|
|
3423
|
+
userId: null,
|
|
3424
|
+
},
|
|
3425
|
+
]);
|
|
3426
|
+
|
|
3427
|
+
await assertBalances({ member }, [
|
|
3428
|
+
{
|
|
3429
|
+
type: BalanceItemType.Registration,
|
|
3430
|
+
registrationId: registration2.id,
|
|
3431
|
+
amount: 1,
|
|
3432
|
+
price: 15_00,
|
|
3433
|
+
status: BalanceItemStatus.Due,
|
|
3434
|
+
priceOpen: 15_00,
|
|
3435
|
+
pricePending: 0,
|
|
3436
|
+
userId: null,
|
|
3437
|
+
},
|
|
3438
|
+
]);
|
|
3439
|
+
});
|
|
3440
|
+
|
|
3441
|
+
// Test repeats 5 times because it should be stable
|
|
3442
|
+
// This tests real edge cases that were fixed
|
|
3443
|
+
test.each([1, 2, 3, 4, 5])('If discounts are the same, they are not moved when the one without discount is edited (%i th try)', async () => {
|
|
3444
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
3445
|
+
const { adminToken } = await initAdmin({ organization });
|
|
3446
|
+
|
|
3447
|
+
const bundleDiscount = await initBundleDiscount({
|
|
3448
|
+
organizationRegistrationPeriod,
|
|
3449
|
+
discount: {
|
|
3450
|
+
discounts: [
|
|
3451
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
3452
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
3453
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
3454
|
+
],
|
|
3455
|
+
},
|
|
3456
|
+
});
|
|
3457
|
+
|
|
3458
|
+
const groups = [
|
|
3459
|
+
await new GroupFactory({
|
|
3460
|
+
organization,
|
|
3461
|
+
price: 25_00, // 20% discount = 5_00
|
|
3462
|
+
bundleDiscount,
|
|
3463
|
+
}).create(),
|
|
3464
|
+
await new GroupFactory({
|
|
3465
|
+
organization,
|
|
3466
|
+
price: 25_00, // 20% discount = 5_00 (same as group 1)
|
|
3467
|
+
bundleDiscount,
|
|
3468
|
+
}).create(),
|
|
3469
|
+
await new GroupFactory({
|
|
3470
|
+
organization,
|
|
3471
|
+
price: 25_00, // 20% discount = 5_00 (same as groups 1 & 2)
|
|
3472
|
+
bundleDiscount,
|
|
3473
|
+
}).create(),
|
|
3474
|
+
];
|
|
3475
|
+
|
|
3476
|
+
// Create existing registrations for group 1 & 2 with discounts already applied
|
|
3477
|
+
const registration1 = await new RegistrationFactory({
|
|
3478
|
+
organization,
|
|
3479
|
+
member,
|
|
3480
|
+
group: groups[0],
|
|
3481
|
+
}).create();
|
|
3482
|
+
|
|
3483
|
+
const registration2 = await new RegistrationFactory({
|
|
3484
|
+
organization,
|
|
3485
|
+
member,
|
|
3486
|
+
group: groups[1],
|
|
3487
|
+
}).create();
|
|
3488
|
+
|
|
3489
|
+
const registration3 = await new RegistrationFactory({
|
|
3490
|
+
organization,
|
|
3491
|
+
member,
|
|
3492
|
+
group: groups[2],
|
|
3493
|
+
}).create();
|
|
3494
|
+
|
|
3495
|
+
// Create balance items for existing registrations
|
|
3496
|
+
await new BalanceItemFactory({
|
|
3497
|
+
userId: user.id,
|
|
3498
|
+
memberId: member.id,
|
|
3499
|
+
organizationId: organization.id,
|
|
3500
|
+
registrationId: registration1.id,
|
|
3501
|
+
type: BalanceItemType.Registration,
|
|
3502
|
+
amount: 1,
|
|
3503
|
+
unitPrice: 25_00,
|
|
3504
|
+
status: BalanceItemStatus.Due,
|
|
3505
|
+
}).create();
|
|
3506
|
+
|
|
3507
|
+
await new BalanceItemFactory({
|
|
3508
|
+
userId: user.id,
|
|
3509
|
+
memberId: member.id,
|
|
3510
|
+
organizationId: organization.id,
|
|
3511
|
+
registrationId: registration1.id,
|
|
3512
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3513
|
+
amount: 1,
|
|
3514
|
+
unitPrice: -5_00,
|
|
3515
|
+
status: BalanceItemStatus.Due,
|
|
3516
|
+
relations: new Map([
|
|
3517
|
+
[
|
|
3518
|
+
BalanceItemRelationType.Discount,
|
|
3519
|
+
BalanceItemRelation.create({
|
|
3520
|
+
id: bundleDiscount.id,
|
|
3521
|
+
name: bundleDiscount.name,
|
|
3522
|
+
}),
|
|
3523
|
+
],
|
|
3524
|
+
]),
|
|
3525
|
+
}).create();
|
|
3526
|
+
|
|
3527
|
+
await new BalanceItemFactory({
|
|
3528
|
+
userId: user.id,
|
|
3529
|
+
memberId: member.id,
|
|
3530
|
+
organizationId: organization.id,
|
|
3531
|
+
registrationId: registration2.id,
|
|
3532
|
+
type: BalanceItemType.Registration,
|
|
3533
|
+
amount: 1,
|
|
3534
|
+
unitPrice: 25_00,
|
|
3535
|
+
status: BalanceItemStatus.Due,
|
|
3536
|
+
}).create();
|
|
3537
|
+
|
|
3538
|
+
// No discount on 2 ( = first)
|
|
3539
|
+
|
|
3540
|
+
// Discount on third
|
|
3541
|
+
await new BalanceItemFactory({
|
|
3542
|
+
userId: user.id,
|
|
3543
|
+
memberId: member.id,
|
|
3544
|
+
organizationId: organization.id,
|
|
3545
|
+
registrationId: registration3.id,
|
|
3546
|
+
type: BalanceItemType.Registration,
|
|
3547
|
+
amount: 1,
|
|
3548
|
+
unitPrice: 25_00,
|
|
3549
|
+
status: BalanceItemStatus.Due,
|
|
3550
|
+
}).create();
|
|
3551
|
+
|
|
3552
|
+
await new BalanceItemFactory({
|
|
3553
|
+
userId: user.id,
|
|
3554
|
+
memberId: member.id,
|
|
3555
|
+
organizationId: organization.id,
|
|
3556
|
+
registrationId: registration3.id,
|
|
3557
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3558
|
+
amount: 1,
|
|
3559
|
+
unitPrice: -5_00,
|
|
3560
|
+
status: BalanceItemStatus.Due,
|
|
3561
|
+
relations: new Map([
|
|
3562
|
+
[
|
|
3563
|
+
BalanceItemRelationType.Discount,
|
|
3564
|
+
BalanceItemRelation.create({
|
|
3565
|
+
id: bundleDiscount.id,
|
|
3566
|
+
name: bundleDiscount.name,
|
|
3567
|
+
}),
|
|
3568
|
+
],
|
|
3569
|
+
]),
|
|
3570
|
+
}).create();
|
|
3571
|
+
|
|
3572
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
3573
|
+
await registration1.refresh();
|
|
3574
|
+
await registration2.refresh();
|
|
3575
|
+
await registration3.refresh();
|
|
3576
|
+
|
|
3577
|
+
// Check this is what we expect
|
|
3578
|
+
expect(registration2.discounts).toMatchMap(new Map([]));
|
|
3579
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
3580
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3581
|
+
name: bundleDiscount.name,
|
|
3582
|
+
amount: 5_00,
|
|
3583
|
+
})],
|
|
3584
|
+
]));
|
|
3585
|
+
expect(registration3.discounts).toMatchMap(new Map([
|
|
3586
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3587
|
+
name: bundleDiscount.name,
|
|
3588
|
+
amount: 5_00,
|
|
3589
|
+
})],
|
|
3590
|
+
]));
|
|
3591
|
+
|
|
3592
|
+
// Now alter the third registration (the one which didn't had a discount)
|
|
3593
|
+
const checkout = IDRegisterCheckout.create({
|
|
3594
|
+
cart: IDRegisterCart.create({
|
|
3595
|
+
items: [
|
|
3596
|
+
IDRegisterItem.create({
|
|
3597
|
+
groupPrice: groups[1].settings.prices[0],
|
|
3598
|
+
groupId: groups[1].id,
|
|
3599
|
+
organizationId: organization.id,
|
|
3600
|
+
memberId: member.id,
|
|
3601
|
+
replaceRegistrationIds: [registration2.id],
|
|
3602
|
+
}),
|
|
3603
|
+
],
|
|
3604
|
+
}),
|
|
3605
|
+
totalPrice: 0, // no balance change
|
|
3606
|
+
asOrganizationId: organization.id,
|
|
3607
|
+
});
|
|
3608
|
+
|
|
3609
|
+
const response = await post(checkout, organization, adminToken);
|
|
3610
|
+
expect(response.body.registrations.length).toBe(1);
|
|
3611
|
+
const registration2Struct = response.body.registrations[0];
|
|
3612
|
+
|
|
3613
|
+
expect(registration2Struct).toMatchObject({
|
|
3614
|
+
id: registration2.id,
|
|
3615
|
+
registeredAt: expect.any(Date),
|
|
3616
|
+
discounts: new Map([]),
|
|
3617
|
+
});
|
|
3618
|
+
|
|
3619
|
+
// Verify that existing registrations still have their original discounts
|
|
3620
|
+
await registration1.refresh();
|
|
3621
|
+
await registration2.refresh();
|
|
3622
|
+
await registration3.refresh();
|
|
3623
|
+
|
|
3624
|
+
expect(registration2.discounts).toMatchMap(new Map([]));
|
|
3625
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
3626
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3627
|
+
name: bundleDiscount.name,
|
|
3628
|
+
amount: 5_00,
|
|
3629
|
+
})],
|
|
3630
|
+
]));
|
|
3631
|
+
expect(registration3.discounts).toMatchMap(new Map([
|
|
3632
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3633
|
+
name: bundleDiscount.name,
|
|
3634
|
+
amount: 5_00,
|
|
3635
|
+
})],
|
|
3636
|
+
]));
|
|
3637
|
+
});
|
|
3638
|
+
|
|
3639
|
+
// Test repeats 5 times because it should be stable
|
|
3640
|
+
// This tests real edge cases that were fixed
|
|
3641
|
+
test.each([1, 2, 3, 4, 5])('If discounts are the same, they are not moved when the one with discount is edited (%i th try)', async () => {
|
|
3642
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
3643
|
+
const { adminToken } = await initAdmin({ organization });
|
|
3644
|
+
|
|
3645
|
+
const bundleDiscount = await initBundleDiscount({
|
|
3646
|
+
organizationRegistrationPeriod,
|
|
3647
|
+
discount: {
|
|
3648
|
+
discounts: [
|
|
3649
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
3650
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
3651
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
3652
|
+
],
|
|
3653
|
+
},
|
|
3654
|
+
});
|
|
3655
|
+
|
|
3656
|
+
const groups = [
|
|
3657
|
+
await new GroupFactory({
|
|
3658
|
+
organization,
|
|
3659
|
+
price: 25_00, // 20% discount = 5_00
|
|
3660
|
+
bundleDiscount,
|
|
3661
|
+
}).create(),
|
|
3662
|
+
await new GroupFactory({
|
|
3663
|
+
organization,
|
|
3664
|
+
price: 25_00, // 20% discount = 5_00 (same as group 1)
|
|
3665
|
+
bundleDiscount,
|
|
3666
|
+
}).create(),
|
|
3667
|
+
await new GroupFactory({
|
|
3668
|
+
organization,
|
|
3669
|
+
price: 25_00, // 20% discount = 5_00 (same as groups 1 & 2)
|
|
3670
|
+
bundleDiscount,
|
|
3671
|
+
}).create(),
|
|
3672
|
+
];
|
|
3673
|
+
|
|
3674
|
+
// Create existing registrations for group 1 & 2 with discounts already applied
|
|
3675
|
+
const registration1 = await new RegistrationFactory({
|
|
3676
|
+
organization,
|
|
3677
|
+
member,
|
|
3678
|
+
group: groups[0],
|
|
3679
|
+
}).create();
|
|
3680
|
+
|
|
3681
|
+
const registration2 = await new RegistrationFactory({
|
|
3682
|
+
organization,
|
|
3683
|
+
member,
|
|
3684
|
+
group: groups[1],
|
|
3685
|
+
}).create();
|
|
3686
|
+
|
|
3687
|
+
const registration3 = await new RegistrationFactory({
|
|
3688
|
+
organization,
|
|
3689
|
+
member,
|
|
3690
|
+
group: groups[2],
|
|
3691
|
+
}).create();
|
|
3692
|
+
|
|
3693
|
+
// Create balance items for existing registrations
|
|
3694
|
+
await new BalanceItemFactory({
|
|
3695
|
+
userId: user.id,
|
|
3696
|
+
memberId: member.id,
|
|
3697
|
+
organizationId: organization.id,
|
|
3698
|
+
registrationId: registration1.id,
|
|
3699
|
+
type: BalanceItemType.Registration,
|
|
3700
|
+
amount: 1,
|
|
3701
|
+
unitPrice: 25_00,
|
|
3702
|
+
status: BalanceItemStatus.Due,
|
|
3703
|
+
}).create();
|
|
3704
|
+
|
|
3705
|
+
await new BalanceItemFactory({
|
|
3706
|
+
userId: user.id,
|
|
3707
|
+
memberId: member.id,
|
|
3708
|
+
organizationId: organization.id,
|
|
3709
|
+
registrationId: registration1.id,
|
|
3710
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3711
|
+
amount: 1,
|
|
3712
|
+
unitPrice: -5_00,
|
|
3713
|
+
status: BalanceItemStatus.Due,
|
|
3714
|
+
relations: new Map([
|
|
3715
|
+
[
|
|
3716
|
+
BalanceItemRelationType.Discount,
|
|
3717
|
+
BalanceItemRelation.create({
|
|
3718
|
+
id: bundleDiscount.id,
|
|
3719
|
+
name: bundleDiscount.name,
|
|
3720
|
+
}),
|
|
3721
|
+
],
|
|
3722
|
+
]),
|
|
3723
|
+
}).create();
|
|
3724
|
+
|
|
3725
|
+
await new BalanceItemFactory({
|
|
3726
|
+
userId: user.id,
|
|
3727
|
+
memberId: member.id,
|
|
3728
|
+
organizationId: organization.id,
|
|
3729
|
+
registrationId: registration2.id,
|
|
3730
|
+
type: BalanceItemType.Registration,
|
|
3731
|
+
amount: 1,
|
|
3732
|
+
unitPrice: 25_00,
|
|
3733
|
+
status: BalanceItemStatus.Due,
|
|
3734
|
+
}).create();
|
|
3735
|
+
|
|
3736
|
+
// No discount on 2 ( = first)
|
|
3737
|
+
|
|
3738
|
+
// Discount on third
|
|
3739
|
+
await new BalanceItemFactory({
|
|
3740
|
+
userId: user.id,
|
|
3741
|
+
memberId: member.id,
|
|
3742
|
+
organizationId: organization.id,
|
|
3743
|
+
registrationId: registration3.id,
|
|
3744
|
+
type: BalanceItemType.Registration,
|
|
3745
|
+
amount: 1,
|
|
3746
|
+
unitPrice: 25_00,
|
|
3747
|
+
status: BalanceItemStatus.Due,
|
|
3748
|
+
}).create();
|
|
3749
|
+
|
|
3750
|
+
await new BalanceItemFactory({
|
|
3751
|
+
userId: user.id,
|
|
3752
|
+
memberId: member.id,
|
|
3753
|
+
organizationId: organization.id,
|
|
3754
|
+
registrationId: registration3.id,
|
|
3755
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3756
|
+
amount: 1,
|
|
3757
|
+
unitPrice: -5_00,
|
|
3758
|
+
status: BalanceItemStatus.Due,
|
|
3759
|
+
relations: new Map([
|
|
3760
|
+
[
|
|
3761
|
+
BalanceItemRelationType.Discount,
|
|
3762
|
+
BalanceItemRelation.create({
|
|
3763
|
+
id: bundleDiscount.id,
|
|
3764
|
+
name: bundleDiscount.name,
|
|
3765
|
+
}),
|
|
3766
|
+
],
|
|
3767
|
+
]),
|
|
3768
|
+
}).create();
|
|
3769
|
+
|
|
3770
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
3771
|
+
await registration1.refresh();
|
|
3772
|
+
await registration2.refresh();
|
|
3773
|
+
await registration3.refresh();
|
|
3774
|
+
|
|
3775
|
+
// Check this is what we expect
|
|
3776
|
+
expect(registration2.discounts).toMatchMap(new Map([]));
|
|
3777
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
3778
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3779
|
+
name: bundleDiscount.name,
|
|
3780
|
+
amount: 5_00,
|
|
3781
|
+
})],
|
|
3782
|
+
]));
|
|
3783
|
+
expect(registration3.discounts).toMatchMap(new Map([
|
|
3784
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3785
|
+
name: bundleDiscount.name,
|
|
3786
|
+
amount: 5_00,
|
|
3787
|
+
})],
|
|
3788
|
+
]));
|
|
3789
|
+
|
|
3790
|
+
// Now alter the third registration (the one which didn't had a discount)
|
|
3791
|
+
const checkout = IDRegisterCheckout.create({
|
|
3792
|
+
cart: IDRegisterCart.create({
|
|
3793
|
+
items: [
|
|
3794
|
+
IDRegisterItem.create({
|
|
3795
|
+
groupPrice: groups[0].settings.prices[0],
|
|
3796
|
+
groupId: groups[0].id,
|
|
3797
|
+
organizationId: organization.id,
|
|
3798
|
+
memberId: member.id,
|
|
3799
|
+
replaceRegistrationIds: [registration1.id],
|
|
3800
|
+
}),
|
|
3801
|
+
],
|
|
3802
|
+
}),
|
|
3803
|
+
totalPrice: 0, // no balance change
|
|
3804
|
+
asOrganizationId: organization.id,
|
|
3805
|
+
});
|
|
3806
|
+
|
|
3807
|
+
const response = await post(checkout, organization, adminToken);
|
|
3808
|
+
expect(response.body.registrations.length).toBe(1);
|
|
3809
|
+
const registration1Struct = response.body.registrations[0];
|
|
3810
|
+
|
|
3811
|
+
expect(registration1Struct).toMatchObject({
|
|
3812
|
+
id: registration1.id,
|
|
3813
|
+
registeredAt: expect.any(Date),
|
|
3814
|
+
discounts: new Map([
|
|
3815
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3816
|
+
name: bundleDiscount.name,
|
|
3817
|
+
amount: 5_00,
|
|
3818
|
+
})],
|
|
3819
|
+
]),
|
|
3820
|
+
});
|
|
3821
|
+
|
|
3822
|
+
// Verify that existing registrations still have their original discounts
|
|
3823
|
+
await registration1.refresh();
|
|
3824
|
+
await registration2.refresh();
|
|
3825
|
+
await registration3.refresh();
|
|
3826
|
+
|
|
3827
|
+
expect(registration2.discounts).toMatchMap(new Map([]));
|
|
3828
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
3829
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3830
|
+
name: bundleDiscount.name,
|
|
3831
|
+
amount: 5_00,
|
|
3832
|
+
})],
|
|
3833
|
+
]));
|
|
3834
|
+
expect(registration3.discounts).toMatchMap(new Map([
|
|
3835
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3836
|
+
name: bundleDiscount.name,
|
|
3837
|
+
amount: 5_00,
|
|
3838
|
+
})],
|
|
3839
|
+
]));
|
|
3840
|
+
});
|
|
3841
|
+
|
|
3842
|
+
// Test repeats 5 times because it should be stable
|
|
3843
|
+
// This tests real edge cases that were fixed
|
|
3844
|
+
test.each([1, 2, 3, 4, 5])('If discounts are the same, they are not moved when the one with and one without discount are edited (%i th try)', async () => {
|
|
3845
|
+
const { organizationRegistrationPeriod, organization, member, token, user } = await initData();
|
|
3846
|
+
const { adminToken } = await initAdmin({ organization });
|
|
3847
|
+
|
|
3848
|
+
const bundleDiscount = await initBundleDiscount({
|
|
3849
|
+
organizationRegistrationPeriod,
|
|
3850
|
+
discount: {
|
|
3851
|
+
discounts: [
|
|
3852
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
3853
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
3854
|
+
{ value: 20_00, type: GroupPriceDiscountType.Percentage },
|
|
3855
|
+
],
|
|
3856
|
+
},
|
|
3857
|
+
});
|
|
3858
|
+
|
|
3859
|
+
const groups = [
|
|
3860
|
+
await new GroupFactory({
|
|
3861
|
+
organization,
|
|
3862
|
+
price: 25_00, // 20% discount = 5_00
|
|
3863
|
+
bundleDiscount,
|
|
3864
|
+
}).create(),
|
|
3865
|
+
await new GroupFactory({
|
|
3866
|
+
organization,
|
|
3867
|
+
price: 25_00, // 20% discount = 5_00 (same as group 1)
|
|
3868
|
+
bundleDiscount,
|
|
3869
|
+
}).create(),
|
|
3870
|
+
await new GroupFactory({
|
|
3871
|
+
organization,
|
|
3872
|
+
price: 25_00, // 20% discount = 5_00 (same as groups 1 & 2)
|
|
3873
|
+
bundleDiscount,
|
|
3874
|
+
}).create(),
|
|
3875
|
+
];
|
|
3876
|
+
|
|
3877
|
+
// Create existing registrations for group 1 & 2 with discounts already applied
|
|
3878
|
+
const registration1 = await new RegistrationFactory({
|
|
3879
|
+
organization,
|
|
3880
|
+
member,
|
|
3881
|
+
group: groups[0],
|
|
3882
|
+
}).create();
|
|
3883
|
+
|
|
3884
|
+
const registration2 = await new RegistrationFactory({
|
|
3885
|
+
organization,
|
|
3886
|
+
member,
|
|
3887
|
+
group: groups[1],
|
|
3888
|
+
}).create();
|
|
3889
|
+
|
|
3890
|
+
const registration3 = await new RegistrationFactory({
|
|
3891
|
+
organization,
|
|
3892
|
+
member,
|
|
3893
|
+
group: groups[2],
|
|
3894
|
+
}).create();
|
|
3895
|
+
|
|
3896
|
+
// Create balance items for existing registrations
|
|
3897
|
+
await new BalanceItemFactory({
|
|
3898
|
+
userId: user.id,
|
|
3899
|
+
memberId: member.id,
|
|
3900
|
+
organizationId: organization.id,
|
|
3901
|
+
registrationId: registration1.id,
|
|
3902
|
+
type: BalanceItemType.Registration,
|
|
3903
|
+
amount: 1,
|
|
3904
|
+
unitPrice: 25_00,
|
|
3905
|
+
status: BalanceItemStatus.Due,
|
|
3906
|
+
}).create();
|
|
3907
|
+
|
|
3908
|
+
await new BalanceItemFactory({
|
|
3909
|
+
userId: user.id,
|
|
3910
|
+
memberId: member.id,
|
|
3911
|
+
organizationId: organization.id,
|
|
3912
|
+
registrationId: registration1.id,
|
|
3913
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3914
|
+
amount: 1,
|
|
3915
|
+
unitPrice: -5_00,
|
|
3916
|
+
status: BalanceItemStatus.Due,
|
|
3917
|
+
relations: new Map([
|
|
3918
|
+
[
|
|
3919
|
+
BalanceItemRelationType.Discount,
|
|
3920
|
+
BalanceItemRelation.create({
|
|
3921
|
+
id: bundleDiscount.id,
|
|
3922
|
+
name: bundleDiscount.name,
|
|
3923
|
+
}),
|
|
3924
|
+
],
|
|
3925
|
+
]),
|
|
3926
|
+
}).create();
|
|
3927
|
+
|
|
3928
|
+
await new BalanceItemFactory({
|
|
3929
|
+
userId: user.id,
|
|
3930
|
+
memberId: member.id,
|
|
3931
|
+
organizationId: organization.id,
|
|
3932
|
+
registrationId: registration2.id,
|
|
3933
|
+
type: BalanceItemType.Registration,
|
|
3934
|
+
amount: 1,
|
|
3935
|
+
unitPrice: 25_00,
|
|
3936
|
+
status: BalanceItemStatus.Due,
|
|
3937
|
+
}).create();
|
|
3938
|
+
|
|
3939
|
+
// No discount on 2 ( = first)
|
|
3940
|
+
|
|
3941
|
+
// Discount on third
|
|
3942
|
+
await new BalanceItemFactory({
|
|
3943
|
+
userId: user.id,
|
|
3944
|
+
memberId: member.id,
|
|
3945
|
+
organizationId: organization.id,
|
|
3946
|
+
registrationId: registration3.id,
|
|
3947
|
+
type: BalanceItemType.Registration,
|
|
3948
|
+
amount: 1,
|
|
3949
|
+
unitPrice: 25_00,
|
|
3950
|
+
status: BalanceItemStatus.Due,
|
|
3951
|
+
}).create();
|
|
3952
|
+
|
|
3953
|
+
await new BalanceItemFactory({
|
|
3954
|
+
userId: user.id,
|
|
3955
|
+
memberId: member.id,
|
|
3956
|
+
organizationId: organization.id,
|
|
3957
|
+
registrationId: registration3.id,
|
|
3958
|
+
type: BalanceItemType.RegistrationBundleDiscount,
|
|
3959
|
+
amount: 1,
|
|
3960
|
+
unitPrice: -5_00,
|
|
3961
|
+
status: BalanceItemStatus.Due,
|
|
3962
|
+
relations: new Map([
|
|
3963
|
+
[
|
|
3964
|
+
BalanceItemRelationType.Discount,
|
|
3965
|
+
BalanceItemRelation.create({
|
|
3966
|
+
id: bundleDiscount.id,
|
|
3967
|
+
name: bundleDiscount.name,
|
|
3968
|
+
}),
|
|
3969
|
+
],
|
|
3970
|
+
]),
|
|
3971
|
+
}).create();
|
|
3972
|
+
|
|
3973
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
3974
|
+
await registration1.refresh();
|
|
3975
|
+
await registration2.refresh();
|
|
3976
|
+
await registration3.refresh();
|
|
3977
|
+
|
|
3978
|
+
// Check this is what we expect
|
|
3979
|
+
expect(registration2.discounts).toMatchMap(new Map([]));
|
|
3980
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
3981
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3982
|
+
name: bundleDiscount.name,
|
|
3983
|
+
amount: 5_00,
|
|
3984
|
+
})],
|
|
3985
|
+
]));
|
|
3986
|
+
expect(registration3.discounts).toMatchMap(new Map([
|
|
3987
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
3988
|
+
name: bundleDiscount.name,
|
|
3989
|
+
amount: 5_00,
|
|
3990
|
+
})],
|
|
3991
|
+
]));
|
|
3992
|
+
|
|
3993
|
+
// Now alter the third registration (the one which didn't had a discount)
|
|
3994
|
+
const checkout = IDRegisterCheckout.create({
|
|
3995
|
+
cart: IDRegisterCart.create({
|
|
3996
|
+
items: [
|
|
3997
|
+
// Keep the one without last in the cart, since these normally get discount priority
|
|
3998
|
+
IDRegisterItem.create({
|
|
3999
|
+
groupPrice: groups[0].settings.prices[0],
|
|
4000
|
+
groupId: groups[0].id,
|
|
4001
|
+
organizationId: organization.id,
|
|
4002
|
+
memberId: member.id,
|
|
4003
|
+
replaceRegistrationIds: [registration1.id],
|
|
4004
|
+
}),
|
|
4005
|
+
IDRegisterItem.create({
|
|
4006
|
+
groupPrice: groups[1].settings.prices[0],
|
|
4007
|
+
groupId: groups[1].id,
|
|
4008
|
+
organizationId: organization.id,
|
|
4009
|
+
memberId: member.id,
|
|
4010
|
+
replaceRegistrationIds: [registration2.id],
|
|
4011
|
+
}),
|
|
4012
|
+
],
|
|
4013
|
+
}),
|
|
4014
|
+
totalPrice: 0, // no balance change
|
|
4015
|
+
asOrganizationId: organization.id,
|
|
4016
|
+
});
|
|
4017
|
+
|
|
4018
|
+
const response = await post(checkout, organization, adminToken);
|
|
4019
|
+
expect(response.body.registrations.length).toBe(2);
|
|
4020
|
+
|
|
4021
|
+
// Check array matches
|
|
4022
|
+
expect(response.body.registrations).toIncludeSameMembers([
|
|
4023
|
+
expect.objectContaining({
|
|
4024
|
+
id: registration1.id,
|
|
4025
|
+
registeredAt: expect.any(Date),
|
|
4026
|
+
discounts: new Map([
|
|
4027
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
4028
|
+
name: bundleDiscount.name,
|
|
4029
|
+
amount: 5_00,
|
|
4030
|
+
})],
|
|
4031
|
+
]),
|
|
4032
|
+
}),
|
|
4033
|
+
expect.objectContaining({
|
|
4034
|
+
id: registration2.id,
|
|
4035
|
+
registeredAt: expect.any(Date),
|
|
4036
|
+
discounts: new Map([]),
|
|
4037
|
+
}),
|
|
4038
|
+
]);
|
|
4039
|
+
|
|
4040
|
+
// Verify that existing registrations still have their original discounts
|
|
4041
|
+
await registration1.refresh();
|
|
4042
|
+
await registration2.refresh();
|
|
4043
|
+
await registration3.refresh();
|
|
4044
|
+
|
|
4045
|
+
expect(registration2.discounts).toMatchMap(new Map([]));
|
|
4046
|
+
expect(registration1.discounts).toMatchMap(new Map([
|
|
4047
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
4048
|
+
name: bundleDiscount.name,
|
|
4049
|
+
amount: 5_00,
|
|
4050
|
+
})],
|
|
4051
|
+
]));
|
|
4052
|
+
expect(registration3.discounts).toMatchMap(new Map([
|
|
4053
|
+
[bundleDiscount.id, AppliedRegistrationDiscount.create({
|
|
4054
|
+
name: bundleDiscount.name,
|
|
4055
|
+
amount: 5_00,
|
|
4056
|
+
})],
|
|
4057
|
+
]));
|
|
4058
|
+
});
|
|
4059
|
+
});
|
|
4060
|
+
});
|