Skip to content

Commit

Permalink
Implement xrechnung invoice generation
Browse files Browse the repository at this point in the history
- add ability to print xrechnung invoice for business customers
- xrechnung is generated via own generator class
- generator class utilizes xml templates that are replaced with data
- improved test cases for pdf and xml invoice gen
- minor improvements and fixes to pdf gen

Co-authored-by: kibibytium <[email protected]>

close #3357
  • Loading branch information
kitsugo authored and mpfau committed Nov 25, 2024
1 parent 5b64a54 commit 89224c0
Show file tree
Hide file tree
Showing 17 changed files with 1,214 additions and 209 deletions.
10 changes: 0 additions & 10 deletions resources/pdf/metadata.xml

This file was deleted.

17 changes: 17 additions & 0 deletions src/common/api/worker/facades/lazy/CustomerFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
createMembershipRemoveData,
createPaymentDataServicePutData,
CustomDomainReturn,
Customer,
CustomerInfoTypeRef,
CustomerServerProperties,
CustomerServerPropertiesTypeRef,
Expand Down Expand Up @@ -70,6 +71,7 @@ import { KeyLoaderFacade } from "../KeyLoaderFacade.js"
import { RecoverCodeFacade } from "./RecoverCodeFacade.js"
import { encryptKeyWithVersionedKey, VersionedEncryptedKey, VersionedKey } from "../../crypto/CryptoWrapper.js"
import { AsymmetricCryptoFacade } from "../../crypto/AsymmetricCryptoFacade.js"
import { XRechnungInvoiceGenerator } from "../../invoicegen/XRechnungInvoiceGenerator.js"

assertWorkerOrNode()

Expand Down Expand Up @@ -463,6 +465,21 @@ export class CustomerFacade {
}
}

async generateXRechnungInvoice(invoiceNumber: string, customer: Customer, accountingInfo: AccountingInfo): Promise<DataFile> {
const customerInfo = await this.entityClient.load(CustomerInfoTypeRef, customer.customerInfo)
const invoiceData = await this.serviceExecutor.get(InvoiceDataService, createInvoiceDataGetIn({ invoiceNumber }))
const xRechnungGenerator = new XRechnungInvoiceGenerator(invoiceData, invoiceNumber, this.getCustomerId(), customerInfo.registrationMailAddress)
const xRechnungFile = xRechnungGenerator.generate()
return {
_type: "DataFile",
name: String(invoiceNumber) + ".xml",
mimeType: "application/xml",
data: xRechnungFile,
size: xRechnungFile.byteLength,
id: undefined,
}
}

