@stamhoofd/backend 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.env.template.json +63 -0
  2. package/.eslintrc.js +61 -0
  3. package/README.md +40 -0
  4. package/index.ts +172 -0
  5. package/jest.config.js +11 -0
  6. package/migrations.ts +33 -0
  7. package/package.json +48 -0
  8. package/src/crons.ts +845 -0
  9. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +42 -0
  10. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +320 -0
  11. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +171 -0
  12. package/src/endpoints/auth/CreateAdminEndpoint.ts +137 -0
  13. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +68 -0
  14. package/src/endpoints/auth/CreateTokenEndpoint.ts +200 -0
  15. package/src/endpoints/auth/DeleteTokenEndpoint.ts +31 -0
  16. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +70 -0
  17. package/src/endpoints/auth/GetUserEndpoint.test.ts +64 -0
  18. package/src/endpoints/auth/GetUserEndpoint.ts +57 -0
  19. package/src/endpoints/auth/PatchApiUserEndpoint.ts +90 -0
  20. package/src/endpoints/auth/PatchUserEndpoint.ts +122 -0
  21. package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +37 -0
  22. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +41 -0
  23. package/src/endpoints/auth/SignupEndpoint.ts +107 -0
  24. package/src/endpoints/auth/VerifyEmailEndpoint.ts +89 -0
  25. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +95 -0
  26. package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +31 -0
  27. package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +101 -0
  28. package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +53 -0
  29. package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +57 -0
  30. package/src/endpoints/global/files/UploadFile.ts +147 -0
  31. package/src/endpoints/global/files/UploadImage.ts +119 -0
  32. package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +76 -0
  33. package/src/endpoints/global/members/GetMembersCountEndpoint.ts +43 -0
  34. package/src/endpoints/global/members/GetMembersEndpoint.ts +429 -0
  35. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +734 -0
  36. package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +45 -0
  37. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +105 -0
  38. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +146 -0
  39. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +52 -0
  40. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +80 -0
  41. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +49 -0
  42. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +58 -0
  43. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +62 -0
  44. package/src/endpoints/global/payments/ExchangeSTPaymentEndpoint.ts +153 -0
  45. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +134 -0
  46. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +44 -0
  47. package/src/endpoints/global/platform/GetPlatformEnpoint.ts +39 -0
  48. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +63 -0
  49. package/src/endpoints/global/registration/GetPaymentRegistrations.ts +68 -0
  50. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +39 -0
  51. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +80 -0
  52. package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +41 -0
  53. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +134 -0
  54. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +521 -0
  55. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +37 -0
  56. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +115 -0
  57. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +187 -0
  58. package/src/endpoints/organization/dashboard/billing/ActivatePackagesEndpoint.ts +424 -0
  59. package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +67 -0
  60. package/src/endpoints/organization/dashboard/billing/GetBillingStatusEndpoint.ts +39 -0
  61. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +57 -0
  62. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +50 -0
  63. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +50 -0
  64. package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +129 -0
  65. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplateEndpoint.ts +114 -0
  66. package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +50 -0
  67. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +234 -0
  68. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +62 -0
  69. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +85 -0
  70. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +80 -0
  71. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +54 -0
  72. package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +49 -0
  73. package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +63 -0
  74. package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +61 -0
  75. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.test.ts +64 -0
  76. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.ts +84 -0
  77. package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +43 -0
  78. package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +42 -0
  79. package/src/endpoints/organization/dashboard/organization/GetOrganizationSSOEndpoint.ts +43 -0
  80. package/src/endpoints/organization/dashboard/organization/GetRegisterCodeEndpoint.ts +65 -0
  81. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +281 -0
  82. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +338 -0
  83. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +196 -0
  84. package/src/endpoints/organization/dashboard/organization/SetOrganizationSSOEndpoint.ts +50 -0
  85. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +48 -0
  86. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +207 -0
  87. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +202 -0
  88. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +233 -0
  89. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +66 -0
  90. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -0
  91. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +93 -0
  92. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +59 -0
  93. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +78 -0
  94. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +40 -0
  95. package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +69 -0
  96. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +52 -0
  97. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +73 -0
  98. package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +60 -0
  99. package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +47 -0
  100. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +41 -0
  101. package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +217 -0
  102. package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +51 -0
  103. package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +47 -0
  104. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +83 -0
  105. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +68 -0
  106. package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +69 -0
  107. package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +125 -0
  108. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +204 -0
  109. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +278 -0
  110. package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +80 -0
  111. package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +60 -0
  112. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +379 -0
  113. package/src/endpoints/organization/shared/GetDocumentHtml.ts +54 -0
  114. package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +45 -0
  115. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +78 -0
  116. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +34 -0
  117. package/src/endpoints/organization/shared/auth/OpenIDConnectCallbackEndpoint.ts +44 -0
  118. package/src/endpoints/organization/shared/auth/OpenIDConnectStartEndpoint.ts +82 -0
  119. package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +59 -0
  120. package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +51 -0
  121. package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +40 -0
  122. package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +124 -0
  123. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +130 -0
  124. package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +50 -0
  125. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +450 -0
  126. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +335 -0
  127. package/src/helpers/AddressValidator.test.ts +40 -0
  128. package/src/helpers/AddressValidator.ts +256 -0
  129. package/src/helpers/AdminPermissionChecker.ts +1031 -0
  130. package/src/helpers/AuthenticatedStructures.ts +158 -0
  131. package/src/helpers/BuckarooHelper.ts +279 -0
  132. package/src/helpers/CheckSettlements.ts +215 -0
  133. package/src/helpers/Context.ts +202 -0
  134. package/src/helpers/CookieHelper.ts +45 -0
  135. package/src/helpers/ForwardHandler.test.ts +216 -0
  136. package/src/helpers/ForwardHandler.ts +140 -0
  137. package/src/helpers/OpenIDConnectHelper.ts +284 -0
  138. package/src/helpers/StripeHelper.ts +293 -0
  139. package/src/helpers/StripePayoutChecker.ts +188 -0
  140. package/src/middleware/ContextMiddleware.ts +16 -0
  141. package/src/migrations/1646578856-validate-addresses.ts +60 -0
  142. package/src/seeds/0000000000-example.ts +13 -0
  143. package/src/seeds/1715028563-user-permissions.ts +52 -0
  144. package/tests/e2e/stock.test.ts +2120 -0
  145. package/tests/e2e/tickets.test.ts +926 -0
  146. package/tests/helpers/StripeMocker.ts +362 -0
  147. package/tests/helpers/TestServer.ts +21 -0
  148. package/tests/jest.global.setup.ts +29 -0
  149. package/tests/jest.setup.ts +59 -0
  150. package/tsconfig.json +42 -0
