@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,202 @@
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 { BalanceItem, Member, Order, Registration, User } from '@stamhoofd/models';
5
+ import { QueueHandler } from '@stamhoofd/queues';
6
+ import { BalanceItemStatus, MemberBalanceItem, PermissionLevel } from "@stamhoofd/structures";
7
+ import { Formatter } from '@stamhoofd/utility';
8
+
9
+ import { Context } from '../../../../helpers/Context';
10
+
11
+
12
+ type Params = Record<string, never>;
13
+ type Query = undefined;
14
+ type Body = PatchableArrayAutoEncoder<MemberBalanceItem>
15
+ type ResponseBody = MemberBalanceItem[]
16
+
17
+ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
18
+ bodyDecoder = new PatchableArrayDecoder(MemberBalanceItem as Decoder<MemberBalanceItem>, MemberBalanceItem.patchType() as Decoder<AutoEncoderPatchType<MemberBalanceItem>>, StringDecoder)
19
+
20
+ protected doesMatch(request: Request): [true, Params] | [false] {
21
+ if (request.method != "PATCH") {
22
+ return [false];
23
+ }
24
+
25
+ const params = Endpoint.parseParameters(request.url, "/organization/balance", {});
26
+
27
+ if (params) {
28
+ return [true, params as Params];
29
+ }
30
+ return [false];
31
+ }
32
+
33
+ async handle(request: DecodedRequest<Params, Query, Body>) {
34
+ const organization = await Context.setOrganizationScope();
35
+ await Context.authenticate()
36
+
37
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
38
+ throw Context.auth.error()
39
+ }
40
+
41
+ if (request.body.changes.length == 0) {
42
+ return new Response([]);
43
+ }
44
+
45
+ const returnedModels: BalanceItem[] = []
46
+
47
+ // Keep track of updates
48
+ const memberIds: string[] = []
49
+ const registrationIds: string[] = []
50
+
51
+ await QueueHandler.schedule("balance-item-update/"+organization.id, async () => {
52
+ for (const {put} of request.body.getPuts()) {
53
+ // Create a new balance item
54
+ const model = new BalanceItem();
55
+ model.description = put.description;
56
+ model.price = put.price;
57
+ model.organizationId = organization.id;
58
+ model.createdAt = put.createdAt;
59
+ model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Pending;
60
+
61
+ if (put.userId) {
62
+ model.userId = (await this.validateUserId(model, put.userId)).id;
63
+ }
64
+
65
+ if (put.memberId) {
66
+ model.memberId = (await this.validateMemberId(put.memberId)).id;
67
+ memberIds.push(model.memberId)
68
+ }
69
+
70
+ if (!model.userId && !model.memberId) {
71
+ throw new SimpleError({
72
+ code: 'invalid_field',
73
+ message: 'No user or member provided',
74
+ field: 'userId'
75
+ })
76
+ }
77
+
78
+ if (put.registration) {
79
+ const registration = await Registration.getByID(put.registration.id)
80
+ if (!registration || registration.memberId !== model.memberId || registration.organizationId !== organization.id) {
81
+ throw new SimpleError({
82
+ code: 'invalid_field',
83
+ message: 'Registration not found',
84
+ field: 'registration'
85
+ })
86
+ }
87
+ model.registrationId = registration.id
88
+ registrationIds.push(registration.id)
89
+ }
90
+
91
+ await model.save();
92
+ returnedModels.push(model);
93
+ }
94
+
95
+ for (const patch of request.body.getPatches()) {
96
+ // Create a new balance item
97
+ const model = await BalanceItem.getByID(patch.id)
98
+ if (!model || !(await Context.auth.canAccessBalanceItems([model], PermissionLevel.Write))) {
99
+ throw new SimpleError({
100
+ code: 'invalid_field',
101
+ message: 'BalanceItem not found'
102
+ })
103
+ }
104
+ // Check permissions
105
+ if (model.memberId) {
106
+ // Update old
107
+ memberIds.push(model.memberId)
108
+ }
109
+
110
+ if (patch.memberId) {
111
+ model.memberId = (await this.validateMemberId(patch.memberId)).id;
112
+
113
+ // Update new
114
+ memberIds.push(model.memberId)
115
+ }
116
+
117
+ if (model.registrationId) {
118
+ // Update old
119
+ registrationIds.push(model.registrationId)
120
+ }
121
+
122
+ if (patch.createdAt) {
123
+ model.createdAt = patch.createdAt
124
+ }
125
+
126
+ if (patch.registration) {
127
+ const registration = await Registration.getByID(patch.registration.id)
128
+ if (!registration || registration.memberId !== model.memberId || registration.organizationId !== organization.id) {
129
+ throw new SimpleError({
130
+ code: 'invalid_field',
131
+ message: 'Registration not found',
132
+ field: 'registration'
133
+ })
134
+ }
135
+ model.registrationId = registration.id
136
+
137
+ // Update new
138
+ registrationIds.push(model.registrationId)
139
+ } else if (patch.registration === null) {
140
+ model.registrationId = null
141
+ }
142
+ model.description = patch.description ?? model.description;
143
+ model.price = patch.price ?? model.price;
144
+
145
+ if (model.orderId) {
146
+ // Not allowed to change this
147
+ const order = await Order.getByID(model.orderId)
148
+ if (order) {
149
+ model.price = order.totalToPay
150
+ }
151
+ }
152
+
153
+ if (patch.status && patch.status === BalanceItemStatus.Hidden) {
154
+ if (model.pricePaid === 0) {
155
+ model.status = BalanceItemStatus.Hidden
156
+ }
157
+ } else if (patch.status) {
158
+ model.status = model.pricePaid >= model.price ? BalanceItemStatus.Paid : BalanceItemStatus.Pending;
159
+ }
160
+
161
+ await model.save();
162
+ returnedModels.push(model);
163
+ }
164
+ });
165
+
166
+ await Member.updateOutstandingBalance(Formatter.uniqueArray(memberIds))
167
+ await Registration.updateOutstandingBalance(Formatter.uniqueArray(registrationIds), organization.id)
168
+
169
+ return new Response(
170
+ await BalanceItem.getMemberStructure(returnedModels)
171
+ );
172
+ }
173
+
174
+ async validateMemberId(memberId: string) {
175
+ const member = (await Member.getWithRegistrations(memberId))
176
+
177
+ if (!member || !(await Context.auth.canLinkBalanceItemToMember(member))) {
178
+ throw new SimpleError({
179
+ code: 'permission_denied',
180
+ message: 'No permission to link balanace items to this member',
181
+ human: 'Je hebt geen toegang om aanrekeningen te maken verbonden met dit lid',
182
+ field: 'memberId'
183
+ })
184
+ }
185
+
186
+ return member;
187
+ }
188
+
189
+ async validateUserId(balanceItem: BalanceItem, userId: string) {
190
+ const user = await User.getByID(userId);
191
+ if (!user || !await Context.auth.canLinkBalanceItemToUser(balanceItem, user)) {
192
+ throw new SimpleError({
193
+ code: 'permission_denied',
194
+ message: 'No permission to link balanace items to this user',
195
+ human: 'Je hebt geen toegang om aanrekeningen te maken verbonden met deze gebruiker',
196
+ field: 'userId'
197
+ })
198
+ }
199
+
200
+ return user;
201
+ }
202
+ }
@@ -0,0 +1,233 @@
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 { BalanceItem, BalanceItemPayment, Payment, Token } from '@stamhoofd/models';
5
+ import { QueueHandler } from '@stamhoofd/queues';
6
+ import { Payment as PaymentStruct, PaymentGeneral, PaymentMethod, PaymentStatus, PermissionLevel } from "@stamhoofd/structures";
7
+
8
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
9
+ import { Context } from '../../../../helpers/Context';
10
+ import { ExchangePaymentEndpoint } from '../../shared/ExchangePaymentEndpoint';
11
+
12
+ type Params = Record<string, never>;
13
+ type Query = undefined;
14
+ type Body = PatchableArrayAutoEncoder<PaymentGeneral>
15
+ type ResponseBody = PaymentGeneral[]
16
+
17
+ /**
18
+ * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
19
+ */
20
+
21
+ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
+ bodyDecoder = new PatchableArrayDecoder(PaymentGeneral as Decoder<PaymentGeneral>, PaymentStruct.patchType() as Decoder<AutoEncoderPatchType<PaymentGeneral>>, StringDecoder)
23
+
24
+ protected doesMatch(request: Request): [true, Params] | [false] {
25
+ if (request.method != "PATCH") {
26
+ return [false];
27
+ }
28
+
29
+ const params = Endpoint.parseParameters(request.url, "/organization/payments", {});
30
+
31
+ if (params) {
32
+ return [true, params as Params];
33
+ }
34
+ return [false];
35
+ }
36
+
37
+ async handle(request: DecodedRequest<Params, Query, Body>) {
38
+ const organization = await Context.setOrganizationScope();
39
+ await Context.authenticate()
40
+
41
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
42
+ throw Context.auth.error()
43
+ }
44
+
45
+ const changedPayments: Payment[] = []
46
+
47
+ // Modify payments
48
+ for (const {put} of request.body.getPuts()) {
49
+ // Create a new payment
50
+ if (put.balanceItemPayments.length == 0) {
51
+ throw new SimpleError({
52
+ code: "invalid_field",
53
+ message: "You need to add at least one balance item payment",
54
+ human: "Een betaling moet ten minste één afrekening bevatten voor een openstaande rekening.",
55
+ field: "balanceItemPayments"
56
+ })
57
+ }
58
+
59
+ if (![PaymentMethod.Unknown, PaymentMethod.Transfer, PaymentMethod.PointOfSale].includes(put.method)) {
60
+ throw new SimpleError({
61
+ code: "invalid_field",
62
+ message: "Invalid payment method",
63
+ human: "Je kan zelf geen online betalingen aanmaken",
64
+ field: "method"
65
+ })
66
+ }
67
+
68
+ const payment = new Payment()
69
+ payment.organizationId = organization.id
70
+ payment.status = PaymentStatus.Created
71
+ payment.method = put.method
72
+
73
+ if (payment.method == PaymentMethod.Transfer) {
74
+ if (!put.transferSettings) {
75
+ throw new SimpleError({
76
+ code: "invalid_field",
77
+ message: "Transfer settings are required",
78
+ human: "Je moet de overschrijvingsdetails invullen",
79
+ field: "transferSettings"
80
+ })
81
+ }
82
+
83
+ payment.transferSettings = put.transferSettings
84
+
85
+ if (!put.transferDescription) {
86
+ throw new SimpleError({
87
+ code: "invalid_field",
88
+ message: "Transfer description is required",
89
+ human: "Je moet een mededeling invullen voor de overschrijving",
90
+ field: "transferDescription"
91
+ })
92
+ }
93
+
94
+ payment.transferDescription = put.transferDescription
95
+ }
96
+
97
+ const balanceItems: BalanceItem[] = []
98
+ const balanceItemPayments: BalanceItemPayment[] = [];
99
+
100
+ for (const item of put.balanceItemPayments) {
101
+ const balanceItem = await BalanceItem.getByID(item.balanceItem.id)
102
+ if (!balanceItem || balanceItem.organizationId !== organization.id) {
103
+ throw Context.auth.notFoundOrNoAccess("Eén van de afrekeningen die je wilde markeren als betaald bestaat niet (meer).")
104
+ }
105
+ balanceItems.push(balanceItem)
106
+
107
+ // Create payment
108
+ const balanceItemPayment = new BalanceItemPayment()
109
+ balanceItemPayment.organizationId = organization.id
110
+ balanceItemPayment.balanceItemId = balanceItem.id
111
+ balanceItemPayment.price = item.price
112
+ balanceItemPayments.push(balanceItemPayment)
113
+ }
114
+
115
+ // Check permissions
116
+ if (!(await Context.auth.canAccessBalanceItems(balanceItems, PermissionLevel.Write))) {
117
+ throw Context.auth.error('Je hebt geen toegangsrechten tot één van de gekozen afrekeningen voor de betaling die je wilt aanmaken')
118
+ }
119
+
120
+ // Check total price
121
+ const totalPrice = balanceItemPayments.reduce((total, item) => total + item.price, 0)
122
+
123
+ if (totalPrice !== put.price) {
124
+ throw new SimpleError({
125
+ code: "invalid_field",
126
+ message: "Total price doesn't match",
127
+ human: "De totale prijs komt niet overeen met de som van de afrekeningen",
128
+ field: "price"
129
+ })
130
+ }
131
+
132
+ payment.price = totalPrice
133
+
134
+ // Save payment
135
+ await payment.save();
136
+
137
+ // Save balance item payments
138
+ for (const balanceItemPayment of balanceItemPayments) {
139
+ balanceItemPayment.paymentId = payment.id
140
+ await balanceItemPayment.save()
141
+ }
142
+
143
+ // Mark paid or failed
144
+ if (put.status !== PaymentStatus.Created && put.status !== PaymentStatus.Pending) {
145
+ await ExchangePaymentEndpoint.handlePaymentStatusUpdate(payment, organization, put.status)
146
+
147
+ if (put.status === PaymentStatus.Succeeded) {
148
+ payment.paidAt = put.paidAt
149
+ await payment.save()
150
+ }
151
+ } else {
152
+ for (const balanceItem of balanceItems) {
153
+ await balanceItem.markUpdated(payment, organization)
154
+ }
155
+ }
156
+
157
+ changedPayments.push(payment)
158
+ }
159
+
160
+ // Modify payments
161
+ for (const patch of request.body.getPatches()) {
162
+ await QueueHandler.schedule("payments/"+patch.id, async () => {
163
+ const payment = await Payment.getByID(patch.id);
164
+ if (!payment || !(await Context.auth.canAccessPayment(payment, PermissionLevel.Write))) {
165
+ throw new SimpleError({
166
+ code: "not_found",
167
+ message: "Payment not found",
168
+ human: "Deze betaling werd niet gevonden."
169
+ })
170
+ }
171
+
172
+ if (patch.method || patch.paidAt !== undefined) {
173
+ if (payment.method && ![PaymentMethod.Unknown, PaymentMethod.Transfer, PaymentMethod.PointOfSale].includes(payment.method)) {
174
+ throw new SimpleError({
175
+ code: "invalid_field",
176
+ message: "Invalid payment method",
177
+ human: "Je kan online betalingen niet wijzigen"
178
+ })
179
+ }
180
+ }
181
+
182
+ if (patch.transferSettings && payment.method == PaymentMethod.Transfer) {
183
+ if (patch.transferSettings.isPut()) {
184
+ payment.transferSettings = patch.transferSettings
185
+ } else if (payment.transferSettings !== null) {
186
+ payment.transferSettings = payment.transferSettings.patch(patch.transferSettings)
187
+ }
188
+ }
189
+
190
+ if (patch.method) {
191
+ if (![PaymentMethod.Unknown, PaymentMethod.Transfer, PaymentMethod.PointOfSale].includes(patch.method)) {
192
+ throw new SimpleError({
193
+ code: "invalid_field",
194
+ message: "Invalid payment method",
195
+ human: "De betaalmethode die je wilt gebruiken is niet toegestaan",
196
+ field: "method"
197
+ })
198
+ }
199
+ payment.method = patch.method
200
+
201
+ if (payment.method === PaymentMethod.Transfer && patch.transferDescription === undefined && !payment.transferDescription) {
202
+ payment.transferSettings = payment.transferSettings ?? organization.meta.transferSettings
203
+ // TODO: fill in description!
204
+ payment.generateDescription(organization, "")
205
+ }
206
+ }
207
+
208
+ if (patch.transferDescription && payment.method == PaymentMethod.Transfer) {
209
+ payment.transferDescription = patch.transferDescription
210
+ }
211
+
212
+ if (patch.paidAt && payment.paidAt !== null) {
213
+ // Only allow to set the date if it is already set
214
+ payment.paidAt = patch.paidAt
215
+ }
216
+
217
+ await payment.save()
218
+
219
+ if (patch.status) {
220
+ await ExchangePaymentEndpoint.handlePaymentStatusUpdate(payment, organization, patch.status)
221
+ }
222
+
223
+ changedPayments.push(
224
+ payment
225
+ )
226
+ });
227
+ }
228
+
229
+ return new Response(
230
+ await AuthenticatedStructures.paymentsGeneral(changedPayments, true)
231
+ );
232
+ }
233
+ }
@@ -0,0 +1,66 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { RegistrationPeriodList, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct } from "@stamhoofd/structures";
3
+
4
+ import { Group, OrganizationRegistrationPeriod, RegistrationPeriod } from '@stamhoofd/models';
5
+ import { Context } from "../../../../helpers/Context";
6
+ import { Sorter } from "@stamhoofd/utility";
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = undefined;
10
+ type Body = undefined
11
+ type ResponseBody = RegistrationPeriodList
12
+
13
+ /**
14
+ * One endpoint to create, patch and delete members and their registrations and payments
15
+ */
16
+
17
+ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
18
+ protected doesMatch(request: Request): [true, Params] | [false] {
19
+ if (request.method != "GET") {
20
+ return [false];
21
+ }
22
+
23
+ const params = Endpoint.parseParameters(request.url, "/organization/registration-periods", {});
24
+
25
+ if (params) {
26
+ return [true, params as Params];
27
+ }
28
+ return [false];
29
+ }
30
+
31
+ async handle(request: DecodedRequest<Params, Query, Body>) {
32
+ const organization = await Context.setOrganizationScope();
33
+ await Context.authenticate()
34
+
35
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
36
+ throw Context.auth.error()
37
+ }
38
+
39
+ const organizationPeriods = await OrganizationRegistrationPeriod.where({ organizationId: organization.id });
40
+ const periods = await RegistrationPeriod.all();
41
+ const groups = await Group.getAll(organization.id, null)
42
+
43
+ const structs: OrganizationRegistrationPeriodStruct[] = [];
44
+
45
+ for (const period of periods) {
46
+ const organizationPeriod = organizationPeriods.find(p => p.periodId == period.id);
47
+ if (!organizationPeriod) {
48
+ continue;
49
+ }
50
+
51
+ const gs = groups.filter(g => g.periodId === period.id);
52
+ structs.push(organizationPeriod.getStructure(period, gs));
53
+ }
54
+
55
+ // Sort
56
+ periods.sort((a, b) => Sorter.byDateValue(a.startDate, b.startDate))
57
+
58
+ return new Response(
59
+ RegistrationPeriodList.create({
60
+ organizationPeriods: structs,
61
+ periods: periods.map(p => p.getStructure())
62
+ })
63
+ );
64
+ }
65
+
66
+ }