@stamhoofd/backend 2.120.5 → 2.121.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/package.json +12 -12
- package/src/audit-logs/RegistrationInvitationLogger.ts +46 -0
- package/src/audit-logs/init.ts +2 -0
- package/src/crons/index.ts +2 -0
- package/src/crons/invoices.ts +166 -0
- package/src/crons/mollie-chargebacks.ts +87 -0
- package/src/crons.ts +47 -10
- package/src/email-recipient-loaders/payments.ts +84 -41
- package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +51 -0
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +22 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +4 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +495 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +216 -0
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +405 -0
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +168 -0
- package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +15 -0
- package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +3 -4
- package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +42 -19
- package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
- package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +11 -3
- package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +308 -0
- package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +7 -0
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +5 -4
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +7 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +17 -8
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +127 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +13 -4
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +7 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -11
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +14 -19
- package/src/helpers/AdminPermissionChecker.ts +11 -3
- package/src/helpers/AuthenticatedStructures.ts +94 -6
- package/src/helpers/FinancialSupportHelper.ts +21 -0
- package/src/helpers/RecordAnswerHelper.test.ts +746 -0
- package/src/helpers/RecordAnswerHelper.ts +116 -0
- package/src/helpers/StripeHelper.ts +2 -3
- package/src/helpers/ViesHelper.ts +7 -3
- package/src/seeds/1750090030-records-configuration.ts +68 -3
- package/src/seeds/1752848561-groups-registration-periods.ts +26 -2
- package/src/seeds/1779121239-default-invoice-email-template.sql +3 -0
- package/src/services/BalanceItemService.ts +12 -16
- package/src/services/InvoiceService.ts +372 -72
- package/src/services/MollieService.ts +537 -0
- package/src/services/PaymentMandateService.ts +214 -0
- package/src/services/PaymentService.ts +578 -222
- package/src/services/PlatformMembershipService.ts +1 -1
- package/src/services/RegistrationService.ts +66 -5
- package/src/services/STPackageService.ts +0 -7
- package/src/services/data/invoice.hbs.html +686 -0
- package/src/sql-filters/groups.ts +11 -1
- package/src/sql-filters/payments.ts +5 -0
- package/src/sql-filters/registration-invitations.ts +90 -0
- package/src/sql-sorters/registration-invitations.ts +36 -0
- package/vitest.config.js +1 -0
- package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +0 -216
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import type { Mandate , Payment as MolliePaymentType} from '@mollie/api-client';
|
|
2
|
+
import { ApiMode, createMollieClient, MandateMethod, MandateStatus, PaymentMethod as molliePaymentMethod, PaymentStatus as molliePaymentStatus, OnboardingStatus, ProfileStatus, SequenceType } from '@mollie/api-client';
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import type { Payment, User } from '@stamhoofd/models';
|
|
5
|
+
import type { Organization } from '@stamhoofd/models';
|
|
6
|
+
import { MolliePayment, MollieToken, Platform } from '@stamhoofd/models';
|
|
7
|
+
import type { PaymentCustomer } from '@stamhoofd/structures';
|
|
8
|
+
import { MollieOnboarding, MollieProfile, MollieProfileMode, MollieProfileStatus, MollieStatus, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
|
|
9
|
+
import { PaymentMandate, PaymentMandateDetails, PaymentMandateStatus, PaymentMandateType } from '@stamhoofd/structures/PaymentMandate.js';
|
|
10
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
11
|
+
import { DateTime } from 'luxon';
|
|
12
|
+
import { Context } from '../helpers/Context.js';
|
|
13
|
+
|
|
14
|
+
export class MollieService {
|
|
15
|
+
client: ReturnType<typeof createMollieClient>;
|
|
16
|
+
sellingOrganization: Organization;
|
|
17
|
+
createdAt: Date
|
|
18
|
+
|
|
19
|
+
private constructor({sellingOrganization, accessToken}: {sellingOrganization: Organization, accessToken: string}) {
|
|
20
|
+
this.sellingOrganization = sellingOrganization
|
|
21
|
+
this.client = createMollieClient({ accessToken });
|
|
22
|
+
this.createdAt = new Date()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static #cachedServices: Map<string, MollieService> = new Map()
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cached instances can be used for maximum 30 seconds
|
|
29
|
+
*/
|
|
30
|
+
isOutdated() {
|
|
31
|
+
return this.createdAt.getTime() < Date.now() - 30_000
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static async create({sellingOrganization}: {sellingOrganization: Organization}) {
|
|
35
|
+
const cached = this.#cachedServices.get(sellingOrganization.id);
|
|
36
|
+
if (cached && !cached.isOutdated()) {
|
|
37
|
+
return cached;
|
|
38
|
+
}
|
|
39
|
+
const token = await MollieToken.getTokenFor(sellingOrganization.id);
|
|
40
|
+
if (!token) {
|
|
41
|
+
if (cached) {
|
|
42
|
+
this.#cachedServices.delete(sellingOrganization.id)
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const service = new MollieService({ sellingOrganization, accessToken: await token.getAccessToken() });
|
|
47
|
+
this.#cachedServices.set(sellingOrganization.id, service);
|
|
48
|
+
return service;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set initial onboarding values + enable bancontact
|
|
53
|
+
*/
|
|
54
|
+
async setupOnboarding() {
|
|
55
|
+
// Submit onboarding data
|
|
56
|
+
this.sellingOrganization.privateMeta.mollieOnboarding = await this.getOnboardingStatus();
|
|
57
|
+
|
|
58
|
+
if (this.sellingOrganization.privateMeta.mollieOnboarding && this.sellingOrganization.privateMeta.mollieOnboarding.status === MollieStatus.NeedsData) {
|
|
59
|
+
try {
|
|
60
|
+
await this.client.onboarding.submit({
|
|
61
|
+
organization: {
|
|
62
|
+
name: this.sellingOrganization.name,
|
|
63
|
+
address: {
|
|
64
|
+
streetAndNumber: this.sellingOrganization.address.street + ' ' + this.sellingOrganization.address.number,
|
|
65
|
+
postalCode: this.sellingOrganization.address.postalCode,
|
|
66
|
+
city: this.sellingOrganization.address.city,
|
|
67
|
+
country: this.sellingOrganization.address.country,
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
vatRegulation: 'shifted',
|
|
71
|
+
},
|
|
72
|
+
profile: {
|
|
73
|
+
name: this.sellingOrganization.name + ' via Stamhoofd',
|
|
74
|
+
description: $t(`%x1`),
|
|
75
|
+
categoryCode: 8398,
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error('Failed to submit onboarding data', e);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#verifiedCustomerIds: Set<string> = new Set()
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Gets the customer id for a given payingOrganization or user.
|
|
88
|
+
* If no customer exists, it will create a new mollie customer if customer (payment customer) is set as parameter
|
|
89
|
+
*/
|
|
90
|
+
async getCustomerId({ payingOrganization, user, customer }: {
|
|
91
|
+
payingOrganization: Organization | null,
|
|
92
|
+
user: User | null,
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Required to be able to create a new customer
|
|
96
|
+
*/
|
|
97
|
+
customer?: PaymentCustomer
|
|
98
|
+
}): Promise<string | null> {
|
|
99
|
+
if (!payingOrganization) {
|
|
100
|
+
// Not yet supported for users
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this.sellingOrganization.id !== (await Platform.getShared()).membershipOrganizationId) {
|
|
105
|
+
// Not yet supported
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if (payingOrganization.serverMeta.mollieCustomerId) {
|
|
111
|
+
const customerId = payingOrganization.serverMeta.mollieCustomerId
|
|
112
|
+
if (this.#verifiedCustomerIds.has(customerId)) {
|
|
113
|
+
return customerId;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// check still valid
|
|
117
|
+
try {
|
|
118
|
+
const c = await this.client.customers.get(customerId, {testmode: this.testMode});
|
|
119
|
+
|
|
120
|
+
if ((c.mode === ApiMode.test) === this.testMode) {
|
|
121
|
+
this.#verifiedCustomerIds.add(customerId);
|
|
122
|
+
return customerId;
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error('Error getting customer', e)
|
|
126
|
+
// Customer is not valid anymore, we need to create a new one
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!customer) {
|
|
131
|
+
// No existing customer exists
|
|
132
|
+
// won't create a new one if customer is not set
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const mollieCustomer = await this.client.customers.create({
|
|
137
|
+
name: payingOrganization.name,
|
|
138
|
+
email: customer.email ?? undefined,
|
|
139
|
+
metadata: {
|
|
140
|
+
organizationId: payingOrganization.id,
|
|
141
|
+
userId: user?.id,
|
|
142
|
+
},
|
|
143
|
+
testmode: this.testMode
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const customerId = mollieCustomer.id
|
|
147
|
+
this.#verifiedCustomerIds.add(customerId);
|
|
148
|
+
|
|
149
|
+
payingOrganization.serverMeta.mollieCustomerId = mollieCustomer.id
|
|
150
|
+
console.log('Saving new mollie customer', mollieCustomer, 'for organization', payingOrganization.id)
|
|
151
|
+
await payingOrganization.save()
|
|
152
|
+
|
|
153
|
+
return customerId
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#cachedMandates: Map<string, PaymentMandate[]> = new Map()
|
|
157
|
+
|
|
158
|
+
async getMandates({ payingOrganization, user }: {
|
|
159
|
+
payingOrganization: Organization | null,
|
|
160
|
+
user: User | null,
|
|
161
|
+
}): Promise<PaymentMandate[]> {
|
|
162
|
+
const customerId = await this.getCustomerId({payingOrganization, user});
|
|
163
|
+
if (!customerId) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const cached = this.#cachedMandates.get(customerId);
|
|
168
|
+
if (cached) {
|
|
169
|
+
//return cached;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Poll mollie status
|
|
173
|
+
// Mollie payment is required
|
|
174
|
+
const mandates: PaymentMandate[] = []
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const m = await this.client.customerMandates.page({
|
|
178
|
+
customerId,
|
|
179
|
+
limit: 250,
|
|
180
|
+
testmode: this.testMode
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
for (const mandate of m) {
|
|
184
|
+
const paymentMandate = MollieService.mollieManateToStamhoofd({mandate, payingOrganization, user});
|
|
185
|
+
if (paymentMandate) {
|
|
186
|
+
mandates.push(paymentMandate)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
console.error(e)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// todo: remove duplicate mandates?
|
|
194
|
+
return mandates;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async deleteMandate({ mandateId, payingOrganization, user }: {
|
|
198
|
+
mandateId: string,
|
|
199
|
+
payingOrganization: Organization | null,
|
|
200
|
+
user: User | null,
|
|
201
|
+
}) {
|
|
202
|
+
const customerId = await this.getCustomerId({payingOrganization, user});
|
|
203
|
+
if (!customerId) {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await this.client.customerMandates.revoke(
|
|
208
|
+
mandateId,
|
|
209
|
+
{
|
|
210
|
+
customerId,
|
|
211
|
+
testmode: this.testMode
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private static mollieManateToStamhoofd({ mandate, payingOrganization, user }: {
|
|
217
|
+
mandate: Mandate,
|
|
218
|
+
payingOrganization: Organization | null,
|
|
219
|
+
user: User | null,
|
|
220
|
+
}): PaymentMandate | null {
|
|
221
|
+
let type: PaymentMandateType;
|
|
222
|
+
switch (mandate.method) {
|
|
223
|
+
case MandateMethod.creditcard: {
|
|
224
|
+
type = PaymentMandateType.CreditCard
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case MandateMethod.directdebit: {
|
|
229
|
+
type = PaymentMandateType.DirectDebit
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
default: {
|
|
234
|
+
// Not supported
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const details = mandate.details;
|
|
239
|
+
return PaymentMandate.create({
|
|
240
|
+
id: mandate.id,
|
|
241
|
+
status: MollieService.mollieMandateStatusToStamhoofd(mandate),
|
|
242
|
+
|
|
243
|
+
// Todo: support for user default mandates
|
|
244
|
+
isDefault: mandate.id === payingOrganization?.serverMeta.mollieMandateId,
|
|
245
|
+
|
|
246
|
+
createdAt: new Date(mandate.createdAt),
|
|
247
|
+
|
|
248
|
+
provider: PaymentProvider.Mollie,
|
|
249
|
+
|
|
250
|
+
type,
|
|
251
|
+
|
|
252
|
+
details: PaymentMandateDetails.create({
|
|
253
|
+
name: ('consumerName' in details ? details.consumerName : details.cardHolder) ?? undefined,
|
|
254
|
+
cardNumber: 'cardNumber' in details ? details.cardNumber : null,
|
|
255
|
+
iban: 'consumerAccount' in details ? details.consumerAccount : null,
|
|
256
|
+
bic: ('consumerBic' in details ? details.consumerBic : undefined),
|
|
257
|
+
expiryDate: ('cardExpiryDate' in details ? DateTime.fromISO(details.cardExpiryDate, {zone: Formatter.timezone}).toJSDate() : null), // todo: parse date correctly in Brussels timezone!
|
|
258
|
+
brand: ('cardLabel' in details ? details.cardLabel : null),
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private static mollieMandateStatusToStamhoofd(mandate: Mandate): PaymentMandateStatus {
|
|
264
|
+
switch (mandate.status) {
|
|
265
|
+
case MandateStatus.valid: {
|
|
266
|
+
return PaymentMandateStatus.Valid
|
|
267
|
+
}
|
|
268
|
+
case MandateStatus.invalid: {
|
|
269
|
+
return PaymentMandateStatus.Invalid
|
|
270
|
+
}
|
|
271
|
+
case MandateStatus.pending: {
|
|
272
|
+
return PaymentMandateStatus.Pending
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private static paymentMethodToMollie(method: PaymentMethod) {
|
|
278
|
+
switch (method) {
|
|
279
|
+
case PaymentMethod.Bancontact:
|
|
280
|
+
return molliePaymentMethod.bancontact
|
|
281
|
+
case PaymentMethod.CreditCard:
|
|
282
|
+
return molliePaymentMethod.creditcard
|
|
283
|
+
case PaymentMethod.DirectDebit:
|
|
284
|
+
return molliePaymentMethod.directdebit
|
|
285
|
+
case PaymentMethod.iDEAL:
|
|
286
|
+
return molliePaymentMethod.ideal
|
|
287
|
+
case PaymentMethod.Transfer:
|
|
288
|
+
return molliePaymentMethod.banktransfer
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async getProfiles(): Promise<MollieProfile[]> {
|
|
295
|
+
try {
|
|
296
|
+
const response = await this.client.profiles.page({
|
|
297
|
+
limit: 250
|
|
298
|
+
})
|
|
299
|
+
return response.map(p => MollieProfile.create({
|
|
300
|
+
...p,
|
|
301
|
+
mode: p.mode === ApiMode.live ? MollieProfileMode.Live : MollieProfileMode.Test,
|
|
302
|
+
status: p.status === ProfileStatus.unverified ? MollieProfileStatus.Unverified : (p.status === ProfileStatus.blocked ? MollieProfileStatus.Blocked : MollieProfileStatus.Verified)
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
console.error('Failed to parse mollie profiles', e);
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async getOnboardingStatus() {
|
|
312
|
+
try {
|
|
313
|
+
const response = await this.client.onboarding.get();
|
|
314
|
+
return MollieOnboarding.create({
|
|
315
|
+
canReceivePayments: !!response.canReceivePayments,
|
|
316
|
+
canReceiveSettlements: !!response.canReceiveSettlements,
|
|
317
|
+
status: response.status === OnboardingStatus.needsData ? MollieStatus.NeedsData : (response.status === OnboardingStatus.inReview ? MollieStatus.InReview : (MollieStatus.Completed)),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
console.error('Error when requesting Mollie onboarding status:');
|
|
322
|
+
console.error(e);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async getProfileId(website?: string): Promise<string | undefined> {
|
|
328
|
+
if (this.sellingOrganization.privateMeta.mollieProfile?.id) {
|
|
329
|
+
return this.sellingOrganization.privateMeta.mollieProfile.id
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const profiles = await this.client.profiles.page({
|
|
334
|
+
limit: 250,
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// Search profile with Stamhoofd as name
|
|
338
|
+
if (website) {
|
|
339
|
+
for (const profile of profiles) {
|
|
340
|
+
if (profile.website.toLowerCase().includes(website)) {
|
|
341
|
+
return profile.id;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Search profile with Stamhoofd as name
|
|
347
|
+
for (const profile of profiles) {
|
|
348
|
+
if (profile.name.toLowerCase().includes('stamhoofd')) {
|
|
349
|
+
return profile.id;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return profiles[0]?.id ?? undefined;
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
console.error('Error when requesting Mollie profile id:');
|
|
357
|
+
console.error(e);
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
get locale() {
|
|
363
|
+
const preferredLocale = Context.i18n.locale.replace('-', '_');
|
|
364
|
+
return ['en_US', 'en_GB', 'nl_NL', 'nl_BE', 'fr_FR', 'fr_BE', 'de_DE', 'de_AT', 'de_CH', 'es_ES', 'ca_ES', 'pt_PT', 'it_IT', 'nb_NO', 'sv_SE', 'fi_FI', 'da_DK', 'is_IS', 'hu_HU', 'pl_PL', 'lv_LV', 'lt_LT'].includes(preferredLocale) ? (preferredLocale as any) : null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
get testMode() {
|
|
368
|
+
return this.sellingOrganization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
static async createPayment(
|
|
372
|
+
{ sellingOrganization, payment, mandate, redirectUrl, webhookUrl, cancelUrl, description, metadata, payingOrganization, user, customer}: {
|
|
373
|
+
payment: Payment;
|
|
374
|
+
mandate: PaymentMandate | null;
|
|
375
|
+
redirectUrl: string;
|
|
376
|
+
cancelUrl: string;
|
|
377
|
+
webhookUrl: string;
|
|
378
|
+
description: string;
|
|
379
|
+
metadata: { [key: string]: string | undefined };
|
|
380
|
+
sellingOrganization: Organization,
|
|
381
|
+
payingOrganization: Organization | null,
|
|
382
|
+
user: User | null,
|
|
383
|
+
customer: PaymentCustomer,
|
|
384
|
+
},
|
|
385
|
+
): Promise<{ paymentUrl: string | null }> {
|
|
386
|
+
const mollieService = await MollieService.create({sellingOrganization});
|
|
387
|
+
const profileId = await mollieService?.getProfileId();
|
|
388
|
+
const method = MollieService.paymentMethodToMollie(payment.method);
|
|
389
|
+
|
|
390
|
+
if (!mollieService || !profileId || !method) {
|
|
391
|
+
throw new SimpleError({
|
|
392
|
+
code: '',
|
|
393
|
+
message: $t(`%w3`, { method: PaymentMethodHelper.getName(payment.method) }),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const customerId = await mollieService.getCustomerId({
|
|
398
|
+
payingOrganization: payingOrganization ?? null,
|
|
399
|
+
user,
|
|
400
|
+
customer,
|
|
401
|
+
}) ?? undefined
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
const data: Parameters<typeof mollieService.client.payments.create>[0] = {
|
|
405
|
+
amount: {
|
|
406
|
+
currency: 'EUR',
|
|
407
|
+
value: (Math.round(payment.price / 100) / 100).toFixed(2),
|
|
408
|
+
},
|
|
409
|
+
method,
|
|
410
|
+
testmode: mollieService.testMode,
|
|
411
|
+
mandateId: mandate?.id,
|
|
412
|
+
sequenceType: mandate ? SequenceType.recurring : (payment.createMandate ? SequenceType.first : SequenceType.oneoff),
|
|
413
|
+
customerId,
|
|
414
|
+
profileId,
|
|
415
|
+
description,
|
|
416
|
+
redirectUrl,
|
|
417
|
+
cancelUrl,
|
|
418
|
+
webhookUrl,
|
|
419
|
+
metadata: {
|
|
420
|
+
paymentId: payment.id,
|
|
421
|
+
...metadata
|
|
422
|
+
},
|
|
423
|
+
locale: mollieService.locale,
|
|
424
|
+
};
|
|
425
|
+
console.log('Creating payment', data)
|
|
426
|
+
const molliePayment = await mollieService.client.payments.create(data);
|
|
427
|
+
console.log('Payment response', molliePayment)
|
|
428
|
+
|
|
429
|
+
const paymentUrl = molliePayment.getCheckoutUrl();
|
|
430
|
+
|
|
431
|
+
// Save payment
|
|
432
|
+
const dbPayment = new MolliePayment();
|
|
433
|
+
dbPayment.paymentId = payment.id;
|
|
434
|
+
dbPayment.mollieId = molliePayment.id;
|
|
435
|
+
await dbPayment.save();
|
|
436
|
+
|
|
437
|
+
const {status} = await mollieService.getStatusFor(molliePayment, payment, false)
|
|
438
|
+
payment.status = status;
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
paymentUrl: paymentUrl,
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
static async saveChargeInfo(mollieData: MolliePaymentType, payment: Payment) {
|
|
446
|
+
try {
|
|
447
|
+
const details = mollieData.details;
|
|
448
|
+
if (details) {
|
|
449
|
+
if ('consumerName' in details) {
|
|
450
|
+
payment.ibanName = details.consumerName;
|
|
451
|
+
}
|
|
452
|
+
if ('consumerAccount' in details) {
|
|
453
|
+
payment.iban = details.consumerAccount;
|
|
454
|
+
}
|
|
455
|
+
if ('cardHolder' in details) {
|
|
456
|
+
payment.ibanName = details.cardHolder;
|
|
457
|
+
}
|
|
458
|
+
if ('cardNumber' in details) {
|
|
459
|
+
payment.iban = '•••• ' + details.cardNumber;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
payment.mandateId = mollieData.mandateId ?? null
|
|
463
|
+
await payment.save();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch (e) {
|
|
467
|
+
console.error('Failed processing charge', e);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async getStatusFor(mollieData: MolliePaymentType, payment: Payment, cancel = false): Promise<{status: PaymentStatus}> {
|
|
472
|
+
await MollieService.saveChargeInfo(mollieData, payment)
|
|
473
|
+
|
|
474
|
+
if (mollieData.status === molliePaymentStatus.paid) {
|
|
475
|
+
return {
|
|
476
|
+
status: PaymentStatus.Succeeded,
|
|
477
|
+
}
|
|
478
|
+
} else if (mollieData.status === molliePaymentStatus.failed) {
|
|
479
|
+
return {
|
|
480
|
+
status: PaymentStatus.Failed,
|
|
481
|
+
}
|
|
482
|
+
} else if (mollieData.status === molliePaymentStatus.expired) {
|
|
483
|
+
return {
|
|
484
|
+
status: PaymentStatus.Failed,
|
|
485
|
+
}
|
|
486
|
+
} else if (mollieData.status === molliePaymentStatus.canceled) {
|
|
487
|
+
return {
|
|
488
|
+
status: PaymentStatus.Failed,
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Pending payments should be cancellable
|
|
493
|
+
if (cancel && mollieData.isCancelable) {
|
|
494
|
+
console.log('Cancelling Mollie Payment ' + payment.id);
|
|
495
|
+
|
|
496
|
+
// Try to cancel
|
|
497
|
+
try {
|
|
498
|
+
const newData = await this.client.payments.cancel(mollieData.id, {
|
|
499
|
+
testmode: this.testMode
|
|
500
|
+
});
|
|
501
|
+
console.log('Cancelled Mollie Payment ' + payment.id);
|
|
502
|
+
return await this.getStatusFor(newData, payment, false)
|
|
503
|
+
} catch (e) {
|
|
504
|
+
console.error('Failed to cancel Mollie Payment ' + payment.id, {cause: e})
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
} else if (cancel) {
|
|
508
|
+
console.log('Cannot cancel Mollie Payment ' + payment.id);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (mollieData.status === molliePaymentStatus.open ) {
|
|
512
|
+
// Nothink happend yet
|
|
513
|
+
return {
|
|
514
|
+
status: PaymentStatus.Created,
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
status: PaymentStatus.Pending,
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async getStatus(payment: Payment, cancel = false) {
|
|
524
|
+
const molliePayment = await MolliePayment.select().where('paymentId', payment.id).first(false);
|
|
525
|
+
if (!molliePayment) {
|
|
526
|
+
throw new Error('Mollie Payment not found for payment ' + payment.id)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const mollieData = await this.client.payments.get(molliePayment.mollieId, {
|
|
530
|
+
testmode: this.testMode
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
console.log(mollieData)
|
|
534
|
+
|
|
535
|
+
return this.getStatusFor(mollieData, payment, cancel)
|
|
536
|
+
}
|
|
537
|
+
}
|