@@ -0,0 +1,734 @@
1
+ import { OneToManyRelation } from '@simonbackx/simple-database';
2
+ import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
3
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
4
+ import { SimpleError } from "@simonbackx/simple-errors";
5
+ import { BalanceItem, BalanceItemPayment, Document, Group, Member, MemberFactory, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Payment, Platform, Registration, User } from '@stamhoofd/models';
6
+ import { BalanceItemStatus, MemberWithRegistrationsBlob, MembersBlob, PaymentMethod, PaymentStatus, PermissionLevel, Registration as RegistrationStruct, User as UserStruct } from "@stamhoofd/structures";
7
+ import { Formatter } from '@stamhoofd/utility';
8
+
9
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
10
+ import { Context } from '../../../helpers/Context';
11
+
12
+ type Params = Record<string, never>;
13
+ type Query = undefined;
14
+ type Body = PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>
15
+ type ResponseBody = MembersBlob
16
+
17
+ /**
18
+ * One endpoint to create, patch and delete members and their registrations and payments
19
+ */
20
+
21
+ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
23
+ bodyDecoder = new PatchableArrayDecoder(MemberWithRegistrationsBlob as any, MemberWithRegistrationsBlob.patchType(), StringDecoder) as any as Decoder<ConvertArrayToPatchableArray<MemberWithRegistrationsBlob[]>>
24
+
25
+ protected doesMatch(request: Request): [true, Params] | [false] {
26
+ if (request.method != "PATCH") {
27
+ return [false];
28
+ }
29
+
30
+ const params = Endpoint.parseParameters(request.url, "/organization/members", {});
31
+
32
+ if (params) {
33
+ return [true, params as Params];
34
+ }
35
+ return [false];
36
+ }
37
+
38
+ async handle(request: DecodedRequest<Params, Query, Body>) {
39
+ const organization = await Context.setOptionalOrganizationScope();
40
+ await Context.authenticate()
41
+
42
+ // Fast throw first (more in depth checking for patches later)
43
+ if (organization) {
44
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
45
+ throw Context.auth.error()
46
+ }
47
+ } else {
48
+ if (!Context.auth.hasSomePlatformAccess()) {
49
+ throw Context.auth.error()
50
+ }
51
+ }
52
+
53
+ const members: MemberWithRegistrations[] = []
54
+
55
+ // Cache
56
+ const groups: Group[] = []
57
+
58
+ async function getGroup(id: string) {
59
+ const f = groups.find(g => g.id === id)
60
+ if (f) {
61
+ return f
62
+ }
63
+ const group = await Group.getByID(id)
64
+ if (group) {
65
+ groups.push(group)
66
+ return group
67
+ }
68
+ return null
69
+ }
70
+ const updateGroups = new Map<string, Group>()
71
+
72
+ const balanceItemMemberIds: string[] = []
73
+ const balanceItemRegistrationIdsPerOrganization: Map<string, string[]> = new Map()
74
+
75
+ function addBalanceItemRegistrationId(organizationId: string, registrationId: string) {
76
+ const existing = balanceItemRegistrationIdsPerOrganization.get(organizationId);
77
+ if (existing) {
78
+ existing.push(registrationId)
79
+ return;
80
+ }
81
+ balanceItemRegistrationIdsPerOrganization.set(organizationId, [registrationId])
82
+ }
83
+
84
+ // Loop all members one by one
85
+ for (const put of request.body.getPuts()) {
86
+ const struct = put.put
87
+ let member = new Member()
88
+ .setManyRelation(Member.registrations as any as OneToManyRelation<"registrations", Member, Registration & {group: Group}>, [])
89
+ .setManyRelation(Member.users, [])
90
+ member.id = struct.id
91
+
92
+ if (organization && STAMHOOFD.userMode !== 'platform') {
93
+ member.organizationId = organization.id
94
+ }
95
+
96
+ struct.details.cleanData()
97
+ member.details = struct.details
98
+
99
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member);
100
+ if (duplicate) {
101
+ // Merge data
102
+ duplicate.details.merge(member.details)
103
+ member = duplicate
104
+
105
+ // Only save after checking permissions
106
+ }
107
+
108
+ if (struct.registrations.length === 0) {
109
+ throw new SimpleError({
110
+ code: "missing_group",
111
+ message: "Missing group",
112
+ human: "Schrijf een nieuw lid altijd in voor minstens één groep",
113
+ statusCode: 400
114
+ })
115
+ }
116
+
117
+ // Throw early
118
+ for (const registrationStruct of struct.registrations) {
119
+ const group = await getGroup(registrationStruct.groupId)
120
+ if (!group || group.organizationId !== registrationStruct.organizationId || !await Context.auth.canAccessGroup(group, PermissionLevel.Write)) {
121
+ throw Context.auth.notFoundOrNoAccess("Je hebt niet voldoende rechten om leden toe te voegen in deze groep")
122
+ }
123
+
124
+ // Set organization id of member based on registrations
125
+ if (!organization && STAMHOOFD.userMode !== 'platform' && !member.organizationId) {
126
+ member.organizationId = group.organizationId
127
+ }
128
+ }
129
+
130
+ if (STAMHOOFD.userMode !== 'platform' && !member.organizationId) {
131
+ throw new SimpleError({
132
+ code: "missing_organization",
133
+ message: "Missing organization",
134
+ human: "Je moet een organisatie selecteren voor dit lid",
135
+ statusCode: 400
136
+ })
137
+ }
138
+
139
+ /**
140
+ * In development mode, we allow some secret usernames to create fake data
141
+ */
142
+ if ((STAMHOOFD.environment == "development" || STAMHOOFD.environment == "staging") && organization) {
143
+ if (member.details.firstName.toLocaleLowerCase() == "create" && parseInt(member.details.lastName) > 0) {
144
+ const count = parseInt(member.details.lastName);
145
+ let group = groups[0];
146
+
147
+ for (const registrationStruct of struct.registrations) {
148
+ const g = await getGroup(registrationStruct.groupId)
149
+ if (g) {
150
+ group = g
151
+ }
152
+ }
153
+
154
+ await this.createDummyMembers(organization, group, count)
155
+
156
+ // Skip creating this member
157
+ continue;
158
+ }
159
+ }
160
+
161
+ await member.save()
162
+ members.push(member)
163
+ balanceItemMemberIds.push(member.id)
164
+
165
+ // Add registrations
166
+ for (const registrationStruct of struct.registrations) {
167
+ const group = await getGroup(registrationStruct.groupId)
168
+ if (!group || group.organizationId !== registrationStruct.organizationId || !await Context.auth.canAccessGroup(group, PermissionLevel.Write)) {
169
+ throw Context.auth.notFoundOrNoAccess("Je hebt niet voldoende rechten om leden toe te voegen in deze groep")
170
+ }
171
+
172
+ const reg = await this.addRegistration(member, registrationStruct, group)
173
+ addBalanceItemRegistrationId(reg.organizationId, reg.id)
174
+
175
+ // Update occupancy at the end of the call
176
+ updateGroups.set(group.id, group)
177
+ }
178
+
179
+ // Add users if they don't exist (only placeholders allowed)
180
+ for (const placeholder of struct.users) {
181
+ await PatchOrganizationMembersEndpoint.linkUser(placeholder, member)
182
+ }
183
+
184
+ // Auto link users based on data
185
+ await PatchOrganizationMembersEndpoint.updateManagers(member)
186
+ }
187
+
188
+ // Loop all members one by one
189
+ for (let patch of request.body.getPatches()) {
190
+ const member = members.find(m => m.id === patch.id) ?? await Member.getWithRegistrations(patch.id)
191
+ if (!member || !await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
192
+ throw Context.auth.notFoundOrNoAccess("Je hebt geen toegang tot dit lid of het bestaat niet")
193
+ }
194
+ patch = await Context.auth.filterMemberPatch(member, patch)
195
+
196
+ if (patch.details) {
197
+ if (patch.details.isPut()) {
198
+ throw new SimpleError({
199
+ code: "not_allowed",
200
+ message: "Cannot override details",
201
+ human: "Er ging iets mis bij het aanpassen van de gegevens van dit lid. Probeer het later opnieuw en neem contact op als het probleem zich blijft voordoen.",
202
+ field: "details"
203
+ })
204
+ }
205
+
206
+ member.details.patchOrPut(patch.details)
207
+ member.details.cleanData()
208
+ }
209
+
210
+ await member.save();
211
+
212
+ // Update documents
213
+ await Document.updateForMember(member.id)
214
+
215
+ // Update registrations
216
+ for (const patchRegistration of patch.registrations.getPatches()) {
217
+ const registration = member.registrations.find(r => r.id === patchRegistration.id)
218
+ if (!registration || registration.memberId != member.id) {
219
+ throw new SimpleError({
220
+ code: "permission_denied",
221
+ message: "You don't have permissions to access this endpoint",
222
+ human: "Je hebt geen toegang om deze registratie te wijzigen"
223
+ })
224
+ }
225
+
226
+ let group: Group | null = null
227
+
228
+
229
+ if (patchRegistration.groupId) {
230
+ group = await getGroup(patchRegistration.groupId)
231
+ if (group) {
232
+ // We need to update group occupancy because we moved a member to it
233
+ updateGroups.set(group.id, group)
234
+ }
235
+ const oldGroup = await getGroup(registration.groupId)
236
+ if (oldGroup) {
237
+ // We need to update this group occupancy because we moved one member away from it
238
+ updateGroups.set(oldGroup.id, oldGroup)
239
+ }
240
+ } else {
241
+ group = await getGroup(registration.groupId)
242
+ }
243
+
244
+ if (!group || group.organizationId !== (patchRegistration.organizationId ?? registration.organizationId)) {
245
+ throw new SimpleError({
246
+ code: "invalid_field",
247
+ message: "Group doesn't exist",
248
+ human: "De groep naarwaar je dit lid wilt verplaatsen bestaat niet",
249
+ field: "groupId"
250
+ })
251
+ }
252
+
253
+ if (!await Context.auth.canAccessGroup(group, PermissionLevel.Write)) {
254
+ throw Context.auth.error("Je hebt niet voldoende rechten om leden te verplaatsen naar deze groep")
255
+ }
256
+
257
+ if (patchRegistration.cycle && patchRegistration.cycle > group.cycle) {
258
+ throw new SimpleError({
259
+ code: "invalid_field",
260
+ message: "Invalid cycle",
261
+ human: "Je kan een lid niet inschrijven voor een groep die nog moet starten",
262
+ field: "cycle"
263
+ })
264
+ }
265
+
266
+ // TODO: allow group changes
267
+ registration.waitingList = patchRegistration.waitingList ?? registration.waitingList
268
+
269
+ if (!registration.waitingList && registration.registeredAt === null) {
270
+ registration.registeredAt = new Date()
271
+ }
272
+ registration.canRegister = patchRegistration.canRegister ?? registration.canRegister
273
+ if (!registration.waitingList) {
274
+ registration.canRegister = false
275
+ }
276
+ registration.cycle = patchRegistration.cycle ?? registration.cycle
277
+ registration.groupId = patchRegistration.groupId ?? registration.groupId
278
+ registration.organizationId = patchRegistration.organizationId ?? registration.organizationId
279
+
280
+ // Check if we should create a placeholder payment?
281
+
282
+ if (patchRegistration.cycle !== undefined || patchRegistration.waitingList !== undefined || patchRegistration.canRegister !== undefined) {
283
+ // We need to update occupancy (because cycle / waitlist change)
284
+ updateGroups.set(group.id, group)
285
+ }
286
+
287
+ if (patchRegistration.price) {
288
+ // Create balance item
289
+ const balanceItem = new BalanceItem();
290
+ balanceItem.registrationId = registration.id;
291
+ balanceItem.price = patchRegistration.price
292
+ balanceItem.description = group ? `Inschrijving ${group.settings.name}` : `Inschrijving`
293
+ balanceItem.pricePaid = patchRegistration.pricePaid ?? 0
294
+ balanceItem.memberId = registration.memberId;
295
+ balanceItem.userId = member.users[0]?.id ?? null
296
+ balanceItem.organizationId = group.organizationId
297
+ balanceItem.status = BalanceItemStatus.Pending;
298
+ await balanceItem.save();
299
+
300
+ addBalanceItemRegistrationId(registration.organizationId, registration.id)
301
+ balanceItemMemberIds.push(member.id)
302
+
303
+ if (balanceItem.pricePaid > 0) {
304
+ // Create an Unknown payment and attach it to the balance item
305
+ const payment = new Payment();
306
+ payment.userId = member.users[0]?.id ?? null
307
+ payment.organizationId = member.organizationId
308
+ payment.method = PaymentMethod.Unknown
309
+ payment.status = PaymentStatus.Succeeded
310
+ payment.price = balanceItem.pricePaid;
311
+ payment.paidAt = new Date()
312
+ payment.provider = null
313
+ await payment.save()
314
+
315
+ const balanceItemPayment = new BalanceItemPayment()
316
+ balanceItemPayment.balanceItemId = balanceItem.id;
317
+ balanceItemPayment.paymentId = payment.id;
318
+ balanceItemPayment.organizationId = group.organizationId
319
+ balanceItemPayment.price = payment.price;
320
+ await balanceItemPayment.save();
321
+ }
322
+ }
323
+
324
+ await registration.save()
325
+ }
326
+
327
+ for (const deleteId of patch.registrations.getDeletes()) {
328
+ const registration = member.registrations.find(r => r.id === deleteId)
329
+ if (!registration || registration.memberId != member.id) {
330
+ throw new SimpleError({
331
+ code: "permission_denied",
332
+ message: "You don't have permissions to access this endpoint",
333
+ human: "Je hebt geen toegang om deze registratie te wijzigen"
334
+ })
335
+ }
336
+
337
+ if (!await Context.auth.canAccessRegistration(registration, PermissionLevel.Write)) {
338
+ throw Context.auth.error("Je hebt niet voldoende rechten om deze inschrijving te verwijderen")
339
+ }
340
+
341
+ balanceItemMemberIds.push(member.id)
342
+ await BalanceItem.deleteForDeletedRegistration(registration.id)
343
+ await registration.delete()
344
+ member.registrations = member.registrations.filter(r => r.id !== deleteId)
345
+
346
+ const oldGroup = await getGroup(registration.groupId)
347
+ if (oldGroup) {
348
+ // We need to update this group occupancy because we moved one member away from it
349
+ updateGroups.set(oldGroup.id, oldGroup)
350
+ }
351
+ }
352
+
353
+ // Add registrations
354
+ for (const registrationStruct of patch.registrations.getPuts()) {
355
+ const struct = registrationStruct.put
356
+ const group = await getGroup(struct.groupId)
357
+
358
+ if (!group || group.organizationId !== struct.organizationId || !await Context.auth.canAccessGroup(group, PermissionLevel.Write)) {
359
+ throw Context.auth.error("Je hebt niet voldoende rechten om inschrijvingen in deze groep te maken")
360
+ }
361
+
362
+ const reg = await this.addRegistration(member, struct, group)
363
+ balanceItemMemberIds.push(member.id)
364
+ addBalanceItemRegistrationId(reg.organizationId, reg.id)
365
+
366
+ // We need to update this group occupancy because we moved one member away from it
367
+ updateGroups.set(group.id, group)
368
+ }
369
+
370
+ // Update responsibilities
371
+ for (const patchResponsibility of patch.responsibilities.getPatches()) {
372
+ if (!Context.auth.hasPlatformFullAccess() && !(organization && await Context.auth.hasFullAccess(organization.id))) {
373
+ throw Context.auth.error("Je hebt niet voldoende rechten om functies van leden aan te passen")
374
+ }
375
+
376
+ const responsibilityRecord = await MemberResponsibilityRecord.getByID(patchResponsibility.id)
377
+ if (!responsibilityRecord || responsibilityRecord.memberId != member.id || (organization && responsibilityRecord.organizationId !== organization.id)) {
378
+ throw new SimpleError({
379
+ code: "permission_denied",
380
+ message: "You don't have permissions to access this endpoint",
381
+ human: "Je hebt geen toegang om deze functie te wijzigen"
382
+ })
383
+ }
384
+
385
+ const platform = await Platform.getShared()
386
+ const responsibility = platform.config.responsibilities.find(r => r.id === patchResponsibility.responsibilityId)
387
+
388
+ if (responsibility && !responsibility.assignableByOrganizations && !Context.auth.hasPlatformFullAccess()) {
389
+ throw Context.auth.error("Je hebt niet voldoende rechten om deze functie aan te passen")
390
+ }
391
+
392
+ // Allow patching begin and end date
393
+ if (patchResponsibility.endDate !== undefined) {
394
+ if (responsibilityRecord.endDate) {
395
+ if (!Context.auth.hasPlatformFullAccess()) {
396
+ throw Context.auth.error("Je hebt niet voldoende rechten om reeds beëindigde functies aan te passen")
397
+ }
398
+ }
399
+ responsibilityRecord.endDate = patchResponsibility.endDate
400
+ }
401
+
402
+ if (patchResponsibility.startDate !== undefined) {
403
+
404
+ if (patchResponsibility.startDate > new Date()) {
405
+ throw Context.auth.error("Je kan de startdatum van een functie niet in de toekomst zetten")
406
+ }
407
+ const daysDiff = Math.abs((new Date().getTime() - patchResponsibility.startDate.getTime()) / (1000 * 60 * 60 * 24))
408
+
409
+ if (daysDiff > 60 && !Context.auth.hasPlatformFullAccess()) {
410
+ throw Context.auth.error("Je kan de startdatum van een functie niet zoveel verplaatsen")
411
+ }
412
+ responsibilityRecord.startDate = patchResponsibility.startDate
413
+ }
414
+
415
+ await responsibilityRecord.save()
416
+ }
417
+
418
+ // Update responsibilities
419
+ for (const {put} of patch.responsibilities.getPuts()) {
420
+ if (!Context.auth.hasPlatformFullAccess() && !(organization && await Context.auth.hasFullAccess(organization.id))) {
421
+ throw Context.auth.error("Je hebt niet voldoende rechten om functies van leden aan te passen")
422
+ }
423
+
424
+ const platform = await Platform.getShared()
425
+ const responsibility = platform.config.responsibilities.find(r => r.id === put.responsibilityId)
426
+
427
+ if (!responsibility || (!responsibility.assignableByOrganizations && !Context.auth.hasPlatformFullAccess())) {
428
+ throw Context.auth.error("Je hebt niet voldoende rechten om deze functie toe te kennen")
429
+ }
430
+
431
+ const model = new MemberResponsibilityRecord()
432
+ model.memberId = member.id
433
+ model.responsibilityId = responsibility.id
434
+
435
+ if (responsibility.assignableByOrganizations) {
436
+ if (organization) {
437
+ model.organizationId = organization.id
438
+ } else {
439
+ if (!put.organizationId) {
440
+ if (!Context.auth.hasPlatformFullAccess()) {
441
+ throw Context.auth.error("Je hebt niet voldoende rechten om deze functie toe te kennen")
442
+ }
443
+ } else if (!await Context.auth.hasFullAccess(put.organizationId)) {
444
+ throw Context.auth.error("Je hebt niet voldoende rechten om functies van leden toe te kennen voor deze vereniging")
445
+ }
446
+ model.organizationId = put.organizationId
447
+ }
448
+ } else {
449
+ model.organizationId = null
450
+ }
451
+
452
+ // Allow patching begin and end date
453
+ model.endDate = put.endDate
454
+
455
+ if (put.startDate > new Date()) {
456
+ throw Context.auth.error("Je kan de startdatum van een functie niet in de toekomst zetten")
457
+ }
458
+
459
+ model.startDate = put.startDate
460
+
461
+ await model.save()
462
+ }
463
+
464
+ // Link users
465
+ for (const placeholder of patch.users.getPuts()) {
466
+ await PatchOrganizationMembersEndpoint.linkUser(placeholder.put, member)
467
+ }
468
+
469
+ // Unlink users
470
+ for (const userId of patch.users.getDeletes()) {
471
+ await PatchOrganizationMembersEndpoint.unlinkUser(userId, member)
472
+ }
473
+
474
+ // Auto link users based on data
475
+ if (patch.users.changes.length || patch.details) {
476
+ await PatchOrganizationMembersEndpoint.updateManagers(member)
477
+ }
478
+
479
+ if (!members.find(m => m.id === member.id)) {
480
+ members.push(member)
481
+ }
482
+ }
483
+
484
+ // Loop all members one by one
485
+ for (const id of request.body.getDeletes()) {
486
+ const member = await Member.getWithRegistrations(id)
487
+ if (!member || !await Context.auth.canDeleteMember(member)) {
488
+ throw Context.auth.error("Je hebt niet voldoende rechten om dit lid te verwijderen")
489
+ }
490
+
491
+ await User.deleteForDeletedMember(member.id)
492
+ await BalanceItem.deleteForDeletedMember(member.id)
493
+ await member.delete()
494
+
495
+ // Update occupancy of this member because we removed registrations
496
+ const groupIds = member.registrations.flatMap(r => r.groupId)
497
+ for (const id of groupIds) {
498
+ const group = await getGroup(id)
499
+ if (group) {
500
+ // We need to update this group occupancy because we moved one member away from it
501
+ updateGroups.set(group.id, group)
502
+ }
503
+ }
504
+ }
505
+
506
+ await Member.updateOutstandingBalance(Formatter.uniqueArray(balanceItemMemberIds))
507
+ for (const [organizationId, balanceItemRegistrationIds] of balanceItemRegistrationIdsPerOrganization) {
508
+ await Registration.updateOutstandingBalance(Formatter.uniqueArray(balanceItemRegistrationIds), organizationId)
509
+ }
510
+
511
+ // Loop all groups and update occupancy if needed
512
+ for (const group of updateGroups.values()) {
513
+ await group.updateOccupancy()
514
+ await group.save()
515
+ }
516
+
517
+ // We need to refetch the outstanding amounts of members that have changed
518
+ const updatedMembers = balanceItemMemberIds.length > 0 ? await Member.getBlobByIds(...balanceItemMemberIds) : []
519
+ for (const member of updatedMembers) {
520
+ const index = members.findIndex(m => m.id === member.id)
521
+ if (index !== -1) {
522
+ members[index] = member
523
+ }
524
+ }
525
+
526
+ return new Response(
527
+ await AuthenticatedStructures.membersBlob(members)
528
+ );
529
+ }
530
+
531
+ static async checkDuplicate(member: Member) {
532
+ if (!member.details.birthDay) {
533
+ return
534
+ }
535
+ const existingMembers = await Member.where({ organizationId: member.organizationId, firstName: member.details.firstName, lastName: member.details.lastName, birthDay: Formatter.dateIso(member.details.birthDay) });
536
+
537
+ if (existingMembers.length > 0) {
538
+ const withRegistrations = await Member.getBlobByIds(...existingMembers.map(m => m.id))
539
+ for (const member of withRegistrations) {
540
+ if (member.registrations.length > 0) {
541
+ return member
542
+ }
543
+ }
544
+
545
+ if (withRegistrations.length > 0) {
546
+ return withRegistrations[0]
547
+ }
548
+ }
549
+ }
550
+
551
+ async addRegistration(member: Member & Record<"registrations", (Registration & {group: Group})[]> & Record<"users", User[]>, registrationStruct: RegistrationStruct, group: Group) {
552
+ // Check if this member has this registration already.
553
+ // Note: we cannot use the relation here, because invalid ones or reserved ones are not loaded in there
554
+ const existings = await Registration.where({
555
+ memberId: member.id,
556
+ groupId: registrationStruct.groupId,
557
+ cycle: registrationStruct.cycle
558
+ }, { limit: 1 })
559
+ const existing = existings.length > 0 ? existings[0] : null
560
+
561
+ // If the existing is invalid, delete it.
562
+ if (existing && !existing.registeredAt && !existing.waitingList) {
563
+ console.log('Deleting invalid registration', existing.id)
564
+ await existing.delete()
565
+ } else if (existing) {
566
+ throw new SimpleError({
567
+ code: "invalid_field",
568
+ message: "Registration already exists",
569
+ human: existing.waitingList ? "Dit lid staat al op de wachtlijst voor deze groep" : "Dit lid is al ingeschreven voor deze groep",
570
+ field: "groupId"
571
+ });
572
+ }
573
+
574
+ if (!group) {
575
+ throw new SimpleError({
576
+ code: 'invalid_field',
577
+ field: 'groupId',
578
+ message: 'Invalid groupId',
579
+ human: 'Deze inschrijvingsgroep is ongeldig'
580
+ })
581
+ }
582
+
583
+ const registration = new Registration()
584
+ registration.groupId = registrationStruct.groupId
585
+ registration.organizationId = group.organizationId
586
+ registration.periodId = group.periodId
587
+ registration.cycle = registrationStruct.cycle
588
+ registration.memberId = member.id
589
+ registration.registeredAt = registrationStruct.registeredAt
590
+ registration.waitingList = registrationStruct.waitingList
591
+ registration.createdAt = registrationStruct.createdAt ?? new Date()
592
+
593
+ if (registration.waitingList) {
594
+ registration.registeredAt = null
595
+ }
596
+ registration.canRegister = registrationStruct.canRegister
597
+
598
+ if (!registration.waitingList) {
599
+ registration.canRegister = false
600
+ }
601
+ registration.deactivatedAt = registrationStruct.deactivatedAt
602
+
603
+ await registration.save()
604
+ member.registrations.push(registration.setRelation(Registration.group, group))
605
+
606
+ if (registrationStruct.price) {
607
+ // Create balance item
608
+ const balanceItem = new BalanceItem();
609
+ balanceItem.registrationId = registration.id;
610
+ balanceItem.price = registrationStruct.price
611
+ balanceItem.description = group ? `Inschrijving ${group.settings.name}` : `Inschrijving`
612
+ balanceItem.pricePaid = registrationStruct.pricePaid ?? 0
613
+ balanceItem.memberId = registration.memberId;
614
+ balanceItem.userId = member.users[0]?.id ?? null
615
+ balanceItem.organizationId = group.organizationId
616
+ balanceItem.status = BalanceItemStatus.Pending;
617
+ await balanceItem.save();
618
+
619
+ if (balanceItem.pricePaid > 0) {
620
+ // Create an Unknown payment and attach it to the balance item
621
+ const payment = new Payment();
622
+ payment.userId = member.users[0]?.id ?? null
623
+ payment.organizationId = member.organizationId
624
+ payment.method = PaymentMethod.Unknown
625
+ payment.status = PaymentStatus.Succeeded
626
+ payment.price = balanceItem.pricePaid;
627
+ payment.paidAt = new Date()
628
+ payment.provider = null
629
+ await payment.save()
630
+
631
+ const balanceItemPayment = new BalanceItemPayment()
632
+ balanceItemPayment.balanceItemId = balanceItem.id;
633
+ balanceItemPayment.paymentId = payment.id;
634
+ balanceItemPayment.organizationId = group.organizationId
635
+ balanceItemPayment.price = payment.price;
636
+ await balanceItemPayment.save();
637
+ }
638
+ }
639
+
640
+ return registration
641
+ }
642
+
643
+ async createDummyMembers(organization: Organization, group: Group, count: number) {
644
+ const members = await new MemberFactory({
645
+ organization,
646
+ minAge: group.settings.minAge ?? undefined,
647
+ maxAge: group.settings.maxAge ?? undefined
648
+ }).createMultiple(count)
649
+
650
+ for (const m of members) {
651
+ const member = m.setManyRelation(Member.registrations as unknown as OneToManyRelation<"registrations", Member, Registration>, []).setManyRelation(Member.users, [])
652
+ const d = new Date(new Date().getTime() - Math.random() * 60 * 1000 * 60 * 24 * 60)
653
+
654
+ // Create a registration for this member for thisg roup
655
+ const registration = new Registration()
656
+ registration.organizationId = organization.id
657
+ registration.memberId = member.id
658
+ registration.groupId = group.id
659
+ registration.periodId = group.periodId
660
+ registration.cycle = group.cycle
661
+ registration.registeredAt = d
662
+
663
+ member.registrations.push(registration)
664
+ await registration.save()
665
+ }
666
+ }
667
+
668
+ static async updateManagers(member: MemberWithRegistrations) {
669
+ // Check accounts
670
+ const managers = member.details.getManagerEmails()
671
+
672
+ for(const email of managers) {
673
+ const u = member.users.find(u => u.email.toLocaleLowerCase() === email.toLocaleLowerCase())
674
+ if (!u) {
675
+ console.log("Linking user "+email+" to member "+member.id)
676
+ await PatchOrganizationMembersEndpoint.linkUser(UserStruct.create({
677
+ firstName: member.details.parents.find(p => p.email === email)?.firstName,
678
+ lastName: member.details.parents.find(p => p.email === email)?.lastName,
679
+ email,
680
+ }), member)
681
+ }
682
+ }
683
+
684
+ // Delete accounts that should no longer have access
685
+ for (const u of member.users) {
686
+ if (!u.hasAccount()) {
687
+ // And not in managers list (case insensitive)
688
+ if (!managers.find(m => m.toLocaleLowerCase() === u.email.toLocaleLowerCase())) {
689
+ console.log("Unlinking user "+u.email+" from member "+member.id)
690
+ await PatchOrganizationMembersEndpoint.unlinkUser(u.id, member)
691
+ }
692
+ }
693
+ }
694
+ }
695
+
696
+ static async linkUser(user: UserStruct, member: MemberWithRegistrations) {
697
+ const email = user.email
698
+ let u = await User.getForAuthentication(member.organizationId, email, {allowWithoutAccount: true});
699
+ if (u) {
700
+ console.log("Giving an existing user access to a member: "+u.id)
701
+ } else {
702
+ u = new User()
703
+ u.organizationId = member.organizationId
704
+ u.email = email
705
+ u.firstName = user.firstName
706
+ u.lastName = user.lastName
707
+ await u.save()
708
+
709
+ console.log("Created new (placeholder) user that has access to a member: "+u.id)
710
+ }
711
+
712
+ await Member.users.reverse("members").link(u, [member])
713
+
714
+ // Update model relation to correct response
715
+ member.users.push(u)
716
+ }
717
+
718
+ static async unlinkUser(userId: string, member: MemberWithRegistrations) {
719
+ console.log("Removing access for "+ userId +" to member "+member.id)
720
+ const existingIndex = member.users.findIndex(u => u.id === userId)
721
+ if (existingIndex === -1) {
722
+ throw new SimpleError({
723
+ code: "user_not_found",
724
+ message: "Unlinking a user that doesn't exists anymore",
725
+ human: "Je probeert de toegang van een account tot een lid te verwijderen, maar dat account bestaat niet (meer)"
726
+ })
727
+ }
728
+ const existing = member.users[existingIndex]
729
+ await Member.users.reverse("members").unlink(existing, member)
730
+
731
+ // Update model relation to correct response
732
+ member.users.splice(existingIndex, 1)
733
+ }
734
+ }