@stamhoofd/backend 2.8.0 → 2.9.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 +3 -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 +14 -0
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
- 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 +0 -31
- 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 +114 -34
- 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 +3 -2
- 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/GetPaymentsEndpoint.ts +3 -40
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
- 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 +17 -2
- package/src/helpers/AuthenticatedStructures.ts +16 -6
- package/src/helpers/Context.ts +21 -0
- package/src/helpers/EmailResumer.ts +22 -2
- package/src/seeds/1722344160-update-membership.ts +19 -22
- package/src/seeds/1722344161-sync-member-users.ts +60 -0
|
@@ -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,8 +1,8 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, ObjectData, 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, 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';
|
|
@@ -98,6 +98,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
98
98
|
|
|
99
99
|
if (request.body.privateMeta && request.body.privateMeta.isPatch()) {
|
|
100
100
|
organization.privateMeta.emails = request.body.privateMeta.emails.applyTo(organization.privateMeta.emails)
|
|
101
|
+
organization.privateMeta.premises = patchObject(organization.privateMeta.premises, request.body.privateMeta.premises);
|
|
101
102
|
organization.privateMeta.roles = request.body.privateMeta.roles.applyTo(organization.privateMeta.roles)
|
|
102
103
|
organization.privateMeta.responsibilities = request.body.privateMeta.responsibilities.applyTo(organization.privateMeta.responsibilities)
|
|
103
104
|
organization.privateMeta.inheritedResponsibilityRoles = request.body.privateMeta.inheritedResponsibilityRoles.applyTo(organization.privateMeta.inheritedResponsibilityRoles)
|
|
@@ -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
|
}
|
|
@@ -1,54 +1,17 @@
|
|
|
1
|
-
import { AutoEncoder,
|
|
1
|
+
import { AutoEncoder, DateDecoder, EnumDecoder, field, IntegerDecoder, StringDecoder } from "@simonbackx/simple-encoding";
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
3
|
import { Organization, Payment } from "@stamhoofd/models";
|
|
4
4
|
import { PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus } from "@stamhoofd/structures";
|
|
5
5
|
|
|
6
|
+
import { StringArrayDecoder } from "../../../../decoders/StringArrayDecoder";
|
|
6
7
|
import { AuthenticatedStructures } from "../../../../helpers/AuthenticatedStructures";
|
|
7
8
|
import { Context } from "../../../../helpers/Context";
|
|
9
|
+
import { StringNullableDecoder } from "../../../../decoders/StringNullableDecoder";
|
|
8
10
|
|
|
9
11
|
type Params = Record<string, never>;
|
|
10
12
|
type Body = undefined
|
|
11
13
|
type ResponseBody = PaymentGeneral[]
|
|
12
14
|
|
|
13
|
-
export class StringArrayDecoder<T> implements Decoder<T[]> {
|
|
14
|
-
decoder: Decoder<T>;
|
|
15
|
-
|
|
16
|
-
constructor(decoder: Decoder<T>) {
|
|
17
|
-
this.decoder = decoder;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
decode(data: Data): T[] {
|
|
21
|
-
const strValue = data.string;
|
|
22
|
-
|
|
23
|
-
// Split on comma
|
|
24
|
-
const parts = strValue.split(",");
|
|
25
|
-
return parts
|
|
26
|
-
.map((v, index) => {
|
|
27
|
-
return data.clone({
|
|
28
|
-
data: v,
|
|
29
|
-
context: data.context,
|
|
30
|
-
field: data.addToCurrentField(index)
|
|
31
|
-
}).decode(this.decoder)
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class StringNullableDecoder<T> implements Decoder<T | null> {
|
|
37
|
-
decoder: Decoder<T>;
|
|
38
|
-
|
|
39
|
-
constructor(decoder: Decoder<T>) {
|
|
40
|
-
this.decoder = decoder;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
decode(data: Data): T | null {
|
|
44
|
-
if (data.value === 'null') {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return data.decode(this.decoder);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
15
|
|
|
53
16
|
class Query extends AutoEncoder {
|
|
54
17
|
/**
|
|
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-
|
|
|
3
3
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
4
4
|
import { BalanceItem, Member, Order, Registration, User } from '@stamhoofd/models';
|
|
5
5
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
6
|
-
import { BalanceItemStatus,
|
|
6
|
+
import { BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, PermissionLevel } from "@stamhoofd/structures";
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
9
|
import { Context } from '../../../../helpers/Context';
|
|
@@ -11,11 +11,11 @@ import { Context } from '../../../../helpers/Context';
|
|
|
11
11
|
|
|
12
12
|
type Params = Record<string, never>;
|
|
13
13
|
type Query = undefined;
|
|
14
|
-
type Body = PatchableArrayAutoEncoder<
|
|
15
|
-
type ResponseBody =
|
|
14
|
+
type Body = PatchableArrayAutoEncoder<BalanceItemWithPayments>
|
|
15
|
+
type ResponseBody = BalanceItemWithPayments[]
|
|
16
16
|
|
|
17
17
|
export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
18
|
-
bodyDecoder = new PatchableArrayDecoder(
|
|
18
|
+
bodyDecoder = new PatchableArrayDecoder(BalanceItemWithPayments as Decoder<BalanceItemWithPayments>, BalanceItemWithPayments.patchType() as Decoder<AutoEncoderPatchType<BalanceItemWithPayments>>, StringDecoder)
|
|
19
19
|
|
|
20
20
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
21
21
|
if (request.method != "PATCH") {
|
|
@@ -53,7 +53,10 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
53
53
|
// Create a new balance item
|
|
54
54
|
const model = new BalanceItem();
|
|
55
55
|
model.description = put.description;
|
|
56
|
-
model.
|
|
56
|
+
model.amount = put.amount;
|
|
57
|
+
model.type = BalanceItemType.Other
|
|
58
|
+
model.unitPrice = put.unitPrice;
|
|
59
|
+
model.amount = put.amount;
|
|
57
60
|
model.organizationId = organization.id;
|
|
58
61
|
model.createdAt = put.createdAt;
|
|
59
62
|
model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Pending;
|
|
@@ -75,19 +78,6 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
75
78
|
})
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
if (put.registration) {
|
|
79
|
-
const registration = await Registration.getByID(put.registration.id)
|
|
80
|
-
if (!registration || registration.memberId !== model.memberId || registration.organizationId !== organization.id) {
|
|
81
|
-
throw new SimpleError({
|
|
82
|
-
code: 'invalid_field',
|
|
83
|
-
message: 'Registration not found',
|
|
84
|
-
field: 'registration'
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
model.registrationId = registration.id
|
|
88
|
-
registrationIds.push(registration.id)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
81
|
await model.save();
|
|
92
82
|
returnedModels.push(model);
|
|
93
83
|
}
|
|
@@ -101,6 +91,15 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
101
91
|
message: 'BalanceItem not found'
|
|
102
92
|
})
|
|
103
93
|
}
|
|
94
|
+
|
|
95
|
+
if (patch.unitPrice !== undefined) {
|
|
96
|
+
throw new SimpleError({
|
|
97
|
+
code: 'invalid_field',
|
|
98
|
+
message: 'You cannot change the unit price of a balance item',
|
|
99
|
+
human: 'Het is niet mogelijk om de eenheidsprijs van een openstaande schuld te wijzigen. Je kan de openstaande schuld verwijderen en opnieuw aanmaken indien noodzakelijk.'
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
104
103
|
// Check permissions
|
|
105
104
|
if (model.memberId) {
|
|
106
105
|
// Update old
|
|
@@ -123,30 +122,16 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
123
122
|
model.createdAt = patch.createdAt
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
if (patch.registration) {
|
|
127
|
-
const registration = await Registration.getByID(patch.registration.id)
|
|
128
|
-
if (!registration || registration.memberId !== model.memberId || registration.organizationId !== organization.id) {
|
|
129
|
-
throw new SimpleError({
|
|
130
|
-
code: 'invalid_field',
|
|
131
|
-
message: 'Registration not found',
|
|
132
|
-
field: 'registration'
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
model.registrationId = registration.id
|
|
136
|
-
|
|
137
|
-
// Update new
|
|
138
|
-
registrationIds.push(model.registrationId)
|
|
139
|
-
} else if (patch.registration === null) {
|
|
140
|
-
model.registrationId = null
|
|
141
|
-
}
|
|
142
125
|
model.description = patch.description ?? model.description;
|
|
143
|
-
model.
|
|
126
|
+
model.unitPrice = patch.unitPrice ?? model.unitPrice;
|
|
127
|
+
model.amount = patch.amount ?? model.amount;
|
|
144
128
|
|
|
145
129
|
if (model.orderId) {
|
|
146
130
|
// Not allowed to change this
|
|
147
131
|
const order = await Order.getByID(model.orderId)
|
|
148
132
|
if (order) {
|
|
149
|
-
model.
|
|
133
|
+
model.unitPrice = order.totalToPay
|
|
134
|
+
model.amount = 1
|
|
150
135
|
}
|
|
151
136
|
}
|
|
152
137
|
|
|
@@ -167,7 +152,7 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
167
152
|
await Registration.updateOutstandingBalance(Formatter.uniqueArray(registrationIds), organization.id)
|
|
168
153
|
|
|
169
154
|
return new Response(
|
|
170
|
-
await BalanceItem.
|
|
155
|
+
await BalanceItem.getStructureWithPayments(returnedModels)
|
|
171
156
|
);
|
|
172
157
|
}
|
|
173
158
|
|
|
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-
|
|
|
3
3
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
4
4
|
import { BalanceItem, BalanceItemPayment, Order, Payment, Token, Webshop } from '@stamhoofd/models';
|
|
5
5
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
6
|
-
import { BalanceItemStatus, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, PrivatePayment,Webshop as WebshopStruct } from "@stamhoofd/structures";
|
|
6
|
+
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, PrivatePayment,Webshop as WebshopStruct } from "@stamhoofd/structures";
|
|
7
7
|
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
9
|
|
|
@@ -157,11 +157,21 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
157
157
|
// Create balance item
|
|
158
158
|
const balanceItem = new BalanceItem();
|
|
159
159
|
balanceItem.orderId = order.id;
|
|
160
|
-
balanceItem.
|
|
160
|
+
balanceItem.type = BalanceItemType.Order;
|
|
161
|
+
balanceItem.unitPrice = totalPrice
|
|
161
162
|
balanceItem.description = webshop.meta.name
|
|
162
163
|
balanceItem.pricePaid = 0
|
|
163
164
|
balanceItem.organizationId = organization.id;
|
|
164
165
|
balanceItem.status = BalanceItemStatus.Pending;
|
|
166
|
+
balanceItem.relations = new Map([
|
|
167
|
+
[
|
|
168
|
+
BalanceItemRelationType.Webshop,
|
|
169
|
+
BalanceItemRelation.create({
|
|
170
|
+
id: webshop.id,
|
|
171
|
+
name: webshop.meta.name,
|
|
172
|
+
})
|
|
173
|
+
]
|
|
174
|
+
])
|
|
165
175
|
await balanceItem.save();
|
|
166
176
|
|
|
167
177
|
// Create one balance item payment to pay it in one payment
|
|
@@ -245,7 +255,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
245
255
|
const items = await BalanceItem.where({ orderId: model.id })
|
|
246
256
|
if (items.length >= 1) {
|
|
247
257
|
model.markUpdated()
|
|
248
|
-
items[0].
|
|
258
|
+
items[0].unitPrice = model.totalToPay
|
|
249
259
|
items[0].description = model.generateBalanceDescription(webshop)
|
|
250
260
|
items[0].updateStatus();
|
|
251
261
|
await items[0].save()
|
|
@@ -258,7 +268,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
258
268
|
model.markUpdated()
|
|
259
269
|
const balanceItem = new BalanceItem();
|
|
260
270
|
balanceItem.orderId = model.id;
|
|
261
|
-
balanceItem.
|
|
271
|
+
balanceItem.unitPrice = model.totalToPay
|
|
262
272
|
balanceItem.description = model.generateBalanceDescription(webshop)
|
|
263
273
|
balanceItem.pricePaid = 0
|
|
264
274
|
balanceItem.organizationId = organization.id;
|
|
@@ -6,7 +6,7 @@ import { I18n } from '@stamhoofd/backend-i18n';
|
|
|
6
6
|
import { Email } from '@stamhoofd/email';
|
|
7
7
|
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
|
|
8
8
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
9
|
-
import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct,WebshopAuthType } from "@stamhoofd/structures";
|
|
9
|
+
import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct,WebshopAuthType, BalanceItemType, BalanceItemRelationType, BalanceItemRelation } from "@stamhoofd/structures";
|
|
10
10
|
import { Formatter } from '@stamhoofd/utility';
|
|
11
11
|
|
|
12
12
|
import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
@@ -182,12 +182,22 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
182
182
|
|
|
183
183
|
// Create balance item
|
|
184
184
|
const balanceItem = new BalanceItem();
|
|
185
|
+
balanceItem.type = BalanceItemType.Order;
|
|
185
186
|
balanceItem.orderId = order.id;
|
|
186
|
-
balanceItem.
|
|
187
|
+
balanceItem.unitPrice = totalPrice
|
|
187
188
|
balanceItem.description = webshop.meta.name
|
|
188
189
|
balanceItem.pricePaid = 0
|
|
189
190
|
balanceItem.organizationId = organization.id;
|
|
190
191
|
balanceItem.status = BalanceItemStatus.Hidden;
|
|
192
|
+
balanceItem.relations = new Map([
|
|
193
|
+
[
|
|
194
|
+
BalanceItemRelationType.Webshop,
|
|
195
|
+
BalanceItemRelation.create({
|
|
196
|
+
id: webshop.id,
|
|
197
|
+
name: webshop.meta.name,
|
|
198
|
+
})
|
|
199
|
+
]
|
|
200
|
+
])
|
|
191
201
|
await balanceItem.save();
|
|
192
202
|
|
|
193
203
|
// Create one balance item payment to pay it in one payment
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, PatchMap } from "@simonbackx/simple-encoding"
|
|
2
2
|
import { SimpleError } from "@simonbackx/simple-errors"
|
|
3
3
|
import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, Payment, Registration, User, Webshop } from "@stamhoofd/models"
|
|
4
|
-
import { AccessRight, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from "@stamhoofd/structures"
|
|
4
|
+
import { AccessRight, FinancialSupportSettings, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from "@stamhoofd/structures"
|
|
5
5
|
import { Formatter } from "@stamhoofd/utility"
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -949,8 +949,9 @@ export class AdminPermissionChecker {
|
|
|
949
949
|
|
|
950
950
|
const hasRecordAnswers = !!data.details.recordAnswers;
|
|
951
951
|
const hasNotes = data.details.notes !== undefined;
|
|
952
|
+
const isSetFinancialSupportTrue = data.details.requiresFinancialSupport?.value === true;
|
|
952
953
|
|
|
953
|
-
if(hasRecordAnswers || hasNotes) {
|
|
954
|
+
if(hasRecordAnswers || hasNotes || isSetFinancialSupportTrue) {
|
|
954
955
|
const isUserManager = this.isUserManager(member);
|
|
955
956
|
|
|
956
957
|
if (hasRecordAnswers) {
|
|
@@ -1005,6 +1006,20 @@ export class AdminPermissionChecker {
|
|
|
1005
1006
|
statusCode: 400
|
|
1006
1007
|
})
|
|
1007
1008
|
}
|
|
1009
|
+
|
|
1010
|
+
if(isSetFinancialSupportTrue) {
|
|
1011
|
+
const financialSupport = this.platform.config.recordsConfiguration.financialSupport;
|
|
1012
|
+
const preventSelfAssignment = financialSupport?.preventSelfAssignment === true;
|
|
1013
|
+
|
|
1014
|
+
if(preventSelfAssignment) {
|
|
1015
|
+
throw new SimpleError({
|
|
1016
|
+
code: 'permission_denied',
|
|
1017
|
+
message: 'Je hebt geen toegangsrechten om de financiële status van dit lid aan te passen',
|
|
1018
|
+
human: financialSupport.preventSelfAssignmentText ?? FinancialSupportSettings.defaultPreventSelfAssignmentText,
|
|
1019
|
+
statusCode: 400
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1008
1023
|
}
|
|
1009
1024
|
|
|
1010
1025
|
// Has financial write access?
|
|
@@ -26,7 +26,7 @@ export class AuthenticatedStructures {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const {balanceItemPayments, balanceItems} = await Payment.loadBalanceItems(payments)
|
|
29
|
-
const {registrations, orders
|
|
29
|
+
const {registrations, orders} = await Payment.loadBalanceItemRelations(balanceItems);
|
|
30
30
|
|
|
31
31
|
if (checkPermissions) {
|
|
32
32
|
// Note: permission checking is moved here for performacne to avoid loading the data multiple times
|
|
@@ -41,13 +41,10 @@ export class AuthenticatedStructures {
|
|
|
41
41
|
|
|
42
42
|
const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions
|
|
43
43
|
|
|
44
|
-
return
|
|
44
|
+
return Payment.getGeneralStructureFromRelations({
|
|
45
45
|
payments,
|
|
46
46
|
balanceItemPayments,
|
|
47
|
-
balanceItems
|
|
48
|
-
registrations,
|
|
49
|
-
orders,
|
|
50
|
-
groups
|
|
47
|
+
balanceItems
|
|
51
48
|
}, includeSettlements)
|
|
52
49
|
}
|
|
53
50
|
|
|
@@ -154,6 +151,19 @@ export class AuthenticatedStructures {
|
|
|
154
151
|
})
|
|
155
152
|
}
|
|
156
153
|
|
|
154
|
+
static async organizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
|
|
155
|
+
// for now simple loop
|
|
156
|
+
if (organizations.length > 10) {
|
|
157
|
+
console.warn('Trying to load too many organizations at once: ' + organizations.length)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const structs: OrganizationStruct[] = [];
|
|
161
|
+
for (const organization of organizations) {
|
|
162
|
+
structs.push(await this.organization(organization))
|
|
163
|
+
}
|
|
164
|
+
return structs
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
static async adminOrganizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
|
|
158
168
|
const structs: OrganizationStruct[] = [];
|
|
159
169
|
|
package/src/helpers/Context.ts
CHANGED
|
@@ -60,6 +60,27 @@ export class ContextInstance {
|
|
|
60
60
|
return c;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
static async startForUser<T>(user: User, organization: Organization|null, handler: () => Promise<T>): Promise<T> {
|
|
64
|
+
const request = new Request({
|
|
65
|
+
method: 'GET',
|
|
66
|
+
url: '/',
|
|
67
|
+
host: ''
|
|
68
|
+
})
|
|
69
|
+
const context = new ContextInstance(request);
|
|
70
|
+
|
|
71
|
+
if (organization) {
|
|
72
|
+
context.organization = organization
|
|
73
|
+
context.i18n.switchToLocale({ country: organization.address.country })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
context.user = user
|
|
77
|
+
context.#auth = new AdminPermissionChecker(user, await Platform.getSharedPrivateStruct(), context.organization);
|
|
78
|
+
|
|
79
|
+
return await this.asyncLocalStorage.run(context, async () => {
|
|
80
|
+
return await handler()
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
static async start<T>(request: Request, handler: () => Promise<T>): Promise<T> {
|
|
64
85
|
const context = new ContextInstance(request);
|
|
65
86
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Email } from "@stamhoofd/models";
|
|
1
|
+
import { Email, Organization, User } from "@stamhoofd/models";
|
|
2
2
|
import { SQL } from "@stamhoofd/sql";
|
|
3
3
|
import { EmailStatus } from "@stamhoofd/structures";
|
|
4
|
+
import { ContextInstance } from "./Context";
|
|
4
5
|
|
|
5
6
|
export async function resumeEmails() {
|
|
6
7
|
const query = SQL.select()
|
|
@@ -11,7 +12,26 @@ export async function resumeEmails() {
|
|
|
11
12
|
const emails = Email.fromRows(result, Email.table);
|
|
12
13
|
|
|
13
14
|
for (const email of emails) {
|
|
15
|
+
if (!email.userId) {
|
|
16
|
+
console.warn('Cannot retry sending email because userId is not set - which is required for setting the scope', email.id)
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
14
19
|
console.log('Resuming email that has sending status on boot', email.id);
|
|
15
|
-
|
|
20
|
+
|
|
21
|
+
const user = await User.getByID(email.userId);
|
|
22
|
+
if (!user) {
|
|
23
|
+
console.warn('Cannot retry sending email because user not found', email.id)
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const organization = email.organizationId ? (await Organization.getByID(email.organizationId)) : null;
|
|
28
|
+
if (organization === undefined) {
|
|
29
|
+
console.warn('Cannot retry sending email because organization not found', email.id)
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await ContextInstance.startForUser(user, organization, async () => {
|
|
34
|
+
await email.send()
|
|
35
|
+
})
|
|
16
36
|
}
|
|
17
37
|
}
|
|
@@ -22,33 +22,30 @@ export default new Migration(async () => {
|
|
|
22
22
|
value: id,
|
|
23
23
|
sign: '>'
|
|
24
24
|
}
|
|
25
|
-
}, {limit:
|
|
26
|
-
|
|
27
|
-
// const members = await Member.getByIDs(...rawMembers.map(m => m.id));
|
|
28
|
-
|
|
29
|
-
for (const member of rawMembers) {
|
|
30
|
-
const memberWithRegistrations = await Member.getWithRegistrations(member.id);
|
|
31
|
-
if(memberWithRegistrations) {
|
|
32
|
-
await memberWithRegistrations.updateMemberships();
|
|
33
|
-
await memberWithRegistrations.save();
|
|
34
|
-
} else {
|
|
35
|
-
throw new Error("Member with registrations not found: " + member.id);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
c++;
|
|
39
|
-
|
|
40
|
-
if (c%1000 === 0) {
|
|
41
|
-
process.stdout.write('.');
|
|
42
|
-
}
|
|
43
|
-
if (c%10000 === 0) {
|
|
44
|
-
process.stdout.write('\n');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
25
|
+
}, {limit: 500, sort: ['id']});
|
|
47
26
|
|
|
48
27
|
if (rawMembers.length === 0) {
|
|
49
28
|
break;
|
|
50
29
|
}
|
|
51
30
|
|
|
31
|
+
const promises: Promise<any>[] = [];
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
for (const member of rawMembers) {
|
|
35
|
+
promises.push((async () => {
|
|
36
|
+
await Member.updateMembershipsForId(member.id, true);
|
|
37
|
+
c++;
|
|
38
|
+
|
|
39
|
+
if (c%1000 === 0) {
|
|
40
|
+
process.stdout.write('.');
|
|
41
|
+
}
|
|
42
|
+
if (c%10000 === 0) {
|
|
43
|
+
process.stdout.write('\n');
|
|
44
|
+
}
|
|
45
|
+
})())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await Promise.all(promises);
|
|
52
49
|
id = rawMembers[rawMembers.length - 1].id;
|
|
53
50
|
}
|
|
54
51
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Member } from '@stamhoofd/models';
|
|
3
|
+
import { MemberUserSyncer } from '../helpers/MemberUserSyncer';
|
|
4
|
+
import { logger } from '@simonbackx/simple-logging';
|
|
5
|
+
|
|
6
|
+
export default new Migration(async () => {
|
|
7
|
+
if (STAMHOOFD.environment == "test") {
|
|
8
|
+
console.log("skipped in tests")
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if(STAMHOOFD.userMode !== "platform") {
|
|
13
|
+
console.log("skipped seed update-membership because usermode not platform")
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
process.stdout.write('\n');
|
|
18
|
+
let c = 0;
|
|
19
|
+
let id: string = '';
|
|
20
|
+
|
|
21
|
+
await logger.setContext({tags: ['silent-seed', 'seed']}, async () => {
|
|
22
|
+
while(true) {
|
|
23
|
+
const rawMembers = await Member.where({
|
|
24
|
+
id: {
|
|
25
|
+
value: id,
|
|
26
|
+
sign: '>'
|
|
27
|
+
}
|
|
28
|
+
}, {limit: 500, sort: ['id']});
|
|
29
|
+
|
|
30
|
+
if (rawMembers.length === 0) {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const membersWithRegistrations = await Member.getBlobByIds(...rawMembers.map(m => m.id));
|
|
35
|
+
|
|
36
|
+
const promises: Promise<any>[] = [];
|
|
37
|
+
|
|
38
|
+
for (const memberWithRegistrations of membersWithRegistrations) {
|
|
39
|
+
promises.push((async () => {
|
|
40
|
+
await MemberUserSyncer.onChangeMember(memberWithRegistrations);
|
|
41
|
+
c++;
|
|
42
|
+
|
|
43
|
+
if (c%1000 === 0) {
|
|
44
|
+
process.stdout.write('.');
|
|
45
|
+
}
|
|
46
|
+
if (c%10000 === 0) {
|
|
47
|
+
process.stdout.write('\n');
|
|
48
|
+
}
|
|
49
|
+
})());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await Promise.all(promises);
|
|
53
|
+
id = rawMembers[rawMembers.length - 1].id;
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// Do something here
|
|
59
|
+
return Promise.resolve()
|
|
60
|
+
})
|