@stamhoofd/backend 2.120.6 → 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.
Files changed (61) hide show
  1. package/package.json +12 -12
  2. package/src/audit-logs/RegistrationInvitationLogger.ts +46 -0
  3. package/src/audit-logs/init.ts +2 -0
  4. package/src/crons/index.ts +2 -0
  5. package/src/crons/invoices.ts +166 -0
  6. package/src/crons/mollie-chargebacks.ts +87 -0
  7. package/src/crons.ts +47 -10
  8. package/src/email-recipient-loaders/payments.ts +84 -41
  9. package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +51 -0
  10. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +22 -3
  11. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +4 -0
  12. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
  13. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +495 -0
  14. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +216 -0
  15. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +405 -0
  16. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +168 -0
  17. package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +15 -0
  18. package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +3 -4
  19. package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
  20. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
  21. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +42 -19
  22. package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
  23. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +11 -3
  24. package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +308 -0
  25. package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
  26. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +7 -0
  27. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +5 -4
  28. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +7 -2
  29. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +17 -8
  30. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -3
  31. package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +127 -0
  32. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +13 -4
  33. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +7 -1
  34. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
  35. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -11
  36. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +14 -19
  37. package/src/helpers/AdminPermissionChecker.ts +11 -3
  38. package/src/helpers/AuthenticatedStructures.ts +94 -6
  39. package/src/helpers/FinancialSupportHelper.ts +21 -0
  40. package/src/helpers/RecordAnswerHelper.test.ts +746 -0
  41. package/src/helpers/RecordAnswerHelper.ts +116 -0
  42. package/src/helpers/StripeHelper.ts +2 -3
  43. package/src/helpers/ViesHelper.ts +7 -3
  44. package/src/seeds/1750090030-records-configuration.ts +68 -3
  45. package/src/seeds/1752848561-groups-registration-periods.ts +26 -2
  46. package/src/seeds/1779121239-default-invoice-email-template.sql +3 -0
  47. package/src/services/BalanceItemService.ts +12 -16
  48. package/src/services/InvoiceService.ts +372 -72
  49. package/src/services/MollieService.ts +537 -0
  50. package/src/services/PaymentMandateService.ts +214 -0
  51. package/src/services/PaymentService.ts +578 -222
  52. package/src/services/PlatformMembershipService.ts +1 -1
  53. package/src/services/RegistrationService.ts +66 -5
  54. package/src/services/STPackageService.ts +0 -7
  55. package/src/services/data/invoice.hbs.html +686 -0
  56. package/src/sql-filters/groups.ts +11 -1
  57. package/src/sql-filters/payments.ts +5 -0
  58. package/src/sql-filters/registration-invitations.ts +90 -0
  59. package/src/sql-sorters/registration-invitations.ts +36 -0
  60. package/vitest.config.js +1 -0
  61. package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +0 -216
@@ -1,79 +1,22 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
- import { BalanceItem, BalanceItemPayment, Invoice, InvoicedBalanceItem, Organization, Payment } from '@stamhoofd/models';
3
- import { InvoicedBalanceItem as InvoicedBalanceItemStruct, Invoice as InvoiceStruct } from '@stamhoofd/structures';
2
+ import { Image, Organization, Platform} from '@stamhoofd/models';
3
+ import { BalanceItem, Invoice, InvoicedBalanceItem, Payment } from '@stamhoofd/models';
4
+ import { render } from '@stamhoofd/models/helpers/Handlebars.js';
5
+ import { InvoiceCounter } from '@stamhoofd/models/helpers/InvoiceCounter.js';
6
+ import type { Address, Invoice as InvoiceStruct } from '@stamhoofd/structures';
7
+ import { CountryHelper, File, getVATExcemptInvoiceNote, getVATExcemptReasonName, PaymentMethod, PaymentMethodHelper, PaymentStatus } from '@stamhoofd/structures';
8
+ import type { OrganizationInvoiceSettings } from '@stamhoofd/structures/OrganizationInvoiceSettings.js';
4
9
  import { Formatter } from '@stamhoofd/utility';
10
+ import fs from 'fs/promises';
5
11
  import { ViesHelper } from '../helpers/ViesHelper.js';
