@stamhoofd/backend 2.89.2 → 2.90.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/package.json +12 -11
  2. package/src/boot.ts +2 -0
  3. package/src/crons/balance-emails.ts +1 -6
  4. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +1 -1
  5. package/src/endpoints/admin/organizations/SearchUitpasOrganizersEndpoint.ts +42 -0
  6. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +4 -4
  7. package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +3 -3
  8. package/src/endpoints/global/events/GetEventsEndpoint.ts +2 -2
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +23 -2
  10. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +6 -6
  11. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +8 -6
  12. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
  13. package/src/endpoints/global/platform/GetPlatformEndpoint.ts +1 -0
  14. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +10 -8
  15. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +11 -0
  16. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +2 -2
  17. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +3 -6
  18. package/src/endpoints/organization/dashboard/organization/GetUitpasClientIdEndpoint.ts +38 -0
  19. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +31 -1
  20. package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +108 -0
  21. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +1 -1
  22. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -2
  23. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +1 -1
  24. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +9 -1
  25. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +3 -2
  26. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +1 -1
  27. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +2 -9
  28. package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +1 -7
  29. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +68 -1
  30. package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +27 -20
  31. package/src/helpers/AdminPermissionChecker.ts +129 -22
  32. package/src/helpers/AuthenticatedStructures.ts +13 -10
  33. package/src/helpers/Context.ts +1 -1
  34. package/src/helpers/UitpasTokenRepository.ts +125 -35
  35. package/src/helpers/ViesHelper.ts +2 -1
  36. package/src/seeds/0000000002-clear-stamhoofd-email-templates.ts +13 -0
  37. package/src/seeds/0000000003-default-email-templates.ts +20 -0
  38. package/src/seeds/data/default-email-templates.sql +55 -0
  39. package/src/services/RegistrationService.ts +6 -4
  40. package/src/services/uitpas/UitpasService.test.ts +23 -0
  41. package/src/services/uitpas/UitpasService.ts +222 -0
  42. package/src/services/uitpas/checkPermissionsFor.ts +111 -0
  43. package/src/services/uitpas/checkUitpasNumbers.ts +180 -0
  44. package/src/services/uitpas/getSocialTariffForEvent.ts +90 -0
  45. package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +181 -0
  46. package/src/services/uitpas/searchUitpasOrganizers.ts +93 -0
  47. package/src/sql-filters/audit-logs.ts +26 -6
  48. package/src/sql-filters/balance-item-payments.ts +23 -8
  49. package/src/sql-filters/base-registration-filter-compilers.ts +74 -23
  50. package/src/sql-filters/documents.ts +46 -13
  51. package/src/sql-filters/event-notifications.ts +48 -12
  52. package/src/sql-filters/events.ts +62 -26
  53. package/src/sql-filters/groups.ts +12 -12
  54. package/src/sql-filters/members.ts +325 -137
  55. package/src/sql-filters/orders.ts +96 -48
  56. package/src/sql-filters/organization-registration-periods.ts +16 -4
  57. package/src/sql-filters/organizations.ts +105 -99
  58. package/src/sql-filters/payments.ts +97 -47
  59. package/src/sql-filters/receivable-balances.ts +56 -19
  60. package/src/sql-filters/registration-periods.ts +16 -4
  61. package/src/sql-filters/registrations.ts +2 -2
  62. package/src/sql-filters/shared/EmailRelationFilterCompilers.ts +14 -8
  63. package/src/sql-filters/tickets.ts +26 -6
  64. package/tests/e2e/charge-members.test.ts +1 -0
  65. package/src/helpers/UitpasNumberValidator.test.ts +0 -23
  66. package/src/helpers/UitpasNumberValidator.ts +0 -185
@@ -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, compileToSQLFilter, applySQLSorter } from '@stamhoofd/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';
@@ -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 { compileToSQLFilter, applySQLSorter } from '@stamhoofd/sql';
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 { compileToSQLFilter, applySQLSorter, SQL, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
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 { compileToSQLFilter, applySQLSorter, SQL, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
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 in old v243', async () => {
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', '/v243/webshop/' + webshop.id);
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
- if (Context.version < 244) {
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 { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
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.uitpasEventId) {
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 (!request.body.reducedPrice) {
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('Je moet een verlaagd tarief opgeven voor de UiTPAS.'),
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('Je moet UiTPAS-nummers opgeven.'),
75
+ human: $t('f792eda7-03b9-465d-807d-3d08ba148c8b'),
61
76
  });
62
77
  }
63
- try {
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
  });