async loadAccountingInfo(): Promise<AccountingInfo> {
const customer = await this.entityClient.load(CustomerTypeRef, assertNotNull(this.userFacade.getUser()?.customer))
const customerInfo = await this.entityClient.load(CustomerInfoTypeRef, customer.customerInfo)
Expand Down
123 changes: 123 additions & 0 deletions src/common/api/worker/invoicegen/InvoiceUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import InvoiceTexts from "./InvoiceTexts.js"

export const enum VatType {
NO_VAT = "0",
ADD_VAT = "1",
VAT_INCLUDED_SHOWN = "2",
VAT_INCLUDED_HIDDEN = "3",
NO_VAT_REVERSE_CHARGE = "4",
}

export const enum InvoiceType {
INVOICE = "0",
CREDIT = "1",
REFERRAL_CREDIT = "2",
}

export const enum PaymentMethod {
INVOICE = "0",
CREDIT_CARD = "1",
SEPA_UNUSED = "2",
PAYPAL = "3",
ACCOUNT_BALANCE = "4",
}

export const enum InvoiceItemType {
PREMIUM_USER = "0",
StarterUser = "1",
StarterUserPackage = "2",
StarterUserPackageUpgrade = "3",
StoragePackage = "4",
StoragePackageUpgrade = "5",
EmailAliasPackage = "6",
EmailAliasPackageUpgrade = "7",
SharedMailGroup = "8",
WhitelabelFeature = "9",
ContactForm_UNUSED = "10",
WhitelabelChild = "11",
LocalAdminGroup = "12",
Discount = "13",
SharingFeature = "14",
Credit = "15",
GiftCard = "16",
BusinessFeature = "17",
GiftCardMigration = "18",
ReferralCredit = "19",
CancelledReferralCredit = "20",
RevolutionaryAccount = "21",
LegendAccount = "22",
EssentialAccount = "23",
AdvancedAccount = "24",
UnlimitedAccount = "25",
}

/**
* Returns the language code of country as either "en" or "de"
* "de" is only returned if the country is Germany or Austria
* @param country
*/
export function countryUsesGerman(country: string): "de" | "en" {
return country === "DE" || country === "AT" ? "de" : "en"
}

/**
* Get the name of a given InvoiceItemType
*/
export function getInvoiceItemTypeName(type: NumberString, languageCode: "en" | "de"): string {
switch (type) {
case InvoiceItemType.PREMIUM_USER:
return InvoiceTexts[languageCode].premiumUser
case InvoiceItemType.StarterUser:
return InvoiceTexts[languageCode].starterUser
case InvoiceItemType.StarterUserPackage:
return InvoiceTexts[languageCode].starterUserPackage
case InvoiceItemType.StarterUserPackageUpgrade:
return InvoiceTexts[languageCode].starterUserPackageUpgrade
case InvoiceItemType.StoragePackage:
return InvoiceTexts[languageCode].storagePackage
case InvoiceItemType.StoragePackageUpgrade:
return InvoiceTexts[languageCode].storagePackageUpgrade
case InvoiceItemType.EmailAliasPackage:
return InvoiceTexts[languageCode].emailAliasPackage
case InvoiceItemType.EmailAliasPackageUpgrade:
return InvoiceTexts[languageCode].emailAliasPackageUpgrade
case InvoiceItemType.SharedMailGroup:
return InvoiceTexts[languageCode].sharedMailGroup
case InvoiceItemType.WhitelabelFeature:
return InvoiceTexts[languageCode].whitelabelFeature
case InvoiceItemType.ContactForm_UNUSED:
return InvoiceTexts[languageCode].contactFormUnused
case InvoiceItemType.WhitelabelChild:
return InvoiceTexts[languageCode].whitelabelChild
case InvoiceItemType.LocalAdminGroup:
return InvoiceTexts[languageCode].localAdminGroup
case InvoiceItemType.Discount:
return InvoiceTexts[languageCode].discount
case InvoiceItemType.SharingFeature:
return InvoiceTexts[languageCode].sharingFeature
case InvoiceItemType.Credit:
return InvoiceTexts[languageCode].creditType
case InvoiceItemType.GiftCard:
return InvoiceTexts[languageCode].giftCard
case InvoiceItemType.BusinessFeature:
return InvoiceTexts[languageCode].businessFeature
case InvoiceItemType.GiftCardMigration:
return InvoiceTexts[languageCode].giftCardMigration
case InvoiceItemType.ReferralCredit:
return InvoiceTexts[languageCode].referralCredit
case InvoiceItemType.CancelledReferralCredit:
return InvoiceTexts[languageCode].cancelledReferralCredit
case InvoiceItemType.RevolutionaryAccount:
return InvoiceTexts[languageCode].revolutionaryAccount
case InvoiceItemType.LegendAccount:
return InvoiceTexts[languageCode].legendAccount
case InvoiceItemType.EssentialAccount:
return InvoiceTexts[languageCode].essentialAccount
case InvoiceItemType.AdvancedAccount:
return InvoiceTexts[languageCode].advancedAccount
case InvoiceItemType.UnlimitedAccount:
return InvoiceTexts[languageCode].unlimitedAccount
default:
throw new Error("Unknown InvoiceItemType " + type)
}
}
150 changes: 27 additions & 123 deletions src/common/api/worker/invoicegen/PdfInvoiceGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,19 @@ import { MARGIN_LEFT, MARGIN_TOP, PDF_FONTS, PDF_IMAGES, PdfDocument, TABLE_VERT
import InvoiceTexts from "./InvoiceTexts.js"
import { PdfWriter } from "../pdf/PdfWriter.js"
import { InvoiceDataGetOut } from "../../entities/sys/TypeRefs.js"

const enum VatType {
NO_VAT = "0",
ADD_VAT = "1",
VAT_INCLUDED_SHOWN = "2",
VAT_INCLUDED_HIDDEN = "3",
NO_VAT_REVERSE_CHARGE = "4",
}

const enum InvoiceType {
INVOICE = "0",
CREDIT = "1",
REFERRAL_CREDIT = "2",
}

const enum PaymentMethod {
INVOICE = "0",
CREDIT_CARD = "1",
SEPA_UNUSED = "2",
PAYPAL = "3",
ACCOUNT_BALANCE = "4",
}

const enum InvoiceItemType {
PREMIUM_USER = "0",
StarterUser = "1",
StarterUserPackage = "2",
StarterUserPackageUpgrade = "3",
StoragePackage = "4",
StoragePackageUpgrade = "5",
EmailAliasPackage = "6",
EmailAliasPackageUpgrade = "7",
SharedMailGroup = "8",
WhitelabelFeature = "9",
ContactForm_UNUSED = "10",
WhitelabelChild = "11",
LocalAdminGroup = "12",
Discount = "13",
SharingFeature = "14",
Credit = "15",
GiftCard = "16",
BusinessFeature = "17",
GiftCardMigration = "18",
ReferralCredit = "19",
CancelledReferralCredit = "20",
RevolutionaryAccount = "21",
LegendAccount = "22",
EssentialAccount = "23",
AdvancedAccount = "24",
UnlimitedAccount = "25",
}
import { countryUsesGerman, getInvoiceItemTypeName, InvoiceItemType, InvoiceType, PaymentMethod, VatType } from "./InvoiceUtils.js"

/**
* Object generating a PDF invoice document.
* This document is ONLY responsible for rendering the data it gets and formatting it in a way that does not change anything about it.
* This generator is ONLY responsible for rendering the data it gets and formatting it in a way that does not change anything about it.
* If adjustments to the data must be made prior to rendering, then these should take place within the RenderInvoice service.
*/
export class PdfInvoiceGenerator {
private readonly doc: PdfDocument
private readonly languageCode: "de" | "en" = "en"
private readonly invoiceNumber: string
private readonly customerId: string
private invoice: InvoiceDataGetOut
private invoiceNumber: string
private customerId: string

constructor(pdfWriter: PdfWriter, invoice: InvoiceDataGetOut, invoiceNumber: string, customerId: string) {
this.invoice = invoice
Expand Down Expand Up @@ -170,14 +120,14 @@ export class PdfInvoiceGenerator {
// Entry with all invoice info
tableData.push([
this.formatAmount(invoiceItem.itemType, invoiceItem.amount),
this.getInvoiceItemTypeName(invoiceItem.itemType),
getInvoiceItemTypeName(invoiceItem.itemType, this.languageCode),
invoiceItem.singlePrice == null ? "" : this.formatInvoiceCurrency(invoiceItem.singlePrice),
this.formatInvoiceCurrency(invoiceItem.totalPrice),
])
// Entry with date range
tableData.push(["", `${this.formatInvoiceDate(invoiceItem.startDate)} - ${this.formatInvoiceDate(invoiceItem.endDate)}`, "", ""])
}
const tableEndPoint = await this.doc.addTable([MARGIN_LEFT, MARGIN_TOP + 120], 165, columns, tableData)
const tableEndPoint = await this.doc.addTable([MARGIN_LEFT, MARGIN_TOP + 120], 165, columns, tableData, this.getTableRowsForFirstPage())

this.renderTableSummary(tableEndPoint, columns)
this.doc.changeTextCursorPosition([MARGIN_LEFT, tableEndPoint + 4 * TABLE_VERTICAL_SPACING])
Expand All @@ -190,6 +140,8 @@ export class PdfInvoiceGenerator {
// Line break that's to be removed if no VAT appears in the summary
let additionalVerticalSpace = 1

this.doc.changeFont(PDF_FONTS.REGULAR, 11)

// Sub total
this.doc.addTableRow([MARGIN_LEFT, tableEndPoint], columns, [
"",
Expand Down Expand Up @@ -231,7 +183,7 @@ export class PdfInvoiceGenerator {
* Additional blocks displayed below the table depending on invoice type, vat type and payment method
*/
renderAdditional() {
this.doc.changeFont(PDF_FONTS.REGULAR, 12)
this.doc.changeFont(PDF_FONTS.REGULAR, 11)

// No VAT / VAT not shown in table
switch (this.invoice.vatType) {
Expand All @@ -249,6 +201,7 @@ export class PdfInvoiceGenerator {
.addText(`${InvoiceTexts[this.languageCode].yourVatId} `)
.changeFont(PDF_FONTS.BOLD, 11)
.addText(`${this.invoice.vatIdNumber}`)
.changeFont(PDF_FONTS.REGULAR, 11)
} else {
this.doc.addText(InvoiceTexts[this.languageCode].netPricesNoVatInGermany)
}
Expand Down Expand Up @@ -320,6 +273,23 @@ export class PdfInvoiceGenerator {
.addText(InvoiceTexts[this.languageCode].legalBankAccount)
}

/**
* Determines how many table rows (invoice items) can be rendered on the first page depending on the texts that follow after the table
*/
getTableRowsForFirstPage(): number {
if (
this.invoice.paymentMethod === PaymentMethod.INVOICE &&
this.invoice.vatIdNumber != null &&
(this.invoice.vatType === VatType.NO_VAT || this.invoice.vatType === VatType.NO_VAT_REVERSE_CHARGE)
) {
// In these scenarios, there will be a lot of text after the table summary, so few rows can render
return 4
} else {
// In all other scenarios, there will be little text after the table summary, so more rows can render
return 8
}
}

/**
* Get the name of a given InvoiceType
*/
Expand All @@ -340,68 +310,6 @@ export class PdfInvoiceGenerator {
}
}

/**
* Get the name of a given InvoiceItemType
*/
getInvoiceItemTypeName(type: NumberString): string {
switch (type) {
case InvoiceItemType.PREMIUM_USER:
return InvoiceTexts[this.languageCode].premiumUser
case InvoiceItemType.StarterUser:
return InvoiceTexts[this.languageCode].starterUser
case InvoiceItemType.StarterUserPackage:
return InvoiceTexts[this.languageCode].starterUserPackage
case InvoiceItemType.StarterUserPackageUpgrade:
return InvoiceTexts[this.languageCode].starterUserPackageUpgrade
case InvoiceItemType.StoragePackage:
return InvoiceTexts[this.languageCode].storagePackage
case InvoiceItemType.StoragePackageUpgrade:
return InvoiceTexts[this.languageCode].storagePackageUpgrade
case InvoiceItemType.EmailAliasPackage:
return InvoiceTexts[this.languageCode].emailAliasPackage
case InvoiceItemType.EmailAliasPackageUpgrade:
return InvoiceTexts[this.languageCode].emailAliasPackageUpgrade
case InvoiceItemType.SharedMailGroup:
return InvoiceTexts[this.languageCode].sharedMailGroup
case InvoiceItemType.WhitelabelFeature:
return InvoiceTexts[this.languageCode].whitelabelFeature
case InvoiceItemType.ContactForm_UNUSED:
return InvoiceTexts[this.languageCode].contactFormUnused
case InvoiceItemType.WhitelabelChild:
return InvoiceTexts[this.languageCode].whitelabelChild
case InvoiceItemType.LocalAdminGroup:
return InvoiceTexts[this.languageCode].localAdminGroup
case InvoiceItemType.Discount:
return InvoiceTexts[this.languageCode].discount
case InvoiceItemType.SharingFeature:
return InvoiceTexts[this.languageCode].sharingFeature
case InvoiceItemType.Credit:
return InvoiceTexts[this.languageCode].creditType
case InvoiceItemType.GiftCard:
return InvoiceTexts[this.languageCode].giftCard
case InvoiceItemType.BusinessFeature:
return InvoiceTexts[this.languageCode].businessFeature
case InvoiceItemType.GiftCardMigration:
return InvoiceTexts[this.languageCode].giftCardMigration
case InvoiceItemType.ReferralCredit:
return InvoiceTexts[this.languageCode].referralCredit
case InvoiceItemType.CancelledReferralCredit:
return InvoiceTexts[this.languageCode].cancelledReferralCredit
case InvoiceItemType.RevolutionaryAccount:
return InvoiceTexts[this.languageCode].revolutionaryAccount
case InvoiceItemType.LegendAccount:
return InvoiceTexts[this.languageCode].legendAccount
case InvoiceItemType.EssentialAccount:
return InvoiceTexts[this.languageCode].essentialAccount
case InvoiceItemType.AdvancedAccount:
return InvoiceTexts[this.languageCode].advancedAccount
case InvoiceItemType.UnlimitedAccount:
return InvoiceTexts[this.languageCode].unlimitedAccount
default:
throw new Error("Unknown InvoiceItemType " + type)
}
}

/**
* Format the date depending on document language (dd.mm.yyyy) / (dd. Mon yyyy)
*/
Expand Down Expand Up @@ -442,7 +350,3 @@ export class PdfInvoiceGenerator {
}
}
}

function countryUsesGerman(country: string): "de" | "en" {
return country === "DE" || country === "AT" ? "de" : "en"
}
Loading

0 comments on commit 89224c0

Please sign in to comment.