12
+ import { BalanceItemService } from './BalanceItemService.js';
13
+ import { VERSION } from 'luxon';
14
+ import {v4 as uuidv4} from 'uuid'
15
+ import { PutObjectCommand } from '@aws-sdk/client-s3';
16
+ import { signInternal } from '@stamhoofd/backend-env';
6
17
 
7
18
  export class InvoiceService {
8
- static async invoicePayment(payment: Payment) {
9
- if (!payment.customer) {
10
- throw new SimpleError({
11
- code: 'missing_customer',
12
- message: 'Missing customer',
13
- field: 'customer',
14
- human: $t('%1Iw'),
15
- });
16
- }
17
-
18
- if (payment.price % 100 !== 0) {
19
- throw new SimpleError({
20
- code: 'invalid_price_decimals',
21
- message: 'Cannot invoice a payment with a price having more than two decimals',
22
- });
23
- }
24
-
25
- const organization = payment.organizationId ? await Organization.getByID(payment.organizationId) : null;
26
- if (!organization) {
27
- throw new SimpleError({
28
- code: 'missing_organization',
29
- message: 'Cannot invoice a payment without corresponding organization',
30
- statusCode: 500,
31
- });
32
- }
33
-
34
- // Find default company
35
- const seller = organization.meta.companies[0];
36
-
37
- if (!seller) {
38
- throw new SimpleError({
39
- code: 'missing_company',
40
- message: 'Missing invoice settings (companies)',
41
- human: $t('%1Ix', {
42
- 'organization-name': organization.name,
43
- }),
44
- });
45
- }
46
-
47
- const items: InvoicedBalanceItemStruct[] = [];
48
- const balanceItemPayments = await BalanceItemPayment.select().where('paymentId', payment.id).fetch();
49
- const balanceItems = await BalanceItem.getByIDs(...Formatter.uniqueArray(balanceItemPayments.map(d => d.balanceItemId)));
50
-
51
- for (const balanceItemPayment of balanceItemPayments) {
52
- const balanceItem = balanceItems.find(b => b.id === balanceItemPayment.balanceItemId);
53
- if (!balanceItem) {
54
- throw new SimpleError({
55
- code: 'missing_balance_item',
56
- message: 'Balance item missing for balanceItemPayment ' + balanceItemPayment.id,
57
- statusCode: 500,
58
- });
59
- }
60
-
61
- const item = InvoicedBalanceItemStruct.createFor(balanceItem.getStructure(), balanceItemPayment.price);
62
- items.push(item);
63
- }
64
-
65
- const struct = InvoiceStruct.create({
66
- organizationId: organization.id,
67
- seller,
68
- customer: payment.customer,
69
- payingOrganizationId: payment.payingOrganizationId,
70
- items,
71
- });
72
-
73
- return await this.createFrom(organization, struct, { payments: [payment], balanceItems });
74
- }
75
-
76
- static async createFrom(organization: { id: string }, struct: InvoiceStruct, options?: { payments?: Payment[]; balanceItems?: BalanceItem[] }) {
19
+ static async createFrom(organization: { id: string, privateMeta: {invoiceSettings: OrganizationInvoiceSettings} }, struct: InvoiceStruct, options?: { payments?: Payment[]; balanceItems?: BalanceItem[] }) {
77
20
  if (struct.number) {
78
21
  throw new SimpleError({
79
22
  code: 'invalid_field',
@@ -103,6 +46,7 @@ export class InvoiceService {
103
46
  throw new SimpleError({
104
47
  code: 'invalid_invoiced_amount',
105
48
  message: 'Unexpected 0 totalBalanceInvoicedAmount',
49
+ statusCode: 400
106
50
  });
107
51
  }
108
52
 
@@ -114,6 +58,14 @@ export class InvoiceService {
114
58
  });
115
59
  }
116
60
 
61
+ if (struct.totalWithVAT === 0) {
62
+ throw new SimpleError({
63
+ code: 'invalid_invoiced_amount',
64
+ message: 'Cannot invoice zero',
65
+ statusCode: 400,
66
+ });
67
+ }
68
+
117
69
  model.seller = struct.seller;
118
70
  model.organizationId = organization.id;
119
71
  model.payingOrganizationId = struct.payingOrganizationId;
@@ -168,7 +120,17 @@ export class InvoiceService {
168
120
  }
169
121
  }
170
122
 
171
- const balanceItems = options?.balanceItems ?? await BalanceItem.getByIDs(...Formatter.uniqueArray(struct.items.map(i => i.balanceItemId)));
123
+ const balanceItemIds = Formatter.uniqueArray(struct.items.map(i => i.balanceItemId));
124
+
125
+ // Make sure priceInvoiced is up to date for these balances
126
+ const affected = await BalanceItem.updateInvoiced(balanceItemIds);
127
+
128
+ if (affected && options?.balanceItems) {
129
+ // Force update
130
+ options!.balanceItems = undefined;
131
+ }
132
+
133
+ const balanceItems = options?.balanceItems ?? await BalanceItem.getByIDs(...balanceItemIds);
172
134
  await model.save();
173
135
 
174
136
  try {
@@ -184,6 +146,94 @@ export class InvoiceService {
184
146
  }
185
147
 
186
148
  // Todo: check we are not invoicing more than maximum invoiceable for these items
149
+ const maximumInvoiceable = balanceItem.priceTotal; // € - 10
150
+ const alreadyInvoiced = balanceItem.priceInvoiced; // € 5
151
+ const left = maximumInvoiceable - alreadyInvoiced; // € -15
152
+ const goingToInvoice = item.balanceInvoicedAmount;
153
+
154
+ if (item.quantity === 0) {
155
+ // should not be saved!
156
+ throw new SimpleError({
157
+ statusCode: 400,
158
+ code: 'zero_quantity',
159
+ message: 'Cannot invoice a quantity of zero',
160
+ human: $t('%1RZ')
161
+ });
162
+ }
163
+
164
+ if (left < 0) {
165
+ if (goingToInvoice > 0) {
166
+ // Item should be credited, yet we are trying to invoice it
167
+ throw new SimpleError({
168
+ code: 'error',
169
+ message: 'Cannot invoice',
170
+ human: $t('%1RB', {
171
+ 'a-euro': Formatter.price(goingToInvoice),
172
+ 'name': balanceItem.getStructure().itemTitle,
173
+ 'left-euro': Formatter.price(-left),
174
+ })
175
+ })
176
+ }
177
+
178
+ if (goingToInvoice < left) {
179
+ // too much
180
+ throw new SimpleError({
181
+ code: 'error',
182
+ message: 'Cannot invoice',
183
+ human: $t('%1Tm', {
184
+ 'a-euro': Formatter.price(-goingToInvoice),
185
+ 'name': balanceItem.getStructure().itemTitle,
186
+ 'left-euro': Formatter.price(-left),
187
+ })
188
+ })
189
+ }
190
+ }
191
+
192
+ if (left === 0) {
193
+ if (goingToInvoice < 0) {
194
+ throw new SimpleError({
195
+ code: 'error',
196
+ message: 'Cannot invoice',
197
+ human: $t('%1R7', {
198
+ 'a-euro': Formatter.price(-goingToInvoice),
199
+ 'name': balanceItem.getStructure().itemTitle,
200
+ })
201
+ })
202
+ } else if (goingToInvoice > 0) {
203
+ throw new SimpleError({
204
+ code: 'error',
205
+ message: 'Cannot invoice',
206
+ human: $t('%1QE', {
207
+ 'a-euro': Formatter.price(-goingToInvoice),
208
+ 'name': balanceItem.getStructure().itemTitle,
209
+ })
210
+ })
211
+ }
212
+ }
213
+
214
+ if (left > 0) {
215
+ if (goingToInvoice < 0) {
216
+ throw new SimpleError({
217
+ code: 'error',
218
+ message: 'Cannot invoice',
219
+ human: $t('%1RA', {
220
+ 'a-euro': Formatter.price(-goingToInvoice),
221
+ 'name': balanceItem.getStructure().itemTitle,
222
+ 'left-euro': Formatter.price(left),
223
+ })
224
+ })
225
+ } else if (goingToInvoice > left) {
226
+ throw new SimpleError({
227
+ code: 'error',
228
+ message: 'Cannot invoice',
229
+ human: $t('%1TS', {
230
+ 'a-euro': Formatter.price(-goingToInvoice),
231
+ 'name': balanceItem.getStructure().itemTitle,
232
+ 'left-euro': Formatter.price(left),
233
+ })
234
+ })
235
+ }
236
+ }
187
237
 
188
238
  const invoiced = new InvoicedBalanceItem();
189
239
  invoiced.invoiceId = model.id;
@@ -212,6 +262,15 @@ export class InvoiceService {
212
262
  payment.invoiceId = model.id;
213
263
  await payment.save();
214
264
  }
265
+
266
+ // Finalize invoice by generating a number
267
+ await InvoiceCounter.assignNextNumber(model, organization.privateMeta.invoiceSettings);
268
+
269
+ // Update invoiced cache
270
+ await BalanceItemService.updateInvoiced(struct.items.map(i => i.balanceItemId))
271
+
272
+ // Create PDF
273
+ await this.generatePdf(model)
215
274
  }
216
275
  catch (e) {
217
276
  try {
@@ -225,4 +284,245 @@ export class InvoiceService {
225
284
 
226
285
  return model;
227
286
  }
287
+
288
+ static async generateHtml(invoice: Invoice) {
289
+ const organization = await Organization.getByID(invoice.organizationId, true)
290
+ const platform = await Platform.getShared()
291
+ const payments = await Payment.select().where('invoiceId', invoice.id).fetch();
292
+ const payment = payments[0] ?? null;
293
+
294
+ const invoicedItems = await InvoicedBalanceItem.select().where('invoiceId', invoice.id).fetch();
295
+
296
+ const seller = invoice.seller;
297
+ const customer = invoice.customer;
298
+ const company = customer.company;
299
+
300
+ const formatAddress = (address: Address | null, includeCountry = false): string => {
301
+ if (!address) {
302
+ return '';
303
+ }
304
+ const arr = [
305
+ `${address.street} ${address.number}, ${address.city}`,
306
+ ];
307
+ if (includeCountry) {
308
+ arr.push(CountryHelper.getName(address.country))
309
+ }
310
+ return arr.join('\n');
311
+ };
312
+
313
+ const totalPrice = invoice.totalWithVAT;
314
+ const isCreditNote = totalPrice < 0;
315
+
316
+ const date = invoice.invoicedAt ?? invoice.createdAt;
317
+ const isPaid = payment?.status === PaymentStatus.Succeeded;
318
+ const dueDate = isPaid
319
+ ? date
320
+ : (invoice.dueAt ?? new Date(date.getTime() + 30 * 24 * 60 * 60 * 1000));
321
+
322
+ const VATTotal = invoice.VATTotal.map(subtotal => ({
323
+ VATPercentage: subtotal.VATPercentage,
324
+ percentageLabel: Formatter.percentage(subtotal.VATPercentage * 100),
325
+ taxablePrice: subtotal.taxablePrice,
326
+ VAT: subtotal.VAT,
327
+ VATExcempt: subtotal.VATExcempt,
328
+ VATExcemptName: subtotal.VATExcempt ? getVATExcemptReasonName(subtotal.VATExcempt) : null,
329
+ }));
330
+
331
+ const hasMultipleVATRates = VATTotal.length > 1;
332
+ const firstVATSubtotal = invoice.VATTotal[0] ?? null;
333
+ const singleVATRateLabel = !hasMultipleVATRates && firstVATSubtotal
334
+ ? Formatter.percentage(firstVATSubtotal.VATPercentage * 100)
335
+ : '';
336
+
337
+ let vatExcemptNote: string | null = null;
338
+ if (!hasMultipleVATRates && firstVATSubtotal) {
339
+ if (firstVATSubtotal.VATExcempt) {
340
+ vatExcemptNote = getVATExcemptInvoiceNote(firstVATSubtotal.VATExcempt);
341
+ }
342
+ else if (firstVATSubtotal.VATPercentage === 0) {
343
+ vatExcemptNote = $t('%1Q3');
344
+ }
345
+ }
346
+
347
+ const trimmedVATNumber = company?.VATNumber?.replace(/\D+/g, '').replace(/^\d{0,2}/, '');
348
+ const showCompanyNumber = !!company?.companyNumber
349
+ && (!company?.VATNumber || company.companyNumber.replace(/\D+/g, '') !== trimmedVATNumber);
350
+
351
+ const showDueDate = !!invoice.number && totalPrice >= 0;
352
+ const hasRoundingAmount = invoice.payableRoundingAmount !== 0;
353
+
354
+ const showPaidMessage = !!payment && payment.method !== null && payment.status === PaymentStatus.Succeeded && totalPrice >= 0;
355
+ const showTransferMessage = !!payment && payment.method === PaymentMethod.Transfer && payment.status !== PaymentStatus.Succeeded && totalPrice >= 0;
356
+ const showDirectDebitMessage = !!payment && payment.method === PaymentMethod.DirectDebit && payment.status !== PaymentStatus.Succeeded && totalPrice >= 0;
357
+ const showStripeMessage = !payment && !!invoice.stripeAccountId && totalPrice >= 0;
358
+
359
+ const customerName = customer.firstName && customer.lastName
360
+ ? `${customer.firstName} ${customer.lastName}`
361
+ : (customer.firstName ?? customer.lastName ?? '');
362
+
363
+ const context = {
364
+ // TODO: replace with hosted asset URLs
365
+ logoUrl: organization.meta.horizontalLogo?.getPathForSize(400, undefined),
366
+ firstPageBackgroundUrl: organization.privateMeta.invoiceSettings.background?.getPublicPath(),
367
+ otherPageBackgroundUrl: organization.privateMeta.invoiceSettings.secondBackground?.getPublicPath() ?? organization.privateMeta.invoiceSettings.background?.getPublicPath(),
368
+ fontSemiBoldUrl: '',
369
+ fontMediumUrl: '',
370
+ colors: {
371
+ primary: organization.meta.color ?? platform.config.color ?? '#0053ff',
372
+ },
373
+
374
+ sender: {
375
+ name: seller.name,
376
+ address: formatAddress(seller.address, false),
377
+ vatNumber: seller.VATNumber ?? '',
378
+ bankAccount: organization.meta.registrationPaymentConfiguration.transferSettings.iban ?? '',
379
+ },
380
+
381
+ invoice: {
382
+ id: invoice.id,
383
+ number: invoice.number,
384
+ meta: {
385
+ date,
386
+ items: invoicedItems.map(item => ({
387
+ amount: item.quantity / 1_00_00,
388
+ name: item.name,
389
+ description: item.description,
390
+ unitPriceExclVAT: item.unitPrice,
391
+ priceExclVAT: item.totalWithoutVAT,
392
+ percentageLabel: Formatter.percentage(item.VATPercentage * 100),
393
+ })),
394
+ priceWithoutVAT: invoice.totalWithoutVAT,
395
+ VAT: invoice.VATTotalAmount,
396
+ VATTotal,
397
+ hasMultipleVATRates,
398
+ singleVATRateLabel,
399
+ vatExcemptNote,
400
+ payableRoundingAmount: invoice.payableRoundingAmount,
401
+ totalPrice,
402
+ },
403
+ customer: {
404
+ name: customer.dynamicName,
405
+ contactName: !!customer.company && !!customer.name ? customer.name : null,
406
+ address: company?.address ? formatAddress(company.address ?? null, seller.address?.country !== company.address.country ) : null,
407
+ companyNumber: company?.companyNumber ?? '',
408
+ VATNumber: company?.VATNumber ?? '',
409
+ },
410
+ },
411
+
412
+ payment: payment
413
+ ? {
414
+ methodName: payment.method ? PaymentMethodHelper.getName(payment.method) : '',
415
+ transferDescription: payment.transferDescription ?? '',
416
+ }
417
+ : null,
418
+
419
+ isCreditNote,
420
+ showDueDate,
421
+ dueDate,
422
+ showCompanyNumber,
423
+ hasRoundingAmount,
424
+ showPaidMessage,
425
+ showTransferMessage,
426
+ showDirectDebitMessage,
427
+ showStripeMessage,
428
+ };
429
+
430
+ const file = await fs.readFile(import.meta.dirname+'/data/invoice.hbs.html', 'utf-8')
431
+ const renderedHtml = await render(file , context);
432
+ return renderedHtml;
433
+ }
434
+
435
+ static async uploadPdf(invoice: Invoice, fileContent: Buffer) {
436
+ const fileId = uuidv4();
437
+
438
+ let prefix = (STAMHOOFD.SPACES_PREFIX ?? '');
439
+ if (prefix.length > 0) {
440
+ prefix += '/';
441
+ }
442
+
443
+ const envPrefix = STAMHOOFD.environment !== 'production' ? STAMHOOFD.environment : null;
444
+
445
+ if (envPrefix && envPrefix !== (STAMHOOFD.SPACES_PREFIX ?? '')) {
446
+ prefix += envPrefix + '/';
447
+ }
448
+
449
+ const key = prefix + 'invoices/' + fileId + '.pdf';
450
+
451
+ const fileStruct = new File({
452
+ id: fileId,
453
+ server: 'https://' + STAMHOOFD.SPACES_BUCKET + '.' + STAMHOOFD.SPACES_ENDPOINT,
454
+ path: key,
455
+ size: fileContent.byteLength,
456
+ name: (invoice.number ?? invoice.id),
457
+ isPrivate: true,
458
+ contentType: 'application/pdf',
459
+ });
460
+
461
+ const cmd = new PutObjectCommand({
462
+ Bucket: STAMHOOFD.SPACES_BUCKET,
463
+ Key: key,
464
+ Body: fileContent,
465
+ ContentType: 'application/pdf',
466
+ ACL: 'private'
467
+ });
468
+ await Image.getS3Client().send(cmd);
469
+
470
+ // Sign the structure so it is accessible
471
+ if (!await fileStruct.sign()) {
472
+ throw new SimpleError({
473
+ code: 'failed_to_sign',
474
+ message: 'Failed to sign file',
475
+ human: $t('%B6'),
476
+ statusCode: 500,
477
+ });
478
+ }
479
+
480
+ return fileStruct
481
+ }
482
+
483
+ static async generatePdf(invoice: Invoice) {
484
+ const html = await this.generateHtml(invoice);
485
+ if (!html) {
486
+ throw new Error('Failed to render invoice ' + invoice.id)
487
+ }
488
+ const form = new FormData();
489
+
490
+ // File field
491
+
492
+ // Sign fields
493
+ const cacheId = 'invoice-' + invoice.id;
494
+ console.log('html length:', html.length);
495
+ form.append('html', new Blob([html], { type: 'text/html' }));
496
+ form.append('cacheId', cacheId);
497
+ form.append('signature', signInternal(cacheId, invoice.updatedAt.getTime().toString(), html));
498
+ form.append('timestamp', invoice.updatedAt.getTime().toString());
499
+
500
+ const controller = new AbortController();
501
+ const timeout = setTimeout(() => controller.abort(), 30_000);
502
+
503
+ try {
504
+ // Issue with system trusted CA in development
505
+ const result = await fetch((STAMHOOFD.environment === 'development' ? 'http://' : 'https://')+ STAMHOOFD.domains.rendererApi + '/v'+VERSION+'/html-to-pdf', {
506
+ method: 'POST',
507
+ body: form,
508
+ signal: controller.signal,
509
+ });
510
+ if (result.status === 200) {
511
+ // todo
512
+ const buffer = Buffer.from(await result.arrayBuffer())
513
+ const file = await this.uploadPdf(invoice, buffer);
514
+ invoice.pdf = file;
515
+ await invoice.save();
516
+ } else {
517
+ // todo
518
+ }
519
+ } catch (err) {
520
+ if (err instanceof DOMException && err.name === 'AbortError') {
521
+ throw new Error('Request timed out after 30s');
522
+ }
523
+ console.error(err);
524
+ } finally {
525
+ clearTimeout(timeout);
526
+ }
527
+ }
228
528
  }