@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.
- package/.env.template.json +63 -0
- package/.eslintrc.js +61 -0
- package/README.md +40 -0
- package/index.ts +172 -0
- package/jest.config.js +11 -0
- package/migrations.ts +33 -0
- package/package.json +48 -0
- package/src/crons.ts +845 -0
- package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +42 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +320 -0
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +171 -0
- package/src/endpoints/auth/CreateAdminEndpoint.ts +137 -0
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +68 -0
- package/src/endpoints/auth/CreateTokenEndpoint.ts +200 -0
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +31 -0
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +70 -0
- package/src/endpoints/auth/GetUserEndpoint.test.ts +64 -0
- package/src/endpoints/auth/GetUserEndpoint.ts +57 -0
- package/src/endpoints/auth/PatchApiUserEndpoint.ts +90 -0
- package/src/endpoints/auth/PatchUserEndpoint.ts +122 -0
- package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +37 -0
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +41 -0
- package/src/endpoints/auth/SignupEndpoint.ts +107 -0
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +89 -0
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +95 -0
- package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +31 -0
- package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +101 -0
- package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +53 -0
- package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +57 -0
- package/src/endpoints/global/files/UploadFile.ts +147 -0
- package/src/endpoints/global/files/UploadImage.ts +119 -0
- package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +76 -0
- package/src/endpoints/global/members/GetMembersCountEndpoint.ts +43 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +429 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +734 -0
- package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +45 -0
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +105 -0
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +146 -0
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +52 -0
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +80 -0
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +49 -0
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +58 -0
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +62 -0
- package/src/endpoints/global/payments/ExchangeSTPaymentEndpoint.ts +153 -0
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +134 -0
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +44 -0
- package/src/endpoints/global/platform/GetPlatformEnpoint.ts +39 -0
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +63 -0
- package/src/endpoints/global/registration/GetPaymentRegistrations.ts +68 -0
- package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +39 -0
- package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +80 -0
- package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +41 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +134 -0
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +521 -0
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +37 -0
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +115 -0
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +187 -0
- package/src/endpoints/organization/dashboard/billing/ActivatePackagesEndpoint.ts +424 -0
- package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +67 -0
- package/src/endpoints/organization/dashboard/billing/GetBillingStatusEndpoint.ts +39 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +57 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +129 -0
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplateEndpoint.ts +114 -0
- package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +234 -0
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +62 -0
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +85 -0
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +80 -0
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +54 -0
- package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +49 -0
- package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +63 -0
- package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +61 -0
- package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.test.ts +64 -0
- package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.ts +84 -0
- package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +43 -0
- package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +42 -0
- package/src/endpoints/organization/dashboard/organization/GetOrganizationSSOEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/organization/GetRegisterCodeEndpoint.ts +65 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +281 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +338 -0
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +196 -0
- package/src/endpoints/organization/dashboard/organization/SetOrganizationSSOEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +48 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +207 -0
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +202 -0
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +233 -0
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +66 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -0
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +93 -0
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +59 -0
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +78 -0
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +40 -0
- package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +69 -0
- package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +52 -0
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +73 -0
- package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +60 -0
- package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +47 -0
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +41 -0
- package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +217 -0
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +51 -0
- package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +47 -0
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +83 -0
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +68 -0
- package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +69 -0
- package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +125 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +204 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +278 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +80 -0
- package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +60 -0
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +379 -0
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +54 -0
- package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +45 -0
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +78 -0
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +34 -0
- package/src/endpoints/organization/shared/auth/OpenIDConnectCallbackEndpoint.ts +44 -0
- package/src/endpoints/organization/shared/auth/OpenIDConnectStartEndpoint.ts +82 -0
- package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +59 -0
- package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +51 -0
- package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +40 -0
- package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +124 -0
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +130 -0
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +50 -0
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +450 -0
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +335 -0
- package/src/helpers/AddressValidator.test.ts +40 -0
- package/src/helpers/AddressValidator.ts +256 -0
- package/src/helpers/AdminPermissionChecker.ts +1031 -0
- package/src/helpers/AuthenticatedStructures.ts +158 -0
- package/src/helpers/BuckarooHelper.ts +279 -0
- package/src/helpers/CheckSettlements.ts +215 -0
- package/src/helpers/Context.ts +202 -0
- package/src/helpers/CookieHelper.ts +45 -0
- package/src/helpers/ForwardHandler.test.ts +216 -0
- package/src/helpers/ForwardHandler.ts +140 -0
- package/src/helpers/OpenIDConnectHelper.ts +284 -0
- package/src/helpers/StripeHelper.ts +293 -0
- package/src/helpers/StripePayoutChecker.ts +188 -0
- package/src/middleware/ContextMiddleware.ts +16 -0
- package/src/migrations/1646578856-validate-addresses.ts +60 -0
- package/src/seeds/0000000000-example.ts +13 -0
- package/src/seeds/1715028563-user-permissions.ts +52 -0
- package/tests/e2e/stock.test.ts +2120 -0
- package/tests/e2e/tickets.test.ts +926 -0
- package/tests/helpers/StripeMocker.ts +362 -0
- package/tests/helpers/TestServer.ts +21 -0
- package/tests/jest.global.setup.ts +29 -0
- package/tests/jest.setup.ts +59 -0
- 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
|
+
}
|