@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,153 @@
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 { MolliePayment, Organization } from "@stamhoofd/models";
6
+ import { Payment } from "@stamhoofd/models";
7
+ import { STInvoice } from "@stamhoofd/models";
8
+ import { QueueHandler } from '@stamhoofd/queues';
9
+ import { PaymentMethod,PaymentProvider,PaymentStatus, STInvoice as STInvoiceStruct } from "@stamhoofd/structures";
10
+ type Params = {id: string};
11
+ class Query extends AutoEncoder {
12
+ @field({ decoder: BooleanDecoder, optional: true })
13
+ exchange = false
14
+ }
15
+ type Body = undefined
16
+ type ResponseBody = STInvoiceStruct | undefined;
17
+
18
+ /**
19
+ * 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
20
+ */
21
+
22
+ export class ExchangeSTPaymentEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
23
+ queryDecoder = Query as Decoder<Query>
24
+
25
+ protected doesMatch(request: Request): [true, Params] | [false] {
26
+ if (request.method != "POST") {
27
+ return [false];
28
+ }
29
+
30
+ const params = Endpoint.parseParameters(request.url, "/billing/payments/@id", {id: String});
31
+
32
+ if (params) {
33
+ return [true, params as Params];
34
+ }
35
+ return [false];
36
+ }
37
+
38
+ async handle(request: DecodedRequest<Params, Query, Body>) {
39
+ const payment = await Payment.getByID(request.params.id)
40
+ if (!payment) {
41
+ throw new SimpleError({
42
+ code: "",
43
+ message: "Deze link is ongeldig"
44
+ })
45
+ }
46
+
47
+ const invoices = await STInvoice.where({ paymentId: payment.id })
48
+ if (invoices.length > 1) {
49
+ console.error("Received more than 1 invoices for the same payment. Danger zone!")
50
+ throw new Error("Unexpected error")
51
+ }
52
+
53
+ if (invoices.length == 0) {
54
+ console.error("Didn't found and invoice for a given payment!")
55
+ throw new Error("Unexpected error")
56
+ }
57
+
58
+ // Not method on payment because circular references (not supprted in ts)
59
+ const invoice = invoices[0]
60
+
61
+ if (request.query.exchange) {
62
+ // Don't wait for exchanges
63
+ ExchangeSTPaymentEndpoint.pollStatus(payment, invoice).catch(e => {
64
+ console.error(e)
65
+ })
66
+ return new Response(undefined);
67
+ }
68
+
69
+ const updatedInvoice = await ExchangeSTPaymentEndpoint.pollStatus(payment, invoice)
70
+
71
+ if (!updatedInvoice) {
72
+ return new Response(undefined);
73
+ }
74
+
75
+ return new Response(
76
+ await updatedInvoice.getStructure()
77
+ );
78
+ }
79
+
80
+ static async pollStatus(payment: Payment, _invoice: STInvoice): Promise<STInvoice | undefined> {
81
+ // All invoice related logic needs to happen after each ather, not concurrently
82
+ return await QueueHandler.schedule("billing/invoices-"+_invoice.organizationId, async () => {
83
+
84
+ // Get a new copy of the invoice (is required to prevent concurrenty bugs)
85
+ const invoice = await STInvoice.getByID(_invoice.id)
86
+ if (!invoice || invoice.paidAt !== null) {
87
+ return invoice
88
+ }
89
+
90
+ if ((payment.provider === PaymentProvider.Mollie || (payment.provider === null && payment.method == PaymentMethod.DirectDebit)) && (payment.status == PaymentStatus.Pending || payment.status == PaymentStatus.Created || payment.status == PaymentStatus.Failed)) {
91
+ if (payment.method == PaymentMethod.Bancontact || payment.method == PaymentMethod.iDEAL || payment.method == PaymentMethod.CreditCard || payment.method == PaymentMethod.DirectDebit || payment.method == PaymentMethod.Transfer) {
92
+ // check status via mollie
93
+ const molliePayments = await MolliePayment.where({ paymentId: payment.id}, { limit: 1 })
94
+ if (molliePayments.length == 1) {
95
+ const molliePayment = molliePayments[0]
96
+ // check status
97
+ const apiKey = STAMHOOFD.MOLLIE_API_KEY
98
+ if (apiKey) {
99
+ const mollieClient = createMollieClient({ apiKey });
100
+ const mollieData = await mollieClient.payments.get(molliePayment.mollieId)
101
+
102
+ console.log(mollieData) // log to log files to check issues
103
+
104
+ const details = (mollieData.details as any)
105
+ if (details?.cardNumber) {
106
+ payment.iban = "xxxx xxxx xxxx "+details.cardNumber
107
+ }
108
+ if (details?.cardHolder) {
109
+ payment.ibanName = details.cardHolder
110
+ }
111
+ if (details?.consumerAccount) {
112
+ payment.iban = details.consumerAccount
113
+ }
114
+ if (details?.consumerName) {
115
+ payment.ibanName = details.consumerName
116
+ }
117
+
118
+ if (mollieData.status == "paid") {
119
+ payment.status = PaymentStatus.Succeeded
120
+ payment.paidAt = new Date()
121
+ await payment.save();
122
+
123
+ await invoice.markPaid()
124
+
125
+ // Save customer id
126
+ if (mollieData.customerId && _invoice.organizationId) {
127
+ const organization = await Organization.getByID(_invoice.organizationId)
128
+ if (organization) {
129
+ organization.serverMeta.mollieCustomerId = mollieData.customerId
130
+ console.log("Saving mollie customer", mollieData.customerId, "for organization", organization.id)
131
+ await organization.save()
132
+ }
133
+ }
134
+ } else if (mollieData.status == "failed" || mollieData.status == "expired" || mollieData.status == "canceled") {
135
+ payment.status = PaymentStatus.Failed
136
+ await payment.save();
137
+ await invoice.markFailed(payment)
138
+ }
139
+ } else {
140
+ console.error("Mollie api key is missing for Stamhoofd payments! "+payment.id)
141
+ }
142
+ } else {
143
+ console.error("Couldn't find mollie payment for payment "+payment.id)
144
+ }
145
+ } else {
146
+ console.error("Payment method not supported for invoice "+invoice.id+" and payment "+payment.id)
147
+ throw new Error("Unsupported payment method for invoices")
148
+ }
149
+ }
150
+ return invoice
151
+ });
152
+ }
153
+ }
@@ -0,0 +1,134 @@
1
+
2
+ import { AnyDecoder, AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
3
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
4
+ import { SimpleError } from '@simonbackx/simple-errors';
5
+ import { Organization, StripeAccount, StripeCheckoutSession, StripePaymentIntent } from '@stamhoofd/models';
6
+
7
+ import { StripeHelper } from '../../../helpers/StripeHelper';
8
+ import { ExchangePaymentEndpoint } from '../../organization/shared/ExchangePaymentEndpoint';
9
+
10
+ type Params = Record<string, never>;
11
+ class Body extends AutoEncoder {
12
+ /**
13
+ * The account id (internal id, not the stripe id)
14
+ */
15
+ @field({ decoder: StringDecoder })
16
+ type: string
17
+
18
+ @field({ decoder: StringDecoder })
19
+ id: string
20
+
21
+ @field({ decoder: AnyDecoder })
22
+ data: any
23
+ }
24
+
25
+ type Query = undefined
26
+ type ResponseBody = undefined
27
+
28
+ export class StripeWebookEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
29
+ bodyDecoder = Body as Decoder<Body>
30
+
31
+ protected doesMatch(request: Request): [true, Params] | [false] {
32
+ if (request.method !== "POST") {
33
+ return [false];
34
+ }
35
+
36
+ const params = Endpoint.parseParameters(request.url, "/stripe/webhooks", {});
37
+
38
+ if (params) {
39
+ return [true, params as Params];
40
+ }
41
+
42
+ return [false];
43
+ }
44
+
45
+ async handle(request: DecodedRequest<Params, Query, Body>) {
46
+ console.log("Received Stripe Webhook", request.body.type);
47
+
48
+ // Verify webhook signature and extract the event.
49
+ // See https://stripe.com/docs/webhooks/signatures for more information.
50
+ let event;
51
+ try {
52
+ const stripe = StripeHelper.getInstance()
53
+ const sig = request.headers['stripe-signature'];
54
+ if (!sig) {
55
+ throw new SimpleError({
56
+ code: "invalid_signature",
57
+ message: "Invalid signature",
58
+ statusCode: 400
59
+ })
60
+ }
61
+ event = await stripe.webhooks.constructEventAsync(await request.request.bodyPromise!, sig, STAMHOOFD.STRIPE_ENDPOINT_SECRET);
62
+ } catch (err) {
63
+ throw new SimpleError({
64
+ code: "invalid_signature",
65
+ message: "Invalid signature",
66
+ statusCode: 400
67
+ })
68
+ }
69
+
70
+ // Check type
71
+ switch (request.body.type) {
72
+ case "account.updated": {
73
+ console.log(event);
74
+ const account = request.body.data.object;
75
+ if (account && account.id) {
76
+ const id = account.id as string;
77
+ const [model] = await StripeAccount.where({accountId: id}, {limit: 1});
78
+ if (model) {
79
+ model.setMetaFromStripeAccount(account)
80
+ await model.save()
81
+ } else {
82
+ console.warn("Could not find stripe account with id", id)
83
+ }
84
+ }
85
+ break;
86
+ }
87
+ case "payment_intent.amount_capturable_updated":
88
+ case "payment_intent.canceled":
89
+ case "payment_intent.created":
90
+ case "payment_intent.partially_funded":
91
+ case "payment_intent.payment_failed":
92
+ case "payment_intent.processing":
93
+ case "payment_intent.requires_action":
94
+ case "payment_intent.succeeded": {
95
+ const intentId = request.body.data.object.id;
96
+ const [model] = await StripePaymentIntent.where({stripeIntentId: intentId}, {limit: 1})
97
+ if (model && model.organizationId) {
98
+ const organization = await Organization.getByID(model.organizationId)
99
+ if (organization) {
100
+ await ExchangePaymentEndpoint.pollStatus(model.paymentId, organization)
101
+ } else {
102
+ console.warn("Could not find organization with id", model.organizationId)
103
+ }
104
+ } else {
105
+ console.warn("Could not find stripe payment intent with id", intentId)
106
+ }
107
+ break;
108
+ }
109
+ case "checkout.session.async_payment_failed":
110
+ case "checkout.session.async_payment_succeeded":
111
+ case "checkout.session.completed":
112
+ case "checkout.session.expired": {
113
+ const checkoutId = request.body.data.object.id;
114
+ const [model] = await StripeCheckoutSession.where({stripeSessionId: checkoutId}, {limit: 1})
115
+ if (model && model.organizationId) {
116
+ const organization = await Organization.getByID(model.organizationId)
117
+ if (organization) {
118
+ await ExchangePaymentEndpoint.pollStatus(model.paymentId, organization)
119
+ } else {
120
+ console.warn("Could not find organization with id", model.organizationId)
121
+ }
122
+ } else {
123
+ console.warn("Could not find stripe checkout session with id", checkoutId)
124
+ }
125
+ break;
126
+ }
127
+ default: {
128
+ console.log("Unhandled stripe webhook type", request.body.type);
129
+ break;
130
+ }
131
+ }
132
+ return new Response(undefined);
133
+ }
134
+ }
@@ -0,0 +1,44 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { User } from '@stamhoofd/models';
3
+ import { User as UserStruct } from "@stamhoofd/structures";
4
+
5
+ import { Context } from "../../../helpers/Context";
6
+ type Params = Record<string, never>;
7
+ type Query = undefined;
8
+ type Body = undefined
9
+ type ResponseBody = UserStruct[]
10
+
11
+ export class GetPlatformAdminsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
12
+ protected doesMatch(request: Request): [true, Params] | [false] {
13
+ if (request.method != "GET") {
14
+ return [false];
15
+ }
16
+
17
+ const params = Endpoint.parseParameters(request.url, "/platform/admins", {});
18
+
19
+ if (params) {
20
+ return [true, params as Params];
21
+ }
22
+ return [false];
23
+ }
24
+
25
+ async handle(_: DecodedRequest<Params, Query, Body>) {
26
+ await Context.authenticate()
27
+
28
+ // Fast throw first (more in depth checking for patches later)
29
+ if (!Context.auth.canManagePlatformAdmins()) {
30
+ throw Context.auth.error()
31
+ }
32
+
33
+ // Get all admins
34
+ let admins = await User.where({ organizationId: null, permissions: { sign: "!=", value: null }})
35
+
36
+ // Hide api accounts
37
+ admins = admins.filter(a => !a.isApiUser)
38
+ admins = admins.filter(a => !!a.permissions?.globalPermissions)
39
+
40
+ return new Response(
41
+ admins.map(a => UserStruct.create({...a, hasAccount: a.hasAccount()}))
42
+ );
43
+ }
44
+ }
@@ -0,0 +1,39 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { Platform } from "@stamhoofd/models";
3
+ import { Platform as PlatformStruct } from "@stamhoofd/structures";
4
+
5
+ import { Context } from "../../../helpers/Context";
6
+
7
+ type Params = Record<string, never>;
8
+ type Query = undefined;
9
+ type Body = undefined
10
+ type ResponseBody = PlatformStruct;
11
+
12
+ export class GetPlatformEndpoint 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, "/platform", {});
19
+
20
+ if (params) {
21
+ return [true, params as Params];
22
+ }
23
+ return [false];
24
+ }
25
+
26
+ async handle(_: DecodedRequest<Params, Query, Body>) {
27
+ await Context.optionalAuthenticate({allowWithoutAccount: false})
28
+
29
+ if (Context.optionalAuth?.hasSomePlatformAccess()) {
30
+ const platform = await Platform.getSharedPrivateStruct()
31
+ if (!platform.privateConfig) {
32
+ throw new Error("Private config not found")
33
+ }
34
+ return new Response(platform);
35
+ }
36
+ const platform = await Platform.getSharedStruct()
37
+ return new Response(platform);
38
+ }
39
+ }
@@ -0,0 +1,63 @@
1
+ import { AutoEncoderPatchType, Decoder, patchObject } from "@simonbackx/simple-encoding";
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { Platform } from "@stamhoofd/models";
4
+ import { Platform as PlatformStruct } from "@stamhoofd/structures";
5
+
6
+ import { Context } from "../../../helpers/Context";
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = undefined;
10
+ type Body = AutoEncoderPatchType<PlatformStruct>;
11
+ type ResponseBody = PlatformStruct;
12
+
13
+ export class PatchPlatformEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ bodyDecoder = PlatformStruct.patchType() as Decoder<AutoEncoderPatchType<PlatformStruct>>
15
+
16
+ protected doesMatch(request: Request): [true, Params] | [false] {
17
+ if (request.method != "PATCH") {
18
+ return [false];
19
+ }
20
+
21
+ const params = Endpoint.parseParameters(request.url, "/platform", {});
22
+
23
+ if (params) {
24
+ return [true, params as Params];
25
+ }
26
+ return [false];
27
+ }
28
+
29
+ async handle(request: DecodedRequest<Params, Query, Body>) {
30
+ await Context.authenticate()
31
+
32
+ // Fast throw first (more in depth checking for patches later)
33
+ if (!Context.auth.hasPlatformFullAccess()) {
34
+ throw Context.auth.error()
35
+ }
36
+
37
+ const platform = await Platform.getShared()
38
+
39
+ if (request.body.privateConfig) {
40
+ // Did we patch roles?
41
+ if (request.body.privateConfig.roles) {
42
+ if (!Context.auth.canManagePlatformAdmins()) {
43
+ throw Context.auth.error()
44
+ }
45
+
46
+ // Update roles
47
+ platform.privateConfig.roles = patchObject(platform.privateConfig.roles, request.body.privateConfig.roles)
48
+ }
49
+ }
50
+
51
+ if (request.body.config) {
52
+ if (!Context.auth.hasPlatformFullAccess()) {
53
+ throw Context.auth.error()
54
+ }
55
+
56
+ // Update config
57
+ platform.config = patchObject(platform.config, request.body.config)
58
+ }
59
+
60
+ await platform.save()
61
+ return new Response(await Platform.getSharedPrivateStruct());
62
+ }
63
+ }
@@ -0,0 +1,68 @@
1
+ import { ManyToOneRelation } from "@simonbackx/simple-database";
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { SimpleError } from "@simonbackx/simple-errors";
4
+ import { Group, Member, Payment, Registration } from '@stamhoofd/models';
5
+ import { PaymentWithRegistrations } from "@stamhoofd/structures";
6
+ import { Formatter } from "@stamhoofd/utility";
7
+
8
+ import { Context } from "../../../helpers/Context";
9
+ type Params = {id: string};
10
+ type Query = undefined
11
+ type Body = undefined
12
+ type ResponseBody = PaymentWithRegistrations | undefined;
13
+
14
+ export class GetPaymentRegistrations 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/registrations", {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.setUserOrganizationScope();
30
+ const {user} = await Context.authenticate()
31
+
32
+ const payment = await Payment.getByID(request.params.id)
33
+ if (!payment) {
34
+ throw new SimpleError({
35
+ code: "",
36
+ message: "Deze link is ongeldig"
37
+ })
38
+ }
39
+ const registrations = await Member.getRegistrationWithMembersForPayment(payment.id)
40
+ const authorizedMembers = (await Member.getMembersWithRegistrationForUser(user)).map(m => m.id)
41
+ const groups = await Group.getByIDs(...Formatter.uniqueArray(registrations.map(r => r.groupId)))
42
+
43
+ // Check permissions
44
+ for (const registration of registrations) {
45
+ if (!authorizedMembers.includes(registration.member.id)) {
46
+ throw new SimpleError({
47
+ code: "",
48
+ message: "Deze link is ongeldig"
49
+ })
50
+ }
51
+ }
52
+
53
+ return new Response(
54
+ PaymentWithRegistrations.create({
55
+ id: payment.id,
56
+ method: payment.method,
57
+ status: payment.status,
58
+ price: payment.price,
59
+ freeContribution: payment.freeContribution,
60
+ transferDescription: payment.transferDescription,
61
+ paidAt: payment.paidAt,
62
+ createdAt: payment.createdAt,
63
+ updatedAt: payment.updatedAt,
64
+ registrations: registrations.map(r => Member.getRegistrationWithMemberStructure(r.setRelation(Registration.group, groups.find(g => g.id === r.groupId)!)))
65
+ })
66
+ );
67
+ }
68
+ }
@@ -0,0 +1,39 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { BalanceItem, Member } from "@stamhoofd/models";
3
+ import { MemberBalanceItem } from "@stamhoofd/structures";
4
+
5
+ import { Context } from "../../../helpers/Context";
6
+
7
+ type Params = Record<string, never>;
8
+ type Query = undefined
9
+ type Body = undefined
10
+ type ResponseBody = MemberBalanceItem[]
11
+
12
+ export class GetUserBalanceEndpoint 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, "/balance", {});
19
+
20
+ if (params) {
21
+ return [true, params as Params];
22
+ }
23
+ return [false];
24
+ }
25
+
26
+ async handle(_: DecodedRequest<Params, Query, Body>) {
27
+ const organization = await Context.setUserOrganizationScope();
28
+ const {user} = await Context.authenticate()
29
+
30
+ const members = await Member.getMembersWithRegistrationForUser(user)
31
+
32
+ // Get all balance items for this member or users
33
+ const balanceItems = await BalanceItem.balanceItemsForUsersAndMembers(organization?.id ?? null, [user.id], members.map(m => m.id))
34
+
35
+ return new Response(
36
+ await BalanceItem.getMemberStructure(balanceItems)
37
+ );
38
+ }
39
+ }
@@ -0,0 +1,80 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { Document, DocumentTemplate, Member } from '@stamhoofd/models';
3
+ import { Document as DocumentStruct,DocumentStatus } from "@stamhoofd/structures";
4
+ import { Sorter } from "@stamhoofd/utility";
5
+
6
+ import { Context } from "../../../helpers/Context";
7
+ type Params = Record<string, never>;
8
+ type Query = undefined;
9
+ type Body = undefined
10
+ type ResponseBody = DocumentStruct[];
11
+
12
+ /**
13
+ * Get the members of the user
14
+ */
15
+ export class GetUserMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
+ protected doesMatch(request: Request): [true, Params] | [false] {
17
+ if (request.method != "GET") {
18
+ return [false];
19
+ }
20
+
21
+ const params = Endpoint.parseParameters(request.url, "/documents", {});
22
+
23
+ if (params) {
24
+ return [true, params as Params];
25
+ }
26
+
27
+ return [false];
28
+ }
29
+
30
+ async handle(_: DecodedRequest<Params, Query, Body>) {
31
+ const organization = await Context.setUserOrganizationScope();
32
+ const {user} = await Context.authenticate()
33
+
34
+ const members = await Member.getMembersWithRegistrationForUser(user)
35
+ let templates = organization ? await DocumentTemplate.where({ status: 'Published', organizationId: organization.id }) : null
36
+ const memberIds = members.map(m => m.id)
37
+ const templateIds = templates ? templates.map(t => t.id) : null
38
+
39
+ if (memberIds.length == 0 || (templateIds !== null && templateIds.length == 0)) {
40
+ return new Response([]);
41
+ }
42
+
43
+ const w: any = {
44
+ memberId: {
45
+ sign: 'IN',
46
+ value: memberIds
47
+ },
48
+ status: {
49
+ sign: 'IN',
50
+ value: [DocumentStatus.MissingData, DocumentStatus.Published]
51
+ },
52
+ }
53
+
54
+ if (templateIds !== null) {
55
+ w.templateId = {
56
+ sign: 'IN',
57
+ value: templateIds
58
+ }
59
+ }
60
+
61
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
62
+ const documents = await Document.where(w);
63
+
64
+ if (!templates) {
65
+ templates = documents.length > 0 ? await DocumentTemplate.getByIDs(...documents.map(d => d.templateId)) : []
66
+ }
67
+
68
+ const filteredDocuments = documents.filter(document => {
69
+ const template = templates.find(t => t.id == document.templateId)
70
+ if (!template || (!template.updatesEnabled && document.status === DocumentStatus.MissingData)) {
71
+ return false
72
+ }
73
+ return true;
74
+ })
75
+
76
+ filteredDocuments.sort((a, b) => Sorter.byDateValue(a.createdAt, b.createdAt))
77
+
78
+ return new Response(filteredDocuments.map(d => d.getStructure()));
79
+ }
80
+ }
@@ -0,0 +1,41 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { Member } from '@stamhoofd/models';
3
+ import { MembersBlob } from "@stamhoofd/structures";
4
+
5
+ import { AuthenticatedStructures } from "../../../helpers/AuthenticatedStructures";
6
+ import { Context } from "../../../helpers/Context";
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = undefined;
10
+ type Body = undefined
11
+ type ResponseBody = MembersBlob
12
+
13
+ /**
14
+ * Get the members of the user
15
+ */
16
+ export class GetUserMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
17
+ protected doesMatch(request: Request): [true, Params] | [false] {
18
+ if (request.method != "GET") {
19
+ return [false];
20
+ }
21
+
22
+ const params = Endpoint.parseParameters(request.url, "/user/members", {});
23
+
24
+ if (params) {
25
+ return [true, params as Params];
26
+ }
27
+
28
+ return [false];
29
+ }
30
+
31
+ async handle(_: DecodedRequest<Params, Query, Body>) {
32
+ await Context.setUserOrganizationScope();
33
+ const {user} = await Context.authenticate()
34
+
35
+ const members = await Member.getMembersWithRegistrationForUser(user)
36
+
37
+ return new Response(
38
+ await AuthenticatedStructures.membersBlob(members)
39
+ );
40
+ }
41
+ }