@stamhoofd/backend 2.89.1 → 2.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -11
- package/src/boot.ts +2 -0
- package/src/crons/balance-emails.ts +1 -6
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +1 -1
- package/src/endpoints/admin/organizations/SearchUitpasOrganizersEndpoint.ts +42 -0
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +4 -4
- package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +3 -3
- package/src/endpoints/global/events/GetEventsEndpoint.ts +2 -2
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +23 -2
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +6 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +8 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
- package/src/endpoints/global/platform/GetPlatformEndpoint.ts +1 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +10 -8
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +11 -0
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +3 -6
- package/src/endpoints/organization/dashboard/organization/GetUitpasClientIdEndpoint.ts +38 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +31 -1
- package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +108 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -2
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +9 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +2 -9
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +1 -7
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +68 -1
- package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +27 -20
- package/src/helpers/AdminPermissionChecker.ts +129 -22
- package/src/helpers/AuthenticatedStructures.ts +13 -10
- package/src/helpers/Context.ts +1 -1
- package/src/helpers/UitpasTokenRepository.ts +125 -35
- package/src/helpers/ViesHelper.ts +2 -1
- package/src/seeds/0000000002-clear-stamhoofd-email-templates.ts +13 -0
- package/src/seeds/0000000003-default-email-templates.ts +20 -0
- package/src/seeds/data/default-email-templates.sql +55 -0
- package/src/services/RegistrationService.ts +6 -4
- package/src/services/uitpas/UitpasService.test.ts +23 -0
- package/src/services/uitpas/UitpasService.ts +222 -0
- package/src/services/uitpas/checkPermissionsFor.ts +111 -0
- package/src/services/uitpas/checkUitpasNumbers.ts +180 -0
- package/src/services/uitpas/getSocialTariffForEvent.ts +90 -0
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +181 -0
- package/src/services/uitpas/searchUitpasOrganizers.ts +93 -0
- package/src/sql-filters/audit-logs.ts +26 -6
- package/src/sql-filters/balance-item-payments.ts +23 -8
- package/src/sql-filters/base-registration-filter-compilers.ts +74 -23
- package/src/sql-filters/documents.ts +46 -13
- package/src/sql-filters/event-notifications.ts +48 -12
- package/src/sql-filters/events.ts +62 -26
- package/src/sql-filters/groups.ts +12 -12
- package/src/sql-filters/members.ts +325 -137
- package/src/sql-filters/orders.ts +96 -48
- package/src/sql-filters/organization-registration-periods.ts +16 -4
- package/src/sql-filters/organizations.ts +105 -99
- package/src/sql-filters/payments.ts +97 -47
- package/src/sql-filters/receivable-balances.ts +56 -19
- package/src/sql-filters/registration-periods.ts +16 -4
- package/src/sql-filters/registrations.ts +2 -2
- package/src/sql-filters/shared/EmailRelationFilterCompilers.ts +14 -8
- package/src/sql-filters/tickets.ts +26 -6
- package/tests/e2e/charge-members.test.ts +1 -0
- package/src/helpers/UitpasNumberValidator.test.ts +0 -23
- package/src/helpers/UitpasNumberValidator.ts +0 -185
package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { AutoEncoderPatchType, Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import { UitpasClientCredentialsStatus, UitpasClientIdAndSecret, UitpasSetClientCredentialsResponse } from '@stamhoofd/structures';
|
|
5
|
+
import { Context } from '../../../../helpers/Context';
|
|
6
|
+
import { UitpasService } from '../../../../services/uitpas/UitpasService';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, never>;
|
|
9
|
+
type Query = undefined;
|
|
10
|
+
type Body = AutoEncoderPatchType<UitpasClientIdAndSecret>;
|
|
11
|
+
type ResponseBody = UitpasSetClientCredentialsResponse;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export class SetUitpasClientCredentialsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
18
|
+
bodyDecoder = UitpasClientIdAndSecret.patchType() as Decoder<AutoEncoderPatchType<UitpasClientIdAndSecret>>;
|
|
19
|
+
|
|
20
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
21
|
+
if (request.method !== 'POST') {
|
|
22
|
+
return [false];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const params = Endpoint.parseParameters(request.url, '/organization/uitpas-client-credentials', {});
|
|
26
|
+
|
|
27
|
+
if (params) {
|
|
28
|
+
return [true, params as Params];
|
|
29
|
+
}
|
|
30
|
+
return [false];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
34
|
+
const organization = await Context.setOrganizationScope();
|
|
35
|
+
await Context.authenticate();
|
|
36
|
+
|
|
37
|
+
if (!await Context.auth.hasFullAccess(organization.id)) {
|
|
38
|
+
throw Context.auth.error();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (request.body.clientId === '' && request.body.clientSecret === '') {
|
|
42
|
+
// clear
|
|
43
|
+
await UitpasService.clearClientCredentialsFor(organization.id);
|
|
44
|
+
organization.meta.uitpasClientCredentialsStatus = UitpasClientCredentialsStatus.NotConfigured;
|
|
45
|
+
await organization.save();
|
|
46
|
+
const resp = new UitpasSetClientCredentialsResponse();
|
|
47
|
+
resp.status = UitpasClientCredentialsStatus.NotConfigured;
|
|
48
|
+
return new Response(resp);
|
|
49
|
+
}
|
|
50
|
+
if (!request.body.clientId) {
|
|
51
|
+
throw new SimpleError({
|
|
52
|
+
message: 'You must provide a client id',
|
|
53
|
+
code: 'missing_client_id',
|
|
54
|
+
human: $t('Je moet een client id opgeven.'),
|
|
55
|
+
field: 'clientId',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!request.body.clientSecret) {
|
|
59
|
+
throw new SimpleError({
|
|
60
|
+
message: 'You must provide a client secret',
|
|
61
|
+
code: 'missing_client_secret',
|
|
62
|
+
human: $t('Je moet een client secret opgeven.'),
|
|
63
|
+
field: 'clientSecret',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (request.body.clientSecret === UitpasClientIdAndSecret.placeholderClientSecret && (await UitpasService.getClientIdFor(organization.id)) !== request.body.clientId) {
|
|
67
|
+
throw new SimpleError({
|
|
68
|
+
message: 'You cannot use the placeholder client secret for a different client id',
|
|
69
|
+
code: 'invalid_client_secret',
|
|
70
|
+
human: $t('Je kan niet enkel de client id wijzigen. Geef ook de client secret in.'),
|
|
71
|
+
field: 'clientSecret',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const reEvaluation = request.body.clientSecret === UitpasClientIdAndSecret.placeholderClientSecret;
|
|
75
|
+
|
|
76
|
+
if (!organization.meta.uitpasOrganizerId) {
|
|
77
|
+
throw new SimpleError({
|
|
78
|
+
message: 'This organization does not have a uitpas organizer id set',
|
|
79
|
+
code: 'missing_uitpas_organizer_id',
|
|
80
|
+
human: $t('Stel eerst een UiTPAS-organisator in.'),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// store the client credentials and store new status in one operation
|
|
85
|
+
if (!reEvaluation) {
|
|
86
|
+
const valid = await UitpasService.storeIfValid(organization.id, request.body.clientId, request.body.clientSecret);
|
|
87
|
+
if (!valid) {
|
|
88
|
+
throw new SimpleError({
|
|
89
|
+
message: 'The provided client credentials are not valid',
|
|
90
|
+
code: 'invalid_client_credentials',
|
|
91
|
+
human: $t('De opgegeven client credentials zijn niet geldig.'),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
organization.meta.uitpasClientCredentialsStatus = UitpasClientCredentialsStatus.NotChecked;
|
|
96
|
+
await organization.save();
|
|
97
|
+
|
|
98
|
+
// now we update the status (but if this fails, the status will safely remain NOT_CHECKED)
|
|
99
|
+
const { status, human } = await UitpasService.checkPermissionsFor(organization.id, organization.meta.uitpasOrganizerId);
|
|
100
|
+
organization.meta.uitpasClientCredentialsStatus = status;
|
|
101
|
+
await organization.save(); // save the organization to update the status
|
|
102
|
+
|
|
103
|
+
const resp = new UitpasSetClientCredentialsResponse();
|
|
104
|
+
resp.status = status;
|
|
105
|
+
resp.human = human;
|
|
106
|
+
return new Response(resp);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -2,7 +2,7 @@ import { Decoder } from '@simonbackx/simple-encoding';
|
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
4
|
import { Payment } from '@stamhoofd/models';
|
|
5
|
-
import { SQL,
|
|
5
|
+
import { SQL, applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
|
|
6
6
|
import { CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, PaymentGeneral, StamhoofdFilter, TransferSettings, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts
CHANGED
|
@@ -2,14 +2,13 @@ import { Decoder } from '@simonbackx/simple-encoding';
|
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
4
|
import { CachedBalance } from '@stamhoofd/models';
|
|
5
|
-
import {
|
|
5
|
+
import { applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
|
|
6
6
|
import { assertSort, CountFilteredRequest, DetailedReceivableBalance, getSortFilter, LimitedFilteredRequest, PaginatedResponse, ReceivableBalance as ReceivableBalanceStruct, StamhoofdFilter } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
9
9
|
import { Context } from '../../../../helpers/Context';
|
|
10
10
|
import { receivableBalanceFilterCompilers } from '../../../../sql-filters/receivable-balances';
|
|
11
11
|
import { receivableBalanceSorters } from '../../../../sql-sorters/receivable-balances';
|
|
12
|
-
import { BalanceItemService } from '../../../../services/BalanceItemService';
|
|
13
12
|
|
|
14
13
|
type Params = Record<string, never>;
|
|
15
14
|
type Query = LimitedFilteredRequest;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { assertSort, CountFilteredRequest, getSortFilter, LimitedFilteredRequest, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PaginatedResponse, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
3
|
|
|
4
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
4
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
6
|
import { OrganizationRegistrationPeriod } from '@stamhoofd/models';
|
|
6
7
|
import { applySQLSorter, compileToSQLFilter, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
@@ -8,7 +9,6 @@ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStruct
|
|
|
8
9
|
import { Context } from '../../../../helpers/Context';
|
|
9
10
|
import { organizationRegistrationPeriodFilterCompilers } from '../../../../sql-filters/organization-registration-periods';
|
|
10
11
|
import { organizationRegistrationPeriodSorters } from '../../../../sql-sorters/organization-registration-periods';
|
|
11
|
-
import { Decoder } from '@simonbackx/simple-encoding';
|
|
12
12
|
|
|
13
13
|
type Params = Record<string, never>;
|
|
14
14
|
type Query = LimitedFilteredRequest;
|
|
@@ -3,7 +3,7 @@ import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegi
|
|
|
3
3
|
|
|
4
4
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
-
import { Group, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
6
|
+
import { Event, Group, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
7
7
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
9
|
import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater';
|
|
@@ -212,6 +212,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
212
212
|
|
|
213
213
|
for (const groupPut of patch.groups.getPuts()) {
|
|
214
214
|
shouldUpdateSetupSteps = true;
|
|
215
|
+
groupPut.put.settings.throwIfInvalidPrices();
|
|
215
216
|
const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(groupPut.put, organization.id, period, { allowedIds });
|
|
216
217
|
deleteUnreachable = true;
|
|
217
218
|
forceGroupIds.push(group.id);
|
|
@@ -349,6 +350,12 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
349
350
|
|
|
350
351
|
model.deletedAt = new Date();
|
|
351
352
|
await model.save();
|
|
353
|
+
|
|
354
|
+
const events = await Event.select().where('groupId', id).fetch();
|
|
355
|
+
for (const event of events) {
|
|
356
|
+
event.groupId = null;
|
|
357
|
+
await event.save();
|
|
358
|
+
}
|
|
352
359
|
}
|
|
353
360
|
|
|
354
361
|
static async patchGroup(struct: AutoEncoderPatchType<GroupStruct>, period?: RegistrationPeriod | null) {
|
|
@@ -479,6 +486,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
479
486
|
}
|
|
480
487
|
}
|
|
481
488
|
|
|
489
|
+
model.settings.throwIfInvalidPrices();
|
|
482
490
|
await model.updateOccupancy();
|
|
483
491
|
await model.save();
|
|
484
492
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
2
|
import { assertSort, CountFilteredRequest, getOrderSearchFilter, getSortFilter, LimitedFilteredRequest, PaginatedResponse, PrivateOrder, StamhoofdFilter } from '@stamhoofd/structures';
|
|
4
3
|
|
|
5
4
|
import { Order } from '@stamhoofd/models';
|
|
6
|
-
import {
|
|
5
|
+
import { applySQLSorter, compileToSQLFilter, SQL, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
6
|
+
|
|
7
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
7
8
|
import { parsePhoneNumber } from 'libphonenumber-js/max';
|
|
8
9
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
9
10
|
import { Context } from '../../../../helpers/Context';
|
|
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
3
3
|
import { Ticket } from '@stamhoofd/models';
|
|
4
4
|
import { assertSort, CountFilteredRequest, getSortFilter, LimitedFilteredRequest, PaginatedResponse, TicketPrivate } from '@stamhoofd/structures';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { applySQLSorter, compileToSQLFilter, SQL, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
7
7
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
9
|
import { LimitedFilteredRequestHelper } from '../../../../helpers/LimitedFilteredRequestHelper';
|
|
@@ -30,11 +30,11 @@ describe('Endpoint.GetWebshop', () => {
|
|
|
30
30
|
expect((response.body as any).privateMeta).toBeUndefined();
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
test('Allow access without organization scope
|
|
33
|
+
test('Allow access without organization scope', async () => {
|
|
34
34
|
const organization = await new OrganizationFactory({}).create();
|
|
35
35
|
const webshop = await new WebshopFactory({ organizationId: organization.id }).create();
|
|
36
36
|
|
|
37
|
-
const r = Request.buildJson('GET', '/
|
|
37
|
+
const r = Request.buildJson('GET', '/webshop/' + webshop.id);
|
|
38
38
|
|
|
39
39
|
const response = await testServer.test(endpoint, r);
|
|
40
40
|
expect(response.body).toBeDefined();
|
|
@@ -43,13 +43,6 @@ describe('Endpoint.GetWebshop', () => {
|
|
|
43
43
|
expect((response.body as any).privateMeta).toBeUndefined();
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
test('Do not allow access without organization scope in v244', async () => {
|
|
47
|
-
const organization = await new OrganizationFactory({}).create();
|
|
48
|
-
const webshop = await new WebshopFactory({ organizationId: organization.id }).create();
|
|
49
|
-
const r = Request.buildJson('GET', '/v244/webshop/' + webshop.id);
|
|
50
|
-
await expect(testServer.test(endpoint, r)).rejects.toThrow('Please specify the organization in the hostname');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
46
|
test('Get webshop as admin', async () => {
|
|
54
47
|
const organization = await new OrganizationFactory({}).create();
|
|
55
48
|
const user = await new UserFactory({
|
|
@@ -26,13 +26,7 @@ export class GetWebshopEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
29
|
-
|
|
30
|
-
await Context.setOptionalOrganizationScope();
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
await Context.setOrganizationScope();
|
|
34
|
-
}
|
|
35
|
-
|
|
29
|
+
await Context.setOptionalOrganizationScope(); // editing webshop from the admin panel -> no scope
|
|
36
30
|
await Context.optionalAuthenticate();
|
|
37
31
|
|
|
38
32
|
const webshop = await Webshop.getByID(request.params.id);
|
|
@@ -3,7 +3,7 @@ import { Decoder } from '@simonbackx/simple-encoding';
|
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { Email } from '@stamhoofd/email';
|
|
6
|
-
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
|
|
6
|
+
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode, WebshopUitpasNumber } from '@stamhoofd/models';
|
|
7
7
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
8
|
import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderData, OrderResponse, Order as OrderStruct, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, TranslatedString, Version, WebshopAuthType, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
9
9
|
import { Formatter } from '@stamhoofd/utility';
|
|
@@ -12,6 +12,7 @@ import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
|
12
12
|
import { Context } from '../../../helpers/Context';
|
|
13
13
|
import { StripeHelper } from '../../../helpers/StripeHelper';
|
|
14
14
|
import { AuditLogService } from '../../../services/AuditLogService';
|
|
15
|
+
import { UitpasService } from '../../../services/uitpas/UitpasService';
|
|
15
16
|
|
|
16
17
|
type Params = { id: string };
|
|
17
18
|
type Query = undefined;
|
|
@@ -132,6 +133,72 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
132
133
|
request.body.validate(webshopStruct, organization.meta, request.i18n, false, Context.user?.getStructure());
|
|
133
134
|
request.body.update(webshopStruct);
|
|
134
135
|
|
|
136
|
+
// UiTPAS numbers validation
|
|
137
|
+
const articlesWithUitpasSocialTariff = request.body.cart.items.filter(item => item.productPrice.uitpasBaseProductPriceId !== null);
|
|
138
|
+
for (const item of articlesWithUitpasSocialTariff) {
|
|
139
|
+
const uitpasNumbersOnly = item.uitpasNumbers.map(p => p.uitpasNumber);
|
|
140
|
+
|
|
141
|
+
// verify the amount of UiTPAS numbers
|
|
142
|
+
if (uitpasNumbersOnly.length !== item.amount) {
|
|
143
|
+
throw new SimpleError({
|
|
144
|
+
code: 'amount_of_uitpas_numbers_mismatch',
|
|
145
|
+
message: 'The number of UiTPAS numbers and items with UiTPAS social tariff does not match',
|
|
146
|
+
human: $t('6140c642-69b2-43d6-80ba-2af4915c5837'),
|
|
147
|
+
field: 'cart.items.uitpasNumbers',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// verify the UiTPAS numbers are unique (within the order)
|
|
152
|
+
if (uitpasNumbersOnly.length !== Formatter.uniqueArray(uitpasNumbersOnly).length) {
|
|
153
|
+
throw new SimpleError({
|
|
154
|
+
code: 'duplicate_uitpas_numbers',
|
|
155
|
+
message: 'Duplicate uitpas numbers used',
|
|
156
|
+
human: $t('d9ec27f3-dafa-41e8-bcfb-9da564a4a675'),
|
|
157
|
+
field: 'cart.items.uitpasNumbers',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// verify the UiTPAS numbers are not already used for this product
|
|
162
|
+
const hasBeenUsed = await WebshopUitpasNumber.areUitpasNumbersUsed(webshop.id, item.product.id, uitpasNumbersOnly);
|
|
163
|
+
if (hasBeenUsed) {
|
|
164
|
+
throw new SimpleError({
|
|
165
|
+
code: 'uitpas_number_already_used',
|
|
166
|
+
message: 'One or more uitpas numbers are already used',
|
|
167
|
+
human: $t('1ef059c2-e758-4cfa-bc2b-16a581029450'),
|
|
168
|
+
field: 'cart.items.uitpasNumbers',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// verify the UiTPAS numbers are valid for social tariff (static check + API call to UiTPAS)
|
|
173
|
+
if (item.product.uitpasEvent) {
|
|
174
|
+
const basePrice = item.product.prices.find(p => p.id === item.productPrice.uitpasBaseProductPriceId)?.price ?? 0;
|
|
175
|
+
const reducedPrices = await UitpasService.getSocialTariffForUitpasNumbers(organization.id, uitpasNumbersOnly, basePrice, item.product.uitpasEvent.url);
|
|
176
|
+
const expectedReducedPrices = item.uitpasNumbers.map(p => p.price);
|
|
177
|
+
if (reducedPrices.length < expectedReducedPrices.length) {
|
|
178
|
+
// should not happen
|
|
179
|
+
throw new SimpleError({
|
|
180
|
+
code: 'uitpas_social_tariff_price_mismatch',
|
|
181
|
+
message: 'UiTPAS wrong number of prices retruned',
|
|
182
|
+
human: $t('Het kansentarief voor sommige UiTPAS-nummers kon niet worden opgehaald.'),
|
|
183
|
+
field: 'cart.items.uitpasNumbers',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
for (let i = 0; i < expectedReducedPrices.length; i++) {
|
|
187
|
+
if (reducedPrices[i] !== expectedReducedPrices[i]) {
|
|
188
|
+
throw new SimpleError({
|
|
189
|
+
code: 'uitpas_social_tariff_price_mismatch',
|
|
190
|
+
message: 'UiTPAS social tariff have a different price',
|
|
191
|
+
human: $t('Het kansentarief voor deze UiTPAS is {correctPrice} in plaats van {orderPrice}.', { correctPrice: Formatter.price(reducedPrices[i]), orderPrice: Formatter.price(expectedReducedPrices[i]) }),
|
|
192
|
+
field: 'uitpasNumbers.' + i.toString(),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
await UitpasService.checkUitpasNumbers(uitpasNumbersOnly); // Throws if invalid
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
135
202
|
const order = new Order().setRelation(Order.webshop, webshop);
|
|
136
203
|
order.data = request.body; // TODO: validate
|
|
137
204
|
order.organizationId = organization.id;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import {
|
|
2
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
3
|
import { UitpasPriceCheckRequest, UitpasPriceCheckResponse } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
|
-
import { UitpasNumberValidator } from '../../../helpers/UitpasNumberValidator';
|
|
6
5
|
import { Decoder } from '@simonbackx/simple-encoding';
|
|
6
|
+
import { UitpasService } from '../../../services/uitpas/UitpasService';
|
|
7
|
+
import { Context } from '../../../helpers/Context';
|
|
7
8
|
type Params = Record<string, never>;
|
|
8
9
|
type Query = undefined;
|
|
9
10
|
type Body = UitpasPriceCheckRequest;
|
|
@@ -26,30 +27,44 @@ export class RetrieveUitpasSocialTariffPricesEndpoint extends Endpoint<Params, Q
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
29
|
-
if (request.body.
|
|
30
|
+
if (request.body.uitpasEventUrl) {
|
|
30
31
|
// OFFICIAL FLOW
|
|
31
32
|
if (!request.body.uitpasNumbers) {
|
|
32
33
|
// STATIC CHECK
|
|
33
34
|
// request shouldn't include a reduced price
|
|
35
|
+
|
|
36
|
+
// this call should be authenticated, as it is done from the webshop settings
|
|
37
|
+
const organization = await Context.setOrganizationScope({ willAuthenticate: true });
|
|
38
|
+
await Context.authenticate();
|
|
39
|
+
const reducedPrice = await UitpasService.getSocialTariffForEvent(
|
|
40
|
+
organization.id,
|
|
41
|
+
request.body.basePrice,
|
|
42
|
+
request.body.uitpasEventUrl,
|
|
43
|
+
);
|
|
44
|
+
const uitpasPriceCheckResponse = UitpasPriceCheckResponse.create({
|
|
45
|
+
prices: [reducedPrice], // Convert to cents
|
|
46
|
+
});
|
|
47
|
+
return new Response(uitpasPriceCheckResponse);
|
|
34
48
|
}
|
|
35
49
|
else {
|
|
36
50
|
// OFFICIAL FLOW with an UiTPAS number
|
|
37
51
|
// request should include a reduced price (estimate by the frontend)
|
|
52
|
+
const organization = await Context.setOrganizationScope({ willAuthenticate: false });
|
|
53
|
+
const reducedPrices = await UitpasService.getSocialTariffForUitpasNumbers(organization.id, request.body.uitpasNumbers, request.body.basePrice, request.body.uitpasEventUrl); // Throws if invalid
|
|
54
|
+
const uitpasPriceCheckResponse = UitpasPriceCheckResponse.create({
|
|
55
|
+
prices: reducedPrices,
|
|
56
|
+
});
|
|
57
|
+
return new Response(uitpasPriceCheckResponse);
|
|
38
58
|
}
|
|
39
|
-
throw new SimpleError({
|
|
40
|
-
code: 'not_implemented',
|
|
41
|
-
message: 'Official flow not yet implemented',
|
|
42
|
-
human: 'De officiële flow voor het valideren van een UiTPAS-nummer wordt nog niet ondersteund.',
|
|
43
|
-
});
|
|
44
59
|
}
|
|
45
60
|
else {
|
|
46
61
|
// NON-OFFICIAL FLOW
|
|
47
62
|
// request should include UiTPAS-numbers, reduced price AND base price
|
|
48
|
-
if (
|
|
63
|
+
if (request.body.reducedPrice === null) {
|
|
49
64
|
throw new SimpleError({
|
|
50
65
|
code: 'missing_reduced_price',
|
|
51
66
|
message: 'Reduced price must be provided for non-official flow.',
|
|
52
|
-
human: $t('
|
|
67
|
+
human: $t('c66d114d-2ef3-476f-ad00-98fbe3195365'),
|
|
53
68
|
});
|
|
54
69
|
}
|
|
55
70
|
const reducedPrice = request.body.reducedPrice;
|
|
@@ -57,18 +72,10 @@ export class RetrieveUitpasSocialTariffPricesEndpoint extends Endpoint<Params, Q
|
|
|
57
72
|
throw new SimpleError({
|
|
58
73
|
code: 'missing_uitpas_numbers',
|
|
59
74
|
message: 'Uitpas numbers must be provided for non-official flow.',
|
|
60
|
-
human: $t('
|
|
75
|
+
human: $t('f792eda7-03b9-465d-807d-3d08ba148c8b'),
|
|
61
76
|
});
|
|
62
77
|
}
|
|
63
|
-
|
|
64
|
-
await UitpasNumberValidator.checkUitpasNumbers(request.body.uitpasNumbers); // Throws if invalid
|
|
65
|
-
}
|
|
66
|
-
catch (e) {
|
|
67
|
-
if (isSimpleError(e) || isSimpleErrors(e)) {
|
|
68
|
-
e.addNamespace('uitpasNumbers');
|
|
69
|
-
}
|
|
70
|
-
throw e;
|
|
71
|
-
}
|
|
78
|
+
await UitpasService.checkUitpasNumbers(request.body.uitpasNumbers); // Throws if invalid
|
|
72
79
|
const uitpasPriceCheckResponse = UitpasPriceCheckResponse.create({
|
|
73
80
|
prices: request.body.uitpasNumbers.map(_ => reducedPrice), // All reduced prices are the same in this non-official flow
|
|
74
81
|
});
|