@stamhoofd/backend 2.1.3 → 2.3.1
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.json +2 -2
- package/package.json +2 -2
- package/src/endpoints/admin/invoices/GetInvoicesCountEndpoint.ts +47 -0
- package/src/endpoints/admin/invoices/GetInvoicesEndpoint.ts +185 -0
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +88 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +7 -2
- package/src/endpoints/global/members/GetMembersEndpoint.ts +28 -2
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +114 -4
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +9 -2
- package/src/helpers/AuthenticatedStructures.ts +4 -2
package/.env.json
CHANGED
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"rendererApi": "renderer.stamhoofd"
|
|
25
25
|
},
|
|
26
26
|
"translationNamespace": "digit",
|
|
27
|
-
"userMode": "
|
|
27
|
+
"userMode": "platform",
|
|
28
28
|
|
|
29
29
|
"PORT": 9091,
|
|
30
30
|
"DB_HOST": "127.0.0.1",
|
|
31
31
|
"DB_USER": "root",
|
|
32
32
|
"DB_PASS": "root",
|
|
33
|
-
"DB_DATABASE": "stamhoofd",
|
|
33
|
+
"DB_DATABASE": "ksa-stamhoofd",
|
|
34
34
|
|
|
35
35
|
"SMTP_HOST": "0.0.0.0",
|
|
36
36
|
"SMTP_USERNAME": "username",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"postmark": "4.0.2",
|
|
51
51
|
"stripe": "^11.5.0"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "9a2484194d078935d8b50c4aee790196d798435e"
|
|
54
54
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { GetInvoicesEndpoint } from './GetInvoicesEndpoint';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, never>;
|
|
9
|
+
type Query = CountFilteredRequest;
|
|
10
|
+
type Body = undefined;
|
|
11
|
+
type ResponseBody = CountResponse;
|
|
12
|
+
|
|
13
|
+
export class GetInvoicesCountEndpoint 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, "/admin/invoices/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.authenticate()
|
|
31
|
+
|
|
32
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
33
|
+
throw Context.auth.error()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const query = GetInvoicesEndpoint.buildQuery(request.query)
|
|
37
|
+
|
|
38
|
+
const count = await query
|
|
39
|
+
.count();
|
|
40
|
+
|
|
41
|
+
return new Response(
|
|
42
|
+
CountResponse.create({
|
|
43
|
+
count
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
3
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
|
+
import { Organization, Payment, STInvoice } from '@stamhoofd/models';
|
|
6
|
+
import { SQL, SQLFilterDefinitions, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLExpressionFilterCompiler } from "@stamhoofd/sql";
|
|
7
|
+
import { CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, Payment as PaymentStruct, STInvoicePrivate, StamhoofdFilter, getSortFilter } from '@stamhoofd/structures';
|
|
8
|
+
|
|
9
|
+
import { Context } from '../../../helpers/Context';
|
|
10
|
+
|
|
11
|
+
type Params = Record<string, never>;
|
|
12
|
+
type Query = LimitedFilteredRequest;
|
|
13
|
+
type Body = undefined;
|
|
14
|
+
type ResponseBody = PaginatedResponse<STInvoicePrivate[], LimitedFilteredRequest>
|
|
15
|
+
|
|
16
|
+
export const filterCompilers: SQLFilterDefinitions = {
|
|
17
|
+
...baseSQLFilterCompilers,
|
|
18
|
+
id: createSQLExpressionFilterCompiler(
|
|
19
|
+
SQL.column('stamhoofd_invoices', 'id')
|
|
20
|
+
),
|
|
21
|
+
number: createSQLExpressionFilterCompiler(
|
|
22
|
+
SQL.column('stamhoofd_invoices', 'number')
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sorters: SQLSortDefinitions<STInvoice> = {
|
|
27
|
+
'id': {
|
|
28
|
+
getValue(a) {
|
|
29
|
+
return a.id
|
|
30
|
+
},
|
|
31
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
32
|
+
return new SQLOrderBy({
|
|
33
|
+
column: SQL.column('id'),
|
|
34
|
+
direction
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
'number': {
|
|
39
|
+
getValue(a) {
|
|
40
|
+
return a.number
|
|
41
|
+
},
|
|
42
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
43
|
+
return new SQLOrderBy({
|
|
44
|
+
column: SQL.column('number'),
|
|
45
|
+
direction
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class GetInvoicesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
52
|
+
queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>
|
|
53
|
+
|
|
54
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
55
|
+
if (request.method != "GET") {
|
|
56
|
+
return [false];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const params = Endpoint.parseParameters(request.url, "/admin/invoices", {});
|
|
60
|
+
|
|
61
|
+
if (params) {
|
|
62
|
+
return [true, params as Params];
|
|
63
|
+
}
|
|
64
|
+
return [false];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static buildQuery(q: CountFilteredRequest|LimitedFilteredRequest) {
|
|
68
|
+
const query = SQL
|
|
69
|
+
.select(
|
|
70
|
+
SQL.wildcard('stamhoofd_invoices')
|
|
71
|
+
)
|
|
72
|
+
.from(
|
|
73
|
+
SQL.table('stamhoofd_invoices')
|
|
74
|
+
)
|
|
75
|
+
.whereNot(SQL.column('stamhoofd_invoices', 'number'), null);
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if (q.filter) {
|
|
79
|
+
query.where(compileToSQLFilter(q.filter, filterCompilers))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (q.search) {
|
|
83
|
+
let searchFilter: StamhoofdFilter|null = null
|
|
84
|
+
|
|
85
|
+
// todo: auto detect e-mailaddresses and search on admins
|
|
86
|
+
searchFilter = {
|
|
87
|
+
name: {
|
|
88
|
+
$contains: q.search
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (searchFilter) {
|
|
93
|
+
query.where(compileToSQLFilter(searchFilter, filterCompilers))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (q instanceof LimitedFilteredRequest) {
|
|
98
|
+
if (q.pageFilter) {
|
|
99
|
+
query.where(compileToSQLFilter(q.pageFilter, filterCompilers))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
query.orderBy(compileToSQLSorter(q.sort, sorters))
|
|
103
|
+
query.limit(q.limit)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return query
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
110
|
+
await Context.authenticate()
|
|
111
|
+
|
|
112
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
113
|
+
throw Context.auth.error()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
|
|
117
|
+
|
|
118
|
+
if (request.query.limit > maxLimit) {
|
|
119
|
+
throw new SimpleError({
|
|
120
|
+
code: 'invalid_field',
|
|
121
|
+
field: 'limit',
|
|
122
|
+
message: 'Limit can not be more than ' + maxLimit
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (request.query.limit < 1) {
|
|
127
|
+
throw new SimpleError({
|
|
128
|
+
code: 'invalid_field',
|
|
129
|
+
field: 'limit',
|
|
130
|
+
message: 'Limit can not be less than 1'
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const data = await GetInvoicesEndpoint.buildQuery(request.query).fetch()
|
|
135
|
+
const invoices = STInvoice.fromRows(data, 'stamhoofd_invoices');
|
|
136
|
+
|
|
137
|
+
let next: LimitedFilteredRequest|undefined;
|
|
138
|
+
|
|
139
|
+
if (invoices.length >= request.query.limit) {
|
|
140
|
+
const lastObject = invoices[invoices.length - 1];
|
|
141
|
+
const nextFilter = getSortFilter(lastObject, sorters, request.query.sort);
|
|
142
|
+
|
|
143
|
+
next = new LimitedFilteredRequest({
|
|
144
|
+
filter: request.query.filter,
|
|
145
|
+
pageFilter: nextFilter,
|
|
146
|
+
sort: request.query.sort,
|
|
147
|
+
limit: request.query.limit,
|
|
148
|
+
search: request.query.search
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
if (JSON.stringify(nextFilter) === JSON.stringify(request.query.pageFilter)) {
|
|
152
|
+
console.error('Found infinite loading loop for', request.query);
|
|
153
|
+
next = undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Get payments + organizations
|
|
158
|
+
const paymentIds = invoices.flatMap(i => i.paymentId ? [i.paymentId] : [])
|
|
159
|
+
const organizationIds = invoices.flatMap(i => i.organizationId ? [i.organizationId] : [])
|
|
160
|
+
|
|
161
|
+
const payments = await Payment.getByIDs(...paymentIds)
|
|
162
|
+
const organizations = await Organization.getByIDs(...organizationIds)
|
|
163
|
+
|
|
164
|
+
const structures: STInvoicePrivate[] = []
|
|
165
|
+
for (const invoice of invoices) {
|
|
166
|
+
const payment = payments.find(p => p.id === invoice.paymentId)
|
|
167
|
+
const organization = organizations.find(p => p.id === invoice.organizationId)
|
|
168
|
+
structures.push(
|
|
169
|
+
STInvoicePrivate.create({
|
|
170
|
+
...invoice,
|
|
171
|
+
payment: payment ? PaymentStruct.create(payment) : null,
|
|
172
|
+
organization: organization ? (await organization.getStructure({emptyGroups: true})) : undefined,
|
|
173
|
+
settlement: payment?.settlement ?? null,
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return new Response(
|
|
179
|
+
new PaginatedResponse<STInvoicePrivate[], LimitedFilteredRequest>({
|
|
180
|
+
results: structures,
|
|
181
|
+
next
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { SQL, SQLAlias, SQLSum, SQLCount, SQLDistinct, SQLSelectAs } from '@stamhoofd/sql';
|
|
3
|
+
import { ChargeMembershipsSummary } from '@stamhoofd/structures';
|
|
4
|
+
import { Context } from '../../../helpers/Context';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
type Params = Record<string, never>;
|
|
8
|
+
type Query = Record<string, never>;
|
|
9
|
+
type Body = undefined;
|
|
10
|
+
type ResponseBody = ChargeMembershipsSummary;
|
|
11
|
+
|
|
12
|
+
export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
13
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
14
|
+
if (request.method != "GET") {
|
|
15
|
+
return [false];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const params = Endpoint.parseParameters(request.url, "/admin/charge-memberships/summary", {});
|
|
19
|
+
|
|
20
|
+
if (params) {
|
|
21
|
+
return [true, params as Params];
|
|
22
|
+
}
|
|
23
|
+
return [false];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
27
|
+
await Context.authenticate()
|
|
28
|
+
|
|
29
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
30
|
+
throw Context.auth.error()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const query = SQL
|
|
34
|
+
.select(
|
|
35
|
+
new SQLSelectAs(
|
|
36
|
+
new SQLCount(
|
|
37
|
+
new SQLDistinct(
|
|
38
|
+
SQL.column('member_platform_memberships', 'id')
|
|
39
|
+
)
|
|
40
|
+
),
|
|
41
|
+
new SQLAlias('data__memberships')
|
|
42
|
+
),
|
|
43
|
+
new SQLSelectAs(
|
|
44
|
+
new SQLCount(
|
|
45
|
+
new SQLDistinct(
|
|
46
|
+
SQL.column('member_platform_memberships', 'memberId')
|
|
47
|
+
)
|
|
48
|
+
),
|
|
49
|
+
new SQLAlias('data__members')
|
|
50
|
+
),
|
|
51
|
+
new SQLSelectAs(
|
|
52
|
+
new SQLCount(
|
|
53
|
+
new SQLDistinct(
|
|
54
|
+
SQL.column('member_platform_memberships', 'organizationId')
|
|
55
|
+
)
|
|
56
|
+
),
|
|
57
|
+
new SQLAlias('data__organizations')
|
|
58
|
+
),
|
|
59
|
+
new SQLSelectAs(
|
|
60
|
+
new SQLSum(
|
|
61
|
+
SQL.column('member_platform_memberships', 'price')
|
|
62
|
+
),
|
|
63
|
+
new SQLAlias('data__price')
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
.from(
|
|
67
|
+
SQL.table('member_platform_memberships')
|
|
68
|
+
)
|
|
69
|
+
.where(SQL.column('invoiceId'), null)
|
|
70
|
+
.andWhere(SQL.column('invoiceItemDetailId'), null);
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
const result = await query.fetch();
|
|
74
|
+
const members = result[0]['data']['members'] as number;
|
|
75
|
+
const memberships = result[0]['data']['memberships'] as number;
|
|
76
|
+
const organizations = result[0]['data']['organizations'] as number;
|
|
77
|
+
const price = result[0]['data']['price'] as number;
|
|
78
|
+
|
|
79
|
+
return new Response(
|
|
80
|
+
ChargeMembershipsSummary.create({
|
|
81
|
+
memberships: memberships ?? 0,
|
|
82
|
+
members: members ?? 0,
|
|
83
|
+
price: price ?? 0,
|
|
84
|
+
organizations: organizations ?? 0
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -22,6 +22,9 @@ export const filterCompilers: SQLFilterDefinitions = {
|
|
|
22
22
|
name: createSQLExpressionFilterCompiler(
|
|
23
23
|
SQL.column('organizations', 'name')
|
|
24
24
|
),
|
|
25
|
+
active: createSQLExpressionFilterCompiler(
|
|
26
|
+
SQL.column('organizations', 'active')
|
|
27
|
+
),
|
|
25
28
|
city: createSQLExpressionFilterCompiler(
|
|
26
29
|
SQL.jsonValue(SQL.column('organizations', 'address'), '$.value.city'),
|
|
27
30
|
undefined,
|
|
@@ -271,11 +274,13 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
271
274
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
272
275
|
await Context.authenticate()
|
|
273
276
|
|
|
274
|
-
|
|
277
|
+
const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
|
|
278
|
+
|
|
279
|
+
if (request.query.limit > maxLimit) {
|
|
275
280
|
throw new SimpleError({
|
|
276
281
|
code: 'invalid_field',
|
|
277
282
|
field: 'limit',
|
|
278
|
-
message: 'Limit can not be more than
|
|
283
|
+
message: 'Limit can not be more than ' + maxLimit
|
|
279
284
|
})
|
|
280
285
|
}
|
|
281
286
|
|
|
@@ -136,6 +136,7 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
136
136
|
SQL.column('members', 'id'),
|
|
137
137
|
),
|
|
138
138
|
{
|
|
139
|
+
...baseSQLFilterCompilers,
|
|
139
140
|
// Alias for responsibilityId
|
|
140
141
|
"id": createSQLColumnFilterCompiler(SQL.column('member_responsibility_records', 'responsibilityId')),
|
|
141
142
|
"responsibilityId": createSQLColumnFilterCompiler(SQL.column('member_responsibility_records', 'responsibilityId')),
|
|
@@ -145,6 +146,29 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
145
146
|
}
|
|
146
147
|
),
|
|
147
148
|
|
|
149
|
+
platformMemberships: createSQLRelationFilterCompiler(
|
|
150
|
+
SQL.select()
|
|
151
|
+
.from(
|
|
152
|
+
SQL.table('member_platform_memberships')
|
|
153
|
+
)
|
|
154
|
+
.where(
|
|
155
|
+
SQL.column('memberId'),
|
|
156
|
+
SQL.column('members', 'id'),
|
|
157
|
+
),
|
|
158
|
+
{
|
|
159
|
+
...baseSQLFilterCompilers,
|
|
160
|
+
"id": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'id')),
|
|
161
|
+
"membershipTypeId": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'membershipTypeId')),
|
|
162
|
+
"organizationId": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'organizationId')),
|
|
163
|
+
"periodId": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'periodId')),
|
|
164
|
+
"price": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'price')),
|
|
165
|
+
"invoiceId": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'invoiceId')),
|
|
166
|
+
"startDate": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'startDate')),
|
|
167
|
+
"endDate": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'endDate')),
|
|
168
|
+
"expireDate": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'expireDate')),
|
|
169
|
+
}
|
|
170
|
+
),
|
|
171
|
+
|
|
148
172
|
/**
|
|
149
173
|
* @deprecated?
|
|
150
174
|
*/
|
|
@@ -388,11 +412,13 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
388
412
|
await Context.setOptionalOrganizationScope();
|
|
389
413
|
await Context.authenticate()
|
|
390
414
|
|
|
391
|
-
|
|
415
|
+
const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
|
|
416
|
+
|
|
417
|
+
if (request.query.limit > maxLimit) {
|
|
392
418
|
throw new SimpleError({
|
|
393
419
|
code: 'invalid_field',
|
|
394
420
|
field: 'limit',
|
|
395
|
-
message: 'Limit can not be more than
|
|
421
|
+
message: 'Limit can not be more than ' + maxLimit
|
|
396
422
|
})
|
|
397
423
|
}
|
|
398
424
|
|
|
@@ -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, BalanceItemPayment, Document, Group, Member, MemberFactory, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Payment, Platform, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
|
|
6
|
-
import { BalanceItemStatus, MemberWithRegistrationsBlob, MembersBlob, PaymentMethod, PaymentStatus, PermissionLevel, Registration as RegistrationStruct, User as UserStruct } from "@stamhoofd/structures";
|
|
5
|
+
import { BalanceItem, MemberPlatformMembership, BalanceItemPayment, Document, Group, Member, MemberFactory, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Payment, Platform, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
|
|
6
|
+
import { BalanceItemStatus, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, PaymentMethod, PaymentStatus, PermissionLevel, Registration as RegistrationStruct, User as UserStruct } from "@stamhoofd/structures";
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
9
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -52,6 +52,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
52
52
|
|
|
53
53
|
const members: MemberWithRegistrations[] = []
|
|
54
54
|
|
|
55
|
+
const platform = await Platform.getShared()
|
|
56
|
+
|
|
55
57
|
// Cache
|
|
56
58
|
const groups: Group[] = []
|
|
57
59
|
|
|
@@ -71,6 +73,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
71
73
|
|
|
72
74
|
const balanceItemMemberIds: string[] = []
|
|
73
75
|
const balanceItemRegistrationIdsPerOrganization: Map<string, string[]> = new Map()
|
|
76
|
+
const updateMembershipMemberIds = new Set<string>()
|
|
74
77
|
|
|
75
78
|
function addBalanceItemRegistrationId(organizationId: string, registrationId: string) {
|
|
76
79
|
const existing = balanceItemRegistrationIdsPerOrganization.get(organizationId);
|
|
@@ -169,6 +172,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
169
172
|
await member.save()
|
|
170
173
|
members.push(member)
|
|
171
174
|
balanceItemMemberIds.push(member.id)
|
|
175
|
+
updateMembershipMemberIds.add(member.id)
|
|
172
176
|
|
|
173
177
|
// Add registrations
|
|
174
178
|
for (const registrationStruct of struct.registrations) {
|
|
@@ -232,7 +236,12 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
let group: Group | null = null
|
|
235
|
-
|
|
239
|
+
|
|
240
|
+
console.log('Patch registration', patchRegistration)
|
|
241
|
+
|
|
242
|
+
if (patchRegistration.group) {
|
|
243
|
+
patchRegistration.groupId = patchRegistration.group.id
|
|
244
|
+
}
|
|
236
245
|
|
|
237
246
|
if (patchRegistration.groupId) {
|
|
238
247
|
group = await getGroup(patchRegistration.groupId)
|
|
@@ -291,6 +300,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
291
300
|
}
|
|
292
301
|
registration.cycle = patchRegistration.cycle ?? registration.cycle
|
|
293
302
|
registration.groupId = patchRegistration.groupId ?? registration.groupId
|
|
303
|
+
registration.group = group
|
|
294
304
|
registration.organizationId = patchRegistration.organizationId ?? registration.organizationId
|
|
295
305
|
|
|
296
306
|
// Check if we should create a placeholder payment?
|
|
@@ -338,6 +348,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
338
348
|
}
|
|
339
349
|
|
|
340
350
|
await registration.save()
|
|
351
|
+
updateMembershipMemberIds.add(member.id)
|
|
341
352
|
}
|
|
342
353
|
|
|
343
354
|
for (const deleteId of patch.registrations.getDeletes()) {
|
|
@@ -362,7 +373,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
362
373
|
})
|
|
363
374
|
}
|
|
364
375
|
|
|
365
|
-
balanceItemMemberIds.push(member.id)
|
|
376
|
+
balanceItemMemberIds.push(member.id)
|
|
377
|
+
updateMembershipMemberIds.add(member.id)
|
|
366
378
|
await BalanceItem.deleteForDeletedRegistration(registration.id)
|
|
367
379
|
await registration.delete()
|
|
368
380
|
member.registrations = member.registrations.filter(r => r.id !== deleteId)
|
|
@@ -391,6 +403,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
391
403
|
|
|
392
404
|
const reg = await this.addRegistration(member, struct, group)
|
|
393
405
|
balanceItemMemberIds.push(member.id)
|
|
406
|
+
updateMembershipMemberIds.add(member.id)
|
|
394
407
|
addBalanceItemRegistrationId(reg.organizationId, reg.id)
|
|
395
408
|
|
|
396
409
|
// We need to update this group occupancy because we moved one member away from it
|
|
@@ -506,6 +519,97 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
506
519
|
await PatchOrganizationMembersEndpoint.updateManagers(member)
|
|
507
520
|
}
|
|
508
521
|
|
|
522
|
+
// Add platform memberships
|
|
523
|
+
for (const {put} of patch.platformMemberships.getPuts()) {
|
|
524
|
+
if (put.periodId !== platform.periodId) {
|
|
525
|
+
throw new SimpleError({
|
|
526
|
+
code: "invalid_field",
|
|
527
|
+
message: "Invalid period",
|
|
528
|
+
human: "Je kan geen aansluitingen maken voor een andere werkjaar dan het actieve werkjaar",
|
|
529
|
+
field: "periodId"
|
|
530
|
+
})
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (organization && put.organizationId !== organization.id) {
|
|
534
|
+
throw new SimpleError({
|
|
535
|
+
code: "invalid_field",
|
|
536
|
+
message: "Invalid organization",
|
|
537
|
+
human: "Je kan geen aansluitingen maken voor een andere vereniging",
|
|
538
|
+
field: "organizationId"
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (!await Context.auth.hasFullAccess(put.organizationId)) {
|
|
543
|
+
throw Context.auth.error("Je hebt niet voldoende rechten om deze aansluiting toe te voegen")
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!platform.config.membershipTypes.find(t => t.id === put.membershipTypeId)) {
|
|
547
|
+
throw new SimpleError({
|
|
548
|
+
code: "invalid_field",
|
|
549
|
+
field: "membershipTypeId",
|
|
550
|
+
message: "Invalid membership type",
|
|
551
|
+
human: "Dit aansluitingstype bestaat niet"
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Check duplicate memberships
|
|
556
|
+
|
|
557
|
+
// Check dates
|
|
558
|
+
|
|
559
|
+
// Calculate prices
|
|
560
|
+
|
|
561
|
+
const membership = new MemberPlatformMembership()
|
|
562
|
+
membership.id = put.id
|
|
563
|
+
membership.memberId = member.id
|
|
564
|
+
membership.membershipTypeId = put.membershipTypeId
|
|
565
|
+
membership.organizationId = put.organizationId
|
|
566
|
+
membership.periodId = put.periodId
|
|
567
|
+
|
|
568
|
+
membership.startDate = put.startDate
|
|
569
|
+
membership.endDate = put.endDate
|
|
570
|
+
membership.expireDate = put.expireDate
|
|
571
|
+
|
|
572
|
+
await membership.calculatePrice()
|
|
573
|
+
await membership.save()
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Delete platform memberships
|
|
577
|
+
for (const id of patch.platformMemberships.getDeletes()) {
|
|
578
|
+
const membership = await MemberPlatformMembership.getByID(id)
|
|
579
|
+
|
|
580
|
+
if (!membership || membership.memberId !== member.id) {
|
|
581
|
+
throw new SimpleError({
|
|
582
|
+
code: "invalid_field",
|
|
583
|
+
field: "id",
|
|
584
|
+
message: "Invalid id",
|
|
585
|
+
human: "Deze aansluiting bestaat niet"
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (!await Context.auth.hasFullAccess(membership.organizationId)) {
|
|
590
|
+
throw Context.auth.error("Je hebt niet voldoende rechten om deze aansluiting te verwijderen")
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (membership.periodId !== platform.periodId) {
|
|
594
|
+
throw new SimpleError({
|
|
595
|
+
code: "invalid_field",
|
|
596
|
+
message: "Invalid period",
|
|
597
|
+
human: "Je kan geen aansluitingen meer verwijderen voor een ander werkjaar dan het actieve werkjaar",
|
|
598
|
+
field: "periodId"
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (membership.invoiceId || membership.invoiceItemDetailId) {
|
|
603
|
+
throw new SimpleError({
|
|
604
|
+
code: "invalid_field",
|
|
605
|
+
message: "Invalid invoice",
|
|
606
|
+
human: "Je kan geen aansluiting verwijderen die al werd gefactureerd",
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
await membership.delete()
|
|
611
|
+
}
|
|
612
|
+
|
|
509
613
|
if (!members.find(m => m.id === member.id)) {
|
|
510
614
|
members.push(member)
|
|
511
615
|
}
|
|
@@ -553,6 +657,12 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
553
657
|
}
|
|
554
658
|
}
|
|
555
659
|
|
|
660
|
+
for (const member of members) {
|
|
661
|
+
if (updateMembershipMemberIds.has(member.id)) {
|
|
662
|
+
await member.updateMemberships()
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
556
666
|
return new Response(
|
|
557
667
|
await AuthenticatedStructures.membersBlob(members)
|
|
558
668
|
);
|
|
@@ -200,11 +200,17 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
200
200
|
|
|
201
201
|
if (item.waitingList) {
|
|
202
202
|
registration.waitingList = true
|
|
203
|
+
registration.canRegister = false
|
|
203
204
|
registration.reservedUntil = null
|
|
204
205
|
await registration.save()
|
|
205
206
|
} else {
|
|
206
|
-
registration.waitingList
|
|
207
|
-
|
|
207
|
+
if (registration.waitingList && registration.canRegister) {
|
|
208
|
+
// Keep data: otherwise people cannot retry if the payment fails
|
|
209
|
+
// We'll mark the registration as valid after the payment
|
|
210
|
+
} else {
|
|
211
|
+
registration.waitingList = false
|
|
212
|
+
registration.canRegister = false
|
|
213
|
+
}
|
|
208
214
|
registration.price = item.calculatedPrice
|
|
209
215
|
payRegistrations.push({
|
|
210
216
|
registration,
|
|
@@ -286,6 +292,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
286
292
|
// registration.paymentId = payment.id
|
|
287
293
|
|
|
288
294
|
registration.reservedUntil = null
|
|
295
|
+
registration.canRegister = false
|
|
289
296
|
|
|
290
297
|
if (payment.method == PaymentMethod.Transfer || payment.method == PaymentMethod.PointOfSale || payment.status == PaymentStatus.Succeeded) {
|
|
291
298
|
await registration.markValid()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
2
|
-
import { Group, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from "@stamhoofd/models";
|
|
3
|
-
import {
|
|
2
|
+
import { Group, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from "@stamhoofd/models";
|
|
3
|
+
import { MemberPlatformMembership as MemberPlatformMembershipStruct, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, MemberWithRegistrationsBlob, MembersBlob, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, User as UserStruct, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { Context } from "./Context";
|
|
6
6
|
|
|
@@ -145,9 +145,11 @@ export class AuthenticatedStructures {
|
|
|
145
145
|
|
|
146
146
|
// Load responsibilities
|
|
147
147
|
const responsibilities = members.length > 0 ? await MemberResponsibilityRecord.where({ memberId: { sign: 'IN', value: members.map(m => m.id) } }) : []
|
|
148
|
+
const platformMemberships = members.length > 0 ? await MemberPlatformMembership.where({ memberId: { sign: 'IN', value: members.map(m => m.id) } }) : []
|
|
148
149
|
|
|
149
150
|
for (const blob of memberBlobs) {
|
|
150
151
|
blob.responsibilities = responsibilities.filter(r => r.memberId == blob.id).map(r => MemberResponsibilityRecordStruct.create(r))
|
|
152
|
+
blob.platformMemberships = platformMemberships.filter(r => r.memberId == blob.id).map(r => MemberPlatformMembershipStruct.create(r))
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
return MembersBlob.create({
|