@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.
- 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
|
@@ -1,79 +1,22 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
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
|
}
|