@stamhoofd/backend 2.90.3 → 2.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -10
- package/src/audit-logs/EmailLogger.ts +4 -4
- package/src/audit-logs/ModelLogger.ts +0 -1
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +20 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +30 -7
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +207 -0
- package/src/endpoints/global/email/GetEmailEndpoint.ts +5 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +404 -8
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +67 -22
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +6 -4
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -7
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +112 -105
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +10 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
- package/src/endpoints/organization/dashboard/webshops/SearchUitpasEventsEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
- package/src/helpers/AdminPermissionChecker.ts +81 -10
- package/src/helpers/FlagMomentCleanup.ts +13 -1
- package/src/helpers/GroupedThrottledQueue.ts +5 -3
- package/src/helpers/PeriodHelper.ts +10 -137
- package/src/helpers/SetupStepUpdater.ts +54 -7
- package/src/helpers/UitpasTokenRepository.ts +3 -3
- package/src/seeds/1750090030-records-configuration.ts +5 -1
- package/src/seeds/1752848560-groups-registration-periods.ts +768 -0
- package/src/seeds/1755181288-remove-duplicate-members.ts +145 -0
- package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
- package/src/services/BalanceItemService.ts +12 -7
- package/src/services/DocumentService.ts +0 -1
- package/src/services/RegistrationService.ts +30 -1
- package/src/services/uitpas/UitpasService.ts +72 -3
- package/src/services/uitpas/cancelTicketSales.ts +1 -1
- package/src/services/uitpas/checkPermissionsFor.ts +9 -9
- package/src/services/uitpas/checkUitpasNumbers.ts +3 -2
- package/src/services/uitpas/getSocialTariffForEvent.ts +4 -4
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +5 -5
- package/src/services/uitpas/registerTicketSales.ts +4 -4
- package/src/services/uitpas/searchUitpasEvents.ts +3 -3
- package/src/services/uitpas/searchUitpasOrganizers.ts +3 -3
- package/src/sql-filters/emails.ts +65 -0
- package/src/sql-filters/members.ts +1 -1
- package/src/sql-filters/organizations.ts +52 -0
- package/src/sql-sorters/emails.ts +47 -0
- package/tests/e2e/register.test.ts +1 -1
|
@@ -14,6 +14,7 @@ import { Context } from '../../../helpers/Context';
|
|
|
14
14
|
import { StripeHelper } from '../../../helpers/StripeHelper';
|
|
15
15
|
import { BalanceItemService } from '../../../services/BalanceItemService';
|
|
16
16
|
import { RegistrationService } from '../../../services/RegistrationService';
|
|
17
|
+
import { PaymentService } from '../../../services/PaymentService';
|
|
17
18
|
type Params = Record<string, never>;
|
|
18
19
|
type Query = undefined;
|
|
19
20
|
type Body = IDRegisterCheckout;
|
|
@@ -1047,120 +1048,126 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
1047
1048
|
|
|
1048
1049
|
let paymentUrl: string | null = null;
|
|
1049
1050
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
else if (payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) {
|
|
1062
|
-
if (!checkout.redirectUrl || !checkout.cancelUrl) {
|
|
1063
|
-
throw new Error('Should have been caught earlier');
|
|
1051
|
+
try {
|
|
1052
|
+
// Update balance items
|
|
1053
|
+
if (payment.method === PaymentMethod.Transfer) {
|
|
1054
|
+
// Send a small reminder email
|
|
1055
|
+
try {
|
|
1056
|
+
await Registration.sendTransferEmail(user, organization, payment);
|
|
1057
|
+
}
|
|
1058
|
+
catch (e) {
|
|
1059
|
+
console.error('Failed to send transfer email');
|
|
1060
|
+
console.error(e);
|
|
1061
|
+
}
|
|
1064
1062
|
}
|
|
1063
|
+
else if (payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) {
|
|
1064
|
+
if (!checkout.redirectUrl || !checkout.cancelUrl) {
|
|
1065
|
+
throw new Error('Should have been caught earlier');
|
|
1066
|
+
}
|
|
1065
1067
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
});
|
|
1100
|
-
paymentUrl = stripeResult.paymentUrl;
|
|
1101
|
-
}
|
|
1102
|
-
else if (payment.provider === PaymentProvider.Mollie) {
|
|
1103
|
-
// Mollie payment
|
|
1104
|
-
const token = await MollieToken.getTokenFor(organization.id);
|
|
1105
|
-
if (!token) {
|
|
1106
|
-
throw new SimpleError({
|
|
1107
|
-
code: '',
|
|
1108
|
-
message: $t(`b77e1f68-8928-42a2-802b-059fa73bedc3`, { method: PaymentMethodHelper.getName(payment.method) }),
|
|
1068
|
+
const _redirectUrl = new URL(checkout.redirectUrl);
|
|
1069
|
+
_redirectUrl.searchParams.set('paymentId', payment.id);
|
|
1070
|
+
_redirectUrl.searchParams.set('organizationId', organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
|
|
1071
|
+
|
|
1072
|
+
const _cancelUrl = new URL(checkout.cancelUrl);
|
|
1073
|
+
_cancelUrl.searchParams.set('paymentId', payment.id);
|
|
1074
|
+
_cancelUrl.searchParams.set('cancel', 'true');
|
|
1075
|
+
_cancelUrl.searchParams.set('organizationId', organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
|
|
1076
|
+
|
|
1077
|
+
const redirectUrl = _redirectUrl.href;
|
|
1078
|
+
const cancelUrl = _cancelUrl.href;
|
|
1079
|
+
|
|
1080
|
+
const webhookUrl = 'https://' + organization.getApiHost() + '/v' + Version + '/payments/' + encodeURIComponent(payment.id) + '?exchange=true';
|
|
1081
|
+
|
|
1082
|
+
if (payment.provider === PaymentProvider.Stripe) {
|
|
1083
|
+
const stripeResult = await StripeHelper.createPayment({
|
|
1084
|
+
payment,
|
|
1085
|
+
stripeAccount,
|
|
1086
|
+
redirectUrl,
|
|
1087
|
+
cancelUrl,
|
|
1088
|
+
statementDescriptor: organization.name,
|
|
1089
|
+
metadata: {
|
|
1090
|
+
organization: organization.id,
|
|
1091
|
+
user: user.id,
|
|
1092
|
+
payment: payment.id,
|
|
1093
|
+
},
|
|
1094
|
+
i18n: Context.i18n,
|
|
1095
|
+
lineItems: balanceItemPayments,
|
|
1096
|
+
organization,
|
|
1097
|
+
customer: {
|
|
1098
|
+
name: user.name ?? payMembers[0]?.details.name ?? $t(`bd1e59c8-3d4c-4097-ab35-0ce7b20d0e50`),
|
|
1099
|
+
email: user.email,
|
|
1100
|
+
},
|
|
1109
1101
|
});
|
|
1102
|
+
paymentUrl = stripeResult.paymentUrl;
|
|
1110
1103
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1104
|
+
else if (payment.provider === PaymentProvider.Mollie) {
|
|
1105
|
+
// Mollie payment
|
|
1106
|
+
const token = await MollieToken.getTokenFor(organization.id);
|
|
1107
|
+
if (!token) {
|
|
1108
|
+
throw new SimpleError({
|
|
1109
|
+
code: '',
|
|
1110
|
+
message: $t(`b77e1f68-8928-42a2-802b-059fa73bedc3`, { method: PaymentMethodHelper.getName(payment.method) }),
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
const profileId = organization.privateMeta.mollieProfile?.id ?? await token.getProfileId(organization.getHost());
|
|
1114
|
+
if (!profileId) {
|
|
1115
|
+
throw new SimpleError({
|
|
1116
|
+
code: '',
|
|
1117
|
+
message: $t(`5574469f-8eee-47fe-9fb6-1b097142ac75`, { method: PaymentMethodHelper.getName(payment.method) }),
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
|
|
1121
|
+
const locale = Context.i18n.locale.replace('-', '_');
|
|
1122
|
+
const molliePayment = await mollieClient.payments.create({
|
|
1123
|
+
amount: {
|
|
1124
|
+
currency: 'EUR',
|
|
1125
|
+
value: (totalPrice / 100).toFixed(2),
|
|
1126
|
+
},
|
|
1127
|
+
method: payment.method == PaymentMethod.Bancontact ? molliePaymentMethod.bancontact : (payment.method == PaymentMethod.iDEAL ? molliePaymentMethod.ideal : molliePaymentMethod.creditcard),
|
|
1128
|
+
testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
|
|
1129
|
+
profileId,
|
|
1130
|
+
description,
|
|
1131
|
+
redirectUrl,
|
|
1132
|
+
webhookUrl,
|
|
1133
|
+
metadata: {
|
|
1134
|
+
paymentId: payment.id,
|
|
1135
|
+
},
|
|
1136
|
+
locale: ['en_US', 'en_GB', 'nl_NL', 'nl_BE', 'fr_FR', 'fr_BE', 'de_DE', 'de_AT', 'de_CH', 'es_ES', 'ca_ES', 'pt_PT', 'it_IT', 'nb_NO', 'sv_SE', 'fi_FI', 'da_DK', 'is_IS', 'hu_HU', 'pl_PL', 'lv_LV', 'lt_LT'].includes(locale) ? (locale as any) : null,
|
|
1116
1137
|
});
|
|
1117
|
-
|
|
1118
|
-
const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
|
|
1119
|
-
const locale = Context.i18n.locale.replace('-', '_');
|
|
1120
|
-
const molliePayment = await mollieClient.payments.create({
|
|
1121
|
-
amount: {
|
|
1122
|
-
currency: 'EUR',
|
|
1123
|
-
value: (totalPrice / 100).toFixed(2),
|
|
1124
|
-
},
|
|
1125
|
-
method: payment.method == PaymentMethod.Bancontact ? molliePaymentMethod.bancontact : (payment.method == PaymentMethod.iDEAL ? molliePaymentMethod.ideal : molliePaymentMethod.creditcard),
|
|
1126
|
-
testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
|
|
1127
|
-
profileId,
|
|
1128
|
-
description,
|
|
1129
|
-
redirectUrl,
|
|
1130
|
-
webhookUrl,
|
|
1131
|
-
metadata: {
|
|
1132
|
-
paymentId: payment.id,
|
|
1133
|
-
},
|
|
1134
|
-
locale: ['en_US', 'en_GB', 'nl_NL', 'nl_BE', 'fr_FR', 'fr_BE', 'de_DE', 'de_AT', 'de_CH', 'es_ES', 'ca_ES', 'pt_PT', 'it_IT', 'nb_NO', 'sv_SE', 'fi_FI', 'da_DK', 'is_IS', 'hu_HU', 'pl_PL', 'lv_LV', 'lt_LT'].includes(locale) ? (locale as any) : null,
|
|
1135
|
-
});
|
|
1136
|
-
paymentUrl = molliePayment.getCheckoutUrl();
|
|
1138
|
+
paymentUrl = molliePayment.getCheckoutUrl();
|
|
1137
1139
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1140
|
+
// Save payment
|
|
1141
|
+
const dbPayment = new MolliePayment();
|
|
1142
|
+
dbPayment.paymentId = payment.id;
|
|
1143
|
+
dbPayment.mollieId = molliePayment.id;
|
|
1144
|
+
await dbPayment.save();
|
|
1145
|
+
}
|
|
1146
|
+
else if (payment.provider === PaymentProvider.Payconiq) {
|
|
1147
|
+
paymentUrl = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, webhookUrl);
|
|
1148
|
+
}
|
|
1149
|
+
else if (payment.provider == PaymentProvider.Buckaroo) {
|
|
1150
|
+
// Increase request timeout because buckaroo is super slow (in development)
|
|
1151
|
+
Context.request.request?.setTimeout(60 * 1000);
|
|
1152
|
+
const buckaroo = new BuckarooHelper(organization.privateMeta?.buckarooSettings?.key ?? '', organization.privateMeta?.buckarooSettings?.secret ?? '', organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production');
|
|
1153
|
+
const ip = Context.request.getIP();
|
|
1154
|
+
paymentUrl = await buckaroo.createPayment(payment, ip, description, redirectUrl, webhookUrl);
|
|
1155
|
+
await payment.save();
|
|
1156
|
+
|
|
1157
|
+
// TypeScript doesn't understand that the status can change and isn't a const....
|
|
1158
|
+
if ((payment.status as any) === PaymentStatus.Failed) {
|
|
1159
|
+
throw new SimpleError({
|
|
1160
|
+
code: 'payment_failed',
|
|
1161
|
+
message: $t(`b77e1f68-8928-42a2-802b-059fa73bedc3`, { method: PaymentMethodHelper.getName(payment.method) }),
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1161
1164
|
}
|
|
1162
1165
|
}
|
|
1163
1166
|
}
|
|
1167
|
+
catch (e) {
|
|
1168
|
+
await PaymentService.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
1169
|
+
throw e;
|
|
1170
|
+
}
|
|
1164
1171
|
|
|
1165
1172
|
return {
|
|
1166
1173
|
payment,
|
|
@@ -303,7 +303,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
303
303
|
throw new SimpleError({
|
|
304
304
|
code: 'invalid_field',
|
|
305
305
|
message: 'You cannot set the uitpasClientCredentialsStatus manually',
|
|
306
|
-
human: $t('
|
|
306
|
+
human: $t('d8937ba8-6689-4c76-9841-d5a00c99074b'),
|
|
307
307
|
});
|
|
308
308
|
}
|
|
309
309
|
|
package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts
CHANGED
|
@@ -51,7 +51,7 @@ export class SetUitpasClientCredentialsEndpoint extends Endpoint<Params, Query,
|
|
|
51
51
|
throw new SimpleError({
|
|
52
52
|
message: 'You must provide a client id',
|
|
53
53
|
code: 'missing_client_id',
|
|
54
|
-
human: $t('
|
|
54
|
+
human: $t('9b9ec483-63b8-4696-ade6-0eb18f9008e6'),
|
|
55
55
|
field: 'clientId',
|
|
56
56
|
});
|
|
57
57
|
}
|
|
@@ -59,7 +59,7 @@ export class SetUitpasClientCredentialsEndpoint extends Endpoint<Params, Query,
|
|
|
59
59
|
throw new SimpleError({
|
|
60
60
|
message: 'You must provide a client secret',
|
|
61
61
|
code: 'missing_client_secret',
|
|
62
|
-
human: $t('
|
|
62
|
+
human: $t('58de00fb-3b0a-45a6-9214-7d11b4175779'),
|
|
63
63
|
field: 'clientSecret',
|
|
64
64
|
});
|
|
65
65
|
}
|
|
@@ -67,7 +67,7 @@ export class SetUitpasClientCredentialsEndpoint extends Endpoint<Params, Query,
|
|
|
67
67
|
throw new SimpleError({
|
|
68
68
|
message: 'You cannot use the placeholder client secret for a different client id',
|
|
69
69
|
code: 'invalid_client_secret',
|
|
70
|
-
human: $t('
|
|
70
|
+
human: $t('bbc79280-7ae2-4b8d-a900-2d7cbb552428'),
|
|
71
71
|
field: 'clientSecret',
|
|
72
72
|
});
|
|
73
73
|
}
|
|
@@ -77,7 +77,7 @@ export class SetUitpasClientCredentialsEndpoint extends Endpoint<Params, Query,
|
|
|
77
77
|
throw new SimpleError({
|
|
78
78
|
message: 'This organization does not have a uitpas organizer id set',
|
|
79
79
|
code: 'missing_uitpas_organizer_id',
|
|
80
|
-
human: $t('
|
|
80
|
+
human: $t('80fcde9c-c8c7-4fe1-b9a6-51684a23d850'),
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -88,7 +88,7 @@ export class SetUitpasClientCredentialsEndpoint extends Endpoint<Params, Query,
|
|
|
88
88
|
throw new SimpleError({
|
|
89
89
|
message: 'The provided client credentials are not valid',
|
|
90
90
|
code: 'invalid_client_credentials',
|
|
91
|
-
human: $t('
|
|
91
|
+
human: $t('42bbd5c0-8789-4ca1-b667-1c9ecf4d0190'),
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
|
|
2
|
+
import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, SetupStepType, Version } from '@stamhoofd/structures';
|
|
3
3
|
|
|
4
4
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
2
|
import { BalanceItem, Order, Webshop } from '@stamhoofd/models';
|
|
4
3
|
import { PermissionLevel } from '@stamhoofd/structures';
|
|
5
4
|
|
|
6
5
|
import { Context } from '../../../../helpers/Context';
|
|
6
|
+
import { UitpasService } from '../../../../services/uitpas/UitpasService';
|
|
7
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
8
|
|
|
8
9
|
type Params = { id: string };
|
|
9
10
|
type Query = undefined;
|
|
@@ -42,6 +43,14 @@ export class DeleteWebshopEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
42
43
|
throw Context.auth.notFoundOrNoAccess();
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
if (await UitpasService.areThereRegisteredTicketSales(webshop.id)) {
|
|
47
|
+
throw new SimpleError({
|
|
48
|
+
code: 'webshop_has_registered_ticket_sales',
|
|
49
|
+
message: `Webshop ${webshop.id} has registered ticket sales`,
|
|
50
|
+
human: $t(`0b3d6ea1-a70b-428c-9ba4-cc0c327ed415`),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
const orders = await Order.where({ webshopId: webshop.id });
|
|
46
55
|
await BalanceItem.deleteForDeletedOrders(orders.map(o => o.id));
|
|
47
56
|
await webshop.delete();
|
|
@@ -7,6 +7,7 @@ import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceIt
|
|
|
7
7
|
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
9
|
import { AuditLogService } from '../../../../services/AuditLogService';
|
|
10
|
+
import { shouldReserveUitpasNumbers, UitpasService } from '../../../../services/uitpas/UitpasService';
|
|
10
11
|
|
|
11
12
|
type Params = { id: string };
|
|
12
13
|
type Query = undefined;
|
|
@@ -132,6 +133,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
132
133
|
|
|
133
134
|
// TODO: validate before updating stock
|
|
134
135
|
order.data.validate(webshopGetter.struct, organization.meta, request.i18n, true);
|
|
136
|
+
order.data.cart = await UitpasService.validateCart(organization.id, webshop.id, order.data.cart);
|
|
135
137
|
|
|
136
138
|
try {
|
|
137
139
|
await order.updateStock(null, true);
|
|
@@ -230,6 +232,8 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
230
232
|
const previousToPay = model.totalToPay;
|
|
231
233
|
const previousStatus = model.status;
|
|
232
234
|
|
|
235
|
+
const shouldReserveBefore = shouldReserveUitpasNumbers(model.status);
|
|
236
|
+
|
|
233
237
|
model.status = patch.status ?? model.status;
|
|
234
238
|
|
|
235
239
|
// For now, we don't invalidate tickets, because they will get invalidated at scan time (the order status is checked)
|
|
@@ -240,13 +244,16 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
240
244
|
const previousData = model.data.clone();
|
|
241
245
|
if (patch.data) {
|
|
242
246
|
model.data.patchOrPut(patch.data);
|
|
243
|
-
|
|
244
247
|
if (model.status !== OrderStatus.Deleted) {
|
|
245
248
|
// Make sure all data is up to date and validated (= possible corrections happen here too)
|
|
246
249
|
model.data.validate(webshopGetter.struct, organization.meta, request.i18n, true);
|
|
247
250
|
}
|
|
248
251
|
}
|
|
249
252
|
|
|
253
|
+
if ((patch.data || !shouldReserveBefore) && shouldReserveUitpasNumbers(model.status)) {
|
|
254
|
+
model.data.cart = await UitpasService.validateCart(organization.id, webshop.id, model.data.cart, model.id);
|
|
255
|
+
}
|
|
256
|
+
|
|
250
257
|
if (model.status === OrderStatus.Deleted) {
|
|
251
258
|
model.data.removePersonalData();
|
|
252
259
|
|
|
@@ -41,7 +41,7 @@ export class SearchUitpasEventsEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
41
41
|
throw new SimpleError({
|
|
42
42
|
code: 'no_uitpas_organizer_id',
|
|
43
43
|
message: `No UiTPAS organizer ID set for organization`,
|
|
44
|
-
human: $t(`
|
|
44
|
+
human: $t(`aaf56535-c13b-4f92-9ba4-7309cae3e078`),
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
const uitpasOrganizersResponse = await UitpasService.searchUitpasEvents(organization.id, organization.meta.uitpasOrganizerId, request.query.text);
|
|
@@ -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
|
|
6
|
+
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } 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';
|
|
@@ -133,72 +133,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
133
133
|
request.body.validate(webshopStruct, organization.meta, request.i18n, false, Context.user?.getStructure());
|
|
134
134
|
request.body.update(webshopStruct);
|
|
135
135
|
|
|
136
|
-
|
|
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;
|
|
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].price !== expectedReducedPrices[i].price) {
|
|
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].price), orderPrice: Formatter.price(expectedReducedPrices[i].price) }),
|
|
192
|
-
field: 'uitpasNumbers.' + i.toString(),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
item.uitpasNumbers[i].uitpasTariffId = reducedPrices[i].uitpasTariffId;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
await UitpasService.checkUitpasNumbers(uitpasNumbersOnly); // Throws if invalid
|
|
200
|
-
}
|
|
201
|
-
}
|
|
136
|
+
request.body.cart = await UitpasService.validateCart(organization.id, webshop.id, request.body.cart);
|
|
202
137
|
|
|
203
138
|
const order = new Order().setRelation(Order.webshop, webshop);
|
|
204
139
|
order.data = request.body; // TODO: validate
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, PatchMap } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
-
import { BalanceItem, CachedBalance, Document, EmailTemplate, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
|
|
3
|
+
import { BalanceItem, CachedBalance, Document, Email, EmailTemplate, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
|
|
4
4
|
import { AccessRight, EmailTemplate as EmailTemplateStruct, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordSettings, ResourcePermissions } from '@stamhoofd/structures';
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { MemberRecordStore } from '../services/MemberRecordStore';
|
|
@@ -379,11 +379,6 @@ export class AdminPermissionChecker {
|
|
|
379
379
|
return false;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
// Check permissions aren't scoped to a specific organization, and they mismatch
|
|
383
|
-
if (!this.checkScope(registration.organizationId)) {
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
382
|
const organizationPermissions = await this.getOrganizationPermissions(registration.organizationId);
|
|
388
383
|
|
|
389
384
|
if (!organizationPermissions) {
|
|
@@ -714,7 +709,7 @@ export class AdminPermissionChecker {
|
|
|
714
709
|
}
|
|
715
710
|
|
|
716
711
|
if (!template.organizationId) {
|
|
717
|
-
return this.hasPlatformFullAccess();
|
|
712
|
+
return this.hasPlatformFullAccess() || !!this.platformPermissions?.hasAccessRight(AccessRight.ManageEmailTemplates);
|
|
718
713
|
}
|
|
719
714
|
|
|
720
715
|
if (await this.hasFullAccess(template.organizationId)) {
|
|
@@ -739,6 +734,11 @@ export class AdminPermissionChecker {
|
|
|
739
734
|
return true;
|
|
740
735
|
}
|
|
741
736
|
|
|
737
|
+
const o = await this.getOrganizationPermissions(template.organizationId);
|
|
738
|
+
if (o && o.hasAccessRight(AccessRight.ManageEmailTemplates)) {
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
742
|
return false;
|
|
743
743
|
}
|
|
744
744
|
|
|
@@ -850,8 +850,79 @@ export class AdminPermissionChecker {
|
|
|
850
850
|
return this.hasSomeAccess(organizationId);
|
|
851
851
|
}
|
|
852
852
|
|
|
853
|
-
canSendEmails() {
|
|
854
|
-
|
|
853
|
+
async canSendEmails(organizationId: Organization | string | null) {
|
|
854
|
+
if (organizationId) {
|
|
855
|
+
return (await this.getOrganizationPermissions(organizationId))?.hasAccessRightForSomeResourceOfType(PermissionsResourceType.Senders, AccessRight.SendMessages) ?? false;
|
|
856
|
+
}
|
|
857
|
+
return this.platformPermissions?.hasAccessRightForSomeResourceOfType(PermissionsResourceType.Senders, AccessRight.SendMessages) ?? false;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Fast check if this user can read at least one email in the system.
|
|
862
|
+
*/
|
|
863
|
+
async canReadEmails(organizationId: Organization | string | null) {
|
|
864
|
+
if (await this.canSendEmails(organizationId)) {
|
|
865
|
+
// A user can reads its own emails, so they can read.
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
if (organizationId) {
|
|
869
|
+
return (await this.getOrganizationPermissions(organizationId))?.hasAccessForSomeResourceOfType(PermissionsResourceType.Senders, PermissionLevel.Read) ?? false;
|
|
870
|
+
}
|
|
871
|
+
return this.platformPermissions?.hasAccessForSomeResourceOfType(PermissionsResourceType.Senders, PermissionLevel.Read) ?? false;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async canReadAllEmails(organizationId: Organization | string | null, senderId = ''): Promise<boolean> {
|
|
875
|
+
if (organizationId) {
|
|
876
|
+
return (await this.getOrganizationPermissions(organizationId))?.hasResourceAccess(PermissionsResourceType.Senders, senderId, PermissionLevel.Read) ?? false;
|
|
877
|
+
}
|
|
878
|
+
return this.platformPermissions?.hasResourceAccess(PermissionsResourceType.Senders, senderId, PermissionLevel.Read) ?? false;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async canAccessEmail(email: Email, level: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
|
|
882
|
+
if (!this.checkScope(email.organizationId)) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (email.userId === this.user.id) {
|
|
887
|
+
// User can always read their own emails
|
|
888
|
+
// Note; for sending we'll always need to use 'canSendEmailsFrom' externally
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (email.organizationId) {
|
|
893
|
+
const organizationPermissions = await this.getOrganizationPermissions(email.organizationId);
|
|
894
|
+
if (!organizationPermissions) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
if (!email.senderId) {
|
|
898
|
+
return organizationPermissions.hasResourceAccess(PermissionsResourceType.Senders, '', level);
|
|
899
|
+
}
|
|
900
|
+
return organizationPermissions.hasResourceAccess(PermissionsResourceType.Senders, email.senderId, level);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Platform email
|
|
904
|
+
const platformPermissions = this.platformPermissions;
|
|
905
|
+
if (!platformPermissions) {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
if (!email.senderId) {
|
|
909
|
+
return platformPermissions.hasResourceAccess(PermissionsResourceType.Senders, '', level);
|
|
910
|
+
}
|
|
911
|
+
return platformPermissions.hasResourceAccess(PermissionsResourceType.Senders, email.senderId, level);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async canSendEmail(email: Email): Promise<boolean> {
|
|
915
|
+
if (email.senderId) {
|
|
916
|
+
return await this.canSendEmailsFrom(email.organizationId, email.senderId);
|
|
917
|
+
}
|
|
918
|
+
return await this.canSendEmails(email.organizationId);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async canSendEmailsFrom(organizationId: Organization | string | null, senderId: string): Promise<boolean> {
|
|
922
|
+
if (organizationId) {
|
|
923
|
+
return (await this.getOrganizationPermissions(organizationId))?.hasResourceAccessRight(PermissionsResourceType.Senders, senderId, AccessRight.SendMessages) ?? false;
|
|
924
|
+
}
|
|
925
|
+
return this.platformPermissions?.hasResourceAccessRight(PermissionsResourceType.Senders, senderId, AccessRight.SendMessages) ?? false;
|
|
855
926
|
}
|
|
856
927
|
|
|
857
928
|
async canReadEmailTemplates(organizationId: string) {
|
|
@@ -914,7 +985,7 @@ export class AdminPermissionChecker {
|
|
|
914
985
|
*/
|
|
915
986
|
async hasSomeAccess(organizationOrId: string | Organization): Promise<boolean> {
|
|
916
987
|
const organizationPermissions = await this.getOrganizationPermissions(organizationOrId);
|
|
917
|
-
return !!organizationPermissions;
|
|
988
|
+
return !!organizationPermissions && !organizationPermissions.isEmpty;
|
|
918
989
|
}
|
|
919
990
|
|
|
920
991
|
async canManageAdmins(organizationId: string) {
|
|
@@ -26,7 +26,9 @@ export class FlagMomentCleanup {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
static async getActiveMemberResponsibilityRecordsForOrganizationWithoutRegistrationInCurrentPeriod() {
|
|
29
|
-
const
|
|
29
|
+
const platform = await Platform.getShared();
|
|
30
|
+
const currentPeriodId = platform.periodId;
|
|
31
|
+
const platformResponsibilityIds = platform.config.responsibilities.map(r => r.id);
|
|
30
32
|
|
|
31
33
|
return await MemberResponsibilityRecord.select()
|
|
32
34
|
.whereNot('organizationId', null)
|
|
@@ -62,6 +64,16 @@ export class FlagMomentCleanup {
|
|
|
62
64
|
).where(
|
|
63
65
|
SQL.column(Group.table, 'type'),
|
|
64
66
|
GroupType.Membership,
|
|
67
|
+
).where(
|
|
68
|
+
SQL.where(
|
|
69
|
+
SQL.column(Group.table, 'defaultAgeGroupId'),
|
|
70
|
+
'!=',
|
|
71
|
+
null,
|
|
72
|
+
).or(
|
|
73
|
+
SQL.column(MemberResponsibilityRecord.table, 'responsibilityId'),
|
|
74
|
+
'!=',
|
|
75
|
+
platformResponsibilityIds,
|
|
76
|
+
),
|
|
65
77
|
).where(
|
|
66
78
|
SQL.column(Group.table, 'deletedAt'),
|
|
67
79
|
null,
|