@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.
Files changed (27) hide show
  1. package/LICENSE +6 -2
  2. package/package.json +10 -10
  3. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +12 -3
  4. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -1
  5. package/src/endpoints/global/events/GetEventsEndpoint.ts +8 -71
  6. package/src/endpoints/global/events/PatchEventsEndpoint.ts +5 -3
  7. package/src/endpoints/global/members/GetMembersEndpoint.ts +2 -1
  8. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +16 -4
  9. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +43 -12
  10. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +19 -1
  11. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +4 -0
  12. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +2 -5
  13. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +1 -0
  14. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +71 -9
  15. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +9 -0
  16. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +9 -0
  17. package/src/helpers/AdminPermissionChecker.ts +1 -1
  18. package/src/helpers/MemberUserSyncer.ts +22 -11
  19. package/src/helpers/MembershipCharger.ts +2 -0
  20. package/src/helpers/MembershipHelper.ts +55 -0
  21. package/src/helpers/PeriodHelper.ts +39 -3
  22. package/src/helpers/SetupStepsUpdater.ts +1 -1
  23. package/src/helpers/StripeHelper.ts +23 -2
  24. package/src/seeds/1722344162-update-membership.ts +17 -0
  25. package/src/sql-filters/events.ts +26 -0
  26. package/src/sql-sorters/events.ts +58 -0
  27. package/src/seeds/1722344160-update-membership.ts +0 -54
package/LICENSE CHANGED
@@ -1,4 +1,8 @@
1
- GNU AFFERO GENERAL PUBLIC LICENSE
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.25.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.25.2",
40
- "@stamhoofd/backend-middleware": "2.25.2",
41
- "@stamhoofd/email": "2.25.2",
42
- "@stamhoofd/models": "2.25.2",
43
- "@stamhoofd/queues": "2.25.2",
44
- "@stamhoofd/sql": "2.25.2",
45
- "@stamhoofd/structures": "2.25.2",
46
- "@stamhoofd/utility": "2.25.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": "2eda2d1bc9aa4ea061b002d265a5615a87eeef8b"
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, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler } from "@stamhoofd/sql";
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
- ...baseSQLFilterCompilers,
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.id
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
- await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(patch.group)
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.id
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 > new Date()) {
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
- * Events for direct charges
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
- event = await stripe.webhooks.constructEventAsync(await request.request.bodyPromise!, sig, STAMHOOFD.STRIPE_ENDPOINT_SECRET);
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
- const [model] = await StripePaymentIntent.where({stripeIntentId: intentId}, {limit: 1})
103
- if (model && model.organizationId) {
104
- const organization = await Organization.getByID(model.organizationId)
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
- query.orderBy(compileToSQLSorter(assertSort(q.sort, [
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, organizationPeriod.periodId, {allowedIds})
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, organizationPeriod.periodId)
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
- model.periodId
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, periodId: string, options?: {allowedIds?: string[]}): Promise<Group> {
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 = 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
- model.periodId
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.requiresFinancialSupport?.value === true;
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
- await this.linkUser(email, member, true)
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
- user.firstName = member.details.firstName
193
- user.lastName = member.details.lastName
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
- user.firstName = parents[0].firstName
207
- user.lastName = parents[0].lastName
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
- user.firstName = member.details.firstName
226
- user.lastName = member.details.lastName
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
- user.firstName = parents[0].firstName
233
- user.lastName = parents[0].lastName
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
- })