@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,80 @@
|
|
|
1
|
+
import { ArrayDecoder, AutoEncoderPatchType, Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
+
import { SimpleError, SimpleErrors } from "@simonbackx/simple-errors";
|
|
4
|
+
import { Ticket, Token, Webshop } from '@stamhoofd/models';
|
|
5
|
+
import { PermissionLevel, TicketPrivate } from "@stamhoofd/structures";
|
|
6
|
+
|
|
7
|
+
import { Context } from '../../../../helpers/Context';
|
|
8
|
+
|
|
9
|
+
type Params = { id: string };
|
|
10
|
+
type Query = undefined;
|
|
11
|
+
type Body = AutoEncoderPatchType<TicketPrivate>[]
|
|
12
|
+
type ResponseBody = TicketPrivate[]
|
|
13
|
+
|
|
14
|
+
export class PatchWebshopTicketsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
15
|
+
bodyDecoder = new ArrayDecoder(TicketPrivate.patchType() as Decoder<AutoEncoderPatchType<TicketPrivate>>)
|
|
16
|
+
|
|
17
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
18
|
+
if (request.method != "PATCH") {
|
|
19
|
+
return [false];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const params = Endpoint.parseParameters(request.url, "/webshop/@id/tickets/private", { id: String });
|
|
23
|
+
|
|
24
|
+
if (params) {
|
|
25
|
+
return [true, params as Params];
|
|
26
|
+
}
|
|
27
|
+
return [false];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
31
|
+
const organization = await Context.setOrganizationScope();
|
|
32
|
+
await Context.authenticate()
|
|
33
|
+
|
|
34
|
+
// Fast throw first (more in depth checking for patches later)
|
|
35
|
+
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
36
|
+
throw Context.auth.error()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (request.body.length == 0) {
|
|
40
|
+
return new Response([]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const webshop = await Webshop.getByID(request.params.id)
|
|
44
|
+
if (!webshop || !await Context.auth.canAccessWebshopTickets(webshop, PermissionLevel.Write)) {
|
|
45
|
+
throw Context.auth.notFoundOrNoAccess("Je hebt geen toegang om tickets te wijzigen van deze webshop")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tickets: Ticket[] = []
|
|
49
|
+
const errors = new SimpleErrors()
|
|
50
|
+
|
|
51
|
+
for (const patch of request.body) {
|
|
52
|
+
const model = await Ticket.getByID(patch.id)
|
|
53
|
+
if (!model || model.webshopId !== webshop.id) {
|
|
54
|
+
errors.addError(new SimpleError({
|
|
55
|
+
code: "ticket_not_found",
|
|
56
|
+
field: patch.id,
|
|
57
|
+
message: "Ticket with id "+patch.id+" does not exist"
|
|
58
|
+
}))
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (patch.scannedAt !== undefined) {
|
|
63
|
+
model.scannedAt = patch.scannedAt
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (patch.scannedBy !== undefined) {
|
|
67
|
+
model.scannedBy = patch.scannedBy
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await model.save()
|
|
71
|
+
|
|
72
|
+
tickets.push(model)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
errors.throwIfNotEmpty();
|
|
76
|
+
return new Response(
|
|
77
|
+
tickets.map(ticket => TicketPrivate.create(ticket))
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
+
import { Token, Webshop } from '@stamhoofd/models';
|
|
4
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
5
|
+
import { PermissionLevel, PrivateWebshop, WebshopPrivateMetaData } from "@stamhoofd/structures";
|
|
6
|
+
|
|
7
|
+
import { Context } from "../../../../helpers/Context";
|
|
8
|
+
|
|
9
|
+
type Params = { id: string };
|
|
10
|
+
type Query = undefined;
|
|
11
|
+
type Body = undefined;
|
|
12
|
+
type ResponseBody = PrivateWebshop;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 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
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export class VerifyWebshopDomainEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
19
|
+
|
|
20
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
21
|
+
if (request.method != "POST") {
|
|
22
|
+
return [false];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const params = Endpoint.parseParameters(request.url, "/webshop/@id/verify-domain", { id: String });
|
|
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
|
+
// Fast throw first (more in depth checking for patches later)
|
|
38
|
+
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
39
|
+
throw Context.auth.error()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return await QueueHandler.schedule("webshop-stock/"+request.params.id, async () => {
|
|
43
|
+
const webshop = await Webshop.getByID(request.params.id)
|
|
44
|
+
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
|
|
45
|
+
throw Context.auth.notFoundOrNoAccess()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (webshop.domain !== null) {
|
|
49
|
+
webshop.privateMeta.dnsRecords = WebshopPrivateMetaData.buildDNSRecords(webshop.domain)
|
|
50
|
+
await webshop.updateDNSRecords()
|
|
51
|
+
} else {
|
|
52
|
+
webshop.privateMeta.dnsRecords = []
|
|
53
|
+
webshop.meta.domainActive = false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await webshop.save()
|
|
57
|
+
return new Response(PrivateWebshop.create(webshop));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { createMollieClient } from '@mollie/api-client';
|
|
2
|
+
import { AutoEncoder, BooleanDecoder, Decoder, field } from '@simonbackx/simple-encoding';
|
|
3
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
4
|
+
import { SimpleError } from "@simonbackx/simple-errors";
|
|
5
|
+
import { BalanceItem, BalanceItemPayment, Member, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Registration, STPendingInvoice } from '@stamhoofd/models';
|
|
6
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
7
|
+
import { Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, STInvoiceItem } from "@stamhoofd/structures";
|
|
8
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
9
|
+
|
|
10
|
+
import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
11
|
+
import { Context } from '../../../helpers/Context';
|
|
12
|
+
import { StripeHelper } from '../../../helpers/StripeHelper';
|
|
13
|
+
|
|
14
|
+
function calculateFee(totalPrice: number, fixed: number, percentageTimes100: number) {
|
|
15
|
+
return Math.round(fixed + Math.max(1, totalPrice * percentageTimes100 / 100 / 100)); // € 0,21 + 0,2%
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Params = {id: string};
|
|
19
|
+
class Query extends AutoEncoder {
|
|
20
|
+
@field({ decoder: BooleanDecoder, optional: true })
|
|
21
|
+
exchange = false
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* If possible, cancel the payment if it is not yet paid/pending
|
|
25
|
+
*/
|
|
26
|
+
@field({ decoder: BooleanDecoder, optional: true })
|
|
27
|
+
cancel = false
|
|
28
|
+
}
|
|
29
|
+
type Body = undefined
|
|
30
|
+
type ResponseBody = PaymentStruct | undefined;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 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
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
37
|
+
queryDecoder = Query as Decoder<Query>
|
|
38
|
+
|
|
39
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
40
|
+
if (request.method != "POST") {
|
|
41
|
+
return [false];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const params = Endpoint.parseParameters(request.url, "/payments/@id", {id: String});
|
|
45
|
+
|
|
46
|
+
if (params) {
|
|
47
|
+
return [true, params as Params];
|
|
48
|
+
}
|
|
49
|
+
return [false];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
53
|
+
const organization = await Context.setOrganizationScope()
|
|
54
|
+
|
|
55
|
+
// Not method on payment because circular references (not supprted in ts)
|
|
56
|
+
const payment = await ExchangePaymentEndpoint.pollStatus(request.params.id, organization, request.query.cancel)
|
|
57
|
+
if (!payment) {
|
|
58
|
+
throw new SimpleError({
|
|
59
|
+
code: "",
|
|
60
|
+
message: "Deze link is ongeldig"
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (request.query.exchange) {
|
|
65
|
+
return new Response(undefined);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return new Response(
|
|
69
|
+
PaymentStruct.create({
|
|
70
|
+
id: payment.id,
|
|
71
|
+
method: payment.method,
|
|
72
|
+
provider: payment.provider,
|
|
73
|
+
status: payment.status,
|
|
74
|
+
price: payment.price,
|
|
75
|
+
transferDescription: payment.transferDescription,
|
|
76
|
+
paidAt: payment.paidAt,
|
|
77
|
+
createdAt: payment.createdAt,
|
|
78
|
+
updatedAt: payment.updatedAt
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static async updateOutstanding(items: BalanceItem[], organizationId: string) {
|
|
84
|
+
// Update outstanding amount of related members and registrations
|
|
85
|
+
const memberIds: string[] = Formatter.uniqueArray(items.map(p => p.memberId).filter(id => id !== null)) as any
|
|
86
|
+
await Member.updateOutstandingBalance(memberIds)
|
|
87
|
+
|
|
88
|
+
const registrationIds: string[] = Formatter.uniqueArray(items.map(p => p.registrationId).filter(id => id !== null)) as any
|
|
89
|
+
await Registration.updateOutstandingBalance(registrationIds, organizationId)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus) {
|
|
93
|
+
if (payment.status === status) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const wasPaid = payment.paidAt !== null
|
|
97
|
+
if (status === PaymentStatus.Succeeded) {
|
|
98
|
+
payment.status = PaymentStatus.Succeeded
|
|
99
|
+
payment.paidAt = new Date()
|
|
100
|
+
await payment.save();
|
|
101
|
+
|
|
102
|
+
// Prevent concurrency issues
|
|
103
|
+
await QueueHandler.schedule("balance-item-update/"+organization.id, async () => {
|
|
104
|
+
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
105
|
+
(await BalanceItemPayment.where({paymentId: payment.id})).map(r => r.setRelation(BalanceItemPayment.payment, payment))
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
for (const balanceItemPayment of balanceItemPayments) {
|
|
109
|
+
await balanceItemPayment.markPaid(organization);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await this.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
if (!wasPaid && payment.provider === PaymentProvider.Buckaroo && payment.method) {
|
|
116
|
+
// Charge transaction fees
|
|
117
|
+
let fee = 0
|
|
118
|
+
|
|
119
|
+
if (payment.method === PaymentMethod.iDEAL) {
|
|
120
|
+
fee = calculateFee(payment.price, 21, 20); // € 0,21 + 0,2%
|
|
121
|
+
} else if (payment.method === PaymentMethod.Bancontact || payment.method === PaymentMethod.Payconiq) {
|
|
122
|
+
fee = calculateFee(payment.price, 24, 20); // € 0,24 + 0,2%
|
|
123
|
+
} else {
|
|
124
|
+
fee = calculateFee(payment.price, 25, 150); // € 0,25 + 1,5%
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const name = "Transactiekosten voor "+PaymentMethodHelper.getName(payment.method)
|
|
128
|
+
const item = STInvoiceItem.create({
|
|
129
|
+
name,
|
|
130
|
+
description: "Via Buckaroo",
|
|
131
|
+
amount: 1,
|
|
132
|
+
unitPrice: fee,
|
|
133
|
+
canUseCredits: false
|
|
134
|
+
})
|
|
135
|
+
console.log("Scheduling transaction fee charge for ", payment.id, item)
|
|
136
|
+
await QueueHandler.schedule("billing/invoices-"+organization.id, async () => {
|
|
137
|
+
await STPendingInvoice.addItems(organization, [item])
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// If OLD status was succeeded, we need to revert the actions
|
|
144
|
+
if (payment.status === PaymentStatus.Succeeded) {
|
|
145
|
+
// No longer succeeded
|
|
146
|
+
await QueueHandler.schedule("balance-item-update/"+organization.id, async () => {
|
|
147
|
+
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
148
|
+
(await BalanceItemPayment.where({paymentId: payment.id})).map(r => r.setRelation(BalanceItemPayment.payment, payment))
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
for (const balanceItemPayment of balanceItemPayments) {
|
|
152
|
+
await balanceItemPayment.undoPaid(organization);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await this.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (status == PaymentStatus.Failed) {
|
|
160
|
+
await QueueHandler.schedule("balance-item-update/"+organization.id, async () => {
|
|
161
|
+
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
162
|
+
(await BalanceItemPayment.where({paymentId: payment.id})).map(r => r.setRelation(BalanceItemPayment.payment, payment))
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
for (const balanceItemPayment of balanceItemPayments) {
|
|
166
|
+
await balanceItemPayment.markFailed(organization);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await this.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// If OLD status was FAILED, we need to revert the actions
|
|
174
|
+
if (payment.status === PaymentStatus.Failed) { // OLD FAILED!! -> NOW PENDING
|
|
175
|
+
await QueueHandler.schedule("balance-item-update/"+organization.id, async () => {
|
|
176
|
+
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
177
|
+
(await BalanceItemPayment.where({paymentId: payment.id})).map(r => r.setRelation(BalanceItemPayment.payment, payment))
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
for (const balanceItemPayment of balanceItemPayments) {
|
|
181
|
+
await balanceItemPayment.undoFailed(organization);
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
payment.status = status
|
|
187
|
+
payment.paidAt = null
|
|
188
|
+
await payment.save();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* ID of payment is needed because of race conditions (need to fetch payment in a race condition save queue)
|
|
193
|
+
*/
|
|
194
|
+
static async pollStatus(paymentId: string, organization: Organization, cancel = false): Promise<Payment | undefined> {
|
|
195
|
+
// Prevent polling the same payment multiple times at the same time: create a queue to prevent races
|
|
196
|
+
return await QueueHandler.schedule("payments/"+paymentId, async () => {
|
|
197
|
+
// Get a new copy of the payment (is required to prevent concurreny bugs)
|
|
198
|
+
const payment = await Payment.getByID(paymentId)
|
|
199
|
+
if (!payment) {
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const testMode = organization.privateMeta.useTestPayments ?? STAMHOOFD.environment != 'production'
|
|
204
|
+
|
|
205
|
+
if (payment.status == PaymentStatus.Pending || payment.status == PaymentStatus.Created || (payment.provider === PaymentProvider.Buckaroo && payment.status == PaymentStatus.Failed)) {
|
|
206
|
+
if (payment.provider === PaymentProvider.Stripe) {
|
|
207
|
+
try {
|
|
208
|
+
let status = await StripeHelper.getStatus(payment, cancel || this.shouldTryToCancel(payment.status, payment), testMode)
|
|
209
|
+
|
|
210
|
+
if (this.isManualExpired(status, payment)) {
|
|
211
|
+
console.error('Manually marking Stripe payment as expired', payment.id)
|
|
212
|
+
status = PaymentStatus.Failed
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await this.handlePaymentStatusUpdate(payment, organization, status)
|
|
216
|
+
} catch (e) {
|
|
217
|
+
console.error('Payment check failed Stripe', payment.id, e);
|
|
218
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
219
|
+
console.error('Manually marking Stripe payment as expired', payment.id)
|
|
220
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} else if (payment.provider === PaymentProvider.Mollie) {
|
|
224
|
+
// check status via mollie
|
|
225
|
+
const molliePayments = await MolliePayment.where({ paymentId: payment.id}, { limit: 1 })
|
|
226
|
+
if (molliePayments.length == 1) {
|
|
227
|
+
const molliePayment = molliePayments[0]
|
|
228
|
+
// check status
|
|
229
|
+
const token = await MollieToken.getTokenFor(organization.id)
|
|
230
|
+
|
|
231
|
+
if (token) {
|
|
232
|
+
try {
|
|
233
|
+
const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
|
|
234
|
+
const mollieData = await mollieClient.payments.get(molliePayment.mollieId, {
|
|
235
|
+
testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment != 'production',
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
console.log(mollieData) // log to log files to check issues
|
|
239
|
+
|
|
240
|
+
const details = (mollieData.details as any)
|
|
241
|
+
if (details?.consumerName) {
|
|
242
|
+
payment.ibanName = details.consumerName
|
|
243
|
+
}
|
|
244
|
+
if (details?.consumerAccount) {
|
|
245
|
+
payment.iban = details.consumerAccount
|
|
246
|
+
}
|
|
247
|
+
if (details?.cardHolder) {
|
|
248
|
+
payment.ibanName = details.cardHolder
|
|
249
|
+
}
|
|
250
|
+
if (details?.cardNumber) {
|
|
251
|
+
payment.iban = "xxxx xxxx xxxx "+details.cardNumber
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (mollieData.status == "paid") {
|
|
255
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Succeeded)
|
|
256
|
+
} else if (mollieData.status == "failed" || mollieData.status == "expired" || mollieData.status == "canceled") {
|
|
257
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed)
|
|
258
|
+
} else if (this.isManualExpired(payment.status, payment)) {
|
|
259
|
+
// Mollie still returning pending after 1 day: mark as failed
|
|
260
|
+
console.error('Manually marking Mollie payment as expired', payment.id)
|
|
261
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed)
|
|
262
|
+
}
|
|
263
|
+
} catch (e) {
|
|
264
|
+
console.error('Payment check failed Mollie', payment.id, e);
|
|
265
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
266
|
+
console.error('Manually marking Mollie payment as expired', payment.id)
|
|
267
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
console.warn("Mollie payment is missing for organization "+organization.id+" while checking payment status...")
|
|
272
|
+
|
|
273
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
274
|
+
console.error('Manually marking payment without mollie token as expired', payment.id)
|
|
275
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
280
|
+
console.error('Manually marking payment without mollie payments as expired', payment.id)
|
|
281
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} else if (payment.provider == PaymentProvider.Buckaroo) {
|
|
285
|
+
const helper = new BuckarooHelper(organization.privateMeta.buckarooSettings?.key ?? "", organization.privateMeta.buckarooSettings?.secret ?? "", organization.privateMeta.useTestPayments ?? STAMHOOFD.environment != 'production')
|
|
286
|
+
try {
|
|
287
|
+
let status = await helper.getStatus(payment)
|
|
288
|
+
|
|
289
|
+
if (this.isManualExpired(status, payment)) {
|
|
290
|
+
console.error('Manually marking Buckaroo payment as expired', payment.id)
|
|
291
|
+
status = PaymentStatus.Failed
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await this.handlePaymentStatusUpdate(payment, organization, status)
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.error('Payment check failed Buckaroo', payment.id, e);
|
|
297
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
298
|
+
console.error('Manually marking Buckaroo payment as expired', payment.id)
|
|
299
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
} else if (payment.provider == PaymentProvider.Payconiq) {
|
|
304
|
+
// Check status
|
|
305
|
+
|
|
306
|
+
const payconiqPayments = await PayconiqPayment.where({ paymentId: payment.id}, { limit: 1 })
|
|
307
|
+
if (payconiqPayments.length == 1) {
|
|
308
|
+
const payconiqPayment = payconiqPayments[0]
|
|
309
|
+
|
|
310
|
+
if (cancel) {
|
|
311
|
+
console.error('Cancelling Payconiq payment on request', payment.id)
|
|
312
|
+
await payconiqPayment.cancel(organization)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let status = await payconiqPayment.getStatus(organization)
|
|
316
|
+
|
|
317
|
+
if (!cancel && this.shouldTryToCancel(status, payment)) {
|
|
318
|
+
console.error('Manually cancelling Payconiq payment', payment.id)
|
|
319
|
+
if (await payconiqPayment.cancel(organization)) {
|
|
320
|
+
status = PaymentStatus.Failed
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (this.isManualExpired(status, payment)) {
|
|
325
|
+
console.error('Manually marking Payconiq payment as expired', payment.id)
|
|
326
|
+
status = PaymentStatus.Failed
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
await this.handlePaymentStatusUpdate(payment, organization, status)
|
|
330
|
+
|
|
331
|
+
} else {
|
|
332
|
+
console.warn("Payconiq payment is missing for organization "+organization.id+" while checking payment status...")
|
|
333
|
+
|
|
334
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
335
|
+
console.error('Manually marking Payconiq payment as expired because not found', payment.id)
|
|
336
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
console.error('Invalid payment provider', payment.provider, 'for payment', payment.id);
|
|
341
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
342
|
+
console.error('Manually marking unknown payment as expired', payment.id)
|
|
343
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return payment
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
static isManualExpired(status: PaymentStatus, payment: Payment) {
|
|
352
|
+
if ((status == PaymentStatus.Pending || status === PaymentStatus.Created) && payment.method !== PaymentMethod.DirectDebit) {
|
|
353
|
+
// If payment is not succeeded after one day, mark as failed
|
|
354
|
+
if (payment.createdAt < new Date(new Date().getTime() - 60*1000*60*24)) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Try to cancel a payment that is still pending
|
|
363
|
+
*/
|
|
364
|
+
static shouldTryToCancel(status: PaymentStatus, payment: Payment) {
|
|
365
|
+
if ((status == PaymentStatus.Pending || status === PaymentStatus.Created) && payment.method !== PaymentMethod.DirectDebit) {
|
|
366
|
+
let timeout = STAMHOOFD.environment === 'development' ? 60*1000*2 : 60*1000*30;
|
|
367
|
+
|
|
368
|
+
// If payconiq and not yet 'identified' (scanned), cancel after 5 minutes
|
|
369
|
+
if (payment.provider === PaymentProvider.Payconiq && status === PaymentStatus.Created) {
|
|
370
|
+
timeout = STAMHOOFD.environment === 'development' ? 60*1000*1 : 60*1000*5;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (payment.createdAt < new Date(new Date().getTime() - timeout)) {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { SimpleError } from "@simonbackx/simple-errors";
|
|
3
|
+
import { signInternal } from "@stamhoofd/backend-env";
|
|
4
|
+
import { Document } from "@stamhoofd/models";
|
|
5
|
+
|
|
6
|
+
import { Context } from "../../../helpers/Context";
|
|
7
|
+
type Params = { id: string };
|
|
8
|
+
type Query = undefined;
|
|
9
|
+
type Body = undefined
|
|
10
|
+
type ResponseBody = Buffer
|
|
11
|
+
|
|
12
|
+
export class GetDocumentHtml extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
13
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
14
|
+
if (request.method != "GET") {
|
|
15
|
+
return [false];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const params = Endpoint.parseParameters(request.url, "/documents/@id/html", { id: String});
|
|
19
|
+
|
|
20
|
+
if (params) {
|
|
21
|
+
return [true, params as Params];
|
|
22
|
+
}
|
|
23
|
+
return [false];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
27
|
+
const organization = await Context.setOrganizationScope()
|
|
28
|
+
await Context.authenticate()
|
|
29
|
+
|
|
30
|
+
const document = await Document.getByID(request.params.id)
|
|
31
|
+
if (!document || !(await Context.auth.canAccessDocument(document))) {
|
|
32
|
+
throw new SimpleError({
|
|
33
|
+
code: "not_found",
|
|
34
|
+
message: "Onbekend document"
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const html = await document.getRenderedHtml(organization);
|
|
39
|
+
if (!html) {
|
|
40
|
+
throw new SimpleError({
|
|
41
|
+
code: "failed_generating",
|
|
42
|
+
message: "Er ging iets mis bij het aanmaken van het document. Probeer later opieuw en neem contact met ons op als het probleem blijft herhalen."
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const response = new Response(Buffer.from(html, 'utf8'))
|
|
47
|
+
response.headers["content-type"] = "text/plain; charset=utf-8" // avoid JS execution
|
|
48
|
+
response.headers["content-length"] = Buffer.byteLength(html, 'utf8').toString()
|
|
49
|
+
response.headers["x-cache-id"] = 'document-' + document.id;
|
|
50
|
+
response.headers["x-cache-timestamp"] = document.updatedAt.getTime().toString();
|
|
51
|
+
response.headers["x-cache-signature"] = signInternal('document-' + document.id, document.updatedAt.getTime().toString(), html)
|
|
52
|
+
return response
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { SimpleError } from "@simonbackx/simple-errors";
|
|
3
|
+
import { Payment } from "@stamhoofd/models";
|
|
4
|
+
import { PaymentGeneral } from "@stamhoofd/structures";
|
|
5
|
+
|
|
6
|
+
import { AuthenticatedStructures } from "../../../helpers/AuthenticatedStructures";
|
|
7
|
+
import { Context } from "../../../helpers/Context";
|
|
8
|
+
|
|
9
|
+
type Params = { id: string };
|
|
10
|
+
type Query = undefined
|
|
11
|
+
type Body = undefined
|
|
12
|
+
type ResponseBody = PaymentGeneral
|
|
13
|
+
|
|
14
|
+
export class GetPaymentEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
15
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
16
|
+
if (request.method != "GET") {
|
|
17
|
+
return [false];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const params = Endpoint.parseParameters(request.url, "/payments/@id", { id: String});
|
|
21
|
+
|
|
22
|
+
if (params) {
|
|
23
|
+
return [true, params as Params];
|
|
24
|
+
}
|
|
25
|
+
return [false];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
29
|
+
await Context.setOrganizationScope()
|
|
30
|
+
await Context.authenticate()
|
|
31
|
+
|
|
32
|
+
const payment = await Payment.getByID(request.params.id);
|
|
33
|
+
if (!payment) {
|
|
34
|
+
throw new SimpleError({
|
|
35
|
+
code: "not_found",
|
|
36
|
+
message: "Payment not found",
|
|
37
|
+
human: "Je hebt geen toegang tot deze betaling"
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return new Response(
|
|
42
|
+
await AuthenticatedStructures.paymentGeneral(payment, true)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|