@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,158 @@
1
+ import { SimpleError } from "@simonbackx/simple-errors";
2
+ import { Group, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from "@stamhoofd/models";
3
+ import { OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, User as UserStruct, Group as GroupStruct, MembersBlob, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, Webshop as WebshopStruct,WebshopPreview, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
4
+
5
+ import { Context } from "./Context";
6
+
7
+ /**
8
+ * Builds authenticated structures for the current user
9
+ */
10
+ export class AuthenticatedStructures {
11
+ static async paymentGeneral(payment: Payment, checkPermissions = true): Promise<PaymentGeneral> {
12
+ return (await this.paymentsGeneral([payment], checkPermissions))[0]
13
+ }
14
+
15
+ /**
16
+ *
17
+ * @param payments
18
+ * @param checkPermissions Only set to undefined when not returned in the API + not for public use
19
+ * @returns
20
+ */
21
+ static async paymentsGeneral(payments: Payment[], checkPermissions = true): Promise<PaymentGeneral[]> {
22
+ if (payments.length === 0) {
23
+ return []
24
+ }
25
+
26
+ const {balanceItemPayments, balanceItems} = await Payment.loadBalanceItems(payments)
27
+ const {registrations, orders, members, groups} = await Payment.loadBalanceItemRelations(balanceItems);
28
+
29
+ if (checkPermissions) {
30
+ // Note: permission checking is moved here for performacne to avoid loading the data multiple times
31
+ if (!(await Context.auth.canAccessBalanceItems(balanceItems, PermissionLevel.Read, {registrations, orders, members}))) {
32
+ throw new SimpleError({
33
+ code: "not_found",
34
+ message: "Payment not found",
35
+ human: "Je hebt geen toegang tot deze betaling"
36
+ })
37
+ }
38
+ }
39
+
40
+ const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions
41
+
42
+ return Payment.getGeneralStructureFromRelations({
43
+ payments,
44
+ balanceItemPayments,
45
+ balanceItems,
46
+ registrations,
47
+ orders,
48
+ members,
49
+ groups
50
+ }, includeSettlements)
51
+ }
52
+
53
+ static async group(group: Group) {
54
+ if (!await Context.optionalAuth?.canAccessGroup(group)) {
55
+ return group.getStructure()
56
+ }
57
+ return group.getPrivateStructure()
58
+ }
59
+
60
+ static async webshop(webshop: Webshop) {
61
+ if (await Context.optionalAuth?.canAccessWebshop(webshop)) {
62
+ return PrivateWebshop.create(webshop)
63
+ }
64
+ return WebshopStruct.create(webshop)
65
+ }
66
+
67
+ static async organization(organization: Organization): Promise<OrganizationStruct> {
68
+ if (await Context.optionalAuth?.canAccessPrivateOrganizationData(organization)) {
69
+ const groups = await Group.getAll(organization.id, organization.periodId)
70
+ const webshops = await Webshop.where({ organizationId: organization.id }, { select: Webshop.selectColumnsWithout(undefined, "products", "categories")})
71
+ const webshopStructures: WebshopPreview[] = []
72
+
73
+ for (const w of webshops) {
74
+ if (!await Context.auth.canAccessWebshop(w)) {
75
+ continue
76
+ }
77
+ webshopStructures.push(WebshopPreview.create(w))
78
+ }
79
+
80
+ const oPeriods = await OrganizationRegistrationPeriod.where({ periodId: organization.periodId }, {limit: 1})
81
+ let oPeriod = oPeriods[0];
82
+ const period = (await RegistrationPeriod.getByID(organization.periodId))!
83
+
84
+ if (!oPeriod) {
85
+ const organizationPeriod = new OrganizationRegistrationPeriod();
86
+ organizationPeriod.organizationId = organization.id;
87
+ organizationPeriod.periodId = period.id
88
+ organizationPeriod.settings.categories = organization.meta.categories
89
+ organizationPeriod.settings.rootCategoryId = organization.meta.rootCategoryId
90
+ await organizationPeriod.save();
91
+
92
+ oPeriod = organizationPeriod
93
+ }
94
+
95
+ return OrganizationStruct.create({
96
+ id: organization.id,
97
+ name: organization.name,
98
+ meta: organization.meta,
99
+ address: organization.address,
100
+ registerDomain: organization.registerDomain,
101
+ uri: organization.uri,
102
+ website: organization.website,
103
+ privateMeta: organization.privateMeta,
104
+ webshops: webshopStructures,
105
+ createdAt: organization.createdAt,
106
+ period: oPeriod.getStructure(period, groups)
107
+ })
108
+ }
109
+
110
+ return await organization.getStructure()
111
+ }
112
+
113
+ static async adminOrganizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
114
+ const structs: OrganizationStruct[] = [];
115
+ const admins = await User.getAdmins(organizations.map(o => o.id))
116
+
117
+ for (const organization of organizations) {
118
+ const base = await organization.getStructure({emptyGroups: true})
119
+ base.admins = admins.filter(a => a.permissions?.organizationPermissions.has(organization.id)).map(a => UserStruct.create({...a, hasAccount: a.hasAccount()}))
120
+ structs.push(base)
121
+ }
122
+
123
+ return structs
124
+ }
125
+
126
+ static async membersBlob(members: MemberWithRegistrations[], includeContextOrganization = false): Promise<MembersBlob> {
127
+ const organizations = new Map<string, Organization>()
128
+ const memberBlobs: MemberWithRegistrationsBlob[] = []
129
+ for (const member of members) {
130
+ for (const registration of member.registrations) {
131
+ if (includeContextOrganization || registration.organizationId !== Context.auth.organization?.id) {
132
+ const found = organizations.get(registration.id);
133
+ if (!found) {
134
+ const organization = await Context.auth.getOrganization(registration.organizationId)
135
+ organizations.set(organization.id, organization)
136
+ }
137
+ }
138
+ }
139
+
140
+ const blob = member.getStructureWithRegistrations()
141
+ memberBlobs.push(
142
+ await Context.auth.filterMemberData(member, blob)
143
+ )
144
+ }
145
+
146
+ // Load responsibilities
147
+ const responsibilities = await MemberResponsibilityRecord.where({ memberId: { sign: 'IN', value: members.map(m => m.id) } })
148
+
149
+ for (const blob of memberBlobs) {
150
+ blob.responsibilities = responsibilities.filter(r => r.memberId == blob.id).map(r => MemberResponsibilityRecordStruct.create(r))
151
+ }
152
+
153
+ return MembersBlob.create({
154
+ members: memberBlobs,
155
+ organizations: await Promise.all([...organizations.values()].map(o => this.organization(o)))
156
+ })
157
+ }
158
+ }
@@ -0,0 +1,279 @@
1
+ import { SimpleError } from '@simonbackx/simple-errors';
2
+ import { BuckarooPayment, Payment } from '@stamhoofd/models';
3
+ import { PaymentMethod, PaymentStatus } from '@stamhoofd/structures';
4
+ import axios from 'axios';
5
+ import crypto from 'crypto';
6
+
7
+ export class BuckarooHelper {
8
+ key: string;
9
+ secret: string;
10
+ testMode: boolean;
11
+
12
+ constructor(key: string, secret: string, testMode: boolean) {
13
+ this.key = key;
14
+ this.secret = secret;
15
+ this.testMode = testMode;
16
+ }
17
+
18
+ getEncodedContent(content: string) {
19
+ if (content) {
20
+ return crypto.createHash('md5').update(content).digest("base64")
21
+ }
22
+
23
+ return content;
24
+ }
25
+
26
+ calculateHMAC(method: string, url: string, content: string): string {
27
+ method = method.toUpperCase()
28
+
29
+ // Remove protocol from url
30
+ url = url.replace(/^https?:\/\//, '')
31
+
32
+ // Uri encode
33
+ url = encodeURIComponent(url)
34
+
35
+ // To lowercase (should be last)
36
+ url = url.toLowerCase()
37
+
38
+ const timestamp = Math.floor(Date.now() / 1000)
39
+
40
+ // Nonce: A random sequence of characters, this should differ from for each request.
41
+ const nonce = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
42
+
43
+ const encodedContent = this.getEncodedContent(content)
44
+ const rawData = this.key + method + url + timestamp + nonce + encodedContent;
45
+
46
+ // The HMAC SHA256 of rawData using secret
47
+ const hash = crypto.createHmac('sha256', this.secret).update(rawData).digest('base64');
48
+
49
+ return "hmac " + this.key + ":" + hash + ":" + nonce + ":" + timestamp;
50
+ }
51
+
52
+ async request(method: "GET" | "POST", uri: string, content: any) {
53
+
54
+ const json = content ? JSON.stringify(content) : "";
55
+ // Finally, if you want to perform live transactions, sent the API requests to https://checkout.buckaroo.nl/json/Transaction
56
+ const url = (!this.testMode ? "https://checkout.buckaroo.nl" : "https://testcheckout.buckaroo.nl")+uri;
57
+
58
+ console.log("[BUCKAROO REQUEST]", method, url, content ? "\n [BUCKAROO REQUEST] " : undefined, json)
59
+
60
+ const response = await axios.request({
61
+ method,
62
+ url,
63
+ headers: {
64
+ 'Content-Type': json.length > 0 ? 'application/json' : "text/plain",
65
+ 'Authorization': this.calculateHMAC(method, url, json)
66
+ },
67
+ data: json
68
+
69
+ })
70
+ console.log("[BUCKAROO RESPONSE]", method, url, "\n[BUCKAROO RESPONSE]", JSON.stringify(response.data))
71
+ return response.data
72
+ }
73
+
74
+ async createTest(): Promise<boolean> {
75
+ const service = {
76
+ "Name": "bancontactmrcash",
77
+ "Action": "Pay",
78
+ "Parameters": [
79
+ {
80
+ "Name": "savetoken",
81
+ "Value": "false"
82
+ }
83
+ ]
84
+ };
85
+ const data = {
86
+ "Currency": "EUR",
87
+ "AmountDebit": "0.01",
88
+ "Invoice": "TESTPAYMENT-"+(new Date().getTime())+"-"+Math.round(Math.random()*100000),
89
+ "ClientIP": {
90
+ "Type": 0, // 0 = ipv4, 1 = ipv6
91
+ "Address": "0.0.0.0"
92
+ },
93
+ "Services": {
94
+ "ServiceList": [
95
+ service
96
+ ]
97
+ },
98
+ "ContinueOnIncomplete": "1", // iDEAL
99
+ "Description": "Test payment",
100
+ "PushURL": "",
101
+ "PushURLFailure": "",
102
+ "ReturnURL": "https://stamhoofd.be",
103
+ "ReturnURLCancel": "https://stamhoofd.be",
104
+ "ReturnURLError": "https://stamhoofd.be",
105
+ "ReturnURLReject": "https://stamhoofd.be",
106
+ }
107
+
108
+ try {
109
+ const response = await this.request("POST", "/json/Transaction", data)
110
+ const key = response["Key"]
111
+
112
+ if (!key) {
113
+ return false
114
+ }
115
+ const status = this.getStatusFromResponse(response)
116
+
117
+ return status === PaymentStatus.Pending || status === PaymentStatus.Created || status === PaymentStatus.Succeeded
118
+ } catch (e) {
119
+ console.error(e)
120
+ }
121
+ return false
122
+ }
123
+
124
+ async createPayment(payment: Payment, ip: string, description: string, returnUrl: string, exchangeUrl: string): Promise<string | null> {
125
+ let service: any;
126
+
127
+ switch (payment.method) {
128
+ case PaymentMethod.iDEAL: {
129
+ service = {
130
+ "Name": "ideal",
131
+ "Action": "Pay",
132
+ "Parameters": []
133
+ };
134
+ break;
135
+ }
136
+ case PaymentMethod.CreditCard: {
137
+ service ={
138
+ "Name": "mastercard",
139
+ "Action": "Pay"
140
+ };
141
+ break;
142
+ }
143
+
144
+ case PaymentMethod.Bancontact: {
145
+ service = {
146
+ "Name": "bancontactmrcash",
147
+ "Action": "Pay",
148
+ "Parameters": [
149
+ {
150
+ "Name": "savetoken",
151
+ "Value": "false"
152
+ }
153
+ ]
154
+ };
155
+ break;
156
+ }
157
+
158
+ case PaymentMethod.Payconiq: {
159
+ service = {
160
+ "Name": "payconiq",
161
+ "Action": "Pay"
162
+ };
163
+ break;
164
+ }
165
+ }
166
+
167
+ const data = {
168
+ "Currency": "EUR",
169
+ "AmountDebit": (payment.price / 100).toFixed(2),
170
+ "Invoice": "ID " + payment.id,
171
+ "ClientIP": {
172
+ "Type": 0, // 0 = ipv4, 1 = ipv6
173
+ "Address": ip
174
+ },
175
+ "Services": {
176
+ "ServiceList": [
177
+ service
178
+ ]
179
+ },
180
+ "ContinueOnIncomplete": "1", // iDEAL
181
+ "Description": description,
182
+ "PushURL": exchangeUrl,
183
+ "PushURLFailure": exchangeUrl,
184
+ "ReturnURL": returnUrl,
185
+ "ReturnURLCancel": returnUrl,
186
+ "ReturnURLError": returnUrl,
187
+ "ReturnURLReject": returnUrl,
188
+ }
189
+
190
+ try {
191
+ const response = await this.request("POST", "/json/Transaction", data)
192
+ const key = response["Key"]
193
+
194
+ if (!key) {
195
+ throw new Error("Failed to create payment, missing key")
196
+ }
197
+
198
+ payment.status = this.getStatusFromResponse(response)
199
+
200
+ // Save payment
201
+ const dbPayment = new BuckarooPayment()
202
+ dbPayment.paymentId = payment.id
203
+ dbPayment.transactionKey = key
204
+ await dbPayment.save();
205
+
206
+ return response["RequiredAction"]?.["RedirectURL"] ?? null;
207
+ } catch (e) {
208
+ console.error(e)
209
+ throw new SimpleError({
210
+ code: "buckaroo_error",
211
+ message: "Failed to create payment",
212
+ human: "Er ging iets mis bij het starten van de betaling. Herlaad de pagina en probeer het opnieuw."
213
+ })
214
+ }
215
+
216
+ }
217
+
218
+ private getStatusFromResponse(data: any) {
219
+ const status: string = data["Status"]?.["Code"]?.["Code"]?.toString() ?? ""
220
+ if (status === "190") {
221
+ return PaymentStatus.Succeeded
222
+ }
223
+
224
+ if (["490", "491", "492", "890", "891", "690"].includes(status)) {
225
+ return PaymentStatus.Failed
226
+ }
227
+
228
+ if (["790"].includes(status)) {
229
+ // Pending input
230
+ return PaymentStatus.Created
231
+ }
232
+
233
+ if (["791", "792"].includes(status)) {
234
+ // Pending input
235
+ return PaymentStatus.Pending
236
+ }
237
+
238
+ console.warn("Unknown buckaroo status: " + status+" for ", data)
239
+ return PaymentStatus.Pending
240
+ }
241
+
242
+ /**
243
+ * Get the status of a payment
244
+ */
245
+ async getStatus(payment: Payment) {
246
+ const buckarooPayments = await BuckarooPayment.where({ paymentId: payment.id}, { limit: 1 })
247
+ if (buckarooPayments.length != 1) {
248
+ throw new Error("Failed to find Buckaroo payment for payment " + payment.id)
249
+ }
250
+ const buckarooPayment = buckarooPayments[0]
251
+
252
+ // Send request
253
+ const response = await this.request("GET", "/json/transaction/status/" + buckarooPayment.transactionKey, undefined)
254
+ const parameters = response["Services"]?.[0]?.["Parameters"]
255
+
256
+ if (parameters && Array.isArray(parameters)) {
257
+ const iban = parameters.find(p => p.Name.toLowerCase() === "customeriban")?.Value
258
+
259
+ if (iban) {
260
+ payment.iban = iban
261
+ }
262
+
263
+ const name = parameters.find(p => p.Name.toLowerCase() === "customeraccountname")?.Value
264
+
265
+ if (name) {
266
+ payment.ibanName = name
267
+ }
268
+ }
269
+
270
+ const name = response["CustomerName"]
271
+
272
+ if (name) {
273
+ payment.ibanName = name
274
+ }
275
+
276
+ // Read status
277
+ return this.getStatusFromResponse(response)
278
+ }
279
+ }
@@ -0,0 +1,215 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
+ import { MolliePayment, MollieToken, Order, Organization, PayconiqPayment, Payment, StripeAccount } from '@stamhoofd/models';
4
+ import { Settlement } from '@stamhoofd/structures'
5
+ import axios from 'axios';
6
+
7
+ import { StripePayoutChecker } from './StripePayoutChecker';
8
+
9
+ type MollieSettlement = {
10
+ id: string;
11
+ reference: string;
12
+ createdAt: string;
13
+ settledAt: string;
14
+ status: "open" | "pending" | "paidout" | "failed";
15
+ amount: {
16
+ currenty: string;
17
+ value: string;
18
+ }
19
+ }
20
+
21
+ type MolliePaymentJSON = {
22
+ id: string;
23
+ }
24
+
25
+ let lastSettlementCheck: Date | null = null
26
+
27
+ export async function checkAllStripePayouts(checkAll = false) {
28
+ if (STAMHOOFD.environment !== "production") {
29
+ console.log("Skip settlement check")
30
+ return
31
+ }
32
+
33
+ // Stripe payouts
34
+ const stripeAccounts = await StripeAccount.where({ status: 'active' })
35
+ for (const account of stripeAccounts) {
36
+ try {
37
+ console.log("Checking settlements for ", account.accountId)
38
+
39
+ const checker = new StripePayoutChecker({
40
+ secretKey: STAMHOOFD.STRIPE_SECRET_KEY,
41
+ stripeAccount: account.accountId
42
+ })
43
+ await checker.checkSettlements(checkAll)
44
+ } catch (e) {
45
+ console.error(e)
46
+ }
47
+ }
48
+ }
49
+
50
+ export async function checkSettlements(checkAll = false) {
51
+ if (STAMHOOFD.environment !== "production") {
52
+ return
53
+ }
54
+
55
+ if (!checkAll && lastSettlementCheck && (lastSettlementCheck > new Date(new Date().getTime() - 24 * 60 * 60 * 1000))) {
56
+ console.log("Skip settlement check")
57
+ return
58
+ }
59
+
60
+ console.log("Checking settlements...")
61
+ lastSettlementCheck = new Date()
62
+
63
+ // Mollie payment is required
64
+ const token = STAMHOOFD.MOLLIE_ORGANIZATION_TOKEN
65
+ if (!token) {
66
+ console.error("Missing mollie organization token")
67
+ } else {
68
+ await checkMollieSettlementsFor(token, checkAll)
69
+ }
70
+
71
+ // Loop all mollie tokens created after given date (when settlement permission was added)
72
+ try {
73
+ // Stripe payouts
74
+ await checkAllStripePayouts(checkAll)
75
+
76
+ const mollieTokens = await MollieToken.all()
77
+ for (const token of mollieTokens) {
78
+ if (token.createdAt < new Date(2021, 8 /* september! */, 8)) {
79
+ console.log("Skipped mollie token that is too old")
80
+ } else {
81
+ try {
82
+ await token.refreshIfNeeded()
83
+ await checkMollieSettlementsFor(token.accessToken, checkAll)
84
+ } catch (e) {
85
+ console.error(e)
86
+ }
87
+ }
88
+ }
89
+ } catch (e) {
90
+ console.error(e)
91
+ }
92
+ }
93
+
94
+ // Check settlements once a week on tuesday morning/night
95
+ export async function checkMollieSettlementsFor(token: string, checkAll = false) {
96
+ // Check last 2 weeks + 3 day margin, unless we check them all
97
+ const d = new Date()
98
+ d.setDate(d.getDate() - 17)
99
+
100
+ console.log("Checking settlements for given token...")
101
+
102
+ // Loop all organizations with online paymetns the last week
103
+ try {
104
+ const request = await axios.get("https://api.mollie.com/v2/settlements?limit="+(checkAll ? 250 : 14), {
105
+ headers: {
106
+ "Authorization": "Bearer "+token
107
+ }
108
+ })
109
+ if (request.status === 200) {
110
+ // get data
111
+ try {
112
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
113
+ const data = request.data
114
+ // Read the data
115
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
116
+ if (data._embedded?.settlements) {
117
+ const settlements = data._embedded.settlements as MollieSettlement[];
118
+
119
+ for (const settlement of settlements) {
120
+ if (settlement.settledAt === null) {
121
+ // Skip: this is the open settlement
122
+ continue;
123
+ }
124
+
125
+ const settledAt = new Date(settlement.settledAt)
126
+
127
+ if (isNaN(settledAt.getTime())) {
128
+ console.error('Received an invalid settledAt from Mollie', settlement, 'for token', token);
129
+ continue;
130
+ }
131
+
132
+ if (checkAll || settledAt > d) {
133
+ await updateSettlement(token, settlement)
134
+ }
135
+ }
136
+ } else {
137
+ console.error("Unreadable settlements")
138
+ }
139
+ } catch (e) {
140
+ console.error(request.data)
141
+ throw e;
142
+ }
143
+
144
+ } else {
145
+ console.error("Failed to fetch settlements")
146
+ console.error(request.data)
147
+ }
148
+
149
+ } catch (e) {
150
+ console.error(e)
151
+ }
152
+ }
153
+
154
+ async function updateSettlement(token: string, settlement: MollieSettlement, fromPaymentId?: string) {
155
+ const limit = 250
156
+
157
+ // Loop all payments of this settlement
158
+ const request = await axios.get("https://api.mollie.com/v2/settlements/"+settlement.id+"/payments?limit="+limit+(fromPaymentId ? ("&from="+encodeURIComponent(fromPaymentId)) : ""), {
159
+ headers: {
160
+ "Authorization": "Bearer "+token
161
+ }
162
+ })
163
+
164
+ if (request.status === 200) {
165
+ const molliePayments = request.data._embedded.payments as MolliePaymentJSON[]
166
+
167
+ for (const mollie of molliePayments) {
168
+ // Search payment
169
+ const mps = await MolliePayment.where({ mollieId: mollie.id })
170
+ if (mps.length == 1) {
171
+ const mp = mps[0]
172
+ const payment = await Payment.getByID(mp.paymentId)
173
+ if (payment) {
174
+ payment.settlement = Settlement.create({
175
+ id: settlement.id,
176
+ reference: settlement.reference,
177
+ settledAt: new Date(settlement.settledAt),
178
+ amount: Math.round(parseFloat(settlement.amount.value)*100)
179
+ })
180
+ const saved = await payment.save()
181
+
182
+ if (saved) {
183
+ // Mark order as 'updated', or the frontend won't pull in the updates
184
+ const order = await Order.getForPayment(null, payment.id)
185
+ if (order) {
186
+ order.updatedAt = new Date();
187
+ order.forceSaveProperty('updatedAt');
188
+ await order.save();
189
+ }
190
+
191
+ // TODO: Mark registrations as 'saved'
192
+ }
193
+
194
+
195
+ if (STAMHOOFD.environment === "development") {
196
+ console.log("Updated settlement of payment "+payment.id)
197
+ console.log(payment.settlement)
198
+ }
199
+ } else {
200
+ console.log("Missing payment "+mp.paymentId)
201
+ }
202
+ } else {
203
+ // Probably a payment in a different system/platform
204
+ //console.log("No mollie payment found for id "+mollie.id)
205
+ }
206
+ }
207
+
208
+ // Check next page
209
+ if (request.data._links.next) {
210
+ await updateSettlement(token, settlement, molliePayments[molliePayments.length - 1].id)
211
+ }
212
+ } else {
213
+ console.error(request.data)
214
+ }
215
+ }