@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
package/.env.template.json
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"rendererApi": "renderer.stamhoofd"
|
|
23
23
|
},
|
|
24
24
|
"translationNamespace": "stamhoofd",
|
|
25
|
+
"platformName": "stamhoofd",
|
|
25
26
|
"userMode": "organization",
|
|
26
27
|
|
|
27
28
|
"PORT": 9091,
|
|
@@ -59,5 +60,6 @@
|
|
|
59
60
|
|
|
60
61
|
"NOLT_SSO_SECRET_KEY": "",
|
|
61
62
|
"INTERNAL_SECRET_KEY": "",
|
|
62
|
-
"CRONS_DISABLED": false
|
|
63
|
+
"CRONS_DISABLED": false,
|
|
64
|
+
"WHITELISTED_EMAIL_DESTINATIONS": ["*"]
|
|
63
65
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"license": "UNLICENCED",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"dev": "
|
|
12
|
+
"dev": "echo 'Waiting for shared backend packages...' && wait-on ../../shared/env/dist/index.js && echo 'Start building backend API' && concurrently -r 'yarn build --watch --preserveWatchOutput' \"wait-on ./dist/index.js && nodemon --quiet --inspect=5858 --watch dist --watch '../../../shared/*/dist/' --watch '../../shared/*/dist/' --ext .ts,.json,.sql,.js --watch .env.json --delay 1000ms --exec 'node --enable-source-maps ./dist/index.js' --signal SIGTERM\"",
|
|
13
13
|
"dev:backend": "yarn dev",
|
|
14
14
|
"build": "rm -rf ./dist/src/migrations && rm -rf ./dist/src/seeds && tsc -b",
|
|
15
15
|
"build:full": "yarn clear && yarn build",
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"postmark": "4.0.2",
|
|
51
51
|
"stripe": "^16.6.0"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "d1959fa457e81e41797c4f7fe216095505f50aeb"
|
|
54
54
|
}
|
package/src/crons.ts
CHANGED
|
@@ -140,7 +140,7 @@ async function checkWebshopDNS() {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
async function checkReplies() {
|
|
143
|
-
if (STAMHOOFD.environment
|
|
143
|
+
if (STAMHOOFD.environment !== "production") {
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -200,7 +200,7 @@ async function checkReplies() {
|
|
|
200
200
|
let lastPostmarkCheck: Date | null = null
|
|
201
201
|
let lastPostmarkId: string | null = null
|
|
202
202
|
async function checkPostmarkBounces() {
|
|
203
|
-
if (STAMHOOFD.environment
|
|
203
|
+
if (STAMHOOFD.environment !== "production") {
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -842,4 +842,4 @@ export const crons = async () => {
|
|
|
842
842
|
}
|
|
843
843
|
}
|
|
844
844
|
schedulingJobs = false;
|
|
845
|
-
};
|
|
845
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Data, Decoder } from "@simonbackx/simple-encoding";
|
|
2
|
+
|
|
3
|
+
export class StringArrayDecoder<T> implements Decoder<T[]> {
|
|
4
|
+
decoder: Decoder<T>;
|
|
5
|
+
|
|
6
|
+
constructor(decoder: Decoder<T>) {
|
|
7
|
+
this.decoder = decoder;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
decode(data: Data): T[] {
|
|
11
|
+
const strValue = data.string;
|
|
12
|
+
|
|
13
|
+
// Split on comma
|
|
14
|
+
const parts = strValue.split(",");
|
|
15
|
+
return parts
|
|
16
|
+
.map((v, index) => {
|
|
17
|
+
return data.clone({
|
|
18
|
+
data: v,
|
|
19
|
+
context: data.context,
|
|
20
|
+
field: data.addToCurrentField(index)
|
|
21
|
+
}).decode(this.decoder)
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Decoder, Data } from "@simonbackx/simple-encoding";
|
|
2
|
+
|
|
3
|
+
export class StringNullableDecoder<T> implements Decoder<T | null> {
|
|
4
|
+
decoder: Decoder<T>;
|
|
5
|
+
|
|
6
|
+
constructor(decoder: Decoder<T>) {
|
|
7
|
+
this.decoder = decoder;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
decode(data: Data): T | null {
|
|
11
|
+
if (data.value === 'null') {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return data.decode(this.decoder);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -19,6 +19,9 @@ export const filterCompilers: SQLFilterDefinitions = {
|
|
|
19
19
|
id: createSQLExpressionFilterCompiler(
|
|
20
20
|
SQL.column('organizations', 'id')
|
|
21
21
|
),
|
|
22
|
+
uri: createSQLExpressionFilterCompiler(
|
|
23
|
+
SQL.column('organizations', 'uri')
|
|
24
|
+
),
|
|
22
25
|
name: createSQLExpressionFilterCompiler(
|
|
23
26
|
SQL.column('organizations', 'name')
|
|
24
27
|
),
|
|
@@ -160,6 +163,17 @@ const sorters: SQLSortDefinitions<Organization> = {
|
|
|
160
163
|
})
|
|
161
164
|
}
|
|
162
165
|
},
|
|
166
|
+
'uri': {
|
|
167
|
+
getValue(a) {
|
|
168
|
+
return a.uri
|
|
169
|
+
},
|
|
170
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
171
|
+
return new SQLOrderBy({
|
|
172
|
+
column: SQL.column('uri'),
|
|
173
|
+
direction
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
},
|
|
163
177
|
'type': {
|
|
164
178
|
getValue(a) {
|
|
165
179
|
return a.meta.type
|
|
@@ -100,6 +100,7 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
|
|
103
|
+
model.throwIfNotReadyToSend()
|
|
103
104
|
model.send().catch(console.error)
|
|
104
105
|
}
|
|
105
106
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, patchObject, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
3
|
import { Event, Group, Organization, Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
4
|
-
import { Event as EventStruct, GroupType, PermissionLevel } from "@stamhoofd/structures";
|
|
4
|
+
import { Event as EventStruct, GroupType, NamedObject, PermissionLevel } from "@stamhoofd/structures";
|
|
5
5
|
|
|
6
6
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
7
|
import { SQL, SQLWhereSign } from '@stamhoofd/sql';
|
|
@@ -81,6 +81,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
81
81
|
event.endDate = put.endDate
|
|
82
82
|
event.meta = put.meta
|
|
83
83
|
event.typeId = await PatchEventsEndpoint.validateEventType(put.typeId)
|
|
84
|
+
event.meta.organizationCache = eventOrganization ? NamedObject.create({id: eventOrganization.id, name: eventOrganization.name}) : null
|
|
84
85
|
await PatchEventsEndpoint.checkEventLimits(event)
|
|
85
86
|
|
|
86
87
|
if (!(await Context.auth.canAccessEvent(event, PermissionLevel.Full))) {
|
|
@@ -128,6 +129,16 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
128
129
|
event.name = patch.name ?? event.name
|
|
129
130
|
event.startDate = patch.startDate ?? event.startDate
|
|
130
131
|
event.endDate = patch.endDate ?? event.endDate
|
|
132
|
+
|
|
133
|
+
if (patch.meta?.organizationCache) {
|
|
134
|
+
throw new SimpleError({
|
|
135
|
+
code: 'invalid_field',
|
|
136
|
+
message: 'Cannot patch organizationCache',
|
|
137
|
+
human: 'Je kan de organizationCache niet aanpassen via een patch',
|
|
138
|
+
field: 'meta.organizationCache'
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
131
142
|
event.meta = patchObject(event.meta, patch.meta)
|
|
132
143
|
|
|
133
144
|
if (patch.organizationId !== undefined) {
|
|
@@ -156,6 +167,15 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
156
167
|
})
|
|
157
168
|
}
|
|
158
169
|
event.organizationId = patch.organizationId
|
|
170
|
+
event.meta.organizationCache = eventOrganization ? NamedObject.create({id: eventOrganization.id, name: eventOrganization.name}) : null
|
|
171
|
+
} else {
|
|
172
|
+
// Update cache
|
|
173
|
+
if (event.organizationId) {
|
|
174
|
+
const eventOrganization = await Organization.getByID(event.organizationId)
|
|
175
|
+
if (eventOrganization) {
|
|
176
|
+
event.meta.organizationCache = NamedObject.create({id: eventOrganization.id, name: eventOrganization.name})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
159
179
|
}
|
|
160
180
|
|
|
161
181
|
event.typeId = patch.typeId ? (await PatchEventsEndpoint.validateEventType(patch.typeId)) : event.typeId
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { Group, Organization } from '@stamhoofd/models';
|
|
3
|
+
import { GroupsWithOrganizations } from "@stamhoofd/structures";
|
|
4
|
+
|
|
5
|
+
import { AutoEncoder, Decoder, field, StringDecoder } from "@simonbackx/simple-encoding";
|
|
6
|
+
import { Formatter } from "@stamhoofd/utility";
|
|
7
|
+
import { StringArrayDecoder } from "../../../decoders/StringArrayDecoder";
|
|
8
|
+
import { AuthenticatedStructures } from "../../../helpers/AuthenticatedStructures";
|
|
9
|
+
import { Context } from "../../../helpers/Context";
|
|
10
|
+
import { SimpleError } from "@simonbackx/simple-errors";
|
|
11
|
+
type Params = Record<string, never>;
|
|
12
|
+
|
|
13
|
+
class Query extends AutoEncoder {
|
|
14
|
+
@field({ decoder: new StringArrayDecoder(StringDecoder) })
|
|
15
|
+
ids: string[]
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* List of organizations the requester already knows and doesn't need to be included in the response
|
|
19
|
+
*/
|
|
20
|
+
@field({ decoder: new StringArrayDecoder(StringDecoder), optional: true })
|
|
21
|
+
excludeOrganizationIds: string[] = []
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Body = undefined
|
|
25
|
+
type ResponseBody = GroupsWithOrganizations
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the members of the user
|
|
29
|
+
*/
|
|
30
|
+
export class GetGroupsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
31
|
+
queryDecoder = Query as Decoder<Query>
|
|
32
|
+
|
|
33
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
34
|
+
if (request.method != "GET") {
|
|
35
|
+
return [false];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const params = Endpoint.parseParameters(request.url, "/groups", {});
|
|
39
|
+
|
|
40
|
+
if (params) {
|
|
41
|
+
return [true, params as Params];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [false];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
48
|
+
await Context.setOptionalOrganizationScope();
|
|
49
|
+
await Context.optionalAuthenticate()
|
|
50
|
+
|
|
51
|
+
if (request.query.ids.length === 0) {
|
|
52
|
+
return new Response(
|
|
53
|
+
GroupsWithOrganizations.create({
|
|
54
|
+
groups: [],
|
|
55
|
+
organizations: []
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (request.query.ids.length > 100) {
|
|
61
|
+
throw new SimpleError({
|
|
62
|
+
code: "too_many_ids",
|
|
63
|
+
message: "You can't request more than 100 groups at once"
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const groups = await Group.getByIDs(...request.query.ids)
|
|
68
|
+
const organizationIds = Formatter.uniqueArray(groups.map(g => g.organizationId).filter(id => !request.query.excludeOrganizationIds.includes(id)));
|
|
69
|
+
|
|
70
|
+
const organizations = organizationIds.length > 0 ? (await Organization.getByIDs(...organizationIds)) : [];
|
|
71
|
+
|
|
72
|
+
return new Response(
|
|
73
|
+
GroupsWithOrganizations.create({
|
|
74
|
+
groups: await AuthenticatedStructures.groups(groups),
|
|
75
|
+
organizations: await AuthenticatedStructures.organizations(organizations)
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -252,37 +252,6 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
252
252
|
}
|
|
253
253
|
),
|
|
254
254
|
|
|
255
|
-
/**
|
|
256
|
-
* @deprecated?
|
|
257
|
-
*/
|
|
258
|
-
activeRegistrations: createSQLRelationFilterCompiler(
|
|
259
|
-
SQL.select()
|
|
260
|
-
.from(
|
|
261
|
-
SQL.table('registrations')
|
|
262
|
-
).join(
|
|
263
|
-
SQL.join(
|
|
264
|
-
SQL.table('groups')
|
|
265
|
-
).where(
|
|
266
|
-
SQL.column('groups', 'id'),
|
|
267
|
-
SQL.column('registrations', 'groupId')
|
|
268
|
-
)
|
|
269
|
-
)
|
|
270
|
-
.where(
|
|
271
|
-
SQL.column('memberId'),
|
|
272
|
-
SQL.column('members', 'id'),
|
|
273
|
-
).whereNot(
|
|
274
|
-
SQL.column('registeredAt'),
|
|
275
|
-
null,
|
|
276
|
-
).where(
|
|
277
|
-
SQL.column('deactivatedAt'),
|
|
278
|
-
null,
|
|
279
|
-
).where(
|
|
280
|
-
SQL.column('groups', 'deletedAt'),
|
|
281
|
-
null
|
|
282
|
-
),
|
|
283
|
-
registrationFilterCompilers
|
|
284
|
-
),
|
|
285
|
-
|
|
286
255
|
organizations: createSQLRelationFilterCompiler(
|
|
287
256
|
SQL.select()
|
|
288
257
|
.from(
|
|
@@ -2,8 +2,8 @@ import { OneToManyRelation } from '@simonbackx/simple-database';
|
|
|
2
2
|
import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } 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,
|
|
6
|
-
import {
|
|
5
|
+
import { BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Platform, Registration, User } from '@stamhoofd/models';
|
|
6
|
+
import { MemberWithRegistrationsBlob, MembersBlob, PermissionLevel } from "@stamhoofd/structures";
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
9
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
2
|
import { BalanceItem, Member } from "@stamhoofd/models";
|
|
3
|
-
import {
|
|
3
|
+
import { BalanceItemWithPayments } from "@stamhoofd/structures";
|
|
4
4
|
|
|
5
5
|
import { Context } from "../../../helpers/Context";
|
|
6
6
|
|
|
7
7
|
type Params = Record<string, never>;
|
|
8
8
|
type Query = undefined
|
|
9
9
|
type Body = undefined
|
|
10
|
-
type ResponseBody =
|
|
10
|
+
type ResponseBody = BalanceItemWithPayments[]
|
|
11
11
|
|
|
12
12
|
export class GetUserBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
13
13
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
@@ -33,7 +33,7 @@ export class GetUserBalanceEndpoint extends Endpoint<Params, Query, Body, Respon
|
|
|
33
33
|
const balanceItems = await BalanceItem.balanceItemsForUsersAndMembers(organization?.id ?? null, [user.id], members.map(m => m.id))
|
|
34
34
|
|
|
35
35
|
return new Response(
|
|
36
|
-
await BalanceItem.
|
|
36
|
+
await BalanceItem.getStructureWithPayments(balanceItems)
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -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, IDRegisterCheckout, BalanceItemWithPayments, 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;
|
|
@@ -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
|
}
|