@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,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
+ }