@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,134 @@
1
+ import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { SimpleError } from '@simonbackx/simple-errors';
4
+ import { Document, Member } from '@stamhoofd/models';
5
+ import { MemberWithRegistrationsBlob, MembersBlob } from "@stamhoofd/structures";
6
+
7
+ import { Context } from '../../../helpers/Context';
8
+ import { PatchOrganizationMembersEndpoint } from '../../global/members/PatchOrganizationMembersEndpoint';
9
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
10
+ type Params = Record<string, never>;
11
+ type Query = undefined;
12
+ type Body = PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>
13
+ type ResponseBody = MembersBlob
14
+
15
+ /**
16
+ * Allow to add, patch and delete multiple members simultaneously, which is needed in order to sync relational data that is saved encrypted in multiple members (e.g. parents)
17
+ */
18
+ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
+ bodyDecoder = new PatchableArrayDecoder(MemberWithRegistrationsBlob as Decoder<MemberWithRegistrationsBlob>, MemberWithRegistrationsBlob.patchType() as Decoder<AutoEncoderPatchType<MemberWithRegistrationsBlob>>, StringDecoder)
20
+
21
+ protected doesMatch(request: Request): [true, Params] | [false] {
22
+ if (request.method != "PATCH") {
23
+ return [false];
24
+ }
25
+
26
+ const params = Endpoint.parseParameters(request.url, "/members", {});
27
+
28
+ if (params) {
29
+ return [true, params as Params];
30
+ }
31
+ return [false];
32
+ }
33
+
34
+ async handle(request: DecodedRequest<Params, Query, Body>) {
35
+ const organization = await Context.setUserOrganizationScope();
36
+ const {user} = await Context.authenticate()
37
+
38
+ // Process changes
39
+ const addedMembers: Member[] = []
40
+ for (const put of request.body.getPuts()) {
41
+ const struct = put.put
42
+
43
+ const member = new Member()
44
+ member.id = struct.id
45
+ member.organizationId = organization?.id ?? null
46
+
47
+ struct.details.cleanData()
48
+ member.details = struct.details
49
+
50
+ if (!struct.details) {
51
+ throw new SimpleError({
52
+ code: "invalid_data",
53
+ message: "No details provided",
54
+ human: "Opgelet! Je gebruikt een oudere versie van de inschrijvingspagina die niet langer wordt ondersteund. Herlaad de website grondig en wis je browser cache.",
55
+ field: "details"
56
+ })
57
+ }
58
+
59
+ // Check for duplicates and prevent creating a duplicate member by a user
60
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member);
61
+ if (duplicate) {
62
+ // Merge data
63
+ duplicate.details.merge(member.details)
64
+ await duplicate.save()
65
+ addedMembers.push(duplicate)
66
+ continue;
67
+ }
68
+
69
+ await member.save()
70
+ addedMembers.push(member)
71
+ }
72
+
73
+ if (addedMembers.length > 0) {
74
+ // Give access to created members
75
+ await Member.users.reverse("members").link(user, addedMembers)
76
+
77
+ }
78
+
79
+ // Modify members
80
+ const members = await Member.getMembersWithRegistrationForUser(user)
81
+
82
+ for (const member of addedMembers) {
83
+ const updatedMember = members.find(m => m.id === member.id);
84
+ if (updatedMember) {
85
+ // Make sure we also give access to other parents
86
+ await PatchOrganizationMembersEndpoint.updateManagers(updatedMember)
87
+ await Document.updateForMember(updatedMember.id)
88
+ }
89
+ }
90
+
91
+ for (let struct of request.body.getPatches()) {
92
+ const member = members.find((m) => m.id == struct.id)
93
+ if (!member) {
94
+ throw new SimpleError({
95
+ code: "invalid_member",
96
+ message: "This member does not exist or you don't have permissions to modify this member",
97
+ human: "Je probeert een lid aan te passen die niet (meer) bestaat. Er ging ergens iets mis."
98
+ })
99
+ }
100
+ struct = await Context.auth.filterMemberPatch(member, struct)
101
+
102
+ if (struct.details) {
103
+ if (struct.details.isPut()) {
104
+ throw new SimpleError({
105
+ code: "not_allowed",
106
+ message: "Cannot override details",
107
+ 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.",
108
+ field: "details"
109
+ })
110
+ }
111
+ member.details.patchOrPut(struct.details)
112
+ member.details.cleanData()
113
+ }
114
+
115
+ if (!member.details) {
116
+ throw new SimpleError({
117
+ code: "invalid_data",
118
+ message: "No details provided",
119
+ human: "Opgelet! Je gebruikt een oudere versie van de inschrijvingspagina die niet langer wordt ondersteund. Herlaad de website grondig en wis je browser cache.",
120
+ field: "details"
121
+ })
122
+ }
123
+ await member.save();
124
+ await PatchOrganizationMembersEndpoint.updateManagers(member)
125
+
126
+ // Update documents
127
+ await Document.updateForMember(member.id)
128
+ }
129
+
130
+ return new Response(
131
+ await AuthenticatedStructures.membersBlob(members)
132
+ );
133
+ }
134
+ }
@@ -0,0 +1,521 @@
1
+ import { createMollieClient, PaymentMethod as molliePaymentMethod } from '@mollie/api-client';
2
+ import { ManyToOneRelation } from '@simonbackx/simple-database';
3
+ import { Decoder } from '@simonbackx/simple-encoding';
4
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
5
+ import { SimpleError } from '@simonbackx/simple-errors';
6
+ import { I18n } from '@stamhoofd/backend-i18n';
7
+ import { Email } from '@stamhoofd/email';
8
+ import { BalanceItem, BalanceItemPayment, Group, Member, MolliePayment, MollieToken, PayconiqPayment, Payment, Platform, RateLimiter, Registration } from '@stamhoofd/models';
9
+ import { BalanceItemStatus, IDRegisterCheckout, MemberBalanceItem, MemberDetails, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PlatformFamily, RegisterItem, RegisterResponse, Version } from "@stamhoofd/structures";
10
+ import { Formatter } from '@stamhoofd/utility';
11
+
12
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
13
+ import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
14
+ import { Context } from '../../../helpers/Context';
15
+ import { StripeHelper } from '../../../helpers/StripeHelper';
16
+ import { ExchangePaymentEndpoint } from '../../organization/shared/ExchangePaymentEndpoint';
17
+ type Params = Record<string, never>;
18
+ type Query = undefined;
19
+ type Body = IDRegisterCheckout
20
+ type ResponseBody = RegisterResponse
21
+
22
+ export const demoLimiter = new RateLimiter({
23
+ limits: [
24
+ {
25
+ // Max 10 per hour
26
+ limit: 10,
27
+ duration: 60 * 1000 * 60
28
+ },
29
+ {
30
+ // Max 20 a day
31
+ limit: 20,
32
+ duration: 24 * 60 * 1000 * 60
33
+ }
34
+ ]
35
+ });
36
+
37
+ export type RegistrationWithMemberAndGroup = Registration & { member: Member } & { group: Group }
38
+
39
+ /**
40
+ * Allow to add, patch and delete multiple members simultaneously, which is needed in order to sync relational data that is saved encrypted in multiple members (e.g. parents)
41
+ */
42
+ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
43
+ bodyDecoder = IDRegisterCheckout as Decoder<IDRegisterCheckout>
44
+
45
+ protected doesMatch(request: Request): [true, Params] | [false] {
46
+ if (request.method != "POST") {
47
+ return [false];
48
+ }
49
+
50
+ const params = Endpoint.parseParameters(request.url, "/members/register", {});
51
+
52
+ if (params) {
53
+ if (request.getVersion() < 257) {
54
+ throw new SimpleError({
55
+ code: "not_supported",
56
+ message: "This version is no longer supported",
57
+ human: "Oops! Je gebruikt een oude versie van de applicatie om in te schrijven. Herlaad de website en verwijder indien nodig de cache van jouw browser."
58
+ })
59
+ }
60
+ return [true, params as Params];
61
+ }
62
+ return [false];
63
+ }
64
+
65
+ async handle(request: DecodedRequest<Params, Query, Body>) {
66
+ const organization = await Context.setOrganizationScope();
67
+ const {user} = await Context.authenticate()
68
+
69
+ // For non paid organizations, limit amount of tests
70
+ if (!organization.meta.packages.isPaid) {
71
+ const limiter = demoLimiter
72
+
73
+ try {
74
+ limiter.track(organization.id, 1);
75
+ } catch (e) {
76
+ Email.sendInternal({
77
+ to: "hallo@stamhoofd.be",
78
+ subject: "[Limiet] Limiet bereikt voor aantal inschrijvingen",
79
+ text: "Beste, \nDe limiet werd bereikt voor het aantal inschrijvingen per dag. \nVereniging: "+organization.id+" ("+organization.name+")" + "\n\n" + e.message + "\n\nStamhoofd"
80
+ }, new I18n("nl", "BE"))
81
+
82
+ throw new SimpleError({
83
+ code: "too_many_emails_period",
84
+ message: "Too many e-mails limited",
85
+ human: "Oeps! Om spam te voorkomen limiteren we het aantal test inschrijvingen die je per uur of dag kan plaatsen. Probeer over een uur opnieuw of schakel over naar een betaald abonnement.",
86
+ field: "recipients"
87
+ })
88
+ }
89
+ }
90
+
91
+ const members = await Member.getMembersWithRegistrationForUser(user)
92
+ const groups = await Group.getAll(organization.id, organization.periodId)
93
+
94
+ const blob = await AuthenticatedStructures.membersBlob(members, true)
95
+ const family = PlatformFamily.create(blob, {platform: await Platform.getSharedStruct(), contextOrganization: await organization.getStructure()})
96
+ const checkout = request.body.hydrate({family})
97
+
98
+ const registrations: RegistrationWithMemberAndGroup[] = []
99
+ const payRegistrations: {registration: RegistrationWithMemberAndGroup, item: RegisterItem}[] = []
100
+
101
+ if (checkout.cart.isEmpty) {
102
+ throw new SimpleError({
103
+ code: "empty_data",
104
+ message: "Oeps, jouw mandje is leeg. Voeg eerst inschrijvingen toe voor je verder gaat."
105
+ })
106
+ }
107
+
108
+ // Update occupancies
109
+ // TODO: might not be needed in the future (for performance)
110
+ for (const group of groups) {
111
+ if (request.body.cart.items.find(i => i.groupId == group.id)) {
112
+ await group.updateOccupancy()
113
+ }
114
+ }
115
+
116
+ // Validate balance items (can only happen serverside)
117
+ const balanceItemIds = request.body.cart.balanceItems.map(i => i.item.id)
118
+ let memberBalanceItems: MemberBalanceItem[] = []
119
+ let balanceItems: BalanceItem[] = []
120
+ if (balanceItemIds.length > 0) {
121
+ balanceItems = await BalanceItem.where({ id: { sign:'IN', value: balanceItemIds }, organizationId: organization.id })
122
+ if (balanceItems.length != balanceItemIds.length) {
123
+ throw new SimpleError({
124
+ code: "invalid_data",
125
+ message: "Oeps, één of meerdere openstaande bedragen in jouw winkelmandje zijn aangepast. Herlaad de pagina en probeer opnieuw."
126
+ })
127
+ }
128
+ memberBalanceItems = await BalanceItem.getMemberStructure(balanceItems)
129
+ }
130
+
131
+ // Validate the cart
132
+ checkout.validate({memberBalanceItems})
133
+
134
+ // Recalculate the price
135
+ checkout.updatePrices()
136
+
137
+ const totalPrice = checkout.totalPrice
138
+
139
+ if (totalPrice < 0) {
140
+ throw new SimpleError({
141
+ code: "empty_data",
142
+ message: "Oeps! De totaalprijs is negatief."
143
+ })
144
+ }
145
+
146
+ const registrationMemberRelation = new ManyToOneRelation(Member, "member")
147
+ registrationMemberRelation.foreignKey = "memberId"
148
+
149
+ mainLoop: for (const item of checkout.cart.items) {
150
+ const member = members.find(m => m.id == item.memberId)
151
+ if (!member) {
152
+ throw new SimpleError({
153
+ code: "invalid_member",
154
+ message: "Het lid dat je probeert in te schrijven konden we niet meer terugvinden. Je herlaadt best even de pagina om opnieuw te proberen."
155
+ })
156
+ }
157
+
158
+ const group = groups.find(g => g.id == item.groupId);
159
+ if (!group) {
160
+ throw new SimpleError({
161
+ code: "invalid_member",
162
+ message: "De groep waarin je een lid probeert in te schrijven lijkt niet meer te bestaan. Je herlaadt best even de pagina om opnieuw te proberen."
163
+ })
164
+ }
165
+
166
+ // Check if this member is already registered in this group?
167
+ const existingRegistrations = await Registration.where({ memberId: member.id, groupId: item.groupId, cycle: group.cycle })
168
+ let registration: RegistrationWithMemberAndGroup | undefined = undefined;
169
+
170
+ for (const existingRegistration of existingRegistrations) {
171
+ registration = existingRegistration
172
+ .setRelation(registrationMemberRelation, member as Member)
173
+ .setRelation(Registration.group, group)
174
+
175
+ if (existingRegistration.waitingList && item.waitingList) {
176
+ // already on waiting list, no need to repeat it
177
+ // skip without error
178
+ registrations.push(registration)
179
+ continue mainLoop;
180
+ }
181
+
182
+ if (!existingRegistration.waitingList && existingRegistration.registeredAt !== null) {
183
+ // already registered, no need to put it on the waiting list or register (and pay) again
184
+ registrations.push(registration)
185
+ continue mainLoop;
186
+ }
187
+ }
188
+
189
+ if (!registration) {
190
+ registration = new Registration()
191
+ .setRelation(registrationMemberRelation, member as Member)
192
+ .setRelation(Registration.group, group)
193
+ registration.organizationId = organization.id
194
+ registration.periodId = group.periodId
195
+ }
196
+
197
+ registration.memberId = member.id
198
+ registration.groupId = group.id
199
+ registration.cycle = group.cycle
200
+
201
+ if (item.waitingList) {
202
+ registration.waitingList = true
203
+ registration.reservedUntil = null
204
+ await registration.save()
205
+ } else {
206
+ registration.waitingList = false
207
+ registration.canRegister = false
208
+ registration.price = item.calculatedPrice
209
+ payRegistrations.push({
210
+ registration,
211
+ item
212
+ });
213
+ }
214
+ registrations.push(registration)
215
+ }
216
+
217
+ // Validate payment method
218
+ if (totalPrice > 0) {
219
+ const allowedPaymentMethods = organization.meta.registrationPaymentConfiguration.paymentMethods
220
+
221
+ if (!checkout.paymentMethod || !allowedPaymentMethods.includes(checkout.paymentMethod)) {
222
+ throw new SimpleError({
223
+ code: "invalid_payment_method",
224
+ message: "Oeps, je hebt geen geldige betaalmethode geselecteerd. Selecteer een betaalmethode en probeer opnieuw. Herlaad de pagina indien nodig."
225
+ })
226
+ }
227
+ } else {
228
+ checkout.paymentMethod = PaymentMethod.Unknown
229
+ }
230
+
231
+ const payment = new Payment()
232
+ payment.userId = user.id
233
+ payment.organizationId = organization.id
234
+ payment.method = checkout.paymentMethod
235
+ payment.status = PaymentStatus.Created
236
+ payment.price = totalPrice
237
+ payment.freeContribution = checkout.freeContribution
238
+
239
+ if (payment.method == PaymentMethod.Transfer) {
240
+ // remark: we cannot add the lastnames, these will get added in the frontend when it is decrypted
241
+ payment.transferSettings = organization.mappedTransferSettings
242
+
243
+ if (!payment.transferSettings.iban) {
244
+ throw new SimpleError({
245
+ code: "no_iban",
246
+ message: "No IBAN",
247
+ human: "Er is geen rekeningnummer ingesteld voor overschrijvingen. Contacteer de beheerder."
248
+ })
249
+ }
250
+
251
+ const m = [...payRegistrations.map(r => r.registration.member.details), ...memberBalanceItems.map(i => members.find(m => m.id === i.memberId)?.details).filter(n => n !== undefined)] as MemberDetails[]
252
+ payment.generateDescription(
253
+ organization,
254
+ Formatter.groupNamesByFamily(m),
255
+ {
256
+ name: Formatter.groupNamesByFamily(m),
257
+ naam: Formatter.groupNamesByFamily(m),
258
+ email: user.email
259
+ }
260
+ )
261
+ }
262
+ payment.paidAt = null
263
+
264
+ if (totalPrice == 0) {
265
+ payment.status = PaymentStatus.Succeeded
266
+ payment.method = PaymentMethod.Unknown
267
+ payment.paidAt = new Date()
268
+ }
269
+
270
+ // Determine the payment provider
271
+ // Throws if invalid
272
+ const {provider, stripeAccount} = await organization.getPaymentProviderFor(payment.method, organization.privateMeta.registrationPaymentConfiguration)
273
+ payment.provider = provider
274
+ payment.stripeAccountId = stripeAccount?.id ?? null
275
+
276
+ await payment.save()
277
+ const items: BalanceItem[] = []
278
+ const itemPayments: (BalanceItemPayment & { balanceItem: BalanceItem })[] = []
279
+
280
+ // Save registrations and add extra data if needed
281
+ for (const bundle of payRegistrations) {
282
+ const registration = bundle.registration;
283
+
284
+ if (!registration.waitingList) {
285
+ // Replaced with balance items
286
+ // registration.paymentId = payment.id
287
+
288
+ registration.reservedUntil = null
289
+
290
+ if (payment.method == PaymentMethod.Transfer || payment.method == PaymentMethod.PointOfSale || payment.status == PaymentStatus.Succeeded) {
291
+ await registration.markValid()
292
+ } else {
293
+ // Reserve registration for 30 minutes (if needed)
294
+ const group = groups.find(g => g.id === registration.groupId)
295
+
296
+ if (group && group.settings.maxMembers !== null) {
297
+ registration.reservedUntil = new Date(new Date().getTime() + 1000*60*30)
298
+ }
299
+ await registration.save()
300
+ }
301
+ }
302
+
303
+ await registration.save()
304
+
305
+ // Create balance item
306
+ const balanceItem = new BalanceItem();
307
+ balanceItem.registrationId = registration.id;
308
+ balanceItem.price = bundle.item.calculatedPrice
309
+ balanceItem.description = `Inschrijving ${registration.group.settings.name}`
310
+ balanceItem.pricePaid = payment.status == PaymentStatus.Succeeded ? bundle.item.calculatedPrice : 0;
311
+ balanceItem.memberId = registration.memberId;
312
+ balanceItem.userId = user.id
313
+ balanceItem.organizationId = organization.id;
314
+ balanceItem.status = payment.status == PaymentStatus.Succeeded ? BalanceItemStatus.Paid : (payment.method == PaymentMethod.Transfer || payment.method == PaymentMethod.PointOfSale ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden);
315
+ await balanceItem.save();
316
+
317
+ // Create one balance item payment to pay it in one payment
318
+ const balanceItemPayment = new BalanceItemPayment()
319
+ balanceItemPayment.balanceItemId = balanceItem.id;
320
+ balanceItemPayment.paymentId = payment.id;
321
+ balanceItemPayment.organizationId = organization.id;
322
+ balanceItemPayment.price = balanceItem.price;
323
+ await balanceItemPayment.save();
324
+
325
+ items.push(balanceItem)
326
+ itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
327
+ }
328
+
329
+ const oldestMember = members.slice().sort((a, b) => b.details.defaultAge - a.details.defaultAge)[0]
330
+ if (checkout.freeContribution) {
331
+ // Create balance item
332
+ const balanceItem = new BalanceItem();
333
+ balanceItem.price = checkout.freeContribution
334
+ balanceItem.description = `Vrije bijdrage`
335
+ balanceItem.pricePaid = payment.status == PaymentStatus.Succeeded ? balanceItem.price : 0;
336
+ balanceItem.userId = user.id
337
+ balanceItem.organizationId = organization.id;
338
+
339
+ // Connect this to the oldest member
340
+
341
+ if (oldestMember) {
342
+ balanceItem.memberId = oldestMember.id;
343
+ }
344
+ balanceItem.status = payment.status == PaymentStatus.Succeeded ? BalanceItemStatus.Paid : (payment.method == PaymentMethod.Transfer || payment.method == PaymentMethod.PointOfSale ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden);
345
+ await balanceItem.save();
346
+
347
+ // Create one balance item payment to pay it in one payment
348
+ const balanceItemPayment = new BalanceItemPayment()
349
+ balanceItemPayment.balanceItemId = balanceItem.id;
350
+ balanceItemPayment.paymentId = payment.id;
351
+ balanceItemPayment.organizationId = organization.id;
352
+ balanceItemPayment.price = balanceItem.price;
353
+ await balanceItemPayment.save();
354
+
355
+ items.push(balanceItem)
356
+ itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
357
+ }
358
+
359
+ if (checkout.administrationFee) {
360
+ // Create balance item
361
+ const balanceItem = new BalanceItem();
362
+ balanceItem.price = checkout.administrationFee
363
+ balanceItem.description = `Administratiekosten`
364
+ balanceItem.pricePaid = payment.status == PaymentStatus.Succeeded ? balanceItem.price : 0;
365
+ balanceItem.userId = user.id
366
+ balanceItem.organizationId = organization.id;
367
+
368
+ // Connect this to the oldest member
369
+
370
+ if (oldestMember) {
371
+ balanceItem.memberId = oldestMember.id;
372
+ }
373
+ balanceItem.status = payment.status == PaymentStatus.Succeeded ? BalanceItemStatus.Paid : (payment.method == PaymentMethod.Transfer || payment.method == PaymentMethod.PointOfSale ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden);
374
+ await balanceItem.save();
375
+
376
+ // Create one balance item payment to pay it in one payment
377
+ const balanceItemPayment = new BalanceItemPayment()
378
+ balanceItemPayment.balanceItemId = balanceItem.id;
379
+ balanceItemPayment.paymentId = payment.id;
380
+ balanceItemPayment.organizationId = organization.id;
381
+ balanceItemPayment.price = balanceItem.price;
382
+ await balanceItemPayment.save();
383
+
384
+ items.push(balanceItem)
385
+ itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
386
+ }
387
+
388
+ // Create a payment pending balance item
389
+ for (const item of checkout.cart.balanceItems) {
390
+ // Create one balance item payment to pay it in one payment
391
+ const balanceItemPayment = new BalanceItemPayment()
392
+ balanceItemPayment.balanceItemId = item.item.id;
393
+ balanceItemPayment.paymentId = payment.id;
394
+ balanceItemPayment.organizationId = organization.id;
395
+ balanceItemPayment.price = item.price;
396
+ await balanceItemPayment.save();
397
+
398
+ const balanceItem = balanceItems.find(i => i.id === item.item.id)
399
+ if (!balanceItem) {
400
+ throw new Error('Balance item not found')
401
+ }
402
+ itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
403
+ }
404
+ items.push(...balanceItems)
405
+ await ExchangePaymentEndpoint.updateOutstanding(items, organization.id)
406
+
407
+ // Update occupancy
408
+ for (const group of groups) {
409
+ if (registrations.find(p => p.groupId === group.id)) {
410
+ await group.updateOccupancy()
411
+ await group.save()
412
+ }
413
+ }
414
+
415
+ // Update balance items
416
+ if (payment.method == PaymentMethod.Transfer) {
417
+ // Send a small reminder email
418
+ try {
419
+ await Registration.sendTransferEmail(user, organization, payment)
420
+ } catch (e) {
421
+ console.error("Failed to send transfer email")
422
+ console.error(e)
423
+ }
424
+ }
425
+
426
+ let paymentUrl: string | null = null
427
+ const description = 'Inschrijving '+organization.name
428
+ if (payment.status != PaymentStatus.Succeeded) {
429
+ const redirectUrl = "https://"+organization.getHost()+'/payment?id='+encodeURIComponent(payment.id)
430
+ const cancelUrl = "https://"+organization.getHost()+'/payment?id='+encodeURIComponent(payment.id) + '&cancel=true'
431
+ const webhookUrl = 'https://'+organization.getApiHost()+"/v"+Version+"/payments/"+encodeURIComponent(payment.id)+"?exchange=true"
432
+
433
+ if (payment.provider === PaymentProvider.Stripe) {
434
+ const stripeResult = await StripeHelper.createPayment({
435
+ payment,
436
+ stripeAccount,
437
+ redirectUrl,
438
+ cancelUrl,
439
+ statementDescriptor: organization.name,
440
+ metadata: {
441
+ organization: organization.id,
442
+ user: user.id,
443
+ payment: payment.id
444
+ },
445
+ i18n: request.i18n,
446
+ lineItems: itemPayments,
447
+ organization,
448
+ customer: {
449
+ name: user.name ?? oldestMember?.details.name ?? 'Onbekend',
450
+ email: user.email,
451
+ }
452
+ });
453
+ paymentUrl = stripeResult.paymentUrl
454
+ } else if (payment.provider === PaymentProvider.Mollie) {
455
+
456
+ // Mollie payment
457
+ const token = await MollieToken.getTokenFor(organization.id)
458
+ if (!token) {
459
+ throw new SimpleError({
460
+ code: "",
461
+ message: "Betaling via " + PaymentMethodHelper.getName(payment.method) + " is onbeschikbaar"
462
+ })
463
+ }
464
+ const profileId = organization.privateMeta.mollieProfile?.id ?? await token.getProfileId(organization.getHost())
465
+ if (!profileId) {
466
+ throw new SimpleError({
467
+ code: "",
468
+ message: "Betaling via " + PaymentMethodHelper.getName(payment.method) + " is tijdelijk onbeschikbaar"
469
+ })
470
+ }
471
+ const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
472
+ const molliePayment = await mollieClient.payments.create({
473
+ amount: {
474
+ currency: 'EUR',
475
+ value: (totalPrice / 100).toFixed(2)
476
+ },
477
+ method: payment.method == PaymentMethod.Bancontact ? molliePaymentMethod.bancontact : (payment.method == PaymentMethod.iDEAL ? molliePaymentMethod.ideal : molliePaymentMethod.creditcard),
478
+ testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment != 'production',
479
+ profileId,
480
+ description,
481
+ redirectUrl,
482
+ webhookUrl,
483
+ metadata: {
484
+ paymentId: payment.id,
485
+ },
486
+ });
487
+ paymentUrl = molliePayment.getCheckoutUrl()
488
+
489
+ // Save payment
490
+ const dbPayment = new MolliePayment()
491
+ dbPayment.paymentId = payment.id
492
+ dbPayment.mollieId = molliePayment.id
493
+ await dbPayment.save();
494
+ } else if (payment.provider === PaymentProvider.Payconiq) {
495
+ paymentUrl = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, webhookUrl)
496
+ } else if (payment.provider == PaymentProvider.Buckaroo) {
497
+ // Increase request timeout because buckaroo is super slow (in development)
498
+ request.request.request?.setTimeout(60 * 1000)
499
+ const buckaroo = new BuckarooHelper(organization.privateMeta?.buckarooSettings?.key ?? "", organization.privateMeta?.buckarooSettings?.secret ?? "", organization.privateMeta.useTestPayments ?? STAMHOOFD.environment != 'production')
500
+ const ip = request.request.getIP()
501
+ paymentUrl = await buckaroo.createPayment(payment, ip, description, redirectUrl, webhookUrl)
502
+ await payment.save()
503
+
504
+ // TypeScript doesn't understand that the status can change and isn't a const....
505
+ if ((payment.status as any) === PaymentStatus.Failed) {
506
+ throw new SimpleError({
507
+ code: "payment_failed",
508
+ message: "Betaling via " + PaymentMethodHelper.getName(payment.method) + " is onbeschikbaar"
509
+ })
510
+ }
511
+ }
512
+ }
513
+
514
+ return new Response(RegisterResponse.create({
515
+ payment: PaymentStruct.create(payment),
516
+ members: await AuthenticatedStructures.membersBlob(members),
517
+ registrations: registrations.map(r => Member.getRegistrationWithMemberStructure(r)),
518
+ paymentUrl
519
+ }));
520
+ }
521
+ }