@stamhoofd/backend 2.17.4 → 2.18.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.
@@ -1,424 +0,0 @@
1
- import { createMollieClient, PaymentMethod as molliePaymentMethod, SequenceType } from '@mollie/api-client';
2
- import { BankTransferDetails } from '@mollie/api-client/dist/types/src/data/payments/data';
3
- import { ArrayDecoder, AutoEncoder, AutoEncoderPatchType, BooleanDecoder, Decoder, EnumDecoder, field, StringDecoder } from "@simonbackx/simple-encoding";
4
- import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
5
- import { isSimpleError, isSimpleErrors, SimpleError } from "@simonbackx/simple-errors";
6
- import { MolliePayment, Payment, Registration, STCredit, STInvoice, STPackage, STPendingInvoice, Token } from "@stamhoofd/models";
7
- import { QueueHandler } from '@stamhoofd/queues';
8
- import { Organization as OrganizationStruct, OrganizationPatch, PaymentMethod, PaymentProvider, PaymentStatus, STInvoiceItem, STInvoiceResponse, STPackage as STPackageStruct, STPackageBundle, STPackageBundleHelper, STPricingType, TransferSettings, User as UserStruct, Version } from "@stamhoofd/structures";
9
-
10
- import { Context } from '../../../../helpers/Context';
11
-
12
- type Params = Record<string, never>;
13
- type Query = undefined;
14
- type ResponseBody = STInvoiceResponse;
15
-
16
- class Body extends AutoEncoder {
17
- @field({ decoder: new ArrayDecoder(new EnumDecoder(STPackageBundle)), optional: true })
18
- bundles: STPackageBundle[] = []
19
-
20
- @field({ decoder: new ArrayDecoder(StringDecoder), optional: true })
21
- renewPackageIds: string[] = []
22
-
23
- @field({ decoder: BooleanDecoder, optional: true })
24
- includePending = false
25
-
26
- @field({ decoder: new EnumDecoder(PaymentMethod) })
27
- paymentMethod: PaymentMethod
28
-
29
- @field({ decoder: BooleanDecoder, optional: true })
30
- proForma = false
31
-
32
- @field({ decoder: OrganizationPatch, optional: true })
33
- organizationPatch?: AutoEncoderPatchType<OrganizationStruct>
34
-
35
- @field({ decoder: UserStruct.patchType(), optional: true })
36
- userPatch?: AutoEncoderPatchType<UserStruct>
37
- }
38
-
39
- export class ActivatePackagesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
40
- bodyDecoder = Body as Decoder<Body>
41
-
42
- protected doesMatch(request: Request): [true, Params] | [false] {
43
- if (request.method != "POST") {
44
- return [false];
45
- }
46
-
47
- const params = Endpoint.parseParameters(request.url, "/billing/activate-packages", {});
48
-
49
- if (params) {
50
- return [true, params as Params];
51
- }
52
- return [false];
53
- }
54
-
55
- async handle(request: DecodedRequest<Params, Query, Body>) {
56
- const organization = await Context.setOrganizationScope();
57
- const {user} = await Context.authenticate()
58
-
59
- // If the user has permission, we'll also search if he has access to the organization's key
60
- if (!await Context.auth.canActivatePackages(organization.id)) {
61
- throw Context.auth.error()
62
- }
63
-
64
- // Apply patches if needed
65
- if (request.body.userPatch) {
66
- user.firstName = request.body.userPatch.firstName ?? user.firstName
67
- user.lastName = request.body.userPatch.lastName ?? user.lastName
68
-
69
- if (!request.body.proForma) {
70
- await user.save()
71
- }
72
- }
73
-
74
- // Apply patches if needed
75
- if (user.firstName && user.lastName) {
76
- organization.privateMeta.billingContact = user.firstName + " " + user.lastName
77
- }
78
-
79
- if (request.body.organizationPatch) {
80
- console.log("Received patch in activatePackagesEndpoint", request.body.organizationPatch)
81
- if (request.body.organizationPatch.address) {
82
- organization.address.patchOrPut(request.body.organizationPatch.address)
83
- }
84
-
85
- if (request.body.organizationPatch.meta) {
86
- organization.meta.patchOrPut(request.body.organizationPatch.meta)
87
- }
88
- }
89
-
90
- if (!request.body.proForma) {
91
- await organization.save();
92
- }
93
-
94
- return await QueueHandler.schedule("billing/invoices-"+organization.id, async () => {
95
- const currentPackages = await STPackage.getForOrganization(organization.id)
96
-
97
- // Create packages
98
- const packages: STPackageStruct[] = [];
99
- for (const bundle of request.body.bundles) {
100
- // Renew after currently running packages
101
- let date = new Date()
102
-
103
- let skip = false
104
- // Do we have a collision?
105
- for (const currentPack of currentPackages) {
106
- if (!STPackageBundleHelper.isCombineable(bundle, STPackageStruct.create(currentPack))) {
107
- if (!STPackageBundleHelper.isStackable(bundle, STPackageStruct.create(currentPack))) {
108
- // WE skip silently
109
- console.error("Tried to activate non combineable, non stackable packages...")
110
- skip = true
111
- continue
112
- /*throw new SimpleError({
113
- code: "not_combineable",
114
- message: "Het pakket dat je wilt activeren is al actief of is niet combineerbaar. Herlaad de pagina, mogelijk zie je een verouderde weergave van jouw geactiveerde pakketten."
115
- })*/
116
- }
117
- if (currentPack.validUntil !== null) {
118
- const end = currentPack.validUntil
119
- if (end > date) {
120
- date = end
121
- }
122
- }
123
-
124
- }
125
- }
126
-
127
- if (skip) {
128
- continue
129
- }
130
- packages.push(STPackageBundleHelper.getCurrentPackage(bundle, date))
131
- }
132
-
133
- const invoice = STInvoice.createFor(organization)
134
- const date = new Date()
135
-
136
- invoice.meta.ipAddress = request.request.getIP()
137
- invoice.meta.userAgent = request.headers["user-agent"] ?? null
138
-
139
- let membersCount: number | null = null
140
-
141
- // Save packages as models
142
- const models: STPackage[] = []
143
- for (const pack of packages) {
144
- const model = new STPackage()
145
- model.id = pack.id
146
- model.meta = pack.meta
147
- model.validUntil = pack.validUntil
148
- model.removeAt = pack.removeAt
149
-
150
- // Not yet valid / active (ignored until valid)
151
- model.validAt = null
152
- model.organizationId = organization.id
153
-
154
- // If type is
155
- let amount = 1
156
-
157
- if (membersCount === null && pack.meta.pricingType === STPricingType.PerMember) {
158
- membersCount = await Registration.getActiveMembers(organization.id)
159
- }
160
-
161
- if (pack.meta.pricingType === STPricingType.PerMember) {
162
- amount = membersCount ?? 1
163
- }
164
-
165
- // Add items to invoice
166
- invoice.meta.items.push(
167
- STInvoiceItem.fromPackage(pack, amount, 0, date)
168
- )
169
-
170
- if (!request.body.proForma) {
171
- await model.save()
172
- }
173
- models.push(model)
174
- }
175
-
176
- // Add renewals
177
- if (request.body.renewPackageIds.length > 0) {
178
- const currentPackages = await STPackage.getForOrganization(organization.id)
179
-
180
- for (const id of request.body.renewPackageIds) {
181
- const pack = currentPackages.find(c => c.id === id)
182
- if (!pack) {
183
- throw new SimpleError({
184
- code: "not_found",
185
- message: "Package not found",
186
- human: "Het pakket dat je wil verlengen kan je helaas niet meer verlengen"
187
- })
188
- }
189
-
190
- // Renew
191
- const model = pack.createRenewed()
192
-
193
- // If type is
194
- let amount = 1
195
-
196
- if (membersCount === null && pack.meta.pricingType === STPricingType.PerMember) {
197
- membersCount = await Registration.getActiveMembers(organization.id)
198
- }
199
-
200
- if (pack.meta.pricingType === STPricingType.PerMember) {
201
- amount = membersCount ?? 1
202
- }
203
-
204
- // Add items to invoice
205
- invoice.meta.items.push(
206
- STInvoiceItem.fromPackage(STPackageStruct.create(model), amount, 0, date)
207
- )
208
-
209
- if (!request.body.proForma) {
210
- await model.save()
211
- }
212
- models.push(model)
213
- }
214
- }
215
-
216
- // Calculate price
217
- if (invoice.meta.priceWithVAT > 0 || request.body.includePending) {
218
-
219
- // Since we are about the pay something:
220
- // also add the items that are in the pending queue
221
- const pendingInvoice = await STPendingInvoice.addAutomaticItems(organization)
222
- if (pendingInvoice && pendingInvoice.invoiceId === null && pendingInvoice.meta.items.length) {
223
- if (!request.body.proForma) {
224
- // Already generate an ID for the invoice
225
- await invoice.save()
226
-
227
- // Block usage of this pending invoice until this payment is finished (failed or succeeded)
228
- pendingInvoice.invoiceId = invoice.id
229
- await pendingInvoice.save()
230
- }
231
-
232
- // Add the items to our invoice
233
- invoice.meta.items.push(...pendingInvoice.meta.items)
234
- }
235
- }
236
-
237
- // Apply credits
238
- await STCredit.applyCredits(organization.id, invoice, request.body.proForma)
239
-
240
- const price = invoice.meta.priceWithVAT
241
-
242
- if (price < 0) {
243
- throw new Error("Unexpected negative price")
244
- }
245
-
246
- if (!request.body.proForma) {
247
- // Create payment
248
- const payment = new Payment()
249
- payment.organizationId = null
250
- payment.method = request.body.paymentMethod
251
- payment.status = PaymentStatus.Created
252
- payment.price = price
253
- payment.paidAt = null
254
- payment.provider = PaymentProvider.Mollie
255
-
256
- // Do some quick validatiosn before creating the payment (otherwise we'll have to mark it as failed)
257
- let _molliePaymentMethod: molliePaymentMethod | undefined = molliePaymentMethod.bancontact
258
- let sequenceType: SequenceType | undefined = SequenceType.first
259
-
260
- if (payment.method == PaymentMethod.iDEAL) {
261
- _molliePaymentMethod = molliePaymentMethod.ideal
262
- } else if (payment.method == PaymentMethod.CreditCard) {
263
- _molliePaymentMethod = molliePaymentMethod.creditcard
264
- } else if (payment.method == PaymentMethod.Transfer) {
265
- _molliePaymentMethod = molliePaymentMethod.banktransfer
266
- sequenceType = SequenceType.oneoff
267
- } else if (payment.method == PaymentMethod.DirectDebit) {
268
- const pendingInvoice = await STPendingInvoice.getForOrganization(organization.id)
269
-
270
- if (pendingInvoice && pendingInvoice.invoiceId !== null && pendingInvoice.invoiceId !== invoice.id) {
271
- throw new SimpleError({
272
- code: "payment_pending",
273
- message: "Payment pending",
274
- human: "Er is momenteel al een afrekening in behandeling (dit kan 3 werkdagen duren). Probeer een andere betaalmethode."
275
- })
276
- }
277
-
278
- // Use saved payment method
279
- _molliePaymentMethod = undefined
280
- sequenceType = SequenceType.recurring
281
-
282
- if (!organization.serverMeta.mollieCustomerId) {
283
- throw new SimpleError({
284
- code: "no_mollie_customer",
285
- message: "Er is geen opgeslagen betaalmethode gevonden. Probeer te betalen via een andere betaalmethode."
286
- })
287
- }
288
- }
289
-
290
- await payment.save()
291
-
292
- invoice.paymentId = payment.id
293
- invoice.setRelation(STInvoice.payment, payment)
294
-
295
- await invoice.save()
296
-
297
- const description = "Stamhoofd - "+invoice.id
298
-
299
- if (price == 0) {
300
- await invoice.markPaid()
301
- return new Response(STInvoiceResponse.create({
302
- paymentUrl: undefined,
303
- invoice: await invoice.getStructure()
304
- }));
305
- }
306
-
307
- try {
308
- // Mollie payment is required
309
- const apiKey = STAMHOOFD.MOLLIE_API_KEY
310
- if (!apiKey) {
311
- throw new SimpleError({
312
- code: "",
313
- message: "Betalingen zijn tijdelijk onbeschikbaar"
314
- })
315
- }
316
- const mollieClient = createMollieClient({ apiKey });
317
- let customerId = organization.serverMeta.mollieCustomerId
318
-
319
- if (!organization.serverMeta.mollieCustomerId) {
320
- if (payment.method === PaymentMethod.DirectDebit) {
321
- throw new SimpleError({
322
- code: "no_mollie_customer",
323
- message: "Er is geen opgeslagen betaalmethode gevonden. Probeer te betalen via een andere betaalmethode."
324
- })
325
- }
326
- const mollieCustomer = await mollieClient.customers.create({
327
- name: organization.name,
328
- email: user.email,
329
- metadata: {
330
- organizationId: organization.id,
331
- userId: user.id,
332
- }
333
- })
334
-
335
- customerId = mollieCustomer.id
336
- organization.serverMeta.mollieCustomerId = mollieCustomer.id
337
- console.log("Saving new mollie customer", mollieCustomer, "for organization", organization.id)
338
- await organization.save()
339
- }
340
-
341
- const molliePayment = await mollieClient.payments.create({
342
- amount: {
343
- currency: 'EUR',
344
- value: (price / 100).toFixed(2)
345
- },
346
- method: _molliePaymentMethod,
347
- description,
348
- customerId,
349
- sequenceType,
350
- redirectUrl: "https://"+STAMHOOFD.domains.dashboard+'/settings/billing/payment?id='+encodeURIComponent(payment.id),
351
- webhookUrl: 'https://'+STAMHOOFD.domains.api+"/v"+Version+"/billing/payments/"+encodeURIComponent(payment.id)+"?exchange=true",
352
- metadata: {
353
- invoiceId: invoice.id,
354
- paymentId: payment.id,
355
- },
356
- billingEmail: user.email,
357
- });
358
-
359
- if (molliePayment.method === 'creditcard') {
360
- console.log("Corrected payment method to creditcard")
361
- payment.method = PaymentMethod.CreditCard
362
- await payment.save();
363
- }
364
-
365
- console.log(molliePayment)
366
- const paymentUrl = molliePayment.getCheckoutUrl() ?? undefined
367
-
368
- // Save payment
369
- const dbPayment = new MolliePayment()
370
- dbPayment.paymentId = payment.id
371
- dbPayment.mollieId = molliePayment.id
372
- await dbPayment.save();
373
-
374
- if ((molliePayment.details as BankTransferDetails)?.transferReference) {
375
- const details = molliePayment.details as BankTransferDetails
376
- payment.transferSettings = TransferSettings.create({
377
- iban: details.bankAccount,
378
- creditor: 'Stamhoofd',
379
- })
380
- payment.transferDescription = details.transferReference
381
- await payment.save()
382
- }
383
-
384
- if (sequenceType === SequenceType.recurring) {
385
- // Activate package
386
- await invoice.activatePackages(false)
387
- await STPackage.updateOrganizationPackages(organization.id)
388
-
389
- const pendingInvoice = await STPendingInvoice.getForOrganization(organization.id)
390
- if (pendingInvoice) {
391
- pendingInvoice.invoiceId = invoice.id
392
- await pendingInvoice.save()
393
- }
394
- }
395
-
396
- return new Response(STInvoiceResponse.create({
397
- paymentUrl: paymentUrl,
398
- invoice: await invoice.getStructure()
399
- }));
400
- } catch (e) {
401
- console.error(e)
402
- payment.status = PaymentStatus.Failed
403
- await payment.save()
404
- await invoice.markFailed(payment, false)
405
-
406
- if (isSimpleError(e) || isSimpleErrors(e)) {
407
- throw e
408
- }
409
- throw new SimpleError({
410
- code: "payment_failed",
411
- message: "Er ging iets mis bij het aanmaken van de betaling. Probeer later opnieuw of contacteer ons als het probleem zich blijft voordoen ("+request.$t("shared.emails.general")+")"
412
- })
413
- }
414
- }
415
-
416
- // We don't save the invoice, just return it
417
- return new Response(STInvoiceResponse.create({
418
- paymentUrl: undefined,
419
- invoice: await invoice.getStructure()
420
- }));
421
- });
422
- }
423
- }
424
-
@@ -1,67 +0,0 @@
1
- import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
- import { SimpleError } from "@simonbackx/simple-errors";
3
- import { STPackage, Token } from "@stamhoofd/models";
4
- import { QueueHandler } from '@stamhoofd/queues';
5
-
6
- import { Context } from "../../../../helpers/Context";
7
- type Params = {id: string};
8
- type Query = undefined;
9
- type ResponseBody = undefined;
10
- type Body = undefined
11
-
12
- export class DeactivatePackageEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
13
- protected doesMatch(request: Request): [true, Params] | [false] {
14
- if (request.method != "POST") {
15
- return [false];
16
- }
17
-
18
- const params = Endpoint.parseParameters(request.url, "/billing/deactivate-package/@id", {id: String});
19
-
20
- if (params) {
21
- return [true, params as Params];
22
- }
23
- return [false];
24
- }
25
-
26
- async handle(request: DecodedRequest<Params, Query, Body>) {
27
- const organization = await Context.setOrganizationScope();
28
- await Context.authenticate()
29
-
30
- // If the user has permission, we'll also search if he has access to the organization's key
31
- if (!await Context.auth.canDeactivatePackages(organization.id)) {
32
- throw Context.auth.error()
33
- }
34
-
35
- await QueueHandler.schedule("billing/invoices-"+organization.id, async () => {
36
- const packages = await STPackage.getForOrganization(organization.id)
37
-
38
- const pack = packages.find(p => p.id === request.params.id)
39
- if (!pack) {
40
- throw new SimpleError({
41
- code: "not_found",
42
- message: "Package not found",
43
- human: "De functie die je wilt deactiveren is al gedeactiveerd of bestaat niet",
44
- statusCode: 404
45
- })
46
- }
47
-
48
- if (!pack.meta.canDeactivate) {
49
- throw new SimpleError({
50
- code: "not_allowed",
51
- message: "Can't deactivate this package",
52
- human: "Je kan deze functie niet zelf deactiveren. Neem contact met ons op.",
53
- })
54
- }
55
-
56
- // Deactivate
57
- pack.removeAt = new Date()
58
- pack.removeAt.setTime(pack.removeAt.getTime() - 1000)
59
- await pack.save()
60
-
61
- await STPackage.updateOrganizationPackages(organization.id)
62
- });
63
-
64
- return new Response(undefined)
65
- }
66
- }
67
-