@stamhoofd/backend 2.25.2 → 2.27.2
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/LICENSE +6 -2
- package/package.json +10 -10
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +12 -3
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -1
- package/src/endpoints/global/events/GetEventsEndpoint.ts +8 -71
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +5 -3
- package/src/endpoints/global/members/GetMembersEndpoint.ts +2 -1
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +16 -4
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +43 -12
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +19 -1
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +4 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +2 -5
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +71 -9
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +9 -0
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +9 -0
- package/src/helpers/AdminPermissionChecker.ts +1 -1
- package/src/helpers/MemberUserSyncer.ts +22 -11
- package/src/helpers/MembershipCharger.ts +2 -0
- package/src/helpers/MembershipHelper.ts +55 -0
- package/src/helpers/PeriodHelper.ts +39 -3
- package/src/helpers/SetupStepsUpdater.ts +1 -1
- package/src/helpers/StripeHelper.ts +23 -2
- package/src/seeds/1722344162-update-membership.ts +17 -0
- package/src/sql-filters/events.ts +26 -0
- package/src/sql-sorters/events.ts +58 -0
- package/src/seeds/1722344160-update-membership.ts +0 -54
package/LICENSE
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
This license applies to the entire repository except for subfolders that
|
|
2
|
+
have their own license file. In such cases, the license file in the
|
|
3
|
+
subfolder takes precedence.
|
|
4
|
+
|
|
5
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
6
|
Version 3, 19 November 2007
|
|
3
7
|
|
|
4
8
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
@@ -658,4 +662,4 @@ specific requirements.
|
|
|
658
662
|
You should also get your employer (if you work as a programmer) or school,
|
|
659
663
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
660
664
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
661
|
-
<https://www.gnu.org/licenses/>.
|
|
665
|
+
<https://www.gnu.org/licenses/>.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.27.2",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"@simonbackx/simple-encoding": "2.15.1",
|
|
37
37
|
"@simonbackx/simple-endpoints": "1.14.0",
|
|
38
38
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
39
|
-
"@stamhoofd/backend-i18n": "2.
|
|
40
|
-
"@stamhoofd/backend-middleware": "2.
|
|
41
|
-
"@stamhoofd/email": "2.
|
|
42
|
-
"@stamhoofd/models": "2.
|
|
43
|
-
"@stamhoofd/queues": "2.
|
|
44
|
-
"@stamhoofd/sql": "2.
|
|
45
|
-
"@stamhoofd/structures": "2.
|
|
46
|
-
"@stamhoofd/utility": "2.
|
|
39
|
+
"@stamhoofd/backend-i18n": "2.27.2",
|
|
40
|
+
"@stamhoofd/backend-middleware": "2.27.2",
|
|
41
|
+
"@stamhoofd/email": "2.27.2",
|
|
42
|
+
"@stamhoofd/models": "2.27.2",
|
|
43
|
+
"@stamhoofd/queues": "2.27.2",
|
|
44
|
+
"@stamhoofd/sql": "2.27.2",
|
|
45
|
+
"@stamhoofd/structures": "2.27.2",
|
|
46
|
+
"@stamhoofd/utility": "2.27.2",
|
|
47
47
|
"archiver": "^7.0.1",
|
|
48
48
|
"aws-sdk": "^2.885.0",
|
|
49
49
|
"axios": "1.6.8",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"postmark": "4.0.2",
|
|
61
61
|
"stripe": "^16.6.0"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "4c7804970b02fa339d884ace0105a6efc6dab179"
|
|
64
64
|
}
|
|
@@ -3,6 +3,8 @@ import { SQL, SQLAlias, SQLCount, SQLDistinct, SQLSelectAs, SQLSum } from '@stam
|
|
|
3
3
|
import { ChargeMembershipsSummary, ChargeMembershipsTypeSummary } from '@stamhoofd/structures';
|
|
4
4
|
import { Context } from '../../../helpers/Context';
|
|
5
5
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
6
|
+
import { Platform } from '@stamhoofd/models';
|
|
7
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
type Params = Record<string, never>;
|
|
@@ -39,6 +41,9 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
|
|
|
39
41
|
);
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
const platform = await Platform.getShared()
|
|
45
|
+
const chargeVia = platform.membershipOrganizationId
|
|
46
|
+
|
|
42
47
|
const query = SQL
|
|
43
48
|
.select(
|
|
44
49
|
new SQLSelectAs(
|
|
@@ -73,7 +78,9 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
|
|
|
73
78
|
)
|
|
74
79
|
)
|
|
75
80
|
.from('member_platform_memberships')
|
|
76
|
-
.where('balanceItemId', null)
|
|
81
|
+
.where('balanceItemId', null)
|
|
82
|
+
.where('deletedAt', null)
|
|
83
|
+
.whereNot('organizationId', chargeVia)
|
|
77
84
|
|
|
78
85
|
const result = await query.fetch();
|
|
79
86
|
const members = result[0]['data']['members'] as number;
|
|
@@ -88,12 +95,12 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
|
|
|
88
95
|
members: members ?? 0,
|
|
89
96
|
price: price ?? 0,
|
|
90
97
|
organizations: organizations ?? 0,
|
|
91
|
-
membershipsPerType: await this.fetchPerType()
|
|
98
|
+
membershipsPerType: await this.fetchPerType(chargeVia)
|
|
92
99
|
})
|
|
93
100
|
);
|
|
94
101
|
}
|
|
95
102
|
|
|
96
|
-
async fetchPerType() {
|
|
103
|
+
async fetchPerType(chargeVia: string|null) {
|
|
97
104
|
const query = SQL
|
|
98
105
|
.select(
|
|
99
106
|
SQL.column('member_platform_memberships', 'membershipTypeId'),
|
|
@@ -130,6 +137,8 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
|
|
|
130
137
|
)
|
|
131
138
|
.from('member_platform_memberships')
|
|
132
139
|
.where('balanceItemId', null)
|
|
140
|
+
.where('deletedAt', null)
|
|
141
|
+
.whereNot('organizationId', chargeVia)
|
|
133
142
|
.groupBy(
|
|
134
143
|
SQL.column('member_platform_memberships', 'membershipTypeId')
|
|
135
144
|
);
|
|
@@ -4,7 +4,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { Organization } from '@stamhoofd/models';
|
|
6
6
|
import { SQL, compileToSQLFilter, compileToSQLSorter } from "@stamhoofd/sql";
|
|
7
|
-
import { CountFilteredRequest, LimitedFilteredRequest, Organization as OrganizationStruct, PaginatedResponse, PermissionLevel, StamhoofdFilter, getSortFilter } from '@stamhoofd/structures';
|
|
7
|
+
import { CountFilteredRequest, LimitedFilteredRequest, Organization as OrganizationStruct, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
8
8
|
|
|
9
9
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
10
10
|
import { Context } from '../../../helpers/Context';
|
|
@@ -88,6 +88,7 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
88
88
|
query.where(compileToSQLFilter(q.pageFilter, filterCompilers))
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
q.sort = assertSort(q.sort, [{key: 'id'}])
|
|
91
92
|
query.orderBy(compileToSQLSorter(q.sort, sorters))
|
|
92
93
|
query.limit(q.limit)
|
|
93
94
|
}
|
|
@@ -3,85 +3,21 @@ import { Decoder } from '@simonbackx/simple-encoding';
|
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { Event } from '@stamhoofd/models';
|
|
6
|
-
import { SQL, SQLFilterDefinitions,
|
|
7
|
-
import { CountFilteredRequest, Event as EventStruct, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, getSortFilter } from '@stamhoofd/structures';
|
|
8
|
-
import { Formatter } from '@stamhoofd/utility';
|
|
6
|
+
import { SQL, SQLFilterDefinitions, SQLSortDefinitions, compileToSQLFilter, compileToSQLSorter } from "@stamhoofd/sql";
|
|
7
|
+
import { CountFilteredRequest, Event as EventStruct, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
9
8
|
|
|
10
9
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
11
10
|
import { Context } from '../../../helpers/Context';
|
|
11
|
+
import { eventFilterCompilers } from '../../../sql-filters/events';
|
|
12
|
+
import { eventSorters } from '../../../sql-sorters/events';
|
|
12
13
|
|
|
13
14
|
type Params = Record<string, never>;
|
|
14
15
|
type Query = LimitedFilteredRequest;
|
|
15
16
|
type Body = undefined;
|
|
16
17
|
type ResponseBody = PaginatedResponse<EventStruct[], LimitedFilteredRequest>
|
|
17
18
|
|
|
18
|
-
const filterCompilers: SQLFilterDefinitions =
|
|
19
|
-
|
|
20
|
-
id: createSQLColumnFilterCompiler('id'),
|
|
21
|
-
name: createSQLColumnFilterCompiler('name'),
|
|
22
|
-
organizationId: createSQLColumnFilterCompiler('organizationId'),
|
|
23
|
-
startDate: createSQLColumnFilterCompiler('startDate'),
|
|
24
|
-
endDate: createSQLColumnFilterCompiler('endDate'),
|
|
25
|
-
groupIds: createSQLExpressionFilterCompiler(
|
|
26
|
-
SQL.jsonValue(SQL.column('meta'), '$.value.groups[*].id'),
|
|
27
|
-
{isJSONValue: true, isJSONObject: true}
|
|
28
|
-
),
|
|
29
|
-
defaultAgeGroupIds: createSQLExpressionFilterCompiler(
|
|
30
|
-
SQL.jsonValue(SQL.column('meta'), '$.value.defaultAgeGroupIds'),
|
|
31
|
-
{isJSONValue: true, isJSONObject: true}
|
|
32
|
-
),
|
|
33
|
-
organizationTagIds: createSQLExpressionFilterCompiler(
|
|
34
|
-
SQL.jsonValue(SQL.column('meta'), '$.value.organizationTagIds'),
|
|
35
|
-
{isJSONValue: true, isJSONObject: true}
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const sorters: SQLSortDefinitions<Event> = {
|
|
40
|
-
'id': {
|
|
41
|
-
getValue(a) {
|
|
42
|
-
return a.id
|
|
43
|
-
},
|
|
44
|
-
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
45
|
-
return new SQLOrderBy({
|
|
46
|
-
column: SQL.column('id'),
|
|
47
|
-
direction
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
'name': {
|
|
52
|
-
getValue(a) {
|
|
53
|
-
return a.name
|
|
54
|
-
},
|
|
55
|
-
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
56
|
-
return new SQLOrderBy({
|
|
57
|
-
column: SQL.column('name'),
|
|
58
|
-
direction
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
'startDate': {
|
|
63
|
-
getValue(a) {
|
|
64
|
-
return Formatter.dateTimeIso(a.startDate)
|
|
65
|
-
},
|
|
66
|
-
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
67
|
-
return new SQLOrderBy({
|
|
68
|
-
column: SQL.column('startDate'),
|
|
69
|
-
direction
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
'endDate': {
|
|
74
|
-
getValue(a) {
|
|
75
|
-
return Formatter.dateTimeIso(a.endDate)
|
|
76
|
-
},
|
|
77
|
-
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
78
|
-
return new SQLOrderBy({
|
|
79
|
-
column: SQL.column('endDate'),
|
|
80
|
-
direction
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
}
|
|
19
|
+
const filterCompilers: SQLFilterDefinitions = eventFilterCompilers
|
|
20
|
+
const sorters: SQLSortDefinitions<Event> = eventSorters
|
|
85
21
|
|
|
86
22
|
export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
87
23
|
queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>
|
|
@@ -112,7 +48,7 @@ export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
112
48
|
{
|
|
113
49
|
organizationId: null
|
|
114
50
|
}
|
|
115
|
-
]
|
|
51
|
+
],
|
|
116
52
|
};
|
|
117
53
|
}
|
|
118
54
|
|
|
@@ -152,6 +88,7 @@ export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
152
88
|
query.where(compileToSQLFilter(q.pageFilter, filterCompilers))
|
|
153
89
|
}
|
|
154
90
|
|
|
91
|
+
q.sort = assertSort(q.sort, [{key: 'id'}])
|
|
155
92
|
query.orderBy(compileToSQLSorter(q.sort, sorters))
|
|
156
93
|
query.limit(q.limit)
|
|
157
94
|
}
|
|
@@ -104,7 +104,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
104
104
|
const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
|
|
105
105
|
put.group,
|
|
106
106
|
put.group.organizationId,
|
|
107
|
-
period
|
|
107
|
+
period
|
|
108
108
|
)
|
|
109
109
|
await event.syncGroupRequirements(group)
|
|
110
110
|
event.groupId = group.id
|
|
@@ -199,7 +199,9 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
199
199
|
}
|
|
200
200
|
patch.group.id = event.groupId
|
|
201
201
|
patch.group.type = GroupType.EventRegistration
|
|
202
|
-
|
|
202
|
+
|
|
203
|
+
const period = await RegistrationPeriod.getByDate(event.startDate)
|
|
204
|
+
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(patch.group, period)
|
|
203
205
|
} else {
|
|
204
206
|
if (event.groupId) {
|
|
205
207
|
// need to delete old group first
|
|
@@ -222,7 +224,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
222
224
|
const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
|
|
223
225
|
patch.group,
|
|
224
226
|
patch.group.organizationId,
|
|
225
|
-
period
|
|
227
|
+
period
|
|
226
228
|
)
|
|
227
229
|
event.groupId = group.id
|
|
228
230
|
}
|
|
@@ -4,7 +4,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { Member, Platform } from '@stamhoofd/models';
|
|
6
6
|
import { SQL, compileToSQLFilter, compileToSQLSorter } from "@stamhoofd/sql";
|
|
7
|
-
import { CountFilteredRequest, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, getSortFilter } from '@stamhoofd/structures';
|
|
7
|
+
import { CountFilteredRequest, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
8
8
|
import { DataValidator } from '@stamhoofd/utility';
|
|
9
9
|
|
|
10
10
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -174,6 +174,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
174
174
|
query.where(compileToSQLFilter(q.pageFilter, filterCompilers))
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
q.sort = assertSort(q.sort, [{key: 'id'}])
|
|
177
178
|
query.orderBy(compileToSQLSorter(q.sort, sorters))
|
|
178
179
|
query.limit(q.limit)
|
|
179
180
|
}
|
|
@@ -171,8 +171,13 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
171
171
|
})
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
const wasReduced = member.details.shouldApplyReducedPrice
|
|
174
175
|
member.details.patchOrPut(patch.details)
|
|
175
176
|
member.details.cleanData()
|
|
177
|
+
|
|
178
|
+
if (wasReduced !== member.details.shouldApplyReducedPrice) {
|
|
179
|
+
updateMembershipMemberIds.add(member.id)
|
|
180
|
+
}
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
await member.save();
|
|
@@ -213,10 +218,13 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
if (patchResponsibility.startDate !== undefined) {
|
|
216
|
-
|
|
217
|
-
if (patchResponsibility.startDate > new Date()) {
|
|
221
|
+
if (patchResponsibility.startDate.getTime() > Date.now() + 5 * 60 * 1000) {
|
|
218
222
|
throw Context.auth.error("Je kan de startdatum van een functie niet in de toekomst zetten")
|
|
219
223
|
}
|
|
224
|
+
if (patchResponsibility.startDate.getTime() > Date.now()) {
|
|
225
|
+
patchResponsibility.startDate = new Date() // force now
|
|
226
|
+
}
|
|
227
|
+
|
|
220
228
|
const daysDiff = Math.abs((new Date().getTime() - patchResponsibility.startDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
221
229
|
|
|
222
230
|
if (daysDiff > 60 && !Context.auth.hasPlatformFullAccess()) {
|
|
@@ -343,10 +351,14 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
343
351
|
// Allow patching begin and end date
|
|
344
352
|
model.endDate = put.endDate
|
|
345
353
|
|
|
346
|
-
if (put.startDate >
|
|
354
|
+
if (put.startDate.getTime() > Date.now() + 5 * 60 * 1000) {
|
|
347
355
|
throw Context.auth.error("Je kan de startdatum van een functie niet in de toekomst zetten")
|
|
348
356
|
}
|
|
349
357
|
|
|
358
|
+
if (put.startDate.getTime() > Date.now()) {
|
|
359
|
+
put.startDate = new Date() // force now
|
|
360
|
+
}
|
|
361
|
+
|
|
350
362
|
if (put.endDate && put.endDate > new Date(Date.now() + 60*1000)) {
|
|
351
363
|
throw Context.auth.error("Je kan de einddatum van een functie niet in de toekomst zetten - kijk indien nodig je systeemtijd na")
|
|
352
364
|
}
|
|
@@ -430,7 +442,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
430
442
|
membership.endDate = put.endDate
|
|
431
443
|
membership.expireDate = put.expireDate
|
|
432
444
|
|
|
433
|
-
await membership.calculatePrice()
|
|
445
|
+
await membership.calculatePrice(member)
|
|
434
446
|
await membership.save()
|
|
435
447
|
|
|
436
448
|
updateMembershipMemberIds.add(member.id)
|
|
@@ -6,6 +6,7 @@ import { Organization, StripeAccount, StripeCheckoutSession, StripePaymentIntent
|
|
|
6
6
|
|
|
7
7
|
import { StripeHelper } from '../../../helpers/StripeHelper';
|
|
8
8
|
import { ExchangePaymentEndpoint } from '../../organization/shared/ExchangePaymentEndpoint';
|
|
9
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
9
10
|
|
|
10
11
|
type Params = Record<string, never>;
|
|
11
12
|
class Body extends AutoEncoder {
|
|
@@ -19,7 +20,7 @@ class Body extends AutoEncoder {
|
|
|
19
20
|
id: string
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
+
* Set for connect events
|
|
23
24
|
*/
|
|
24
25
|
@field({ decoder: StringDecoder, nullable: true, optional: true })
|
|
25
26
|
account: string|null = null
|
|
@@ -64,7 +65,8 @@ export class StripeWebookEndpoint extends Endpoint<Params, Query, Body, Response
|
|
|
64
65
|
statusCode: 400
|
|
65
66
|
})
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
const secret = request.body.account ? STAMHOOFD.STRIPE_CONNECT_ENDPOINT_SECRET : STAMHOOFD.STRIPE_ENDPOINT_SECRET
|
|
69
|
+
event = await stripe.webhooks.constructEventAsync(await request.request.bodyPromise!, sig, secret);
|
|
68
70
|
} catch (err) {
|
|
69
71
|
throw new SimpleError({
|
|
70
72
|
code: "invalid_signature",
|
|
@@ -99,16 +101,9 @@ export class StripeWebookEndpoint extends Endpoint<Params, Query, Body, Response
|
|
|
99
101
|
case "payment_intent.requires_action":
|
|
100
102
|
case "payment_intent.succeeded": {
|
|
101
103
|
const intentId = request.body.data.object.id;
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
if (organization) {
|
|
106
|
-
await ExchangePaymentEndpoint.pollStatus(model.paymentId, organization)
|
|
107
|
-
} else {
|
|
108
|
-
console.warn("Could not find organization with id", model.organizationId)
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
console.warn("Could not find stripe payment intent with id", intentId)
|
|
104
|
+
|
|
105
|
+
if (intentId && typeof intentId === "string") {
|
|
106
|
+
await this.updateIntent(intentId)
|
|
112
107
|
}
|
|
113
108
|
break;
|
|
114
109
|
}
|
|
@@ -130,6 +125,27 @@ export class StripeWebookEndpoint extends Endpoint<Params, Query, Body, Response
|
|
|
130
125
|
}
|
|
131
126
|
break;
|
|
132
127
|
}
|
|
128
|
+
// Listen for charge changes (transaction fees will be added here, after the payment intent succeeded)
|
|
129
|
+
case "charge.captured":
|
|
130
|
+
case "charge.expired":
|
|
131
|
+
case "charge.failed":
|
|
132
|
+
case "charge.pending":
|
|
133
|
+
case "charge.refunded":
|
|
134
|
+
case "charge.succeeded":
|
|
135
|
+
case "charge.dispute.created":
|
|
136
|
+
case "charge.dispute.updated":
|
|
137
|
+
case "charge.dispute.closed":
|
|
138
|
+
case "charge.refund.updated":
|
|
139
|
+
case "charge.refund.succeeded":
|
|
140
|
+
case "charge.updated": {
|
|
141
|
+
const intentId = request.body.data.object.payment_intent;
|
|
142
|
+
if (intentId && typeof intentId === "string") {
|
|
143
|
+
await this.updateIntent(intentId)
|
|
144
|
+
} else {
|
|
145
|
+
console.log('Received charge event without payment intent', request.body)
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
133
149
|
default: {
|
|
134
150
|
console.log("Unhandled stripe webhook type", request.body.type);
|
|
135
151
|
break;
|
|
@@ -137,4 +153,19 @@ export class StripeWebookEndpoint extends Endpoint<Params, Query, Body, Response
|
|
|
137
153
|
}
|
|
138
154
|
return new Response(undefined);
|
|
139
155
|
}
|
|
156
|
+
|
|
157
|
+
async updateIntent(intentId: string) {
|
|
158
|
+
console.log("[Webooks] Updating intent", intentId)
|
|
159
|
+
const [model] = await StripePaymentIntent.where({stripeIntentId: intentId}, {limit: 1})
|
|
160
|
+
if (model && model.organizationId) {
|
|
161
|
+
const organization = await Organization.getByID(model.organizationId)
|
|
162
|
+
if (organization) {
|
|
163
|
+
await ExchangePaymentEndpoint.pollStatus(model.paymentId, organization)
|
|
164
|
+
} else {
|
|
165
|
+
console.warn("Could not find organization with id", model.organizationId)
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
console.warn("Could not find stripe payment intent with id", intentId)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
140
171
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AutoEncoderPatchType, Decoder, patchObject } from "@simonbackx/simple-encoding";
|
|
1
|
+
import { AutoEncoderPatchType, Decoder, isPatch, isPatchableArray, patchObject } from "@simonbackx/simple-encoding";
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
3
|
import { Organization, Platform, RegistrationPeriod } from "@stamhoofd/models";
|
|
4
4
|
import { MemberResponsibility, PlatformConfig, PlatformPremiseType, Platform as PlatformStruct } from "@stamhoofd/structures";
|
|
@@ -7,6 +7,7 @@ import { SimpleError } from "@simonbackx/simple-errors";
|
|
|
7
7
|
import { Context } from "../../../helpers/Context";
|
|
8
8
|
import { SetupStepUpdater } from "../../../helpers/SetupStepsUpdater";
|
|
9
9
|
import { PeriodHelper } from "../../../helpers/PeriodHelper";
|
|
10
|
+
import { MembershipHelper } from "../../../helpers/MembershipHelper";
|
|
10
11
|
|
|
11
12
|
type Params = Record<string, never>;
|
|
12
13
|
type Query = undefined;
|
|
@@ -75,6 +76,7 @@ export class PatchPlatformEndpoint extends Endpoint<
|
|
|
75
76
|
|
|
76
77
|
let shouldUpdateSetupSteps = false;
|
|
77
78
|
let shouldMoveToPeriod: RegistrationPeriod | null = null;
|
|
79
|
+
let shouldUpdateMemberships = false;
|
|
78
80
|
|
|
79
81
|
if (request.body.config) {
|
|
80
82
|
if (!Context.auth.hasPlatformFullAccess()) {
|
|
@@ -99,6 +101,18 @@ export class PatchPlatformEndpoint extends Endpoint<
|
|
|
99
101
|
platform.config = patchObject(platform.config, newConfig);
|
|
100
102
|
}
|
|
101
103
|
}
|
|
104
|
+
|
|
105
|
+
if (newConfig.membershipTypes && isPatchableArray(newConfig.membershipTypes) && newConfig.membershipTypes.changes.length > 0) {
|
|
106
|
+
shouldUpdateMemberships = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (newConfig.defaultAgeGroups && isPatchableArray(newConfig.defaultAgeGroups) && newConfig.defaultAgeGroups.changes.length > 0) {
|
|
110
|
+
for (const d of newConfig.defaultAgeGroups.getPatches()) {
|
|
111
|
+
if (d.defaultMembershipTypeId !== undefined) {
|
|
112
|
+
shouldUpdateMemberships = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
if (
|
|
@@ -159,6 +173,10 @@ export class PatchPlatformEndpoint extends Endpoint<
|
|
|
159
173
|
|
|
160
174
|
await platform.save();
|
|
161
175
|
|
|
176
|
+
if (shouldUpdateMemberships) {
|
|
177
|
+
MembershipHelper.updateAll().catch(console.error)
|
|
178
|
+
}
|
|
179
|
+
|
|
162
180
|
if (shouldMoveToPeriod) {
|
|
163
181
|
PeriodHelper.moveAllOrganizationsToPeriod(shouldMoveToPeriod).catch(console.error)
|
|
164
182
|
} else if(shouldUpdateSetupSteps) {
|
|
@@ -5,6 +5,7 @@ import { RegistrationPeriod as RegistrationPeriodStruct } from "@stamhoofd/struc
|
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
6
|
import { Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
7
7
|
import { Context } from '../../../helpers/Context';
|
|
8
|
+
import { PeriodHelper } from '../../../helpers/PeriodHelper';
|
|
8
9
|
|
|
9
10
|
type Params = Record<string, never>;
|
|
10
11
|
type Query = undefined;
|
|
@@ -90,6 +91,9 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
await model.save();
|
|
94
|
+
|
|
95
|
+
// Schedule patch of all groups in this period
|
|
96
|
+
PeriodHelper.updateGroupsInPeriod(model).catch(console.error);
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
for (const id of request.body.getDeletes()) {
|
|
@@ -134,11 +134,8 @@ export class GetPaymentsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
134
134
|
query.where(compileToSQLFilter(q.pageFilter, filterCompilers))
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
key: 'id'
|
|
140
|
-
}
|
|
141
|
-
]), sorters))
|
|
137
|
+
q.sort = assertSort(q.sort, [{key: 'id'}])
|
|
138
|
+
query.orderBy(compileToSQLSorter(q.sort, sorters))
|
|
142
139
|
query.limit(q.limit)
|
|
143
140
|
}
|
|
144
141
|
|
|
@@ -69,6 +69,7 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
69
69
|
payment.organizationId = organization.id
|
|
70
70
|
payment.status = PaymentStatus.Created
|
|
71
71
|
payment.method = put.method
|
|
72
|
+
payment.customer = put.customer
|
|
72
73
|
|
|
73
74
|
if (payment.method == PaymentMethod.Transfer) {
|
|
74
75
|
if (!put.transferSettings) {
|
|
@@ -81,6 +81,9 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
81
81
|
let deleteUnreachable = false
|
|
82
82
|
const allowedIds: string[] = []
|
|
83
83
|
|
|
84
|
+
//#region prevent patch category lock if no full platform access
|
|
85
|
+
const originalCategories = organizationPeriod.settings.categories;
|
|
86
|
+
|
|
84
87
|
if (await Context.auth.hasFullAccess(organization.id)) {
|
|
85
88
|
if (patch.settings) {
|
|
86
89
|
if(patch.settings.categories) {
|
|
@@ -117,6 +120,49 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
//#region handle locked categories
|
|
124
|
+
if(!Context.auth.hasPlatformFullAccess()) {
|
|
125
|
+
const categoriesAfterPatch = organizationPeriod.settings.categories;
|
|
126
|
+
|
|
127
|
+
for(const categoryBefore of originalCategories) {
|
|
128
|
+
const locked = categoryBefore.settings.locked;
|
|
129
|
+
|
|
130
|
+
if(locked) {
|
|
131
|
+
// todo: use existing function, now a category could still be deleted if the category is moved to another category and that catetory is deleted
|
|
132
|
+
const categoryId = categoryBefore.id;
|
|
133
|
+
const refCountBefore = originalCategories.filter(c => c.categoryIds.includes(categoryId)).length;
|
|
134
|
+
const refCountAfter = categoriesAfterPatch.filter(c => c.categoryIds.includes(categoryId)).length;
|
|
135
|
+
const isDeleted = refCountAfter < refCountBefore;
|
|
136
|
+
|
|
137
|
+
if(isDeleted) {
|
|
138
|
+
throw Context.auth.error('Je hebt geen toegangsrechten om deze vergrendelde categorie te verwijderen.')
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const categoryAfter = categoriesAfterPatch.find(c => c.id === categoryBefore.id);
|
|
143
|
+
|
|
144
|
+
if(!categoryAfter) {
|
|
145
|
+
if(locked) {
|
|
146
|
+
throw Context.auth.error('Je hebt geen toegangsrechten om deze vergrendelde categorie te verwijderen.')
|
|
147
|
+
}
|
|
148
|
+
} else if(locked !== categoryAfter.settings.locked) {
|
|
149
|
+
throw Context.auth.error('Je hebt geen toegangsrechten om deze categorie te vergrendelen of ontgrendelen.')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if(!locked || !categoryAfter) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const settingsBefore = categoryBefore.settings;
|
|
157
|
+
const settingsAfter = categoryAfter.settings;
|
|
158
|
+
|
|
159
|
+
if(settingsBefore.name !== settingsAfter.name) {
|
|
160
|
+
throw Context.auth.error('Je hebt geen toegangsrechten de naam van deze vergrendelde categorie te wijzigen.')
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
|
|
120
166
|
await organizationPeriod.save();
|
|
121
167
|
|
|
122
168
|
// Check changes to groups
|
|
@@ -129,12 +175,12 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
129
175
|
}
|
|
130
176
|
|
|
131
177
|
for (const groupPut of patch.groups.getPuts()) {
|
|
132
|
-
await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(groupPut.put, organization.id,
|
|
178
|
+
await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(groupPut.put, organization.id, period, {allowedIds})
|
|
133
179
|
deleteUnreachable = true
|
|
134
180
|
}
|
|
135
181
|
|
|
136
182
|
for (const struct of patch.groups.getPatches()) {
|
|
137
|
-
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(struct)
|
|
183
|
+
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(struct, period)
|
|
138
184
|
}
|
|
139
185
|
|
|
140
186
|
|
|
@@ -191,7 +237,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
191
237
|
await organizationPeriod.save();
|
|
192
238
|
|
|
193
239
|
for (const s of struct.groups) {
|
|
194
|
-
await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(s, organization.id,
|
|
240
|
+
await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(s, organization.id, period)
|
|
195
241
|
}
|
|
196
242
|
const groups = await Group.getAll(organization.id, organizationPeriod.periodId)
|
|
197
243
|
|
|
@@ -213,7 +259,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
213
259
|
Member.updateMembershipsForGroupId(id)
|
|
214
260
|
}
|
|
215
261
|
|
|
216
|
-
static async patchGroup(struct: AutoEncoderPatchType<GroupStruct
|
|
262
|
+
static async patchGroup(struct: AutoEncoderPatchType<GroupStruct>, period?: RegistrationPeriod | null) {
|
|
217
263
|
const model = await Group.getByID(struct.id)
|
|
218
264
|
|
|
219
265
|
if (!model || !await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
|
|
@@ -221,6 +267,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
221
267
|
}
|
|
222
268
|
|
|
223
269
|
if (struct.settings) {
|
|
270
|
+
struct.settings.period = undefined // Not allowed to patch manually
|
|
224
271
|
model.settings.patchOrPut(struct.settings)
|
|
225
272
|
}
|
|
226
273
|
|
|
@@ -252,6 +299,15 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
252
299
|
model.defaultAgeGroupId = await this.validateDefaultGroupId(struct.defaultAgeGroupId)
|
|
253
300
|
}
|
|
254
301
|
|
|
302
|
+
if (!period && !model.settings.period) {
|
|
303
|
+
period = await RegistrationPeriod.getByID(model.periodId)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (period) {
|
|
307
|
+
model.periodId = period.id
|
|
308
|
+
model.settings.period = period.getBaseStructure()
|
|
309
|
+
}
|
|
310
|
+
|
|
255
311
|
const patch = struct;
|
|
256
312
|
if (patch.waitingList !== undefined) {
|
|
257
313
|
if (patch.waitingList === null) {
|
|
@@ -272,7 +328,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
272
328
|
}
|
|
273
329
|
patch.waitingList.id = model.waitingListId
|
|
274
330
|
patch.waitingList.type = GroupType.WaitingList
|
|
275
|
-
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(patch.waitingList)
|
|
331
|
+
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(patch.waitingList, period)
|
|
276
332
|
} else {
|
|
277
333
|
if (model.waitingListId) {
|
|
278
334
|
// for now don't delete, as waiting lists can be shared between multiple groups
|
|
@@ -302,10 +358,15 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
302
358
|
|
|
303
359
|
model.waitingListId = existing.id
|
|
304
360
|
} else {
|
|
361
|
+
const requiredPeriod = period ?? await RegistrationPeriod.getByID(model.periodId)
|
|
362
|
+
|
|
363
|
+
if (!requiredPeriod) {
|
|
364
|
+
throw new Error('Unexpected missing period when creating waiting list')
|
|
365
|
+
}
|
|
305
366
|
const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
|
|
306
367
|
patch.waitingList,
|
|
307
368
|
model.organizationId,
|
|
308
|
-
|
|
369
|
+
requiredPeriod
|
|
309
370
|
)
|
|
310
371
|
model.waitingListId = group.id
|
|
311
372
|
}
|
|
@@ -322,7 +383,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
322
383
|
}
|
|
323
384
|
|
|
324
385
|
|
|
325
|
-
static async createGroup(struct: GroupStruct, organizationId: string,
|
|
386
|
+
static async createGroup(struct: GroupStruct, organizationId: string, period: RegistrationPeriod, options?: {allowedIds?: string[]}): Promise<Group> {
|
|
326
387
|
const allowedIds = options?.allowedIds ?? []
|
|
327
388
|
|
|
328
389
|
if (!await Context.auth.hasFullAccess(organizationId)) {
|
|
@@ -339,11 +400,12 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
339
400
|
model.id = struct.id
|
|
340
401
|
model.organizationId = organizationId
|
|
341
402
|
model.defaultAgeGroupId = await this.validateDefaultGroupId(struct.defaultAgeGroupId)
|
|
342
|
-
model.periodId =
|
|
403
|
+
model.periodId = period.id
|
|
343
404
|
model.settings = struct.settings
|
|
344
405
|
model.privateSettings = struct.privateSettings ?? GroupPrivateSettings.create({})
|
|
345
406
|
model.status = struct.status
|
|
346
407
|
model.type = struct.type
|
|
408
|
+
model.settings.period = period.getBaseStructure()
|
|
347
409
|
|
|
348
410
|
if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
|
|
349
411
|
// Create a temporary permission role for this user
|
|
@@ -387,7 +449,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
387
449
|
const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
|
|
388
450
|
struct.waitingList,
|
|
389
451
|
model.organizationId,
|
|
390
|
-
|
|
452
|
+
period
|
|
391
453
|
)
|
|
392
454
|
model.waitingListId = group.id
|
|
393
455
|
}
|
|
@@ -5,6 +5,7 @@ import { PermissionLevel } from '@stamhoofd/structures';
|
|
|
5
5
|
|
|
6
6
|
import { Context } from '../../../../helpers/Context';
|
|
7
7
|
import { StripeHelper } from '../../../../helpers/StripeHelper';
|
|
8
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
8
9
|
|
|
9
10
|
type Params = { id: string };
|
|
10
11
|
type Body = undefined;
|
|
@@ -41,6 +42,14 @@ export class DeleteStripeAccountEndpoint extends Endpoint<Params, Query, Body, R
|
|
|
41
42
|
throw Context.auth.notFoundOrNoAccess("Account niet gevonden")
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
if (model.accountId === STAMHOOFD.STRIPE_ACCOUNT_ID) {
|
|
46
|
+
throw new SimpleError({
|
|
47
|
+
code: "invalid_request",
|
|
48
|
+
message: "Je kan het hoofdaccount van het platform niet verwijderen.",
|
|
49
|
+
statusCode: 400
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
// For now we don't delete them in Stripe because this causes issues with data access
|
|
45
54
|
const stripe = StripeHelper.getInstance()
|
|
46
55
|
|
|
@@ -174,6 +174,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
174
174
|
*/
|
|
175
175
|
static async pollStatus(paymentId: string, organization: Organization, cancel = false): Promise<Payment | undefined> {
|
|
176
176
|
// Prevent polling the same payment multiple times at the same time: create a queue to prevent races
|
|
177
|
+
QueueHandler.cancel("payments/"+paymentId); // Prevent creating more than one queue item for the same payment
|
|
177
178
|
return await QueueHandler.schedule("payments/"+paymentId, async () => {
|
|
178
179
|
// Get a new copy of the payment (is required to prevent concurreny bugs)
|
|
179
180
|
const payment = await Payment.getByID(paymentId)
|
|
@@ -324,6 +325,14 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
324
325
|
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed)
|
|
325
326
|
}
|
|
326
327
|
}
|
|
328
|
+
} else {
|
|
329
|
+
// Do a manual update if needed
|
|
330
|
+
if (payment.status === PaymentStatus.Succeeded) {
|
|
331
|
+
if (payment.provider === PaymentProvider.Stripe) {
|
|
332
|
+
// Update the status
|
|
333
|
+
await StripeHelper.getStatus(payment, false, testMode)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
327
336
|
}
|
|
328
337
|
return payment
|
|
329
338
|
})
|
|
@@ -980,7 +980,7 @@ export class AdminPermissionChecker {
|
|
|
980
980
|
|
|
981
981
|
const hasRecordAnswers = !!data.details.recordAnswers;
|
|
982
982
|
const hasNotes = data.details.notes !== undefined;
|
|
983
|
-
const isSetFinancialSupportTrue = data.details.
|
|
983
|
+
const isSetFinancialSupportTrue = data.details.shouldApplyReducedPrice;
|
|
984
984
|
const isUserManager = this.isUserManager(member);
|
|
985
985
|
|
|
986
986
|
if (hasRecordAnswers) {
|
|
@@ -14,12 +14,14 @@ export class MemberUserSyncerStatic {
|
|
|
14
14
|
// Make sure all these users have access to the member
|
|
15
15
|
for (const email of userEmails) {
|
|
16
16
|
// Link users that are found with these email addresses.
|
|
17
|
-
await this.linkUser(email, member, false)
|
|
17
|
+
await this.linkUser(email, member, false, true)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
for (const email of parentAndUnverifiedEmails) {
|
|
21
21
|
// Link parents and unverified emails
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
// Now we add the responsibility permissions to the parent if there are no userEmails
|
|
24
|
+
await this.linkUser(email, member, userEmails.length > 0, userEmails.length > 0)
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
if (unlinkUsers && !member.details.parentsHaveAccess) {
|
|
@@ -160,7 +162,7 @@ export class MemberUserSyncerStatic {
|
|
|
160
162
|
await this.updateInheritedPermissions(user)
|
|
161
163
|
}
|
|
162
164
|
|
|
163
|
-
async linkUser(email: string, member: MemberWithRegistrations, asParent: boolean) {
|
|
165
|
+
async linkUser(email: string, member: MemberWithRegistrations, asParent: boolean, updateNameIfEqual = true) {
|
|
164
166
|
let user = member.users.find(u => u.email.toLocaleLowerCase() === email.toLocaleLowerCase()) ?? await User.getForAuthentication(member.organizationId, email, {allowWithoutAccount: true})
|
|
165
167
|
|
|
166
168
|
if (user) {
|
|
@@ -189,8 +191,11 @@ export class MemberUserSyncerStatic {
|
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
193
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
|
|
195
|
+
if (updateNameIfEqual) {
|
|
196
|
+
user.firstName = member.details.firstName
|
|
197
|
+
user.lastName = member.details.lastName
|
|
198
|
+
}
|
|
194
199
|
user.memberId = member.id;
|
|
195
200
|
await this.updateInheritedPermissions(user)
|
|
196
201
|
} else {
|
|
@@ -203,8 +208,10 @@ export class MemberUserSyncerStatic {
|
|
|
203
208
|
if (!user.firstName && !user.lastName) {
|
|
204
209
|
const parents = member.details.parents.filter(p => p.email === email)
|
|
205
210
|
if (parents.length === 1) {
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
if (updateNameIfEqual) {
|
|
212
|
+
user.firstName = parents[0].firstName
|
|
213
|
+
user.lastName = parents[0].lastName
|
|
214
|
+
}
|
|
208
215
|
await user.save()
|
|
209
216
|
}
|
|
210
217
|
}
|
|
@@ -222,15 +229,19 @@ export class MemberUserSyncerStatic {
|
|
|
222
229
|
user.email = email
|
|
223
230
|
|
|
224
231
|
if (!asParent) {
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
if (updateNameIfEqual) {
|
|
233
|
+
user.firstName = member.details.firstName
|
|
234
|
+
user.lastName = member.details.lastName
|
|
235
|
+
}
|
|
227
236
|
user.memberId = member.id;
|
|
228
237
|
await this.updateInheritedPermissions(user)
|
|
229
238
|
} else {
|
|
230
239
|
const parents = member.details.parents.filter(p => p.email === email)
|
|
231
240
|
if (parents.length === 1) {
|
|
232
|
-
|
|
233
|
-
|
|
241
|
+
if (updateNameIfEqual) {
|
|
242
|
+
user.firstName = parents[0].firstName
|
|
243
|
+
user.lastName = parents[0].lastName
|
|
244
|
+
}
|
|
234
245
|
}
|
|
235
246
|
|
|
236
247
|
if (user.firstName === member.details.firstName && user.lastName === member.details.lastName) {
|
|
@@ -33,6 +33,8 @@ export const MembershipCharger = {
|
|
|
33
33
|
const memberships = await MemberPlatformMembership.select()
|
|
34
34
|
.where('id', SQLWhereSign.Greater, lastId)
|
|
35
35
|
.where('balanceItemId', null)
|
|
36
|
+
.where('deletedAt', null)
|
|
37
|
+
.whereNot('organizationId', chargeVia)
|
|
36
38
|
.limit(100)
|
|
37
39
|
.orderBy(
|
|
38
40
|
new SQLOrderBy({
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { logger } from '@simonbackx/simple-logging';
|
|
2
|
+
import { Member } from '@stamhoofd/models';
|
|
3
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
4
|
+
|
|
5
|
+
export class MembershipHelper {
|
|
6
|
+
static async updateAll() {
|
|
7
|
+
console.log('Scheduling updateAllMemberships');
|
|
8
|
+
|
|
9
|
+
let c = 0;
|
|
10
|
+
let id: string = '';
|
|
11
|
+
const tag = "updateAllMemberships";
|
|
12
|
+
const batch = 100;
|
|
13
|
+
|
|
14
|
+
QueueHandler.cancel(tag);
|
|
15
|
+
|
|
16
|
+
await QueueHandler.schedule(tag, async () => {
|
|
17
|
+
console.log('Starting updateAllMemberships');
|
|
18
|
+
await logger.setContext({tags: ['silent-seed', 'seed']}, async () => {
|
|
19
|
+
while(true) {
|
|
20
|
+
const rawMembers = await Member.where({
|
|
21
|
+
id: {
|
|
22
|
+
value: id,
|
|
23
|
+
sign: '>'
|
|
24
|
+
}
|
|
25
|
+
}, {limit: batch, sort: ['id']});
|
|
26
|
+
|
|
27
|
+
if (rawMembers.length === 0) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
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%10000 === 0) {
|
|
40
|
+
process.stdout.write(c + ' members updated\n');
|
|
41
|
+
}
|
|
42
|
+
})())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await Promise.all(promises);
|
|
46
|
+
id = rawMembers[rawMembers.length - 1].id;
|
|
47
|
+
|
|
48
|
+
if (rawMembers.length < batch) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
import { Member, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from "@stamhoofd/models";
|
|
2
|
+
import { Group, Member, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from "@stamhoofd/models";
|
|
3
3
|
import { AuthenticatedStructures } from "./AuthenticatedStructures";
|
|
4
4
|
import { PatchOrganizationRegistrationPeriodsEndpoint } from "../endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint";
|
|
5
5
|
import { QueueHandler } from "@stamhoofd/queues";
|
|
6
6
|
import { SetupStepUpdater } from "./SetupStepsUpdater";
|
|
7
|
-
import { PermissionLevel } from "@stamhoofd/structures";
|
|
7
|
+
import { PermissionLevel, Group as GroupStruct } from "@stamhoofd/structures";
|
|
8
8
|
import { MemberUserSyncer } from "./MemberUserSyncer";
|
|
9
9
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
10
10
|
|
|
@@ -162,5 +162,41 @@ export class PeriodHelper {
|
|
|
162
162
|
await SetupStepUpdater.updateSetupStepsForAllOrganizationsInCurrentPeriod()
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
static async updateGroupsInPeriod(period: RegistrationPeriod) {
|
|
166
|
+
const tag = "updateGroupsInPeriod-"+period.id;
|
|
167
|
+
|
|
168
|
+
if (QueueHandler.isRunning(tag)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log(tag);
|
|
173
|
+
|
|
174
|
+
const batchSize = 100;
|
|
175
|
+
await QueueHandler.schedule(tag, async () => {
|
|
176
|
+
let lastId = "";
|
|
177
|
+
|
|
178
|
+
while (true) {
|
|
179
|
+
const groups = await Group.where(
|
|
180
|
+
{
|
|
181
|
+
id: { sign: ">", value: lastId },
|
|
182
|
+
periodId: period.id
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
limit: batchSize,
|
|
186
|
+
sort: ["id"]
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
for (const group of groups) {
|
|
191
|
+
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(GroupStruct.patch({id: group.id}), period);
|
|
192
|
+
lastId = group.id;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (groups.length < batchSize) {
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
166
202
|
}
|
|
@@ -245,7 +245,7 @@ export class SetupStepUpdater {
|
|
|
245
245
|
|
|
246
246
|
const responsibilityIds = organizationBasedResponsibilitiesWithRestriction.map(r => r.id);
|
|
247
247
|
|
|
248
|
-
const allRecords = await MemberResponsibilityRecord.select()
|
|
248
|
+
const allRecords = responsibilityIds.length === 0 ? [] : await MemberResponsibilityRecord.select()
|
|
249
249
|
.where('responsibilityId', responsibilityIds)
|
|
250
250
|
.where('organizationId', organization.id)
|
|
251
251
|
.where(SQL.where('endDate', SQLWhereSign.Greater, now).or('endDate', null))
|
|
@@ -48,6 +48,27 @@ export class StripeHelper {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Call when the charge is updated in Stripe, so we can save fee information in the payment
|
|
53
|
+
*/
|
|
54
|
+
static async updateChargeInfo(model: StripePaymentIntent) {
|
|
55
|
+
const stripe = this.getInstance(model.accountId)
|
|
56
|
+
|
|
57
|
+
const intent = await stripe.paymentIntents.retrieve(model.stripeIntentId, {
|
|
58
|
+
expand: ['latest_charge.balance_transaction']
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
console.log(intent);
|
|
62
|
+
if (intent.status === "succeeded") {
|
|
63
|
+
if (intent.latest_charge !== null && typeof intent.latest_charge !== 'string') {
|
|
64
|
+
const payment = await Payment.getByID(model.paymentId)
|
|
65
|
+
if (payment) {
|
|
66
|
+
await this.saveChargeInfo(model, intent.latest_charge, payment)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
static async getStatus(payment: Payment, cancel = false, testMode = false): Promise<PaymentStatus> {
|
|
52
73
|
if (testMode && !STAMHOOFD.STRIPE_SECRET_KEY.startsWith("sk_test_")) {
|
|
53
74
|
// Do not query anything
|
|
@@ -219,7 +240,7 @@ export class StripeHelper {
|
|
|
219
240
|
payment_method: paymentMethod.id,
|
|
220
241
|
payment_method_types: [payment.method.toLowerCase()],
|
|
221
242
|
statement_descriptor: Formatter.slug(statementDescriptor).substring(0, 22).toUpperCase(),
|
|
222
|
-
application_fee_amount: fee,
|
|
243
|
+
application_fee_amount: fee ? fee : undefined,
|
|
223
244
|
on_behalf_of: !directCharge ? stripeAccount.accountId : undefined,
|
|
224
245
|
confirm: true,
|
|
225
246
|
return_url: redirectUrl,
|
|
@@ -293,7 +314,7 @@ export class StripeHelper {
|
|
|
293
314
|
locale: i18n.language as 'nl',
|
|
294
315
|
payment_intent_data: {
|
|
295
316
|
on_behalf_of: !directCharge ? stripeAccount.accountId : undefined,
|
|
296
|
-
application_fee_amount: fee,
|
|
317
|
+
application_fee_amount: fee ? fee : undefined,
|
|
297
318
|
transfer_data: !directCharge ? {
|
|
298
319
|
destination: stripeAccount.accountId,
|
|
299
320
|
} : undefined,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { MembershipHelper } from '../helpers/MembershipHelper';
|
|
3
|
+
|
|
4
|
+
export default new Migration(async () => {
|
|
5
|
+
if (STAMHOOFD.environment == "test") {
|
|
6
|
+
console.log("skipped in tests")
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if(STAMHOOFD.userMode !== "platform") {
|
|
11
|
+
console.log("skipped seed update-membership because usermode not platform")
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
process.stdout.write('\n');
|
|
16
|
+
await MembershipHelper.updateAll()
|
|
17
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, SQL, SQLValueType } from "@stamhoofd/sql";
|
|
2
|
+
|
|
3
|
+
export const eventFilterCompilers: SQLFilterDefinitions = {
|
|
4
|
+
...baseSQLFilterCompilers,
|
|
5
|
+
id: createSQLColumnFilterCompiler('id'),
|
|
6
|
+
name: createSQLColumnFilterCompiler('name'),
|
|
7
|
+
organizationId: createSQLColumnFilterCompiler('organizationId'),
|
|
8
|
+
startDate: createSQLColumnFilterCompiler('startDate'),
|
|
9
|
+
endDate: createSQLColumnFilterCompiler('endDate'),
|
|
10
|
+
groupIds: createSQLExpressionFilterCompiler(
|
|
11
|
+
SQL.jsonValue(SQL.column('meta'), '$.value.groups[*].id'),
|
|
12
|
+
{isJSONValue: true, isJSONObject: true}
|
|
13
|
+
),
|
|
14
|
+
defaultAgeGroupIds: createSQLExpressionFilterCompiler(
|
|
15
|
+
SQL.jsonValue(SQL.column('meta'), '$.value.defaultAgeGroupIds'),
|
|
16
|
+
{isJSONValue: true, isJSONObject: true}
|
|
17
|
+
),
|
|
18
|
+
organizationTagIds: createSQLExpressionFilterCompiler(
|
|
19
|
+
SQL.jsonValue(SQL.column('meta'), '$.value.organizationTagIds'),
|
|
20
|
+
{isJSONValue: true, isJSONObject: true}
|
|
21
|
+
),
|
|
22
|
+
'meta.visible': createSQLExpressionFilterCompiler(
|
|
23
|
+
SQL.jsonValue(SQL.column('meta'), '$.value.visible'),
|
|
24
|
+
{isJSONValue: true, type: SQLValueType.JSONBoolean}
|
|
25
|
+
),
|
|
26
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Event } from "@stamhoofd/models"
|
|
2
|
+
import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from "@stamhoofd/sql"
|
|
3
|
+
import { Formatter } from "@stamhoofd/utility"
|
|
4
|
+
|
|
5
|
+
export const eventSorters: SQLSortDefinitions<Event> = {
|
|
6
|
+
// WARNING! TEST NEW SORTERS THOROUGHLY!
|
|
7
|
+
// Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
|
|
8
|
+
// An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
|
|
9
|
+
// You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
|
|
10
|
+
// Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
|
|
11
|
+
// And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
|
|
12
|
+
// What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
|
|
13
|
+
|
|
14
|
+
'id': {
|
|
15
|
+
getValue(a) {
|
|
16
|
+
return a.id
|
|
17
|
+
},
|
|
18
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
19
|
+
return new SQLOrderBy({
|
|
20
|
+
column: SQL.column('id'),
|
|
21
|
+
direction
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
'name': {
|
|
26
|
+
getValue(a) {
|
|
27
|
+
return a.name
|
|
28
|
+
},
|
|
29
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
30
|
+
return new SQLOrderBy({
|
|
31
|
+
column: SQL.column('name'),
|
|
32
|
+
direction
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
'startDate': {
|
|
37
|
+
getValue(a) {
|
|
38
|
+
return Formatter.dateTimeIso(a.startDate)
|
|
39
|
+
},
|
|
40
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
41
|
+
return new SQLOrderBy({
|
|
42
|
+
column: SQL.column('startDate'),
|
|
43
|
+
direction
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
'endDate': {
|
|
48
|
+
getValue(a) {
|
|
49
|
+
return Formatter.dateTimeIso(a.endDate)
|
|
50
|
+
},
|
|
51
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
52
|
+
return new SQLOrderBy({
|
|
53
|
+
column: SQL.column('endDate'),
|
|
54
|
+
direction
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
-
import { Member } from '@stamhoofd/models';
|
|
3
|
-
|
|
4
|
-
export default new Migration(async () => {
|
|
5
|
-
if (STAMHOOFD.environment == "test") {
|
|
6
|
-
console.log("skipped in tests")
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
if(STAMHOOFD.userMode !== "platform") {
|
|
11
|
-
console.log("skipped seed update-membership because usermode not platform")
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
process.stdout.write('\n');
|
|
16
|
-
let c = 0;
|
|
17
|
-
let id: string = '';
|
|
18
|
-
|
|
19
|
-
while(true) {
|
|
20
|
-
const rawMembers = await Member.where({
|
|
21
|
-
id: {
|
|
22
|
-
value: id,
|
|
23
|
-
sign: '>'
|
|
24
|
-
}
|
|
25
|
-
}, {limit: 500, sort: ['id']});
|
|
26
|
-
|
|
27
|
-
if (rawMembers.length === 0) {
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
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);
|
|
49
|
-
id = rawMembers[rawMembers.length - 1].id;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Do something here
|
|
53
|
-
return Promise.resolve()
|
|
54
|
-
})
|