@stamhoofd/backend 2.8.0 → 2.13.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/.env.template.json +3 -1
- package/package.json +11 -3
- package/src/crons.ts +3 -3
- package/src/decoders/StringArrayDecoder.ts +24 -0
- package/src/decoders/StringNullableDecoder.ts +18 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +20 -18
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
- package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -9
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +15 -62
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
- package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +165 -35
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +56 -3
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +292 -170
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
- package/src/endpoints/organization/dashboard/payments/legacy/GetPaymentsEndpoint.ts +170 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
- package/src/helpers/AdminPermissionChecker.ts +95 -60
- package/src/helpers/AuthenticatedStructures.ts +16 -6
- package/src/helpers/Context.ts +21 -0
- package/src/helpers/EmailResumer.ts +22 -2
- package/src/helpers/MemberUserSyncer.ts +8 -2
- package/src/helpers/ViesHelper.ts +151 -0
- package/src/seeds/1722344160-update-membership.ts +19 -22
- package/src/seeds/1722344161-sync-member-users.ts +60 -0
- package/.env.json +0 -65
|
@@ -6,7 +6,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
|
|
|
6
6
|
import { I18n } from '@stamhoofd/backend-i18n';
|
|
7
7
|
import { Email } from '@stamhoofd/email';
|
|
8
8
|
import { BalanceItem, BalanceItemPayment, Group, Member, MemberWithRegistrations, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
|
|
9
|
-
import { BalanceItemStatus, IDRegisterCheckout,
|
|
9
|
+
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PermissionLevel, PlatformFamily, PlatformMember, RegisterItem, RegisterResponse, Version } from "@stamhoofd/structures";
|
|
10
10
|
import { Formatter } from '@stamhoofd/utility';
|
|
11
11
|
|
|
12
12
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -102,10 +102,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
102
102
|
const deleteRegistrationModels = (deleteRegistrationIds.length ? (await Registration.getByIDs(...deleteRegistrationIds)) : []).filter(r => r.organizationId === organization.id)
|
|
103
103
|
|
|
104
104
|
const memberIds = Formatter.uniqueArray(
|
|
105
|
-
[...request.body.
|
|
105
|
+
[...request.body.memberIds, ...deleteRegistrationModels.map(i => i.memberId)]
|
|
106
106
|
)
|
|
107
107
|
const members = await Member.getBlobByIds(...memberIds)
|
|
108
|
-
const groupIds =
|
|
108
|
+
const groupIds = request.body.groupIds
|
|
109
109
|
const groups = await Group.getByIDs(...groupIds)
|
|
110
110
|
|
|
111
111
|
for (const group of groups) {
|
|
@@ -177,7 +177,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
177
177
|
|
|
178
178
|
// Validate balance items (can only happen serverside)
|
|
179
179
|
const balanceItemIds = request.body.cart.balanceItems.map(i => i.item.id)
|
|
180
|
-
let memberBalanceItemsStructs:
|
|
180
|
+
let memberBalanceItemsStructs: BalanceItemWithPayments[] = []
|
|
181
181
|
let balanceItemsModels: BalanceItem[] = []
|
|
182
182
|
if (balanceItemIds.length > 0) {
|
|
183
183
|
balanceItemsModels = await BalanceItem.where({ id: { sign:'IN', value: balanceItemIds }, organizationId: organization.id })
|
|
@@ -187,11 +187,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
187
187
|
message: "Oeps, één of meerdere openstaande bedragen in jouw winkelmandje zijn aangepast. Herlaad de pagina en probeer opnieuw."
|
|
188
188
|
})
|
|
189
189
|
}
|
|
190
|
-
memberBalanceItemsStructs = await BalanceItem.
|
|
190
|
+
memberBalanceItemsStructs = await BalanceItem.getStructureWithPayments(balanceItemsModels)
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
console.log('isAdminFromSameOrganization', checkout.isAdminFromSameOrganization)
|
|
194
|
-
|
|
195
193
|
// Validate the cart
|
|
196
194
|
checkout.validate({memberBalanceItems: memberBalanceItemsStructs})
|
|
197
195
|
|
|
@@ -365,33 +363,17 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
365
363
|
}
|
|
366
364
|
}
|
|
367
365
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const registration = bundle.registration;
|
|
371
|
-
|
|
372
|
-
registration.reservedUntil = null
|
|
373
|
-
|
|
374
|
-
if (shouldMarkValid) {
|
|
375
|
-
await registration.markValid()
|
|
376
|
-
} else {
|
|
377
|
-
// Reserve registration for 30 minutes (if needed)
|
|
378
|
-
const group = groups.find(g => g.id === registration.groupId)
|
|
379
|
-
|
|
380
|
-
if (group && group.settings.maxMembers !== null) {
|
|
381
|
-
registration.reservedUntil = new Date(new Date().getTime() + 1000*60*30)
|
|
382
|
-
}
|
|
383
|
-
await registration.save()
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (bundle.item.calculatedPrice === 0) {
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
366
|
+
async function createBalanceItem({registration, amount, unitPrice, description, type, relations}: {amount?: number, registration: RegistrationWithMemberAndGroup, unitPrice: number, description: string, relations: Map<BalanceItemRelationType, BalanceItemRelation>, type: BalanceItemType}) {
|
|
367
|
+
// NOTE: We also need to save zero-price balance items because for online payments, we need to know which registrations to activate after payment
|
|
389
368
|
|
|
390
369
|
// Create balance item
|
|
391
370
|
const balanceItem = new BalanceItem();
|
|
392
371
|
balanceItem.registrationId = registration.id;
|
|
393
|
-
balanceItem.
|
|
394
|
-
balanceItem.
|
|
372
|
+
balanceItem.unitPrice = unitPrice
|
|
373
|
+
balanceItem.amount = amount ?? 1
|
|
374
|
+
balanceItem.description = description
|
|
375
|
+
balanceItem.relations = relations
|
|
376
|
+
balanceItem.type = type
|
|
395
377
|
|
|
396
378
|
// Who needs to receive this money?
|
|
397
379
|
balanceItem.organizationId = organization.id;
|
|
@@ -410,8 +392,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
410
392
|
// because otherwise the total price and pricePaid for the registration would be incorrect
|
|
411
393
|
//balanceItem2.registrationId = registration.id;
|
|
412
394
|
|
|
413
|
-
balanceItem2.
|
|
414
|
-
balanceItem2.
|
|
395
|
+
balanceItem2.unitPrice = unitPrice
|
|
396
|
+
balanceItem2.amount = amount ?? 1
|
|
397
|
+
balanceItem2.description = description
|
|
398
|
+
balanceItem2.relations = relations
|
|
399
|
+
balanceItem2.type = type
|
|
415
400
|
|
|
416
401
|
// Who needs to receive this money?
|
|
417
402
|
balanceItem2.organizationId = request.body.asOrganizationId;
|
|
@@ -438,12 +423,106 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
438
423
|
await balanceItem.save();
|
|
439
424
|
createdBalanceItems.push(balanceItem)
|
|
440
425
|
}
|
|
426
|
+
|
|
427
|
+
// Save registrations and add extra data if needed
|
|
428
|
+
for (const bundle of payRegistrations) {
|
|
429
|
+
const {item, registration} = bundle;
|
|
430
|
+
registration.reservedUntil = null
|
|
431
|
+
|
|
432
|
+
if (shouldMarkValid) {
|
|
433
|
+
await registration.markValid({skipEmail: bundle.item.replaceRegistrations.length > 0})
|
|
434
|
+
} else {
|
|
435
|
+
// Reserve registration for 30 minutes (if needed)
|
|
436
|
+
const group = groups.find(g => g.id === registration.groupId)
|
|
437
|
+
|
|
438
|
+
if (group && group.settings.maxMembers !== null) {
|
|
439
|
+
registration.reservedUntil = new Date(new Date().getTime() + 1000*60*30)
|
|
440
|
+
}
|
|
441
|
+
await registration.save()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Note: we should always create the balance items: even when the price is zero
|
|
445
|
+
// Otherwise we don't know which registrations to activate after payment
|
|
446
|
+
|
|
447
|
+
if (shouldMarkValid && item.calculatedPrice === 0) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Create balance items
|
|
452
|
+
const sharedRelations: [BalanceItemRelationType, BalanceItemRelation][] = [
|
|
453
|
+
[
|
|
454
|
+
BalanceItemRelationType.Member,
|
|
455
|
+
BalanceItemRelation.create({
|
|
456
|
+
id: item.member.id,
|
|
457
|
+
name: item.member.patchedMember.name
|
|
458
|
+
})
|
|
459
|
+
],
|
|
460
|
+
[
|
|
461
|
+
BalanceItemRelationType.Group,
|
|
462
|
+
BalanceItemRelation.create({
|
|
463
|
+
id: item.group.id,
|
|
464
|
+
name: item.group.settings.name
|
|
465
|
+
})
|
|
466
|
+
]
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
if (item.group.settings.prices.length > 1) {
|
|
470
|
+
sharedRelations.push([
|
|
471
|
+
BalanceItemRelationType.GroupPrice,
|
|
472
|
+
BalanceItemRelation.create({
|
|
473
|
+
id: item.groupPrice.id,
|
|
474
|
+
name: item.groupPrice.name
|
|
475
|
+
})
|
|
476
|
+
])
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Base price
|
|
480
|
+
await createBalanceItem({
|
|
481
|
+
registration,
|
|
482
|
+
unitPrice: item.groupPrice.price.forMember(item.member),
|
|
483
|
+
type: BalanceItemType.Registration,
|
|
484
|
+
description: `${item.member.patchedMember.name} bij ${item.group.settings.name}`,
|
|
485
|
+
relations: new Map([
|
|
486
|
+
...sharedRelations
|
|
487
|
+
])
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
// Options
|
|
491
|
+
for (const option of item.options) {
|
|
492
|
+
await createBalanceItem({
|
|
493
|
+
registration,
|
|
494
|
+
amount: option.amount,
|
|
495
|
+
unitPrice: option.option.price.forMember(item.member),
|
|
496
|
+
type: BalanceItemType.Registration,
|
|
497
|
+
description: `${option.optionMenu.name}: ${option.option.name}`,
|
|
498
|
+
relations: new Map([
|
|
499
|
+
...sharedRelations,
|
|
500
|
+
[
|
|
501
|
+
BalanceItemRelationType.GroupOptionMenu,
|
|
502
|
+
BalanceItemRelation.create({
|
|
503
|
+
id: option.optionMenu.id,
|
|
504
|
+
name: option.optionMenu.name,
|
|
505
|
+
})
|
|
506
|
+
],
|
|
507
|
+
[
|
|
508
|
+
BalanceItemRelationType.GroupOption,
|
|
509
|
+
BalanceItemRelation.create({
|
|
510
|
+
id: option.option.id,
|
|
511
|
+
name: option.option.name,
|
|
512
|
+
})
|
|
513
|
+
]
|
|
514
|
+
])
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
}
|
|
441
519
|
|
|
442
520
|
const oldestMember = members.slice().sort((a, b) => b.details.defaultAge - a.details.defaultAge)[0]
|
|
443
521
|
if (checkout.freeContribution && !request.body.asOrganizationId) {
|
|
444
522
|
// Create balance item
|
|
445
523
|
const balanceItem = new BalanceItem();
|
|
446
|
-
balanceItem.
|
|
524
|
+
balanceItem.type = BalanceItemType.FreeContribution
|
|
525
|
+
balanceItem.unitPrice = checkout.freeContribution
|
|
447
526
|
balanceItem.description = `Vrije bijdrage`
|
|
448
527
|
balanceItem.pricePaid = 0;
|
|
449
528
|
balanceItem.userId = user.id
|
|
@@ -462,7 +541,8 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
462
541
|
if (checkout.administrationFee && whoWillPayNow !== 'nobody') {
|
|
463
542
|
// Create balance item
|
|
464
543
|
const balanceItem = new BalanceItem();
|
|
465
|
-
balanceItem.
|
|
544
|
+
balanceItem.type = BalanceItemType.AdministrationFee
|
|
545
|
+
balanceItem.unitPrice = checkout.administrationFee
|
|
466
546
|
balanceItem.description = `Administratiekosten`
|
|
467
547
|
balanceItem.pricePaid = 0;
|
|
468
548
|
balanceItem.organizationId = organization.id;
|
|
@@ -598,11 +678,61 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
598
678
|
}
|
|
599
679
|
|
|
600
680
|
const payment = new Payment()
|
|
601
|
-
payment.userId = user.id
|
|
602
681
|
|
|
603
682
|
// Who will receive this money?
|
|
604
683
|
payment.organizationId = organization.id
|
|
605
684
|
|
|
685
|
+
// Who paid
|
|
686
|
+
payment.payingUserId = user.id
|
|
687
|
+
payment.payingOrganizationId = checkout.asOrganizationId ?? null
|
|
688
|
+
|
|
689
|
+
// Fill in customer:
|
|
690
|
+
payment.customer = PaymentCustomer.create({
|
|
691
|
+
firstName: user.firstName,
|
|
692
|
+
lastName: user.lastName,
|
|
693
|
+
email: user.email,
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
if (checkout.asOrganizationId) {
|
|
697
|
+
if (!checkout.customer) {
|
|
698
|
+
throw new SimpleError({
|
|
699
|
+
code: "missing_fields",
|
|
700
|
+
message: "customer is required when paying as an organization",
|
|
701
|
+
human: "Vul je facturatiegegevens in om verder te gaan."
|
|
702
|
+
})
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (!checkout.customer.company) {
|
|
706
|
+
throw new SimpleError({
|
|
707
|
+
code: "missing_fields",
|
|
708
|
+
message: "customer.company is required when paying as an organization",
|
|
709
|
+
human: "Als je een betaling uitvoert in naam van je vereniging, is het noodzakelijk om facturatiegegevens met bedrijfsgegevens in te vullen."
|
|
710
|
+
})
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const payingOrganization = await Organization.getByID(checkout.asOrganizationId);
|
|
714
|
+
if (!payingOrganization) {
|
|
715
|
+
throw new SimpleError({
|
|
716
|
+
code: "invalid_data",
|
|
717
|
+
message: "Oeps, de organisatie waarvoor je probeert te betalen lijkt niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
|
|
718
|
+
})
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Search company id
|
|
722
|
+
// this avoids needing to check the VAT number every time
|
|
723
|
+
const id = checkout.customer.company.id
|
|
724
|
+
const foundCompany = payingOrganization.meta.companies.find(c => c.id === id)
|
|
725
|
+
|
|
726
|
+
if (!foundCompany) {
|
|
727
|
+
throw new SimpleError({
|
|
728
|
+
code: "invalid_data",
|
|
729
|
+
message: "Oeps, de facturatiegegevens die je probeerde te selecteren lijken niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
|
|
730
|
+
})
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
payment.customer.company = foundCompany
|
|
734
|
+
}
|
|
735
|
+
|
|
606
736
|
payment.method = checkout.paymentMethod
|
|
607
737
|
payment.status = PaymentStatus.Created
|
|
608
738
|
payment.price = totalPrice
|
|
@@ -1,36 +1,24 @@
|
|
|
1
|
-
import { AutoEncoder, Data, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
1
|
+
import { AutoEncoder, Data, Decoder, EnumDecoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
3
|
import { EmailTemplate } from '@stamhoofd/models';
|
|
4
4
|
import { EmailTemplate as EmailTemplateStruct, EmailTemplateType } from '@stamhoofd/structures';
|
|
5
5
|
|
|
6
6
|
import { Context } from '../../../../helpers/Context';
|
|
7
|
+
import { StringNullableDecoder } from '../../../../decoders/StringNullableDecoder';
|
|
8
|
+
import { StringArrayDecoder } from '../../../../decoders/StringArrayDecoder';
|
|
7
9
|
|
|
8
10
|
type Params = Record<string, never>;
|
|
9
11
|
type Body = undefined;
|
|
10
12
|
|
|
11
|
-
export class StringNullableDecoder<T> implements Decoder<T | null> {
|
|
12
|
-
decoder: Decoder<T>;
|
|
13
|
-
|
|
14
|
-
constructor(decoder: Decoder<T>) {
|
|
15
|
-
this.decoder = decoder;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
decode(data: Data): T | null {
|
|
19
|
-
if (data.value === 'null') {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return data.decode(this.decoder);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
13
|
class Query extends AutoEncoder {
|
|
29
14
|
@field({ decoder: new StringNullableDecoder(StringDecoder), optional: true, nullable: true })
|
|
30
15
|
webshopId: string|null = null
|
|
31
16
|
|
|
32
|
-
@field({ decoder: new StringNullableDecoder(StringDecoder), optional: true, nullable: true})
|
|
33
|
-
|
|
17
|
+
@field({ decoder: new StringNullableDecoder(new StringArrayDecoder(StringDecoder)), optional: true, nullable: true})
|
|
18
|
+
groupIds: string[]|null = null
|
|
19
|
+
|
|
20
|
+
@field({ decoder: new StringNullableDecoder(new StringArrayDecoder(new EnumDecoder(EmailTemplateType))), optional: true, nullable: true})
|
|
21
|
+
types: EmailTemplateType[]|null = null
|
|
34
22
|
}
|
|
35
23
|
|
|
36
24
|
type ResponseBody = EmailTemplateStruct[];
|
|
@@ -65,15 +53,24 @@ export class GetEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
65
53
|
}
|
|
66
54
|
}
|
|
67
55
|
|
|
68
|
-
const types = [...Object.values(EmailTemplateType)].filter(type => {
|
|
56
|
+
const types = (request.query.types ?? [...Object.values(EmailTemplateType)]).filter(type => {
|
|
69
57
|
if (!organization) {
|
|
70
|
-
return
|
|
58
|
+
return EmailTemplateStruct.allowPlatformLevel(type)
|
|
71
59
|
}
|
|
72
60
|
return EmailTemplateStruct.allowOrganizationLevel(type)
|
|
73
61
|
})
|
|
74
62
|
|
|
75
63
|
|
|
76
|
-
const templates = organization ?
|
|
64
|
+
const templates = organization ?
|
|
65
|
+
(
|
|
66
|
+
await EmailTemplate.where({ organizationId: organization.id, webshopId: request.query.webshopId ?? null, groupId: request.query.groupIds ? {sign: 'IN', value: request.query.groupIds} : null, type: {sign: 'IN', value: types}})
|
|
67
|
+
)
|
|
68
|
+
: (
|
|
69
|
+
// Required for event emails when logged in as the platform admin
|
|
70
|
+
(request.query.webshopId || request.query.groupIds) ?
|
|
71
|
+
await EmailTemplate.where({ webshopId: request.query.webshopId ?? null, groupId: request.query.groupIds ? {sign: 'IN', value: request.query.groupIds} : null, type: {sign: 'IN', value: types}})
|
|
72
|
+
: []
|
|
73
|
+
);
|
|
77
74
|
const defaultTemplates = await EmailTemplate.where({ organizationId: null, type: {sign: 'IN', value: types} });
|
|
78
75
|
return new Response([...templates, ...defaultTemplates].map(template => EmailTemplateStruct.create(template)))
|
|
79
76
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
-
import { EmailTemplate } from '@stamhoofd/models';
|
|
3
|
+
import { EmailTemplate, Group, Webshop } from '@stamhoofd/models';
|
|
4
4
|
import { EmailTemplate as EmailTemplateStruct, PermissionLevel } from '@stamhoofd/structures';
|
|
5
5
|
|
|
6
6
|
import { Context } from '../../../../helpers/Context';
|
|
7
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
8
|
|
|
8
9
|
type Params = Record<string, never>;
|
|
9
10
|
type Body = PatchableArrayAutoEncoder<EmailTemplateStruct>;
|
|
@@ -67,12 +68,32 @@ export class PatchEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, R
|
|
|
67
68
|
throw Context.auth.error();
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
if (!EmailTemplateStruct.allowPlatformLevel(struct.type) && !organization) {
|
|
72
|
+
throw Context.auth.error();
|
|
73
|
+
}
|
|
74
|
+
|
|
70
75
|
const template = new EmailTemplate()
|
|
71
76
|
template.id = struct.id
|
|
72
77
|
template.organizationId = organization?.id ?? null
|
|
73
78
|
template.webshopId = struct.webshopId
|
|
74
79
|
template.groupId = struct.groupId
|
|
75
80
|
|
|
81
|
+
if (struct.groupId) {
|
|
82
|
+
const group = await Group.getByID(struct.groupId)
|
|
83
|
+
if (!group || !await Context.auth.canAccessGroup(group, PermissionLevel.Full)) {
|
|
84
|
+
throw Context.auth.error();
|
|
85
|
+
}
|
|
86
|
+
template.organizationId = group.organizationId
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (struct.webshopId) {
|
|
90
|
+
const webshop = await Webshop.getByID(struct.webshopId)
|
|
91
|
+
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
|
|
92
|
+
throw Context.auth.error();
|
|
93
|
+
}
|
|
94
|
+
template.organizationId = webshop.organizationId
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
template.html = struct.html
|
|
77
98
|
template.subject = struct.subject
|
|
78
99
|
template.text = struct.text
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { AutoEncoderPatchType, Decoder, ObjectData, patchObject } from '@simonbackx/simple-encoding';
|
|
1
|
+
import { AutoEncoderPatchType, Decoder, isPatchableArray, ObjectData, PatchableArrayAutoEncoder, patchObject } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
3
|
import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
|
|
4
|
-
import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform, RegistrationPeriod, StripeAccount,
|
|
5
|
-
import { BuckarooSettings, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel
|
|
4
|
+
import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform, RegistrationPeriod, StripeAccount, Webshop } from '@stamhoofd/models';
|
|
5
|
+
import { BuckarooSettings, Company, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel } from "@stamhoofd/structures";
|
|
6
6
|
import { Formatter } from '@stamhoofd/utility';
|
|
7
7
|
|
|
8
8
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
9
9
|
import { BuckarooHelper } from '../../../../helpers/BuckarooHelper';
|
|
10
10
|
import { Context } from '../../../../helpers/Context';
|
|
11
|
+
import { ViesHelper } from '../../../../helpers/ViesHelper';
|
|
11
12
|
|
|
12
13
|
type Params = Record<string, never>;
|
|
13
14
|
type Query = undefined;
|
|
@@ -98,6 +99,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
98
99
|
|
|
99
100
|
if (request.body.privateMeta && request.body.privateMeta.isPatch()) {
|
|
100
101
|
organization.privateMeta.emails = request.body.privateMeta.emails.applyTo(organization.privateMeta.emails)
|
|
102
|
+
organization.privateMeta.premises = patchObject(organization.privateMeta.premises, request.body.privateMeta.premises);
|
|
101
103
|
organization.privateMeta.roles = request.body.privateMeta.roles.applyTo(organization.privateMeta.roles)
|
|
102
104
|
organization.privateMeta.responsibilities = request.body.privateMeta.responsibilities.applyTo(organization.privateMeta.responsibilities)
|
|
103
105
|
organization.privateMeta.inheritedResponsibilityRoles = request.body.privateMeta.inheritedResponsibilityRoles.applyTo(organization.privateMeta.inheritedResponsibilityRoles)
|
|
@@ -207,6 +209,10 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
if (request.body.meta) {
|
|
212
|
+
if (request.body.meta.companies) {
|
|
213
|
+
await this.validateCompanies(organization, request.body.meta.companies)
|
|
214
|
+
}
|
|
215
|
+
|
|
210
216
|
const savedPackages = organization.meta.packages
|
|
211
217
|
organization.meta.patchOrPut(request.body.meta)
|
|
212
218
|
organization.meta.packages = savedPackages
|
|
@@ -365,5 +371,52 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
365
371
|
errors.throwIfNotEmpty()
|
|
366
372
|
return new Response(await AuthenticatedStructures.organization(organization));
|
|
367
373
|
}
|
|
374
|
+
|
|
375
|
+
async validateCompanies(organization: Organization, companies: PatchableArrayAutoEncoder<Company>|Company[]) {
|
|
376
|
+
if (isPatchableArray(companies)) {
|
|
377
|
+
for (const patch of companies.getPatches()) {
|
|
378
|
+
// Changed VAT number
|
|
379
|
+
const original = organization.meta.companies.find(c => c.id === patch.id)
|
|
380
|
+
|
|
381
|
+
if (!original) {
|
|
382
|
+
throw new Error('Could not find company')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Changed VAT number
|
|
386
|
+
const prepatched = original.patch(patch)
|
|
387
|
+
await ViesHelper.checkCompany(prepatched, patch)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let c = 0;
|
|
391
|
+
for (const {put} of companies.getPuts()) {
|
|
392
|
+
c++;
|
|
393
|
+
|
|
394
|
+
if ((organization.meta.companies.length + c) > 5) {
|
|
395
|
+
throw new SimpleError({
|
|
396
|
+
code: "invalid_field",
|
|
397
|
+
message: "Too many companies",
|
|
398
|
+
human: "Je kan maximaal 5 bedrijven toevoegen",
|
|
399
|
+
field: "companies"
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await ViesHelper.checkCompany(put, put)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
} else {
|
|
407
|
+
if (companies.length > 5) {
|
|
408
|
+
throw new SimpleError({
|
|
409
|
+
code: "invalid_field",
|
|
410
|
+
message: "Too many companies",
|
|
411
|
+
human: "Je kan maximaal 5 bedrijven toevoegen",
|
|
412
|
+
field: "companies"
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
for (const company of companies) {
|
|
417
|
+
await ViesHelper.checkCompany(company, company)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
368
421
|
}
|
|
369
422
|
|
|
@@ -6,6 +6,7 @@ import NodeRSA from 'node-rsa';
|
|
|
6
6
|
|
|
7
7
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
9
10
|
|
|
10
11
|
type Params = Record<string, never>;
|
|
11
12
|
type Query = undefined;
|
|
@@ -98,7 +99,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
|
|
|
98
99
|
organization.privateMeta.dnsRecords = []
|
|
99
100
|
|
|
100
101
|
if (organization.privateMeta.pendingMailDomain !== null) {
|
|
101
|
-
const defaultFromDomain = "
|
|
102
|
+
const defaultFromDomain = Formatter.slug(STAMHOOFD.platformName) + "." + organization.privateMeta.pendingMailDomain;
|
|
102
103
|
if (organization.privateMeta.pendingRegisterDomain === null || !organization.privateMeta.pendingRegisterDomain.endsWith('.' + organization.privateMeta.pendingMailDomain)) {
|
|
103
104
|
// We set a custom domainname for webshops already
|
|
104
105
|
// This is not used at this moment
|
|
@@ -109,8 +110,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
if (organization.privateMeta.mailFromDomain !== organization.privateMeta.pendingRegisterDomain) {
|
|
112
|
-
|
|
113
|
-
organization.privateMeta.dnsRecords.push(DNSRecord.create({
|
|
113
|
+
organization.privateMeta.dnsRecords.push(DNSRecord.create({
|
|
114
114
|
type: DNSRecordType.CNAME,
|
|
115
115
|
name: organization.privateMeta.mailFromDomain + ".",
|
|
116
116
|
// Use shops for mail domain, to allow reuse
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
2
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
3
3
|
import { BalanceItem, Group, Member } from "@stamhoofd/models";
|
|
4
|
-
import {
|
|
4
|
+
import { BalanceItemWithPayments } from "@stamhoofd/structures";
|
|
5
5
|
|
|
6
6
|
import { Context } from "../../../../helpers/Context";
|
|
7
7
|
|
|
8
8
|
type Params = { id: string };
|
|
9
9
|
type Query = undefined
|
|
10
10
|
type Body = undefined
|
|
11
|
-
type ResponseBody =
|
|
11
|
+
type ResponseBody = BalanceItemWithPayments[]
|
|
12
12
|
|
|
13
13
|
export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
14
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
@@ -42,7 +42,7 @@ export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
42
42
|
const balanceItems = await BalanceItem.balanceItemsForUsersAndMembers(organization.id, member.users.map(u => u.id), [member.id])
|
|
43
43
|
|
|
44
44
|
return new Response(
|
|
45
|
-
await BalanceItem.
|
|
45
|
+
await BalanceItem.getStructureWithPayments(balanceItems)
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
import { Context } from '../../../../helpers/Context';
|
|
6
|
+
import { GetPaymentsEndpoint } from './GetPaymentsEndpoint';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, never>;
|
|
9
|
+
type Query = CountFilteredRequest;
|
|
10
|
+
type Body = undefined;
|
|
11
|
+
type ResponseBody = CountResponse;
|
|
12
|
+
|
|
13
|
+
export class GetPaymentsCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
+
queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>
|
|
15
|
+
|
|
16
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
17
|
+
if (request.method != "GET") {
|
|
18
|
+
return [false];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const params = Endpoint.parseParameters(request.url, "/payments/count", {});
|
|
22
|
+
|
|
23
|
+
if (params) {
|
|
24
|
+
return [true, params as Params];
|
|
25
|
+
}
|
|
26
|
+
return [false];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
|
+
await Context.setOrganizationScope();
|
|
31
|
+
await Context.authenticate()
|
|
32
|
+
const query = await GetPaymentsEndpoint.buildQuery(request.query)
|
|
33
|
+
|
|
34
|
+
const count = await query
|
|
35
|
+
.count();
|
|
36
|
+
|
|
37
|
+
return new Response(
|
|
38
|
+
CountResponse.create({
|
|
39
|
+
count
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|