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