@stamhoofd/backend 2.120.6 → 2.121.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 -12
- package/src/audit-logs/RegistrationInvitationLogger.ts +46 -0
- package/src/audit-logs/init.ts +2 -0
- package/src/crons/index.ts +2 -0
- package/src/crons/invoices.ts +166 -0
- package/src/crons/mollie-chargebacks.ts +87 -0
- package/src/crons.ts +47 -10
- package/src/email-recipient-loaders/payments.ts +84 -41
- package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +51 -0
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +22 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +4 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +495 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +216 -0
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +405 -0
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +168 -0
- package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +15 -0
- package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +3 -4
- package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +42 -19
- package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
- package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +11 -3
- package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +308 -0
- package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +7 -0
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +5 -4
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +7 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +17 -8
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +127 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +13 -4
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +7 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -11
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +14 -19
- package/src/helpers/AdminPermissionChecker.ts +11 -3
- package/src/helpers/AuthenticatedStructures.ts +94 -6
- package/src/helpers/FinancialSupportHelper.ts +21 -0
- package/src/helpers/RecordAnswerHelper.test.ts +746 -0
- package/src/helpers/RecordAnswerHelper.ts +116 -0
- package/src/helpers/StripeHelper.ts +2 -3
- package/src/helpers/ViesHelper.ts +7 -3
- package/src/seeds/1750090030-records-configuration.ts +68 -3
- package/src/seeds/1752848561-groups-registration-periods.ts +26 -2
- package/src/seeds/1779121239-default-invoice-email-template.sql +3 -0
- package/src/services/BalanceItemService.ts +12 -16
- package/src/services/InvoiceService.ts +372 -72
- package/src/services/MollieService.ts +537 -0
- package/src/services/PaymentMandateService.ts +214 -0
- package/src/services/PaymentService.ts +578 -222
- package/src/services/PlatformMembershipService.ts +1 -1
- package/src/services/RegistrationService.ts +66 -5
- package/src/services/STPackageService.ts +0 -7
- package/src/services/data/invoice.hbs.html +686 -0
- package/src/sql-filters/groups.ts +11 -1
- package/src/sql-filters/payments.ts +5 -0
- package/src/sql-filters/registration-invitations.ts +90 -0
- package/src/sql-sorters/registration-invitations.ts +36 -0
- package/vitest.config.js +1 -0
- package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +0 -216
package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
4
|
+
import { CountFilteredRequest, CountResponse, PaymentCustomer, PaymentMethod, PermissionLevel, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
5
|
+
|
|
6
|
+
import { Context } from '../../../../helpers/Context.js';
|
|
7
|
+
import { GetReceivableBalancesEndpoint } from './GetReceivableBalancesEndpoint.js';
|
|
8
|
+
import type { BalanceItem} from '@stamhoofd/models';
|
|
9
|
+
import { Organization, User} from '@stamhoofd/models';
|
|
10
|
+
import { CachedBalance } from '@stamhoofd/models';
|
|
11
|
+
import { PaymentService } from '../../../../services/PaymentService.js';
|
|
12
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
13
|
+
import { PaymentMandateService } from '../../../../services/PaymentMandateService.js';
|
|
14
|
+
import { BalanceItemService } from '../../../../services/BalanceItemService.js';
|
|
15
|
+
|
|
16
|
+
type Params = Record<string, never>;
|
|
17
|
+
type Query = CountFilteredRequest;
|
|
18
|
+
type Body = undefined;
|
|
19
|
+
type ResponseBody = undefined;
|
|
20
|
+
|
|
21
|
+
export class ChargeReceivableBalancesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
22
|
+
queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
|
|
23
|
+
|
|
24
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
25
|
+
if (request.method !== 'POST') {
|
|
26
|
+
return [false];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const params = Endpoint.parseParameters(request.url, '/receivable-balances/charge', {});
|
|
30
|
+
|
|
31
|
+
if (params) {
|
|
32
|
+
return [true, params as Params];
|
|
33
|
+
}
|
|
34
|
+
return [false];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
38
|
+
const sellingOrganization = await Context.setOrganizationScope();
|
|
39
|
+
await Context.authenticate();
|
|
40
|
+
const query = await GetReceivableBalancesEndpoint.buildQuery(request.query);
|
|
41
|
+
|
|
42
|
+
for await (const cachedBalance of query.all()) {
|
|
43
|
+
if (cachedBalance.organizationId !== sellingOrganization.id) {
|
|
44
|
+
throw new SimpleError({
|
|
45
|
+
code: 'wrong_organization',
|
|
46
|
+
message: 'Cannot charge a cached balance from a different organization'
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
// todo
|
|
50
|
+
const items = await CachedBalance.balanceForObjects(cachedBalance.organizationId, [cachedBalance.objectId], cachedBalance.objectType);
|
|
51
|
+
|
|
52
|
+
if (items.length === 0) {
|
|
53
|
+
console.log('Nothing to charge for', cachedBalance.id)
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const map: Map<BalanceItem, number> = new Map();
|
|
58
|
+
for (const i of items) {
|
|
59
|
+
map.set(i, i.priceOpen)
|
|
60
|
+
}
|
|
61
|
+
let payingOrganization: Organization | null = null;
|
|
62
|
+
let user: User | null = null;
|
|
63
|
+
if (cachedBalance.objectType === ReceivableBalanceType.organization) {
|
|
64
|
+
const p = await Organization.getByID(cachedBalance.objectId);
|
|
65
|
+
if (!p || !(await Context.auth.hasFullAccess(p))) {
|
|
66
|
+
console.error('Unexpected missing paying organization id', cachedBalance)
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
payingOrganization = p;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (cachedBalance.objectType === ReceivableBalanceType.user || cachedBalance.objectType === ReceivableBalanceType.userWithoutMembers) {
|
|
73
|
+
const p = await User.getByID(cachedBalance.objectId);
|
|
74
|
+
if (!p || !(Context.auth.checkScope(p.organizationId))) {
|
|
75
|
+
console.error('Unexpected missing user id', cachedBalance)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
user = p
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const mandates = await PaymentMandateService.getMandates({
|
|
82
|
+
sellingOrganization,
|
|
83
|
+
user,
|
|
84
|
+
payingOrganization
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const mandate = mandates.find(m => m.isDefault)
|
|
88
|
+
|
|
89
|
+
if (!mandate) {
|
|
90
|
+
// Not possible
|
|
91
|
+
console.error('No mandates found for', cachedBalance.id)
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const customerUser = user ?? (payingOrganization ? (await payingOrganization.getFullAdmins())[0] : null)
|
|
96
|
+
const customer = PaymentCustomer.create({
|
|
97
|
+
firstName: customerUser?.firstName,
|
|
98
|
+
lastName: customerUser?.lastName,
|
|
99
|
+
email: customerUser?.email ?? (payingOrganization ? (await payingOrganization.getReplyEmails())[0].email : null),
|
|
100
|
+
company: payingOrganization?.defaultCompanies[0]
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
await PaymentService.createPayment({
|
|
104
|
+
balanceItems: map,
|
|
105
|
+
checkout: {
|
|
106
|
+
paymentMethod: PaymentMethod.Unknown,
|
|
107
|
+
totalPrice: null,
|
|
108
|
+
customer,
|
|
109
|
+
cancelUrl: null,
|
|
110
|
+
redirectUrl: null
|
|
111
|
+
},
|
|
112
|
+
user,
|
|
113
|
+
organization: sellingOrganization,
|
|
114
|
+
payingOrganization,
|
|
115
|
+
serviceFeeType: 'system',
|
|
116
|
+
createMandate: null,
|
|
117
|
+
useMandate: mandate,
|
|
118
|
+
paymentConfiguration: sellingOrganization.meta.registrationPaymentConfiguration,
|
|
119
|
+
privatePaymentConfiguration: sellingOrganization.privateMeta.registrationPaymentConfiguration
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Make sure the user can refresh data and see the updated cached amounts
|
|
124
|
+
await BalanceItemService.flushCaches(sellingOrganization.id)
|
|
125
|
+
return new Response(undefined, 201);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
|
|
1
|
+
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
|
-
import type { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder} from '@simonbackx/simple-encoding';
|
|
5
|
+
import type { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
|
|
6
6
|
import { PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
7
7
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
8
|
-
import type { Organization} from '@stamhoofd/models';
|
|
9
|
-
import { Event, Group, OrganizationRegistrationPeriod, Platform, Registration, RegistrationPeriod } from '@stamhoofd/models';
|
|
8
|
+
import type { Organization } from '@stamhoofd/models';
|
|
9
|
+
import { Event, Group, OrganizationRegistrationPeriod, Platform, Registration, RegistrationInvitation, RegistrationPeriod } from '@stamhoofd/models';
|
|
10
10
|
import { SQL } from '@stamhoofd/sql';
|
|
11
11
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
|
|
12
12
|
import { Context } from '../../../../helpers/Context.js';
|
|
13
|
+
import { RecordAnswerHelper } from '../../../../helpers/RecordAnswerHelper.js';
|
|
13
14
|
import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater.js';
|
|
14
15
|
|
|
15
16
|
type Params = Record<string, never>;
|
|
@@ -377,6 +378,9 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
377
378
|
event.groupId = null;
|
|
378
379
|
await event.save();
|
|
379
380
|
}
|
|
381
|
+
|
|
382
|
+
// delete invitations
|
|
383
|
+
await RegistrationInvitation.delete().where('groupId', id);
|
|
380
384
|
}
|
|
381
385
|
|
|
382
386
|
static async patchGroup(struct: AutoEncoderPatchType<GroupStruct>, period?: RegistrationPeriod | null, options: { allowPatchWaitingListPeriod?: boolean; isPatchingEvent?: boolean } = {}) {
|
|
@@ -390,6 +394,11 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
390
394
|
|
|
391
395
|
if (struct.settings) {
|
|
392
396
|
struct.settings.period = undefined; // Not allowed to patch manually
|
|
397
|
+
|
|
398
|
+
if (struct.settings.recordCategories) {
|
|
399
|
+
RecordAnswerHelper.throwIfPatchOrPutIsInvalid(model.settings.recordCategories, struct.settings.recordCategories);
|
|
400
|
+
}
|
|
401
|
+
|
|
393
402
|
model.settings.patchOrPut(struct.settings);
|
|
394
403
|
}
|
|
395
404
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AutoEncoderPatchType, Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
-
import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
|
|
2
|
+
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { Webshop } from '@stamhoofd/models';
|
|
@@ -8,6 +8,7 @@ import { PermissionLevel, PrivateWebshop, WebshopPrivateMetaData } from '@stamho
|
|
|
8
8
|
import { Formatter } from '@stamhoofd/utility';
|
|
9
9
|
|
|
10
10
|
import { Context } from '../../../../helpers/Context.js';
|
|
11
|
+
import { RecordAnswerHelper } from '../../../../helpers/RecordAnswerHelper.js';
|
|
11
12
|
|
|
12
13
|
type Params = { id: string };
|
|
13
14
|
type Query = undefined;
|
|
@@ -56,6 +57,11 @@ export class PatchWebshopEndpoint extends Endpoint<Params, Query, Body, Response
|
|
|
56
57
|
if (request.body.meta.customCode !== undefined && !await Context.auth.hasFullAccess(organization.id)) {
|
|
57
58
|
throw Context.auth.error($t('%15n'));
|
|
58
59
|
}
|
|
60
|
+
|
|
61
|
+
if (request.body.meta.recordCategories) {
|
|
62
|
+
RecordAnswerHelper.throwIfPatchOrPutIsInvalid(webshop.meta.recordCategories, request.body.meta.recordCategories);
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
webshop.meta.patchOrPut(request.body.meta);
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -157,7 +157,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
157
157
|
payment.method = struct.data.paymentMethod;
|
|
158
158
|
payment.status = PaymentStatus.Created;
|
|
159
159
|
payment.price = totalPrice;
|
|
160
|
-
PaymentService.
|
|
160
|
+
PaymentService.roundPayment(payment);
|
|
161
161
|
payment.paidAt = null;
|
|
162
162
|
|
|
163
163
|
// Determine the payment provider (always null because no online payments here)
|
|
@@ -45,10 +45,8 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
48
|
-
const organization = await Context.setOptionalOrganizationScope({ willAuthenticate:
|
|
49
|
-
|
|
50
|
-
await Context.optionalAuthenticate();
|
|
51
|
-
}
|
|
48
|
+
const organization = await Context.setOptionalOrganizationScope({ willAuthenticate: true }); // will authentiate set to true because we allow exchanges for inactive organizations
|
|
49
|
+
await Context.optionalAuthenticate();
|
|
52
50
|
|
|
53
51
|
// Not method on payment because circular references (not supprted in ts)
|
|
54
52
|
const payment = await PaymentService.pollStatus(request.params.id, organization, request.query.cancel);
|
|
@@ -63,19 +61,23 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
63
61
|
return new Response(undefined);
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
//
|
|
64
|
+
// Skip check permissions if order and created less than hour ago
|
|
67
65
|
let checkPermissions = true;
|
|
68
66
|
const hourAgo = new Date();
|
|
69
|
-
hourAgo.setHours(-
|
|
67
|
+
hourAgo.setHours(-2);
|
|
70
68
|
|
|
71
69
|
if (payment.createdAt > hourAgo) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
if (payment.payingOrganizationId) {
|
|
71
|
+
// B2B payments always required
|
|
72
|
+
checkPermissions = true;
|
|
73
|
+
} else {
|
|
74
|
+
const orders = await Order.where({ paymentId: payment.id }, { limit: 1 });
|
|
75
|
+
const isOrder = orders[0] !== undefined;
|
|
76
|
+
if (isOrder) {
|
|
77
|
+
checkPermissions = false;
|
|
78
|
+
}
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
|
-
// #endregion
|
|
79
81
|
|
|
80
82
|
return new Response(
|
|
81
83
|
await AuthenticatedStructures.paymentGeneral(payment, checkPermissions),
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PaymentMethod as molliePaymentMethod } from '@mollie/api-client';
|
|
2
2
|
import type { Decoder } from '@simonbackx/simple-encoding';
|
|
3
|
-
import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
|
|
3
|
+
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
6
|
import { Email } from '@stamhoofd/email';
|
|
7
|
-
import { BalanceItem, BalanceItemPayment, MolliePayment,
|
|
7
|
+
import { BalanceItem, BalanceItemPayment, MolliePayment, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
|
|
8
8
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
9
9
|
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, WebshopTicketType } from '@stamhoofd/structures';
|
|
10
10
|
import { Formatter } from '@stamhoofd/utility';
|
|
11
11
|
|
|
12
12
|
import { BuckarooHelper } from '../../../helpers/BuckarooHelper.js';
|
|
13
13
|
import { Context } from '../../../helpers/Context.js';
|
|
14
|
+
import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper.js';
|
|
14
15
|
import { StripeHelper } from '../../../helpers/StripeHelper.js';
|
|
15
16
|
import { AuditLogService } from '../../../services/AuditLogService.js';
|
|
16
|
-
import {
|
|
17
|
-
import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper.js';
|
|
17
|
+
import { MollieService } from '../../../services/MollieService.js';
|
|
18
18
|
import { PaymentService } from '../../../services/PaymentService.js';
|
|
19
|
+
import { UitpasService } from '../../../services/uitpas/UitpasService.js';
|
|
19
20
|
|
|
20
21
|
type Params = { id: string };
|
|
21
22
|
type Query = undefined;
|
|
@@ -180,7 +181,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
180
181
|
payment.method = request.body.paymentMethod;
|
|
181
182
|
payment.status = PaymentStatus.Created;
|
|
182
183
|
payment.price = totalPrice;
|
|
183
|
-
PaymentService.
|
|
184
|
+
PaymentService.roundPayment(payment);
|
|
184
185
|
totalPrice = payment.price;
|
|
185
186
|
payment.paidAt = null;
|
|
186
187
|
payment.customer = PaymentCustomer.create({
|
|
@@ -191,7 +192,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
191
192
|
|
|
192
193
|
// Determine the payment provider
|
|
193
194
|
// Throws if invalid
|
|
194
|
-
const { provider, stripeAccount } = await organization.getPaymentProviderFor(payment.method, webshop.privateMeta.paymentConfiguration);
|
|
195
|
+
const { provider, stripeAccount } = await organization.getPaymentProviderFor(payment.method, null, webshop.privateMeta.paymentConfiguration);
|
|
195
196
|
payment.provider = provider;
|
|
196
197
|
payment.stripeAccountId = stripeAccount?.id ?? null;
|
|
197
198
|
ServiceFeeHelper.setServiceFee(
|
|
@@ -210,8 +211,6 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
210
211
|
// Save order to get the id
|
|
211
212
|
await order.save();
|
|
212
213
|
|
|
213
|
-
const balanceItemPayments: (BalanceItemPayment & { balanceItem: BalanceItem })[] = [];
|
|
214
|
-
|
|
215
214
|
// Create balance item
|
|
216
215
|
const balanceItem = new BalanceItem();
|
|
217
216
|
balanceItem.type = BalanceItemType.Order;
|
|
@@ -239,7 +238,6 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
239
238
|
balanceItemPayment.organizationId = organization.id;
|
|
240
239
|
balanceItemPayment.price = balanceItem.price;
|
|
241
240
|
await balanceItemPayment.save();
|
|
242
|
-
balanceItemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem));
|
|
243
241
|
|
|
244
242
|
let paymentUrl: string | null = null;
|
|
245
243
|
let paymentQRCode: string | null = null;
|
|
@@ -287,7 +285,6 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
287
285
|
payment: payment.id,
|
|
288
286
|
},
|
|
289
287
|
i18n: request.i18n,
|
|
290
|
-
lineItems: balanceItemPayments,
|
|
291
288
|
organization,
|
|
292
289
|
customer: {
|
|
293
290
|
name: order.data.customer.name,
|
|
@@ -298,28 +295,26 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
298
295
|
}
|
|
299
296
|
else if (payment.provider === PaymentProvider.Mollie) {
|
|
300
297
|
// Mollie payment
|
|
301
|
-
const
|
|
302
|
-
if (!
|
|
298
|
+
const mollieService = await MollieService.create({sellingOrganization: organization});
|
|
299
|
+
if (!mollieService) {
|
|
303
300
|
throw new SimpleError({
|
|
304
301
|
code: '',
|
|
305
302
|
message: $t(`%w3`, { method: PaymentMethodHelper.getName(payment.method) }),
|
|
306
303
|
});
|
|
307
304
|
}
|
|
308
|
-
const profileId =
|
|
305
|
+
const profileId = await mollieService.getProfileId(webshop.getHost());
|
|
309
306
|
if (!profileId) {
|
|
310
307
|
throw new SimpleError({
|
|
311
308
|
code: '',
|
|
312
309
|
message: $t(`%w4`, { method: PaymentMethodHelper.getName(payment.method) }),
|
|
313
310
|
});
|
|
314
311
|
}
|
|
315
|
-
const
|
|
316
|
-
const locale = Context.i18n.locale.replace('-', '_');
|
|
317
|
-
const molliePayment = await mollieClient.payments.create({
|
|
312
|
+
const molliePayment = await mollieService.client.payments.create({
|
|
318
313
|
amount: {
|
|
319
314
|
currency: 'EUR',
|
|
320
315
|
value: (totalPrice / 10000).toFixed(2), // from 4 decimals to 0 decimals
|
|
321
316
|
},
|
|
322
|
-
method: payment.method
|
|
317
|
+
method: payment.method === PaymentMethod.Bancontact ? molliePaymentMethod.bancontact : (payment.method === PaymentMethod.iDEAL ? molliePaymentMethod.ideal : molliePaymentMethod.creditcard),
|
|
323
318
|
testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
|
|
324
319
|
profileId,
|
|
325
320
|
description,
|
|
@@ -331,7 +326,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
331
326
|
webshop: webshop.id,
|
|
332
327
|
payment: payment.id,
|
|
333
328
|
},
|
|
334
|
-
locale:
|
|
329
|
+
locale: mollieService.locale
|
|
335
330
|
});
|
|
336
331
|
console.log(molliePayment);
|
|
337
332
|
paymentUrl = molliePayment.getCheckoutUrl();
|
|
@@ -8,6 +8,8 @@ import { AccessRight, EmailTemplate as EmailTemplateStruct, EventPermissionCheck
|
|
|
8
8
|
import { Formatter } from '@stamhoofd/utility';
|
|
9
9
|
import type { RecordCacheEntry } from '../services/MemberRecordStore.js';
|
|
10
10
|
import { MemberRecordStore } from '../services/MemberRecordStore.js';
|
|
11
|
+
import { getFinancialSupportSettingsAsync } from './FinancialSupportHelper.js';
|
|
12
|
+
import { RecordAnswerHelper } from './RecordAnswerHelper.js';
|
|
11
13
|
import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -1117,7 +1119,7 @@ export class AdminPermissionChecker {
|
|
|
1117
1119
|
return false;
|
|
1118
1120
|
}
|
|
1119
1121
|
|
|
1120
|
-
async hasFullAccess(organizationId: string, level = PermissionLevel.Full): Promise<boolean> {
|
|
1122
|
+
async hasFullAccess(organizationId: string | Organization, level = PermissionLevel.Full): Promise<boolean> {
|
|
1121
1123
|
const organizationPermissions = await this.getOrganizationPermissions(organizationId);
|
|
1122
1124
|
|
|
1123
1125
|
if (!organizationPermissions) {
|
|
@@ -1557,7 +1559,7 @@ export class AdminPermissionChecker {
|
|
|
1557
1559
|
cloned.details.recordAnswers.delete(key);
|
|
1558
1560
|
}
|
|
1559
1561
|
else {
|
|
1560
|
-
if (value) {
|
|
1562
|
+
if (value && RecordAnswerHelper.haveTypesSameClass(value.settings.type, record.type)) {
|
|
1561
1563
|
// Force update
|
|
1562
1564
|
value.settings = record;
|
|
1563
1565
|
}
|
|
@@ -1731,7 +1733,13 @@ export class AdminPermissionChecker {
|
|
|
1731
1733
|
// Has financial write access?
|
|
1732
1734
|
if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Write)) {
|
|
1733
1735
|
if (isUserManager && isSetFinancialSupportTrue) {
|
|
1734
|
-
const financialSupportSettings = this.platform
|
|
1736
|
+
const financialSupportSettings = await getFinancialSupportSettingsAsync(this.platform, async () => {
|
|
1737
|
+
if (member.organizationId) {
|
|
1738
|
+
return await this.getOrganization(member.organizationId);
|
|
1739
|
+
}
|
|
1740
|
+
return null;
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1735
1743
|
const preventSelfAssignment = financialSupportSettings?.preventSelfAssignment === true;
|
|
1736
1744
|
|
|
1737
1745
|
if (preventSelfAssignment) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
-
import type { AuditLog, Document, EventNotification, MemberWithUsersRegistrationsAndGroups, Order, Ticket} from '@stamhoofd/models';
|
|
3
|
-
import { BalanceItem, CachedBalance, Event, Group, Invoice, Member, MemberPlatformMembership, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
|
|
4
|
-
import type { PaymentGeneral} from '@stamhoofd/structures';
|
|
5
|
-
import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, GroupType, InvoicedBalanceItem, InvoiceStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MembersBlob, MemberWithRegistrationsBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentCustomer, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, RegistrationsBlob, RegistrationWithMemberBlob, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct
|
|
2
|
+
import type { AuditLog, Document, EventNotification, MemberWithUsersRegistrationsAndGroups, Order, Ticket } from '@stamhoofd/models';
|
|
3
|
+
import { BalanceItem, CachedBalance, Event, Group, Invoice, Member, MemberPlatformMembership, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationInvitation, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
|
|
4
|
+
import type { PaymentGeneral } from '@stamhoofd/structures';
|
|
5
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, BalanceItem as BalanceItemStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, GroupType, InvitationGroupData, InvitationMemberData, InvoicedBalanceItem, InvoiceStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberRegistrationInvitation, MembersBlob, MemberWithRegistrationsBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentCustomer, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, RegistrationInvitation as RegistrationInvitationStruct, RegistrationsBlob, RegistrationWithMemberBlob, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
6
6
|
import { Sorter } from '@stamhoofd/utility';
|
|
7
7
|
|
|
8
8
|
import { SQL } from '@stamhoofd/sql';
|
|
@@ -34,7 +34,7 @@ export class AuthenticatedStructures {
|
|
|
34
34
|
const { registrations, orders } = await Payment.loadBalanceItemRelations(balanceItems);
|
|
35
35
|
|
|
36
36
|
// Note: permission checking is moved here for performacne to avoid loading the data multiple times
|
|
37
|
-
if (!(await Context.
|
|
37
|
+
if (!(await Context.optionalAuth?.canAccessBalanceItems(balanceItems, PermissionLevel.Read, { registrations, orders }))) {
|
|
38
38
|
throw new SimpleError({
|
|
39
39
|
code: 'permission_denied',
|
|
40
40
|
message: 'Permission denied',
|
|
@@ -43,7 +43,7 @@ export class AuthenticatedStructures {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions;
|
|
46
|
+
const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions && payments.every(p => !!Context.optionalAuth?.checkScope(p.organizationId));
|
|
47
47
|
|
|
48
48
|
const { payingOrganizations } = await Payment.loadPayingOrganizations(payments);
|
|
49
49
|
|
|
@@ -458,6 +458,12 @@ export class AuthenticatedStructures {
|
|
|
458
458
|
.where('periodId', relevantPeriodIds)
|
|
459
459
|
.fetch()
|
|
460
460
|
: [];
|
|
461
|
+
const registrationInvitations = members.length > 0
|
|
462
|
+
? await RegistrationInvitation.select()
|
|
463
|
+
.where('memberId', members.map(m => m.id))
|
|
464
|
+
.fetch() : [];
|
|
465
|
+
|
|
466
|
+
const memberRegistrationInvitations: Map<string, MemberRegistrationInvitation[]> = registrationInvitations.length > 0 ? await this.memberRegistrationInvitations(registrationInvitations) : new Map();
|
|
461
467
|
|
|
462
468
|
// Load organizations
|
|
463
469
|
const organizationIds = responsibilities.map(r => r.organizationId)
|
|
@@ -663,6 +669,7 @@ export class AuthenticatedStructures {
|
|
|
663
669
|
return r.getStructure(group);
|
|
664
670
|
});
|
|
665
671
|
blob.platformMemberships = platformMemberships.filter(r => r.memberId == blob.id).map(r => MemberPlatformMembershipStruct.create(r));
|
|
672
|
+
blob.registrationInvitations = memberRegistrationInvitations.get(blob.id) ?? [];
|
|
666
673
|
}
|
|
667
674
|
|
|
668
675
|
return MembersBlob.create({
|
|
@@ -1220,4 +1227,85 @@ export class AuthenticatedStructures {
|
|
|
1220
1227
|
static async balanceItemsWithPayments(balanceItems: BalanceItem[]) {
|
|
1221
1228
|
return await BalanceItem.getStructureWithPayments(balanceItems);
|
|
1222
1229
|
}
|
|
1230
|
+
|
|
1231
|
+
static async memberRegistrationInvitations(invitations: RegistrationInvitation[]): Promise<Map<string, MemberRegistrationInvitation[]>> {
|
|
1232
|
+
const results = new Map<string, MemberRegistrationInvitation[]>();
|
|
1233
|
+
const groups = await Group.getByIDs(...invitations.map(i => i.groupId));
|
|
1234
|
+
|
|
1235
|
+
for (const invitation of invitations) {
|
|
1236
|
+
const group = groups.find(g => g.id === invitation.groupId);
|
|
1237
|
+
if (!group) {
|
|
1238
|
+
throw new SimpleError({
|
|
1239
|
+
code: 'group_not_found',
|
|
1240
|
+
message: 'Group not found',
|
|
1241
|
+
human: $t(`%1SM`),
|
|
1242
|
+
})
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const result = MemberRegistrationInvitation.create({
|
|
1246
|
+
id: invitation.id,
|
|
1247
|
+
group: InvitationGroupData.create({
|
|
1248
|
+
id: invitation.groupId,
|
|
1249
|
+
name: group.settings.name,
|
|
1250
|
+
type: group.type,
|
|
1251
|
+
periodId: group.periodId,
|
|
1252
|
+
}),
|
|
1253
|
+
organizationId: invitation.organizationId,
|
|
1254
|
+
createdAt: invitation.createdAt
|
|
1255
|
+
})
|
|
1256
|
+
|
|
1257
|
+
let array = results.get(invitation.memberId);
|
|
1258
|
+
if (!array) {
|
|
1259
|
+
array = [result];
|
|
1260
|
+
results.set(invitation.memberId, array);
|
|
1261
|
+
} else {
|
|
1262
|
+
array.push(result);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
return results;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
static async registrationInvitations(invitations: RegistrationInvitation[]): Promise<RegistrationInvitationStruct[]> {
|
|
1270
|
+
const members = await Member.getByIDs(...invitations.map(i => i.memberId));
|
|
1271
|
+
const groups = await Group.getByIDs(...invitations.map(i => i.groupId));
|
|
1272
|
+
|
|
1273
|
+
return invitations.map((invitation) => {
|
|
1274
|
+
const member = members.find(m => m.id === invitation.memberId);
|
|
1275
|
+
if (!member) {
|
|
1276
|
+
throw new SimpleError({
|
|
1277
|
+
code: 'member_not_found',
|
|
1278
|
+
message: 'Member not found',
|
|
1279
|
+
human: $t(`%EO`),
|
|
1280
|
+
})
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const group = groups.find(g => g.id === invitation.groupId);
|
|
1284
|
+
if (!group) {
|
|
1285
|
+
throw new SimpleError({
|
|
1286
|
+
code: 'group_not_found',
|
|
1287
|
+
message: 'Group not found',
|
|
1288
|
+
human: $t(`%1SM`),
|
|
1289
|
+
})
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
return RegistrationInvitationStruct.create({
|
|
1293
|
+
id: invitation.id,
|
|
1294
|
+
group: InvitationGroupData.create({
|
|
1295
|
+
id: group.id,
|
|
1296
|
+
name: group.settings.name,
|
|
1297
|
+
type: group.type,
|
|
1298
|
+
periodId: group.periodId
|
|
1299
|
+
}),
|
|
1300
|
+
organizationId: invitation.organizationId,
|
|
1301
|
+
member: InvitationMemberData.create({
|
|
1302
|
+
id: member.id,
|
|
1303
|
+
firstName: member.firstName,
|
|
1304
|
+
lastName: member.lastName,
|
|
1305
|
+
birthDay: member.details.birthDay,
|
|
1306
|
+
}),
|
|
1307
|
+
createdAt: invitation.createdAt
|
|
1308
|
+
});
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1223
1311
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Organization, Platform } from '@stamhoofd/models';
|
|
2
|
+
import type { FinancialSupportSettings, Organization as OrganizationStruct, Platform as PlatformStruct } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the financial support settings from the platform if userMode is platform, or from the organization otherwise.
|
|
6
|
+
* @param platform
|
|
7
|
+
* @param getOrganization callback to prevent fetching the organization if not needed
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export async function getFinancialSupportSettingsAsync(platform: Platform | PlatformStruct, getOrganization: () => Promise<Organization | OrganizationStruct | null>): Promise<FinancialSupportSettings | null> {
|
|
11
|
+
if (STAMHOOFD.userMode === 'platform') {
|
|
12
|
+
return platform.config.financialSupport;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const organization = await getOrganization();
|
|
16
|
+
if (organization) {
|
|
17
|
+
return organization.meta.financialSupport;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return null;
|
|
21
|
+
}
|