@stamhoofd/backend 2.111.0 → 2.112.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/LICENSE.md +32 -0
- package/package.json +14 -11
- package/src/boot.ts +1 -0
- package/src/email-recipient-loaders/documents.ts +66 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +701 -4
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +21 -10
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +661 -4
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +17 -6
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +291 -8
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +22 -0
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesCountEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +219 -0
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +2 -2
- package/src/endpoints/organization/shared/GetUitpasNumberDetailsEndpoint.ts +72 -0
- package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +3 -2
- package/src/excel-loaders/members.ts +27 -27
- package/src/helpers/AdminPermissionChecker.ts +30 -10
- package/src/helpers/AuthenticatedStructures.ts +24 -5
- package/src/helpers/StripeHelper.ts +11 -1
- package/src/helpers/StripePayoutChecker.ts +7 -0
- package/src/helpers/UitpasTokenRepository.ts +7 -5
- package/src/helpers/passthroughFetch.ts +24 -0
- package/src/helpers/updateMemberDetailsUitpasNumber.ts +149 -0
- package/src/seeds/data/default-email-templates.sql +2 -1
- package/src/seeds/wip/1769088653-uitpas-status.ts +129 -0
- package/src/services/InvoiceService.ts +2 -2
- package/src/services/uitpas/PassholderEndpoints.ts +190 -0
- package/src/services/uitpas/UitpasService.ts +37 -12
- package/src/services/uitpas/checkUitpasNumbers.ts +16 -140
- package/src/services/uitpas/handleUitpasResponse.ts +89 -0
- package/src/sql-filters/invoiced-balance-items.ts +20 -0
- package/src/sql-filters/invoices.ts +122 -0
- package/src/sql-filters/payments.ts +11 -1
- package/src/sql-sorters/invoices.ts +83 -0
- package/src/sql-sorters/payments.ts +33 -0
- package/tests/e2e/bundle-discounts.test.ts +8 -8
- package/tests/e2e/tests-disable-net-connect.test.ts +5 -0
- package/tests/helpers/StripeMocker.ts +5 -5
- package/tests/helpers/UitpasApiMocker.ts +175 -0
- package/tests/helpers/index.ts +1 -0
- package/tests/helpers/resetNock.ts +7 -0
- package/tests/init/index.ts +1 -0
- package/tests/init/initPayconiq.ts +2 -2
- package/tests/init/initStripe.ts +1 -1
- package/tests/init/initUitpasApi.ts +14 -0
- package/tests/jest.global.setup.ts +6 -4
- package/tests/jest.setup.ts +12 -6
- package/LICENSE +0 -665
|
@@ -4,12 +4,13 @@ import { SimpleError } from '@simonbackx/simple-errors';
|
|
|
4
4
|
import { Document, Group, Member, RateLimiter, Registration } from '@stamhoofd/models';
|
|
5
5
|
import { MemberDetails, MembersBlob, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
|
|
6
6
|
|
|
7
|
-
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
8
|
-
import { Context } from '../../../helpers/Context';
|
|
9
|
-
import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
|
|
10
|
-
import { PatchOrganizationMembersEndpoint } from '../../global/members/PatchOrganizationMembersEndpoint';
|
|
11
|
-
import { shouldCheckIfMemberIsDuplicateForPatch } from '../members/shouldCheckIfMemberIsDuplicate';
|
|
12
7
|
import { OneToManyRelation } from '@simonbackx/simple-database';
|
|
8
|
+
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
|
|
9
|
+
import { Context } from '../../../helpers/Context.js';
|
|
10
|
+
import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer.js';
|
|
11
|
+
import { didUitpasReviewChange, updateMemberDetailsUitpasNumber, updateMemberDetailsUitpasNumberForPatch } from '../../../helpers/updateMemberDetailsUitpasNumber.js';
|
|
12
|
+
import { PatchOrganizationMembersEndpoint } from '../../global/members/PatchOrganizationMembersEndpoint.js';
|
|
13
|
+
import { shouldCheckIfMemberIsDuplicateForPatch } from '../members/shouldCheckIfMemberIsDuplicate.js';
|
|
13
14
|
type Params = Record<string, never>;
|
|
14
15
|
type Query = undefined;
|
|
15
16
|
type Body = PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
|
|
@@ -60,7 +61,9 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
60
61
|
member.organizationId = organization?.id ?? null;
|
|
61
62
|
|
|
62
63
|
const securityCode = struct.details.securityCode; // will get cleared after the filter
|
|
63
|
-
Context.auth.filterMemberPut(member, struct, {asUserManager: true});
|
|
64
|
+
Context.auth.filterMemberPut(member, struct, { asUserManager: true });
|
|
65
|
+
|
|
66
|
+
await updateMemberDetailsUitpasNumber(struct.details);
|
|
64
67
|
struct.details.cleanData();
|
|
65
68
|
member.details = struct.details;
|
|
66
69
|
|
|
@@ -102,7 +105,15 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
102
105
|
|
|
103
106
|
shouldCheckDuplicate = shouldCheckIfMemberIsDuplicateForPatch(struct, member.details);
|
|
104
107
|
|
|
108
|
+
const previousUitpasNumber = member.details.uitpasNumberDetails?.uitpasNumber ?? null;
|
|
109
|
+
|
|
110
|
+
const originalReviewTimes = member.details.reviewTimes;
|
|
105
111
|
member.details.patchOrPut(struct.details);
|
|
112
|
+
|
|
113
|
+
if (struct.details.uitpasNumberDetails || didUitpasReviewChange(struct.details.reviewTimes, originalReviewTimes)) {
|
|
114
|
+
await updateMemberDetailsUitpasNumberForPatch(member.id, member.details, previousUitpasNumber);
|
|
115
|
+
}
|
|
116
|
+
|
|
106
117
|
member.details.cleanData();
|
|
107
118
|
this.throwIfInvalidDetails(member.details);
|
|
108
119
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { PatchMap } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { EmailMocker } from '@stamhoofd/email';
|
|
4
|
-
import { BalanceItemFactory, Group, GroupFactory, MemberFactory, MemberWithRegistrations, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, Registration, RegistrationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
5
|
-
import { AccessRight, BalanceItemCartItem, BalanceItemStatus, BalanceItemType, BooleanStatus, Company, GroupOption, GroupOptionMenu, IDRegisterCart, IDRegisterCheckout, IDRegisterItem, OrganizationPackages, PaymentCustomer, PaymentMethod, PermissionLevel, Permissions, PermissionsResourceType, ReduceablePrice, RegisterItemOption, ResourcePermissions, STPackageStatus, STPackageType, UserPermissions, Version } from '@stamhoofd/structures';
|
|
4
|
+
import { BalanceItemFactory, Group, GroupFactory, Member, MemberFactory, MemberWithRegistrations, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, Registration, RegistrationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
5
|
+
import { AccessRight, BalanceItemCartItem, BalanceItemStatus, BalanceItemType, BooleanStatus, Company, GroupOption, GroupOptionMenu, IDRegisterCart, IDRegisterCheckout, IDRegisterItem, OrganizationPackages, PaymentCustomer, PaymentMethod, PermissionLevel, Permissions, PermissionsResourceType, ReduceablePrice, RegisterItemOption, ResourcePermissions, STPackageStatus, STPackageType, UitpasNumberDetails, UitpasSocialTariff, UitpasSocialTariffStatus, UserPermissions, Version } from '@stamhoofd/structures';
|
|
6
6
|
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
7
7
|
import { v4 as uuidv4 } from 'uuid';
|
|
8
|
-
import { assertBalances } from '../../../../tests/assertions/assertBalances';
|
|
9
|
-
import { testServer } from '../../../../tests/helpers/TestServer';
|
|
10
|
-
import { initAdmin, initPermissionRole } from '../../../../tests/init';
|
|
11
|
-
import { initPayconiq } from '../../../../tests/init/initPayconiq';
|
|
12
|
-
import { BalanceItemService } from '../../../services/BalanceItemService';
|
|
13
|
-
import { RegisterMembersEndpoint } from './RegisterMembersEndpoint';
|
|
8
|
+
import { assertBalances } from '../../../../tests/assertions/assertBalances.js';
|
|
9
|
+
import { testServer } from '../../../../tests/helpers/TestServer.js';
|
|
10
|
+
import { initAdmin, initPermissionRole, initUitpasApi } from '../../../../tests/init/index.js';
|
|
11
|
+
import { initPayconiq } from '../../../../tests/init/initPayconiq.js';
|
|
12
|
+
import { BalanceItemService } from '../../../services/BalanceItemService.js';
|
|
13
|
+
import { RegisterMembersEndpoint } from './RegisterMembersEndpoint.js';
|
|
14
14
|
|
|
15
15
|
const baseUrl = `/v${Version}/members/register`;
|
|
16
16
|
|
|
@@ -1248,6 +1248,289 @@ describe('Endpoint.RegisterMembers', () => {
|
|
|
1248
1248
|
const result = await post(body, organization, token);
|
|
1249
1249
|
expect(result).toBeDefined();
|
|
1250
1250
|
});
|
|
1251
|
+
|
|
1252
|
+
describe('Uitpas number', () => {
|
|
1253
|
+
test('should update social tariff status and throw error if the price changed', async () => {
|
|
1254
|
+
// #region arrange
|
|
1255
|
+
initUitpasApi();
|
|
1256
|
+
const { member, group, groupPrice, organization, token } = await initData();
|
|
1257
|
+
member.details.uitpasNumberDetails = UitpasNumberDetails.create({
|
|
1258
|
+
// expired
|
|
1259
|
+
uitpasNumber: '0900000031618',
|
|
1260
|
+
socialTariff: UitpasSocialTariff.create({
|
|
1261
|
+
// but last time checked it was active
|
|
1262
|
+
status: UitpasSocialTariffStatus.Active,
|
|
1263
|
+
updatedAt: new Date(2000, 0, 1),
|
|
1264
|
+
endDate: new Date(2000, 0, 1),
|
|
1265
|
+
}),
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
await member.save();
|
|
1269
|
+
|
|
1270
|
+
const body = IDRegisterCheckout.create({
|
|
1271
|
+
cart: IDRegisterCart.create({
|
|
1272
|
+
items: [
|
|
1273
|
+
IDRegisterItem.create({
|
|
1274
|
+
id: uuidv4(),
|
|
1275
|
+
replaceRegistrationIds: [],
|
|
1276
|
+
options: [],
|
|
1277
|
+
groupPrice,
|
|
1278
|
+
organizationId: organization.id,
|
|
1279
|
+
groupId: group.id,
|
|
1280
|
+
memberId: member.id,
|
|
1281
|
+
}),
|
|
1282
|
+
],
|
|
1283
|
+
balanceItems: [],
|
|
1284
|
+
deleteRegistrationIds: [],
|
|
1285
|
+
}),
|
|
1286
|
+
administrationFee: 0,
|
|
1287
|
+
freeContribution: 0,
|
|
1288
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1289
|
+
// reduced price
|
|
1290
|
+
totalPrice: 12_5000,
|
|
1291
|
+
customer: null,
|
|
1292
|
+
});
|
|
1293
|
+
// #endregion
|
|
1294
|
+
|
|
1295
|
+
// act
|
|
1296
|
+
|
|
1297
|
+
// should throw error
|
|
1298
|
+
await expect(async () => await post(body, organization, token))
|
|
1299
|
+
.rejects
|
|
1300
|
+
.toThrow(STExpect.errorWithCode('changed_price'));
|
|
1301
|
+
|
|
1302
|
+
// should update status
|
|
1303
|
+
const updatedMember = await Member.getByID(member.id);
|
|
1304
|
+
expect(updatedMember!.details.uitpasNumberDetails?.uitpasNumber).toEqual('0900000031618');
|
|
1305
|
+
expect(updatedMember!.details.uitpasNumberDetails?.socialTariff?.status).toEqual(UitpasSocialTariffStatus.Expired);
|
|
1306
|
+
expect(updatedMember!.details.uitpasNumberDetails?.socialTariff?.updatedAt.getTime()).not.toEqual(new Date(2000, 0, 1).getTime());
|
|
1307
|
+
expect(updatedMember!.details.uitpasNumberDetails?.socialTariff?.endDate?.getTime()).not.toEqual(new Date(2000, 0, 1).getTime());
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
test('should not update social tariff status if updated less than 1 week ago', async () => {
|
|
1311
|
+
// #region arrange
|
|
1312
|
+
initUitpasApi();
|
|
1313
|
+
const { member, group, groupPrice, organization, token } = await initData();
|
|
1314
|
+
|
|
1315
|
+
const now = new Date();
|
|
1316
|
+
const weekInMs = 7 * 24 * 3600 * 1000;
|
|
1317
|
+
const oneHourInMs = 3600 * 1000;
|
|
1318
|
+
const lessThanAWeekAgo = new Date(now.getTime() - weekInMs + oneHourInMs);
|
|
1319
|
+
|
|
1320
|
+
member.details.uitpasNumberDetails = UitpasNumberDetails.create({
|
|
1321
|
+
// expired
|
|
1322
|
+
uitpasNumber: '0900000031618',
|
|
1323
|
+
socialTariff: UitpasSocialTariff.create({
|
|
1324
|
+
// but last time checked it was active
|
|
1325
|
+
status: UitpasSocialTariffStatus.Active,
|
|
1326
|
+
updatedAt: lessThanAWeekAgo,
|
|
1327
|
+
endDate: new Date(2000, 0, 1),
|
|
1328
|
+
}),
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
await member.save();
|
|
1332
|
+
|
|
1333
|
+
const body = IDRegisterCheckout.create({
|
|
1334
|
+
cart: IDRegisterCart.create({
|
|
1335
|
+
items: [
|
|
1336
|
+
IDRegisterItem.create({
|
|
1337
|
+
id: uuidv4(),
|
|
1338
|
+
replaceRegistrationIds: [],
|
|
1339
|
+
options: [],
|
|
1340
|
+
groupPrice,
|
|
1341
|
+
organizationId: organization.id,
|
|
1342
|
+
groupId: group.id,
|
|
1343
|
+
memberId: member.id,
|
|
1344
|
+
}),
|
|
1345
|
+
],
|
|
1346
|
+
balanceItems: [],
|
|
1347
|
+
deleteRegistrationIds: [],
|
|
1348
|
+
}),
|
|
1349
|
+
administrationFee: 0,
|
|
1350
|
+
freeContribution: 0,
|
|
1351
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1352
|
+
// reduced price
|
|
1353
|
+
totalPrice: 12_5000,
|
|
1354
|
+
customer: null,
|
|
1355
|
+
});
|
|
1356
|
+
// #endregion
|
|
1357
|
+
|
|
1358
|
+
// act
|
|
1359
|
+
const response = await post(body, organization, token);
|
|
1360
|
+
|
|
1361
|
+
// assert
|
|
1362
|
+
expect(response.body).toBeDefined();
|
|
1363
|
+
expect(response.body.registrations.length).toBe(1);
|
|
1364
|
+
|
|
1365
|
+
// should not update status
|
|
1366
|
+
const updatedMember = await Member.getByID(member.id);
|
|
1367
|
+
expect(updatedMember!.details.uitpasNumberDetails?.uitpasNumber).toEqual('0900000031618');
|
|
1368
|
+
expect(updatedMember!.details.uitpasNumberDetails?.socialTariff?.status).toEqual(UitpasSocialTariffStatus.Active);
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
test('should not apply reduced price if social tariff status is unknown and uitpas api is unavailable', async () => {
|
|
1372
|
+
const mocker = initUitpasApi();
|
|
1373
|
+
mocker.forceFailure();
|
|
1374
|
+
|
|
1375
|
+
const { member, group, groupPrice, organization, token } = await initData();
|
|
1376
|
+
member.details.uitpasNumberDetails = UitpasNumberDetails.create({
|
|
1377
|
+
// active
|
|
1378
|
+
uitpasNumber: '0900011354829',
|
|
1379
|
+
socialTariff: UitpasSocialTariff.create({
|
|
1380
|
+
// but last time checked it was active
|
|
1381
|
+
status: UitpasSocialTariffStatus.Unknown,
|
|
1382
|
+
updatedAt: new Date(2000, 0, 1),
|
|
1383
|
+
endDate: new Date(2000, 0, 1),
|
|
1384
|
+
}),
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
await member.save();
|
|
1388
|
+
|
|
1389
|
+
const body = IDRegisterCheckout.create({
|
|
1390
|
+
cart: IDRegisterCart.create({
|
|
1391
|
+
items: [
|
|
1392
|
+
IDRegisterItem.create({
|
|
1393
|
+
id: uuidv4(),
|
|
1394
|
+
replaceRegistrationIds: [],
|
|
1395
|
+
options: [],
|
|
1396
|
+
groupPrice,
|
|
1397
|
+
organizationId: organization.id,
|
|
1398
|
+
groupId: group.id,
|
|
1399
|
+
memberId: member.id,
|
|
1400
|
+
}),
|
|
1401
|
+
],
|
|
1402
|
+
balanceItems: [],
|
|
1403
|
+
deleteRegistrationIds: [],
|
|
1404
|
+
}),
|
|
1405
|
+
administrationFee: 0,
|
|
1406
|
+
freeContribution: 0,
|
|
1407
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1408
|
+
// normal price
|
|
1409
|
+
totalPrice: 25_0000,
|
|
1410
|
+
customer: null,
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
// act
|
|
1414
|
+
const response = await post(body, organization, token);
|
|
1415
|
+
|
|
1416
|
+
// assert
|
|
1417
|
+
expect(response.body).toBeDefined();
|
|
1418
|
+
expect(response.body.registrations.length).toBe(1);
|
|
1419
|
+
|
|
1420
|
+
// should not update status
|
|
1421
|
+
const updatedMember = await Member.getByID(member.id);
|
|
1422
|
+
expect(updatedMember!.details.uitpasNumberDetails?.uitpasNumber).toEqual('0900011354829');
|
|
1423
|
+
expect(updatedMember!.details.uitpasNumberDetails?.socialTariff?.status).toEqual(UitpasSocialTariffStatus.Unknown);
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
test('should apply reduced price based on legacy rule if uitpas api is not connected and status is unkown', async () => {
|
|
1427
|
+
TestUtils.setEnvironment('UITPAS_API_CLIENT_SECRET', undefined);
|
|
1428
|
+
|
|
1429
|
+
const { member, group, groupPrice, organization, token } = await initData();
|
|
1430
|
+
member.details.uitpasNumberDetails = UitpasNumberDetails.create({
|
|
1431
|
+
// active
|
|
1432
|
+
uitpasNumber: '0900011354819', // Second last number is 1, so has social tariffs in legacy mode
|
|
1433
|
+
socialTariff: UitpasSocialTariff.create({
|
|
1434
|
+
// but last time checked it was active
|
|
1435
|
+
status: UitpasSocialTariffStatus.Unknown,
|
|
1436
|
+
updatedAt: new Date(2000, 0, 1),
|
|
1437
|
+
endDate: new Date(2000, 0, 1),
|
|
1438
|
+
}),
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
await member.save();
|
|
1442
|
+
|
|
1443
|
+
const body = IDRegisterCheckout.create({
|
|
1444
|
+
cart: IDRegisterCart.create({
|
|
1445
|
+
items: [
|
|
1446
|
+
IDRegisterItem.create({
|
|
1447
|
+
id: uuidv4(),
|
|
1448
|
+
replaceRegistrationIds: [],
|
|
1449
|
+
options: [],
|
|
1450
|
+
groupPrice,
|
|
1451
|
+
organizationId: organization.id,
|
|
1452
|
+
groupId: group.id,
|
|
1453
|
+
memberId: member.id,
|
|
1454
|
+
}),
|
|
1455
|
+
],
|
|
1456
|
+
balanceItems: [],
|
|
1457
|
+
deleteRegistrationIds: [],
|
|
1458
|
+
}),
|
|
1459
|
+
administrationFee: 0,
|
|
1460
|
+
freeContribution: 0,
|
|
1461
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1462
|
+
// normal price
|
|
1463
|
+
totalPrice: 12_5000,
|
|
1464
|
+
customer: null,
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
// act
|
|
1468
|
+
const response = await post(body, organization, token);
|
|
1469
|
+
|
|
1470
|
+
// assert
|
|
1471
|
+
expect(response.body).toBeDefined();
|
|
1472
|
+
expect(response.body.registrations.length).toBe(1);
|
|
1473
|
+
|
|
1474
|
+
// should not update status
|
|
1475
|
+
const updatedMember = await Member.getByID(member.id);
|
|
1476
|
+
expect(updatedMember!.details.uitpasNumberDetails?.uitpasNumber).toEqual('0900011354819');
|
|
1477
|
+
expect(updatedMember!.details.uitpasNumberDetails?.socialTariff?.status).toEqual(UitpasSocialTariffStatus.Unknown);
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
test('should not apply reduced price based on legacy rule if uitpas api is not connected and status is unkown', async () => {
|
|
1481
|
+
TestUtils.setEnvironment('UITPAS_API_CLIENT_SECRET', undefined);
|
|
1482
|
+
|
|
1483
|
+
const { member, group, groupPrice, organization, token } = await initData();
|
|
1484
|
+
member.details.uitpasNumberDetails = UitpasNumberDetails.create({
|
|
1485
|
+
// active
|
|
1486
|
+
uitpasNumber: '0900011354829', // Second last number isnot 1, so does not have social tariffs in legacy mode
|
|
1487
|
+
socialTariff: UitpasSocialTariff.create({
|
|
1488
|
+
// but last time checked it was active
|
|
1489
|
+
status: UitpasSocialTariffStatus.Unknown,
|
|
1490
|
+
updatedAt: new Date(2000, 0, 1),
|
|
1491
|
+
endDate: new Date(2000, 0, 1),
|
|
1492
|
+
}),
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
await member.save();
|
|
1496
|
+
|
|
1497
|
+
const body = IDRegisterCheckout.create({
|
|
1498
|
+
cart: IDRegisterCart.create({
|
|
1499
|
+
items: [
|
|
1500
|
+
IDRegisterItem.create({
|
|
1501
|
+
id: uuidv4(),
|
|
1502
|
+
replaceRegistrationIds: [],
|
|
1503
|
+
options: [],
|
|
1504
|
+
groupPrice,
|
|
1505
|
+
organizationId: organization.id,
|
|
1506
|
+
groupId: group.id,
|
|
1507
|
+
memberId: member.id,
|
|
1508
|
+
}),
|
|
1509
|
+
],
|
|
1510
|
+
balanceItems: [],
|
|
1511
|
+
deleteRegistrationIds: [],
|
|
1512
|
+
}),
|
|
1513
|
+
administrationFee: 0,
|
|
1514
|
+
freeContribution: 0,
|
|
1515
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1516
|
+
// normal price
|
|
1517
|
+
totalPrice: 25_0000,
|
|
1518
|
+
customer: null,
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
// act
|
|
1522
|
+
const response = await post(body, organization, token);
|
|
1523
|
+
|
|
1524
|
+
// assert
|
|
1525
|
+
expect(response.body).toBeDefined();
|
|
1526
|
+
expect(response.body.registrations.length).toBe(1);
|
|
1527
|
+
|
|
1528
|
+
// should not update status
|
|
1529
|
+
const updatedMember = await Member.getByID(member.id);
|
|
1530
|
+
expect(updatedMember!.details.uitpasNumberDetails?.uitpasNumber).toEqual('0900011354829');
|
|
1531
|
+
expect(updatedMember!.details.uitpasNumberDetails?.socialTariff?.status).toEqual(UitpasSocialTariffStatus.Unknown);
|
|
1532
|
+
});
|
|
1533
|
+
});
|
|
1251
1534
|
});
|
|
1252
1535
|
|
|
1253
1536
|
describe('Register as organization', () => {
|
|
@@ -13,6 +13,7 @@ import { BuckarooHelper } from '../../../helpers/BuckarooHelper.js';
|
|
|
13
13
|
import { Context } from '../../../helpers/Context.js';
|
|
14
14
|
import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper.js';
|
|
15
15
|
import { StripeHelper } from '../../../helpers/StripeHelper.js';
|
|
16
|
+
import { updateMemberDetailsUitpasNumber } from '../../../helpers/updateMemberDetailsUitpasNumber.js';
|
|
16
17
|
import { BalanceItemService } from '../../../services/BalanceItemService.js';
|
|
17
18
|
import { PaymentService } from '../../../services/PaymentService.js';
|
|
18
19
|
import { RegistrationService } from '../../../services/RegistrationService.js';
|
|
@@ -167,6 +168,8 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
const isBulkUpdate = members.length > 5;
|
|
172
|
+
|
|
170
173
|
for (const member of members) {
|
|
171
174
|
if (!await Context.auth.canAccessMember(
|
|
172
175
|
member,
|
|
@@ -179,6 +182,25 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
179
182
|
statusCode: 403,
|
|
180
183
|
});
|
|
181
184
|
}
|
|
185
|
+
|
|
186
|
+
// update uitpas social tariff
|
|
187
|
+
if (!isBulkUpdate && member.details.uitpasNumberDetails
|
|
188
|
+
&& member.details.uitpasNumberDetails.socialTariff.shouldUpdateForRegsitration(member.details.requiresFinancialSupport)
|
|
189
|
+
) {
|
|
190
|
+
let isUpdated: boolean = false;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
isUpdated = await updateMemberDetailsUitpasNumber(member.details);
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
// catch all errors
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (isUpdated) {
|
|
200
|
+
member.details.cleanData();
|
|
201
|
+
await member.save();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
182
204
|
}
|
|
183
205
|
|
|
184
206
|
const platformMembers: PlatformMember[] = [];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
import { Context } from '../../../../helpers/Context.js';
|
|
6
|
+
import { GetInvoicesEndpoint } from './GetInvoicesEndpoint.js';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, never>;
|
|
9
|
+
type Query = CountFilteredRequest;
|
|
10
|
+
type Body = undefined;
|
|
11
|
+
type ResponseBody = CountResponse;
|
|
12
|
+
|
|
13
|
+
export class GetInvoicesCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
+
queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
|
|
15
|
+
|
|
16
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
17
|
+
if (request.method !== 'GET') {
|
|
18
|
+
return [false];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const params = Endpoint.parseParameters(request.url, '/invoices/count', {});
|
|
22
|
+
|
|
23
|
+
if (params) {
|
|
24
|
+
return [true, params as Params];
|
|
25
|
+
}
|
|
26
|
+
return [false];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
|
+
await Context.setOrganizationScope();
|
|
31
|
+
await Context.authenticate();
|
|
32
|
+
const query = await GetInvoicesEndpoint.buildQuery(request.query);
|
|
33
|
+
|
|
34
|
+
const count = await query
|
|
35
|
+
.count();
|
|
36
|
+
|
|
37
|
+
return new Response(
|
|
38
|
+
CountResponse.create({
|
|
39
|
+
count,
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import { Invoice } from '@stamhoofd/models';
|
|
5
|
+
import { applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
|
|
6
|
+
import { CountFilteredRequest, InvoiceStruct, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
7
|
+
|
|
8
|
+
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
|
|
9
|
+
import { Context } from '../../../../helpers/Context.js';
|
|
10
|
+
import { invoiceFilterCompilers } from '../../../../sql-filters/invoices.js';
|
|
11
|
+
import { invoiceSorters } from '../../../../sql-sorters/invoices.js';
|
|
12
|
+
|
|
13
|
+
type Params = Record<string, never>;
|
|
14
|
+
type Query = LimitedFilteredRequest;
|
|
15
|
+
type Body = undefined;
|
|
16
|
+
type ResponseBody = PaginatedResponse<InvoiceStruct[], LimitedFilteredRequest>;
|
|
17
|
+
|
|
18
|
+
const filterCompilers = invoiceFilterCompilers;
|
|
19
|
+
const sorters = invoiceSorters;
|
|
20
|
+
|
|
21
|
+
export class GetInvoicesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
22
|
+
queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
|
|
23
|
+
|
|
24
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
25
|
+
if (request.method !== 'GET') {
|
|
26
|
+
return [false];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const params = Endpoint.parseParameters(request.url, '/invoices', {});
|
|
30
|
+
|
|
31
|
+
if (params) {
|
|
32
|
+
return [true, params as Params];
|
|
33
|
+
}
|
|
34
|
+
return [false];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
|
|
38
|
+
const organization = Context.organization;
|
|
39
|
+
let scopeFilter: StamhoofdFilter | undefined = undefined;
|
|
40
|
+
|
|
41
|
+
if (!organization) {
|
|
42
|
+
throw Context.auth.error();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!await Context.auth.canManageFinances(organization.id)) {
|
|
46
|
+
throw Context.auth.error();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
scopeFilter = {
|
|
50
|
+
organizationId: organization.id,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const query = Invoice
|
|
54
|
+
.select()
|
|
55
|
+
.setMaxExecutionTime(10 * 1000);
|
|
56
|
+
|
|
57
|
+
if (scopeFilter) {
|
|
58
|
+
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (q.filter) {
|
|
62
|
+
query.where(await compileToSQLFilter(q.filter, filterCompilers));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (q.search) {
|
|
66
|
+
// todo
|
|
67
|
+
|
|
68
|
+
let searchFilter: StamhoofdFilter | null = null;
|
|
69
|
+
searchFilter = {
|
|
70
|
+
$or: [
|
|
71
|
+
{
|
|
72
|
+
customer: {
|
|
73
|
+
name: {
|
|
74
|
+
$contains: q.search,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
customer: {
|
|
80
|
+
company: {
|
|
81
|
+
name: {
|
|
82
|
+
$contains: q.search,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
items: {
|
|
89
|
+
$elemMatch: {
|
|
90
|
+
$or: [
|
|
91
|
+
{
|
|
92
|
+
name: {
|
|
93
|
+
$contains: q.search,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
description: {
|
|
98
|
+
$contains: q.search,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (q.search.includes('@')) {
|
|
109
|
+
searchFilter = {
|
|
110
|
+
$or: [
|
|
111
|
+
{
|
|
112
|
+
customer: {
|
|
113
|
+
email: {
|
|
114
|
+
$contains: q.search,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
customer: {
|
|
120
|
+
company: {
|
|
121
|
+
administrationEmail: {
|
|
122
|
+
$contains: q.search,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (searchFilter) {
|
|
132
|
+
query.where(await compileToSQLFilter(searchFilter, filterCompilers));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (q instanceof LimitedFilteredRequest) {
|
|
137
|
+
if (q.pageFilter) {
|
|
138
|
+
query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
q.sort = assertSort(q.sort, [{ key: 'id' }]);
|
|
142
|
+
applySQLSorter(query, q.sort, sorters);
|
|
143
|
+
query.limit(q.limit);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return query;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static async buildData(requestQuery: LimitedFilteredRequest) {
|
|
150
|
+
const query = await this.buildQuery(requestQuery);
|
|
151
|
+
let invoices: Invoice[];
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
invoices = await query.fetch();
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (error.message.includes('ER_QUERY_TIMEOUT')) {
|
|
158
|
+
throw new SimpleError({
|
|
159
|
+
code: 'timeout',
|
|
160
|
+
message: 'Query took too long',
|
|
161
|
+
human: $t(`dce51638-6129-448b-8a15-e6d778f3a76a`),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let next: LimitedFilteredRequest | undefined;
|
|
168
|
+
|
|
169
|
+
if (invoices.length >= requestQuery.limit) {
|
|
170
|
+
const lastObject = invoices[invoices.length - 1];
|
|
171
|
+
const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
|
|
172
|
+
|
|
173
|
+
next = new LimitedFilteredRequest({
|
|
174
|
+
filter: requestQuery.filter,
|
|
175
|
+
pageFilter: nextFilter,
|
|
176
|
+
sort: requestQuery.sort,
|
|
177
|
+
limit: requestQuery.limit,
|
|
178
|
+
search: requestQuery.search,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
|
|
182
|
+
console.error('Found infinite loading loop for', requestQuery);
|
|
183
|
+
next = undefined;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return new PaginatedResponse<InvoiceStruct[], LimitedFilteredRequest>({
|
|
188
|
+
results: await AuthenticatedStructures.invoices(invoices),
|
|
189
|
+
next,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
194
|
+
await Context.setOrganizationScope();
|
|
195
|
+
await Context.authenticate();
|
|
196
|
+
|
|
197
|
+
const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
|
|
198
|
+
|
|
199
|
+
if (request.query.limit > maxLimit) {
|
|
200
|
+
throw new SimpleError({
|
|
201
|
+
code: 'invalid_field',
|
|
202
|
+
field: 'limit',
|
|
203
|
+
message: 'Limit can not be more than ' + maxLimit,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (request.query.limit < 1) {
|
|
208
|
+
throw new SimpleError({
|
|
209
|
+
code: 'invalid_field',
|
|
210
|
+
field: 'limit',
|
|
211
|
+
message: 'Limit can not be less than 1',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return new Response(
|
|
216
|
+
await GetInvoicesEndpoint.buildData(request.query),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -5,8 +5,8 @@ import { BalanceItem, Member, Order, User } from '@stamhoofd/models';
|
|
|
5
5
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
6
6
|
import { BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, PermissionLevel } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
|
-
import { Context } from '../../../../helpers/Context';
|
|
9
|
-
import { BalanceItemService } from '../../../../services/BalanceItemService';
|
|
8
|
+
import { Context } from '../../../../helpers/Context.js';
|
|
9
|
+
import { BalanceItemService } from '../../../../services/BalanceItemService.js';
|
|
10
10
|
|
|
11
11
|
type Params = Record<string, never>;
|
|
12
12
|
type Query = undefined;
|