@stamhoofd/backend 2.4.0 → 2.6.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 +3 -3
- package/src/endpoints/admin/invoices/GetInvoicesEndpoint.ts +1 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +16 -9
- package/src/endpoints/global/members/GetMembersEndpoint.ts +18 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +21 -8
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +3 -2
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -1
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +2 -1
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +6 -0
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +417 -178
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +88 -11
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +10 -4
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +6 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -2
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -28
- package/src/helpers/AdminPermissionChecker.ts +9 -4
- package/src/helpers/AuthenticatedStructures.ts +89 -28
- package/src/helpers/MemberUserSyncer.ts +5 -5
- package/src/helpers/StripeHelper.ts +83 -40
- package/src/helpers/StripePayoutChecker.ts +7 -5
- package/src/seeds/1722344160-update-membership.ts +57 -0
|
@@ -78,7 +78,7 @@ export class GetWebshopFromDomainEndpoint extends Endpoint<Params, Query, Body,
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
return new Response(GetWebshopFromDomainResult.create({
|
|
81
|
-
organization:
|
|
81
|
+
organization: organization.getBaseStructure(),
|
|
82
82
|
webshop: WebshopStruct.create(webshop)
|
|
83
83
|
}));
|
|
84
84
|
}
|
|
@@ -113,14 +113,14 @@ export class GetWebshopFromDomainEndpoint extends Endpoint<Params, Query, Body,
|
|
|
113
113
|
if (!webshop) {
|
|
114
114
|
// Return organization, so we know the locale + can do some custom logic
|
|
115
115
|
return new Response(GetWebshopFromDomainResult.create({
|
|
116
|
-
organization:
|
|
116
|
+
organization: organization.getBaseStructure(),
|
|
117
117
|
webshop: null,
|
|
118
118
|
webshops: []
|
|
119
119
|
}));
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
return new Response(GetWebshopFromDomainResult.create({
|
|
123
|
-
organization:
|
|
123
|
+
organization: organization.getBaseStructure(),
|
|
124
124
|
webshop: WebshopStruct.create(webshop)
|
|
125
125
|
}));
|
|
126
126
|
}
|
|
@@ -156,7 +156,7 @@ export class GetWebshopFromDomainEndpoint extends Endpoint<Params, Query, Body,
|
|
|
156
156
|
|
|
157
157
|
// Return organization, and the known webshops on this domain
|
|
158
158
|
return new Response(GetWebshopFromDomainResult.create({
|
|
159
|
-
organization:
|
|
159
|
+
organization: organization.getBaseStructure(),
|
|
160
160
|
webshop: null,
|
|
161
161
|
webshops: webshops.map(w => WebshopPreview.create(w)).filter(w => w.isClosed(0) === false).sort((a, b) => Sorter.byStringValue(a.meta.name, b.meta.name))
|
|
162
162
|
}));
|
|
@@ -180,7 +180,7 @@ export class GetWebshopFromDomainEndpoint extends Endpoint<Params, Query, Body,
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
return new Response(GetWebshopFromDomainResult.create({
|
|
183
|
-
organization:
|
|
183
|
+
organization: organization.getBaseStructure(),
|
|
184
184
|
webshop: WebshopStruct.create(webshop)
|
|
185
185
|
}));
|
|
186
186
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
-
import { Group as GroupStruct, GroupPrivateSettings, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from "@stamhoofd/structures";
|
|
2
|
+
import { Group as GroupStruct, GroupPrivateSettings, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version, GroupType } from "@stamhoofd/structures";
|
|
3
3
|
|
|
4
4
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from "@simonbackx/simple-encoding";
|
|
5
5
|
import { Context } from "../../../../helpers/Context";
|
|
6
6
|
import { Group, Member, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from "@stamhoofd/models";
|
|
7
7
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
8
|
+
import { AuthenticatedStructures } from "../../../../helpers/AuthenticatedStructures";
|
|
8
9
|
|
|
9
10
|
type Params = Record<string, never>;
|
|
10
11
|
type Query = undefined;
|
|
@@ -33,13 +34,13 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
33
34
|
|
|
34
35
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
35
36
|
const organization = await Context.setOrganizationScope();
|
|
36
|
-
|
|
37
|
+
await Context.authenticate()
|
|
37
38
|
|
|
38
39
|
if (!await Context.auth.hasFullAccess(organization.id)) {
|
|
39
40
|
throw Context.auth.error()
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
+
const periods: OrganizationRegistrationPeriod[] = [];
|
|
43
44
|
|
|
44
45
|
for (const {put} of request.body.getPuts()) {
|
|
45
46
|
if (!await Context.auth.hasFullAccess(organization.id)) {
|
|
@@ -70,7 +71,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
70
71
|
// Delete unreachable categories first
|
|
71
72
|
await organizationPeriod.cleanCategories(groups);
|
|
72
73
|
await Group.deleteUnreachable(organization.id, organizationPeriod, groups)
|
|
73
|
-
|
|
74
|
+
periods.push(organizationPeriod);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
for (const patch of request.body.getPatches()) {
|
|
@@ -131,28 +132,27 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
131
132
|
|
|
132
133
|
for (const groupPut of patch.groups.getPuts()) {
|
|
133
134
|
await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(groupPut.put, organization.id, organizationPeriod.periodId, {allowedIds})
|
|
135
|
+
deleteUnreachable = true
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
for (const struct of patch.groups.getPatches()) {
|
|
137
139
|
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(struct)
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
const period = await RegistrationPeriod.getByID(organizationPeriod.periodId);
|
|
141
|
-
const groups = await Group.getAll(organization.id, organizationPeriod.periodId)
|
|
142
142
|
|
|
143
143
|
if (deleteUnreachable) {
|
|
144
|
+
const groups = await Group.getAll(organization.id, organizationPeriod.periodId)
|
|
145
|
+
|
|
144
146
|
// Delete unreachable categories first
|
|
145
147
|
await organizationPeriod.cleanCategories(groups);
|
|
146
148
|
await Group.deleteUnreachable(organization.id, organizationPeriod, groups)
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
|
|
150
|
-
structs.push(organizationPeriod.getPrivateStructure(period, groups));
|
|
151
|
-
}
|
|
151
|
+
periods.push(organizationPeriod);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
return new Response(
|
|
155
|
-
|
|
155
|
+
await AuthenticatedStructures.organizationRegistrationPeriods(periods),
|
|
156
156
|
);
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -223,10 +223,64 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
223
223
|
if (struct.defaultAgeGroupId !== undefined) {
|
|
224
224
|
model.defaultAgeGroupId = await this.validateDefaultGroupId(struct.defaultAgeGroupId)
|
|
225
225
|
}
|
|
226
|
+
|
|
227
|
+
const patch = struct;
|
|
228
|
+
if (patch.waitingList !== undefined) {
|
|
229
|
+
if (patch.waitingList === null) {
|
|
230
|
+
// delete
|
|
231
|
+
if (model.waitingListId) {
|
|
232
|
+
// for now don't delete, as waiting lists can be shared between multiple groups
|
|
233
|
+
// await PatchOrganizationRegistrationPeriodsEndpoint.deleteGroup(model.waitingListId)
|
|
234
|
+
model.waitingListId = null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
} else if (patch.waitingList.isPatch()) {
|
|
238
|
+
if (!model.waitingListId) {
|
|
239
|
+
throw new SimpleError({
|
|
240
|
+
code: 'invalid_field',
|
|
241
|
+
field: 'waitingList',
|
|
242
|
+
message: 'Cannot patch waiting list before it is created'
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
patch.waitingList.id = model.waitingListId
|
|
246
|
+
patch.waitingList.type = GroupType.WaitingList
|
|
247
|
+
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(patch.waitingList)
|
|
248
|
+
} else {
|
|
249
|
+
if (model.waitingListId) {
|
|
250
|
+
// for now don't delete, as waiting lists can be shared between multiple groups
|
|
251
|
+
// await PatchOrganizationRegistrationPeriodsEndpoint.deleteGroup(model.waitingListId)
|
|
252
|
+
model.waitingListId = null;
|
|
253
|
+
}
|
|
254
|
+
patch.waitingList.type = GroupType.WaitingList
|
|
255
|
+
|
|
256
|
+
const existing = await Group.getByID(patch.waitingList.id)
|
|
257
|
+
if (existing) {
|
|
258
|
+
if (existing.organizationId !== model.organizationId) {
|
|
259
|
+
throw new SimpleError({
|
|
260
|
+
code: 'invalid_field',
|
|
261
|
+
field: 'waitingList',
|
|
262
|
+
message: 'Waiting list group is already used in another organization'
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
model.waitingListId = existing.id
|
|
267
|
+
} else {
|
|
268
|
+
const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
|
|
269
|
+
patch.waitingList,
|
|
270
|
+
model.organizationId,
|
|
271
|
+
model.periodId
|
|
272
|
+
)
|
|
273
|
+
model.waitingListId = group.id
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
226
277
|
|
|
227
278
|
await model.updateOccupancy()
|
|
228
279
|
await model.save();
|
|
229
|
-
|
|
280
|
+
|
|
281
|
+
if (struct.deletedAt !== undefined || struct.defaultAgeGroupId !== undefined) {
|
|
282
|
+
Member.updateMembershipsForGroupId(model.id)
|
|
283
|
+
}
|
|
230
284
|
}
|
|
231
285
|
|
|
232
286
|
|
|
@@ -278,6 +332,29 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
278
332
|
})
|
|
279
333
|
}
|
|
280
334
|
|
|
335
|
+
if (struct.waitingList) {
|
|
336
|
+
const existing = await Group.getByID(struct.waitingList.id)
|
|
337
|
+
if (existing) {
|
|
338
|
+
if (existing.organizationId !== model.organizationId) {
|
|
339
|
+
throw new SimpleError({
|
|
340
|
+
code: 'invalid_field',
|
|
341
|
+
field: 'waitingList',
|
|
342
|
+
message: 'Waiting list group is already used in another organization'
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
model.waitingListId = existing.id
|
|
347
|
+
} else {
|
|
348
|
+
struct.waitingList.type = GroupType.WaitingList
|
|
349
|
+
const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
|
|
350
|
+
struct.waitingList,
|
|
351
|
+
model.organizationId,
|
|
352
|
+
model.periodId
|
|
353
|
+
)
|
|
354
|
+
model.waitingListId = group.id
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
281
358
|
await model.updateOccupancy()
|
|
282
359
|
await model.save();
|
|
283
360
|
return model;
|
|
@@ -46,17 +46,22 @@ export class ConnectMollieEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
46
46
|
})
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const type =
|
|
49
|
+
const type = STAMHOOFD.STRIPE_CONNECT_METHOD
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
country: organization.address.country,
|
|
53
|
-
// Problem: we cannot set company or business_type, because then it defaults the structure of the company to one that requires a company number
|
|
51
|
+
const sharedData: Stripe.AccountCreateParams = {
|
|
54
52
|
capabilities: {
|
|
55
53
|
card_payments: { requested: true },
|
|
56
54
|
transfers: { requested: true },
|
|
57
55
|
bancontact_payments: { requested: true },
|
|
58
56
|
ideal_payments: { requested: true },
|
|
59
57
|
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let expressData: Stripe.AccountCreateParams = {
|
|
61
|
+
country: organization.address.country,
|
|
62
|
+
controller: {
|
|
63
|
+
requirement_collection: 'application',
|
|
64
|
+
},
|
|
60
65
|
settings: {
|
|
61
66
|
payouts: {
|
|
62
67
|
schedule: {
|
|
@@ -76,6 +81,7 @@ export class ConnectMollieEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
76
81
|
const stripe = StripeHelper.getInstance()
|
|
77
82
|
const account = await stripe.accounts.create({
|
|
78
83
|
type,
|
|
84
|
+
...sharedData,
|
|
79
85
|
...expressData
|
|
80
86
|
});
|
|
81
87
|
|
|
@@ -68,7 +68,10 @@ export class GetStripeAccountLinkEndpoint extends Endpoint<Params, Query, Body,
|
|
|
68
68
|
refresh_url: request.body.refreshUrl,
|
|
69
69
|
return_url: request.body.returnUrl,
|
|
70
70
|
type: 'account_onboarding',
|
|
71
|
-
|
|
71
|
+
collection_options: {
|
|
72
|
+
fields: 'eventually_due',
|
|
73
|
+
future_requirements: 'include'
|
|
74
|
+
}
|
|
72
75
|
});
|
|
73
76
|
|
|
74
77
|
return new Response(ResponseBody.create({
|
|
@@ -59,6 +59,12 @@ export class GetStripeLoginLinkEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
59
59
|
})
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
if (model.meta.type === 'standard') {
|
|
63
|
+
return new Response(ResponseBody.create({
|
|
64
|
+
url: 'https://dashboard.stripe.com/'
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
const stripe = StripeHelper.getInstance()
|
|
63
69
|
const accountLink = await stripe.accounts.createLoginLink(model.accountId);
|
|
64
70
|
|
|
@@ -243,13 +243,18 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
243
243
|
// Update balance item prices for this order if price has changed
|
|
244
244
|
if (previousToPay !== model.totalToPay) {
|
|
245
245
|
const items = await BalanceItem.where({ orderId: model.id })
|
|
246
|
-
if (items.length
|
|
246
|
+
if (items.length >= 1) {
|
|
247
247
|
model.markUpdated()
|
|
248
248
|
items[0].price = model.totalToPay
|
|
249
249
|
items[0].description = model.generateBalanceDescription(webshop)
|
|
250
250
|
items[0].updateStatus();
|
|
251
251
|
await items[0].save()
|
|
252
|
-
|
|
252
|
+
|
|
253
|
+
// Zero out the other items
|
|
254
|
+
const otherItems = items.slice(1)
|
|
255
|
+
await BalanceItem.deleteItems(otherItems)
|
|
256
|
+
} else if (items.length === 0
|
|
257
|
+
&& model.totalToPay > 0) {
|
|
253
258
|
model.markUpdated()
|
|
254
259
|
const balanceItem = new BalanceItem();
|
|
255
260
|
balanceItem.orderId = model.id;
|
|
@@ -2,11 +2,11 @@ import { createMollieClient } from '@mollie/api-client';
|
|
|
2
2
|
import { AutoEncoder, BooleanDecoder, Decoder, field } from '@simonbackx/simple-encoding';
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
4
4
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
5
|
-
import { BalanceItem, BalanceItemPayment,
|
|
5
|
+
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, STPendingInvoice } from '@stamhoofd/models';
|
|
6
6
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
7
|
-
import {
|
|
8
|
-
import { Formatter } from '@stamhoofd/utility';
|
|
7
|
+
import { PaymentGeneral, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, STInvoiceItem } from "@stamhoofd/structures";
|
|
9
8
|
|
|
9
|
+
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
10
10
|
import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
11
11
|
import { Context } from '../../../helpers/Context';
|
|
12
12
|
import { StripeHelper } from '../../../helpers/StripeHelper';
|
|
@@ -27,7 +27,7 @@ class Query extends AutoEncoder {
|
|
|
27
27
|
cancel = false
|
|
28
28
|
}
|
|
29
29
|
type Body = undefined
|
|
30
|
-
type ResponseBody =
|
|
30
|
+
type ResponseBody = PaymentGeneral | undefined
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* 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
|
|
@@ -51,6 +51,9 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
51
51
|
|
|
52
52
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
53
53
|
const organization = await Context.setOrganizationScope()
|
|
54
|
+
if (!request.query.exchange) {
|
|
55
|
+
await Context.authenticate()
|
|
56
|
+
}
|
|
54
57
|
|
|
55
58
|
// Not method on payment because circular references (not supprted in ts)
|
|
56
59
|
const payment = await ExchangePaymentEndpoint.pollStatus(request.params.id, organization, request.query.cancel)
|
|
@@ -66,28 +69,9 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
return new Response(
|
|
69
|
-
|
|
70
|
-
id: payment.id,
|
|
71
|
-
method: payment.method,
|
|
72
|
-
provider: payment.provider,
|
|
73
|
-
status: payment.status,
|
|
74
|
-
price: payment.price,
|
|
75
|
-
transferDescription: payment.transferDescription,
|
|
76
|
-
paidAt: payment.paidAt,
|
|
77
|
-
createdAt: payment.createdAt,
|
|
78
|
-
updatedAt: payment.updatedAt
|
|
79
|
-
})
|
|
72
|
+
await AuthenticatedStructures.paymentGeneral(payment, true)
|
|
80
73
|
);
|
|
81
74
|
}
|
|
82
|
-
|
|
83
|
-
static async updateOutstanding(items: BalanceItem[], organizationId: string) {
|
|
84
|
-
// Update outstanding amount of related members and registrations
|
|
85
|
-
const memberIds: string[] = Formatter.uniqueArray(items.map(p => p.memberId).filter(id => id !== null)) as any
|
|
86
|
-
await Member.updateOutstandingBalance(memberIds)
|
|
87
|
-
|
|
88
|
-
const registrationIds: string[] = Formatter.uniqueArray(items.map(p => p.registrationId).filter(id => id !== null)) as any
|
|
89
|
-
await Registration.updateOutstandingBalance(registrationIds, organizationId)
|
|
90
|
-
}
|
|
91
75
|
|
|
92
76
|
static async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus) {
|
|
93
77
|
if (payment.status === status) {
|
|
@@ -101,15 +85,16 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
101
85
|
|
|
102
86
|
// Prevent concurrency issues
|
|
103
87
|
await QueueHandler.schedule("balance-item-update/"+organization.id, async () => {
|
|
88
|
+
const unloaded = (await BalanceItemPayment.where({paymentId: payment.id})).map(r => r.setRelation(BalanceItemPayment.payment, payment))
|
|
104
89
|
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
105
|
-
|
|
90
|
+
unloaded
|
|
106
91
|
);
|
|
107
92
|
|
|
108
93
|
for (const balanceItemPayment of balanceItemPayments) {
|
|
109
94
|
await balanceItemPayment.markPaid(organization);
|
|
110
95
|
}
|
|
111
96
|
|
|
112
|
-
await
|
|
97
|
+
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
|
|
113
98
|
})
|
|
114
99
|
|
|
115
100
|
if (!wasPaid && payment.provider === PaymentProvider.Buckaroo && payment.method) {
|
|
@@ -152,7 +137,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
152
137
|
await balanceItemPayment.undoPaid(organization);
|
|
153
138
|
}
|
|
154
139
|
|
|
155
|
-
await
|
|
140
|
+
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
|
|
156
141
|
})
|
|
157
142
|
}
|
|
158
143
|
|
|
@@ -166,7 +151,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
166
151
|
await balanceItemPayment.markFailed(organization);
|
|
167
152
|
}
|
|
168
153
|
|
|
169
|
-
await
|
|
154
|
+
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
|
|
170
155
|
})
|
|
171
156
|
}
|
|
172
157
|
|
|
@@ -224,6 +224,12 @@ export class AdminPermissionChecker {
|
|
|
224
224
|
return true
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
if (member.registrations.length === 0 && permissionLevel !== PermissionLevel.Full && (this.organization && await this.hasFullAccess(this.organization.id, PermissionLevel.Full))) {
|
|
228
|
+
// Everyone with at least full access to at least one organization can access this member
|
|
229
|
+
// This allows organizations to register new members themselves
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
227
233
|
for (const registration of member.registrations) {
|
|
228
234
|
if (await this.canAccessRegistration(registration, permissionLevel)) {
|
|
229
235
|
return true;
|
|
@@ -360,8 +366,7 @@ export class AdminPermissionChecker {
|
|
|
360
366
|
permissionLevel: PermissionLevel = PermissionLevel.Read,
|
|
361
367
|
data?: {
|
|
362
368
|
registrations: Registration[],
|
|
363
|
-
orders: Order[]
|
|
364
|
-
members: Member[]
|
|
369
|
+
orders: Order[]
|
|
365
370
|
}
|
|
366
371
|
): Promise<boolean> {
|
|
367
372
|
for (const balanceItem of balanceItems) {
|
|
@@ -397,7 +402,7 @@ export class AdminPermissionChecker {
|
|
|
397
402
|
}
|
|
398
403
|
|
|
399
404
|
// Slight optimization possible here
|
|
400
|
-
const {registrations, orders
|
|
405
|
+
const {registrations, orders} = data ?? (this.user.permissions || permissionLevel === PermissionLevel.Read) ? (await Payment.loadBalanceItemRelations(balanceItems)) : {registrations: [], orders: []}
|
|
401
406
|
|
|
402
407
|
if (this.user.permissions) {
|
|
403
408
|
// We grant permission for a whole payment when the user has at least permission for a part of that payment.
|
|
@@ -425,7 +430,7 @@ export class AdminPermissionChecker {
|
|
|
425
430
|
// Check members
|
|
426
431
|
const userMembers = await Member.getMembersWithRegistrationForUser(this.user)
|
|
427
432
|
for (const member of userMembers) {
|
|
428
|
-
if (
|
|
433
|
+
if (balanceItems.find(m => m.memberId === member.id)) {
|
|
429
434
|
return true;
|
|
430
435
|
}
|
|
431
436
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
2
2
|
import { Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from "@stamhoofd/models";
|
|
3
3
|
import { Event as EventStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, MemberWithRegistrationsBlob, MembersBlob, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, User as UserStruct, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
4
|
+
import { OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, GroupCategory, GroupPrivateSettings, GroupSettings, GroupStatus, Group as GroupStruct, GroupType } from '@stamhoofd/structures';
|
|
4
5
|
|
|
5
6
|
import { Context } from "./Context";
|
|
6
7
|
import { Formatter } from "@stamhoofd/utility";
|
|
@@ -25,11 +26,11 @@ export class AuthenticatedStructures {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const {balanceItemPayments, balanceItems} = await Payment.loadBalanceItems(payments)
|
|
28
|
-
const {registrations, orders,
|
|
29
|
+
const {registrations, orders, groups} = await Payment.loadBalanceItemRelations(balanceItems);
|
|
29
30
|
|
|
30
31
|
if (checkPermissions) {
|
|
31
32
|
// Note: permission checking is moved here for performacne to avoid loading the data multiple times
|
|
32
|
-
if (!(await Context.auth.canAccessBalanceItems(balanceItems, PermissionLevel.Read, {registrations, orders
|
|
33
|
+
if (!(await Context.auth.canAccessBalanceItems(balanceItems, PermissionLevel.Read, {registrations, orders}))) {
|
|
33
34
|
throw new SimpleError({
|
|
34
35
|
code: "not_found",
|
|
35
36
|
message: "Payment not found",
|
|
@@ -40,22 +41,82 @@ export class AuthenticatedStructures {
|
|
|
40
41
|
|
|
41
42
|
const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions
|
|
42
43
|
|
|
43
|
-
return Payment.getGeneralStructureFromRelations({
|
|
44
|
+
return await Payment.getGeneralStructureFromRelations({
|
|
44
45
|
payments,
|
|
45
46
|
balanceItemPayments,
|
|
46
47
|
balanceItems,
|
|
47
48
|
registrations,
|
|
48
49
|
orders,
|
|
49
|
-
members,
|
|
50
50
|
groups
|
|
51
51
|
}, includeSettlements)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
static async group(group: Group) {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
return (await this.groups([group]))[0]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static async groups(groups: Group[]) {
|
|
59
|
+
const waitingListIds = Formatter.uniqueArray(groups.map(g => g.waitingListId).filter(id => id !== null) as string[])
|
|
60
|
+
const waitingLists = waitingListIds.length > 0 ? await Group.getByIDs(...waitingListIds) : []
|
|
61
|
+
|
|
62
|
+
const structs: GroupStruct[] = []
|
|
63
|
+
for (const group of groups) {
|
|
64
|
+
const waitingList = waitingLists.find(g => g.id == group.waitingListId) ?? null
|
|
65
|
+
const waitingListStruct = waitingList ? GroupStruct.create(waitingList) : null
|
|
66
|
+
if (waitingList && waitingListStruct && !await Context.optionalAuth?.canAccessGroup(waitingList)) {
|
|
67
|
+
waitingListStruct.privateSettings = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const struct = GroupStruct.create({
|
|
71
|
+
...group,
|
|
72
|
+
waitingList: waitingListStruct
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
if (!await Context.optionalAuth?.canAccessGroup(group)) {
|
|
76
|
+
struct.privateSettings = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
structs.push(struct)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return structs;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static async organizationRegistrationPeriods(organizationRegistrationPeriods: OrganizationRegistrationPeriod[]) {
|
|
86
|
+
if (organizationRegistrationPeriods.length === 0) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const periodIds = Formatter.uniqueArray(organizationRegistrationPeriods.map(p => p.periodId))
|
|
91
|
+
const periods = await RegistrationPeriod.getByIDs(...periodIds)
|
|
92
|
+
|
|
93
|
+
const groupIds = Formatter.uniqueArray(organizationRegistrationPeriods.flatMap(p => p.settings.categories.flatMap(c => c.groupIds)))
|
|
94
|
+
const groups = groupIds.length ? await Group.getByIDs(...groupIds) : []
|
|
95
|
+
|
|
96
|
+
const groupStructs = await this.groups(groups)
|
|
97
|
+
|
|
98
|
+
const structs: OrganizationRegistrationPeriodStruct[] = []
|
|
99
|
+
for (const organizationPeriod of organizationRegistrationPeriods) {
|
|
100
|
+
const period = periods.find(p => p.id == organizationPeriod.periodId) ?? null
|
|
101
|
+
if (!period) {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
const groupIds = Formatter.uniqueArray(organizationPeriod.settings.categories.flatMap(c => c.groupIds))
|
|
105
|
+
|
|
106
|
+
structs.push(
|
|
107
|
+
OrganizationRegistrationPeriodStruct.create({
|
|
108
|
+
...organizationPeriod,
|
|
109
|
+
period: period.getStructure(),
|
|
110
|
+
groups: groupStructs.filter(gg => groupIds.includes(gg.id)).sort(GroupStruct.defaultSort)
|
|
111
|
+
})
|
|
112
|
+
)
|
|
57
113
|
}
|
|
58
|
-
|
|
114
|
+
|
|
115
|
+
return structs
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static async organizationRegistrationPeriod(organizationRegistrationPeriod: OrganizationRegistrationPeriod) {
|
|
119
|
+
return (await this.organizationRegistrationPeriods([organizationRegistrationPeriod]))[0]
|
|
59
120
|
}
|
|
60
121
|
|
|
61
122
|
static async webshop(webshop: Webshop) {
|
|
@@ -66,6 +127,8 @@ export class AuthenticatedStructures {
|
|
|
66
127
|
}
|
|
67
128
|
|
|
68
129
|
static async organization(organization: Organization): Promise<OrganizationStruct> {
|
|
130
|
+
const organizationPeriod = await organization.getPeriod()
|
|
131
|
+
|
|
69
132
|
if (await Context.optionalAuth?.canAccessPrivateOrganizationData(organization)) {
|
|
70
133
|
const webshops = await Webshop.where({ organizationId: organization.id }, { select: Webshop.selectColumnsWithout(undefined, "products", "categories")})
|
|
71
134
|
const webshopStructures: WebshopPreview[] = []
|
|
@@ -77,35 +140,29 @@ export class AuthenticatedStructures {
|
|
|
77
140
|
webshopStructures.push(WebshopPreview.create(w))
|
|
78
141
|
}
|
|
79
142
|
|
|
80
|
-
const {groups, organizationPeriod, period} = await organization.getPeriod({emptyGroups: false})
|
|
81
|
-
|
|
82
143
|
return OrganizationStruct.create({
|
|
83
|
-
|
|
84
|
-
name: organization.name,
|
|
85
|
-
meta: organization.meta,
|
|
86
|
-
address: organization.address,
|
|
87
|
-
registerDomain: organization.registerDomain,
|
|
88
|
-
uri: organization.uri,
|
|
89
|
-
website: organization.website,
|
|
144
|
+
...organization.getBaseStructure(),
|
|
90
145
|
privateMeta: organization.privateMeta,
|
|
91
146
|
webshops: webshopStructures,
|
|
92
|
-
|
|
93
|
-
period: organizationPeriod.getPrivateStructure(period, groups)
|
|
147
|
+
period: await this.organizationRegistrationPeriod(organizationPeriod)
|
|
94
148
|
})
|
|
95
149
|
}
|
|
96
150
|
|
|
97
|
-
return
|
|
151
|
+
return OrganizationStruct.create({
|
|
152
|
+
...organization.getBaseStructure(),
|
|
153
|
+
period: await this.organizationRegistrationPeriod(organizationPeriod)
|
|
154
|
+
})
|
|
98
155
|
}
|
|
99
156
|
|
|
100
157
|
static async adminOrganizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
|
|
101
158
|
const structs: OrganizationStruct[] = [];
|
|
102
159
|
|
|
103
160
|
for (const organization of organizations) {
|
|
104
|
-
const base =
|
|
161
|
+
const base = organization.getBaseStructure()
|
|
105
162
|
structs.push(base)
|
|
106
163
|
}
|
|
107
164
|
|
|
108
|
-
return structs
|
|
165
|
+
return Promise.resolve(structs)
|
|
109
166
|
}
|
|
110
167
|
|
|
111
168
|
static async userWithMembers(user: User): Promise<UserWithMembers> {
|
|
@@ -114,7 +171,9 @@ export class AuthenticatedStructures {
|
|
|
114
171
|
return UserWithMembers.create({
|
|
115
172
|
...user,
|
|
116
173
|
hasAccount: user.hasAccount(),
|
|
117
|
-
|
|
174
|
+
|
|
175
|
+
// Always include the current context organization - because it is possible we switch organization and we don't want to refetch every time
|
|
176
|
+
members: await this.membersBlob(members, true, user)
|
|
118
177
|
})
|
|
119
178
|
}
|
|
120
179
|
|
|
@@ -207,17 +266,19 @@ export class AuthenticatedStructures {
|
|
|
207
266
|
// Load groups
|
|
208
267
|
const groupIds = events.map(e => e.groupId).filter(id => id !== null) as string[]
|
|
209
268
|
const groups = groupIds.length > 0 ? await Group.getByIDs(...groupIds) : []
|
|
269
|
+
const groupStructs = await this.groups(groups)
|
|
210
270
|
|
|
211
271
|
const result: EventStruct[] = []
|
|
212
272
|
|
|
213
273
|
for (const event of events) {
|
|
214
|
-
const group =
|
|
274
|
+
const group = groupStructs.find(g => g.id == event.groupId) ?? null
|
|
215
275
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
276
|
+
const struct = EventStruct.create({
|
|
277
|
+
...event,
|
|
278
|
+
group
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
result.push(struct)
|
|
221
282
|
}
|
|
222
283
|
|
|
223
284
|
return result
|