diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/converter/EInvoiceConverter.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/converter/EInvoiceConverter.kt index 80b8285..6425f6e 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/converter/EInvoiceConverter.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/converter/EInvoiceConverter.kt @@ -6,12 +6,12 @@ import org.mustangproject.CII.CIIToUBL import org.mustangproject.ZUGFeRD.ZUGFeRDVisualizer import java.io.File -class EInvoiceConverter { +open class EInvoiceConverter { - fun convertInvoiceToHtml(invoice: Invoice, outputFile: File, language: ZUGFeRDVisualizer.Language = ZUGFeRDVisualizer.Language.DE) = + open fun convertInvoiceToHtml(invoice: Invoice, outputFile: File, language: ZUGFeRDVisualizer.Language = ZUGFeRDVisualizer.Language.DE) = convertInvoiceToHtml(createXRechnungXml(invoice), outputFile, language) - fun convertInvoiceToHtml(invoiceXml: String, outputFile: File, language: ZUGFeRDVisualizer.Language = ZUGFeRDVisualizer.Language.DE): String { + open fun convertInvoiceToHtml(invoiceXml: String, outputFile: File, language: ZUGFeRDVisualizer.Language = ZUGFeRDVisualizer.Language.DE): String { val xmlFile = File.createTempFile("Zugferd", ".xml") .also { it.writeText(invoiceXml) } @@ -32,12 +32,12 @@ class EInvoiceConverter { /** * Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language). */ - fun convertCiiToUbl(invoice: Invoice) = convertCiiToUbl(createXRechnungXml(invoice)) + open fun convertCiiToUbl(invoice: Invoice) = convertCiiToUbl(createXRechnungXml(invoice)) /** * Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language). */ - fun convertCiiToUbl(invoiceXml: String): String { + open fun convertCiiToUbl(invoiceXml: String): String { // TODO: extract a common method for this val xmlFile = File.createTempFile("Zugferd", ".xml") .also { it.writeText(invoiceXml) } @@ -56,15 +56,15 @@ class EInvoiceConverter { /** * Converts a CII (Cross Industry Invoice) invoice, e.g. a ZUGFeRD or Factur-X invoice, to UBL (Universal Business Language). */ - fun convertCiiToUbl(xmlFile: File, outputFile: File) { + open fun convertCiiToUbl(xmlFile: File, outputFile: File) { val cii2Ubl = CIIToUBL() cii2Ubl.convert(xmlFile, outputFile) } - private fun createXRechnungXml(invoice: Invoice): String = EInvoiceCreator().createXRechnungXml(invoice) + protected open fun createXRechnungXml(invoice: Invoice): String = EInvoiceCreator().createXRechnungXml(invoice) - private fun copyResource(resourceName: String, outputFile: File, outputFileExtension: String) { + protected open fun copyResource(resourceName: String, outputFile: File, outputFileExtension: String) { javaClass.classLoader.getResourceAsStream(resourceName).use { it?.copyTo(File(outputFile.parentFile, outputFile.nameWithoutExtension + outputFileExtension).outputStream()) } diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/creation/EInvoiceCreator.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/creation/EInvoiceCreator.kt index 8b4d700..e1b2e66 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/creation/EInvoiceCreator.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/creation/EInvoiceCreator.kt @@ -5,11 +5,11 @@ import net.codinux.invoicing.model.Invoice import org.mustangproject.ZUGFeRD.* import java.io.File -class EInvoiceCreator( - private val mapper: MustangMapper = MustangMapper() +open class EInvoiceCreator( + protected open val mapper: MustangMapper = MustangMapper() ) { - fun createXRechnungXml(invoice: Invoice): String { + open fun createXRechnungXml(invoice: Invoice): String { val provider = ZUGFeRD2PullProvider() provider.profile = Profiles.getByName("XRechnung") @@ -20,9 +20,9 @@ class EInvoiceCreator( /** * Synonym for [createFacturXXml] (ZUGFeRD 2 is a synonym for Factur-X). */ - fun createZugferdXml(invoice: Invoice) = createFacturXXml(invoice) + open fun createZugferdXml(invoice: Invoice) = createFacturXXml(invoice) - fun createFacturXXml(invoice: Invoice): String { + open fun createFacturXXml(invoice: Invoice): String { val exporter = ZUGFeRDExporterFromA3() .setProfile("EN16931") // required for XML? @@ -32,9 +32,9 @@ class EInvoiceCreator( /** * Synonym for [createFacturXPdf] (ZUGFeRD 2 is a synonym for Factur-X). */ - fun createZugferdPdf(invoice: Invoice, outputFile: File) = createFacturXPdf(invoice, outputFile) + open fun createZugferdPdf(invoice: Invoice, outputFile: File) = createFacturXPdf(invoice, outputFile) - fun createFacturXPdf(invoice: Invoice, outputFile: File) { + open fun createFacturXPdf(invoice: Invoice, outputFile: File) { val xml = createFacturXXml(invoice) val xmlFile = File.createTempFile(outputFile.nameWithoutExtension, ".xml") .also { it.writeText(xml) } @@ -50,10 +50,10 @@ class EInvoiceCreator( } - fun attachInvoiceXmlToPdf(invoice: Invoice, pdfFile: File, outputFile: File) = + open fun attachInvoiceXmlToPdf(invoice: Invoice, pdfFile: File, outputFile: File) = attachInvoiceXmlToPdf(createFacturXXml(invoice), pdfFile, outputFile) - fun attachInvoiceXmlToPdf(invoiceXml: String, pdfFile: File, outputFile: File) { + open fun attachInvoiceXmlToPdf(invoiceXml: String, pdfFile: File, outputFile: File) { val exporter = ZUGFeRDExporterFromA3() .setZUGFeRDVersion(2) .setProfile("EN16931") // available values: MINIMUM, BASICWL, BASIC, CIUS, EN16931, EXTENDED, XRECHNUNG @@ -68,7 +68,7 @@ class EInvoiceCreator( } - private fun createXml(provider: IXMLProvider, invoice: Invoice): String { + protected open fun createXml(provider: IXMLProvider, invoice: Invoice): String { val transaction = mapper.mapToTransaction(invoice) provider.generateXML(transaction) diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/filesystem/FilesystemInvoiceReader.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/filesystem/FilesystemInvoiceReader.kt index 0834642..7c86c74 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/filesystem/FilesystemInvoiceReader.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/filesystem/FilesystemInvoiceReader.kt @@ -6,16 +6,16 @@ import net.codinux.log.logger import java.nio.file.Path import kotlin.io.path.* -class FilesystemInvoiceReader( - private val eInvoiceReader: EInvoiceReader = EInvoiceReader() +open class FilesystemInvoiceReader( + protected open val eInvoiceReader: EInvoiceReader = EInvoiceReader() ) { private val log by logger() - fun readAllInvoicesOfDirectory(directory: Path, recursive: Boolean = false) = + open fun readAllInvoicesOfDirectory(directory: Path, recursive: Boolean = false) = readInvoicesFromFiles(collectFiles(directory, recursive)) - private fun collectFiles(directory: Path, recursive: Boolean): List = buildList { + protected open fun collectFiles(directory: Path, recursive: Boolean): List = buildList { directory.listDirectoryEntries().forEach { child -> if (child.isRegularFile()) { add(child) @@ -25,13 +25,13 @@ class FilesystemInvoiceReader( } } - fun readInvoicesFromFiles(vararg files: Path) = + open fun readInvoicesFromFiles(vararg files: Path) = readInvoicesFromFiles(files.toList()) - fun readInvoicesFromFiles(files: List): List = + open fun readInvoicesFromFiles(files: List): List = files.mapNotNull { file -> readInvoiceFromFile(file)?.let { InvoiceOnFilesystem(file, it) } } - fun readInvoiceFromFile(file: Path): Invoice? = try { + open fun readInvoiceFromFile(file: Path): Invoice? = try { val extension = file.extension.lowercase() if (extension == "pdf") { diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailReader.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailReader.kt index a6e3741..d2c7667 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailReader.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mail/MailReader.kt @@ -19,22 +19,22 @@ import java.util.* import java.util.concurrent.Executors import kotlin.math.max -class MailReader( - private val eInvoiceReader: EInvoiceReader = EInvoiceReader() +open class MailReader( + protected open val eInvoiceReader: EInvoiceReader = EInvoiceReader() ) { - private data class MessagePart( + protected data class MessagePart( val mediaType: String, val part: Part ) - private val mailDispatcher = Executors.newFixedThreadPool(max(24, Runtime.getRuntime().availableProcessors() * 4)).asCoroutineDispatcher() + protected open val mailDispatcher = Executors.newFixedThreadPool(max(24, Runtime.getRuntime().availableProcessors() * 4)).asCoroutineDispatcher() - private val log by logger() + protected val log by logger() - fun listenForNewReceivedEInvoices(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX", eInvoiceReceived: (MailWithInvoice) -> Unit) = runBlocking { + open fun listenForNewReceivedEInvoices(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX", eInvoiceReceived: (MailWithInvoice) -> Unit) = runBlocking { try { connect(account) { store -> val folder = store.getFolder(emailFolderName) @@ -61,7 +61,7 @@ class MailReader( log.info { "Stopped listening to new received eInvoices of '${account.username}'" } } - private suspend fun keepConnectionOpen(account: MailAccount, folder: Folder) { + protected open suspend fun keepConnectionOpen(account: MailAccount, folder: Folder) { log.info { "Listening to new mails of ${account.username}" } // Use IMAP IDLE to keep the connection alive @@ -78,7 +78,7 @@ class MailReader( } - fun listAllMessagesWithEInvoice(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX"): List { + open fun listAllMessagesWithEInvoice(account: MailAccount, downloadMessageBody: Boolean = false, emailFolderName: String = "INBOX"): List { try { return connect(account) { store -> val inbox = store.getFolder(emailFolderName) @@ -95,7 +95,7 @@ class MailReader( return emptyList() } - private fun listAllMessagesWithEInvoiceInFolder(folder: Folder, downloadMessageBody: Boolean): List = runBlocking { + protected open fun listAllMessagesWithEInvoiceInFolder(folder: Folder, downloadMessageBody: Boolean): List = runBlocking { val messageCount = folder.messageCount if (messageCount <= 0) { return@runBlocking emptyList() @@ -115,7 +115,7 @@ class MailReader( .filterNotNull() } - private fun findEInvoice(message: Message, downloadMessageBody: Boolean): MailWithInvoice? { + protected open fun findEInvoice(message: Message, downloadMessageBody: Boolean): MailWithInvoice? { try { val parts = getAllMessageParts(message) @@ -139,7 +139,7 @@ class MailReader( return null } - private fun findEInvoice(messagePart: MessagePart): MailAttachmentWithEInvoice? { + protected open fun findEInvoice(messagePart: MessagePart): MailAttachmentWithEInvoice? { try { val part = messagePart.part val invoice = tryToReadEInvoice(part, messagePart.mediaType) @@ -160,7 +160,7 @@ class MailReader( return null } - private fun tryToReadEInvoice(part: Part, mediaType: String?): Invoice? = try { + protected open fun tryToReadEInvoice(part: Part, mediaType: String?): Invoice? = try { val filename = part.fileName?.lowercase() ?: "" if (filename.endsWith(".pdf") || mediaType == "application/pdf" || mediaType == "application/octet-stream") { @@ -176,7 +176,7 @@ class MailReader( } - private fun getAllMessageParts(part: Part): List { + protected open fun getAllMessageParts(part: Part): List { return if (part.isMimeType("multipart/*")) { val multipart = part.content as Multipart val parts = IntRange(0, multipart.count - 1).map { multipart.getBodyPart(it) } @@ -202,7 +202,7 @@ class MailReader( * * -> This method removes parameters and return media type (first part) only */ - private fun getMediaType(part: Part): String? = part.contentType?.lowercase()?.let { contentType -> + protected open fun getMediaType(part: Part): String? = part.contentType?.lowercase()?.let { contentType -> val indexOfSeparator = contentType.indexOf(';') if (indexOfSeparator > -1) { @@ -212,11 +212,11 @@ class MailReader( } } - private fun getPlainTextBody(parts: Collection) = getBodyWithMediaType(parts, "text/plain") + protected open fun getPlainTextBody(parts: Collection) = getBodyWithMediaType(parts, "text/plain") - private fun getHtmlBody(parts: Collection) = getBodyWithMediaType(parts, "text/html") + protected open fun getHtmlBody(parts: Collection) = getBodyWithMediaType(parts, "text/html") - private fun getBodyWithMediaType(parts: Collection, mediaType: String): String? = try { + protected open fun getBodyWithMediaType(parts: Collection, mediaType: String): String? = try { val partsForMediaType = parts.filter { it.mediaType == mediaType } if (partsForMediaType.size == 1) { @@ -239,11 +239,11 @@ class MailReader( null } - private fun map(date: Date): Instant = + protected open fun map(date: Date): Instant = date.toInstant() - private fun connect(account: MailAccount, connected: (Store) -> T): T { + protected open fun connect(account: MailAccount, connected: (Store) -> T): T { val properties = mapAccountToJavaMailProperties(account) val session = Session.getInstance(properties) @@ -254,7 +254,7 @@ class MailReader( } } - private fun mapAccountToJavaMailProperties(account: MailAccount) = Properties().apply { + protected open fun mapAccountToJavaMailProperties(account: MailAccount) = Properties().apply { put("mail.store.protocol", "imap") put("mail.imap.host", account.serverAddress) diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mapper/MustangMapper.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mapper/MustangMapper.kt index 8a85f2a..6fbb9ac 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mapper/MustangMapper.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/mapper/MustangMapper.kt @@ -10,9 +10,9 @@ import java.time.LocalDate import java.time.ZoneId import java.util.* -class MustangMapper { +open class MustangMapper { - fun mapToTransaction(invoice: net.codinux.invoicing.model.Invoice): IExportableTransaction = Invoice().apply { + open fun mapToTransaction(invoice: net.codinux.invoicing.model.Invoice): IExportableTransaction = Invoice().apply { this.number = invoice.invoiceNumber this.issueDate = map(invoice.invoicingDate) this.sender = mapParty(invoice.sender) @@ -26,7 +26,7 @@ class MustangMapper { this.referenceNumber = invoice.buyerReference } - fun mapParty(party: Party): TradeParty = TradeParty( + open fun mapParty(party: Party): TradeParty = TradeParty( party.name, party.street, party.postalCode, party.city, party.countryIsoCode ).apply { this.setVATID(party.vatId) @@ -45,7 +45,7 @@ class MustangMapper { } } - fun mapLineItem(item: LineItem): IZUGFeRDExportableItem = Item( + open fun mapLineItem(item: LineItem): IZUGFeRDExportableItem = Item( // description has to be an empty string if not set Product(item.name, item.description ?: "", item.unit, item.vatPercentage), item.price, item.quantity ).apply { @@ -53,7 +53,7 @@ class MustangMapper { } - fun mapToInvoice(invoice: Invoice) = net.codinux.invoicing.model.Invoice( + open fun mapToInvoice(invoice: Invoice) = net.codinux.invoicing.model.Invoice( invoiceNumber = invoice.number, invoicingDate = map(invoice.issueDate), sender = mapParty(invoice.sender), @@ -66,32 +66,32 @@ class MustangMapper { buyerReference = invoice.referenceNumber ) - fun mapParty(party: TradeParty) = Party( + open fun mapParty(party: TradeParty) = Party( party.name, party.street, party.zip, party.location, party.country, party.vatID, party.email ?: party.contact?.eMail, party.contact?.phone, party.contact?.fax, party.contact?.name, party.bankDetails?.firstOrNull()?.let { net.codinux.invoicing.model.BankDetails(it.iban, it.bic, it.accountName) } ) - fun mapLineItem(item: IZUGFeRDExportableItem) = LineItem( + open fun mapLineItem(item: IZUGFeRDExportableItem) = LineItem( item.product.name, item.product.unit, item.quantity, item.price, item.product.vatPercent, item.product.description.takeUnless { it.isBlank() } ) @JvmName("mapNullable") - private fun map(date: LocalDate?) = + protected fun map(date: LocalDate?) = date?.let { map(it) } - private fun map(date: LocalDate): Date = + protected open fun map(date: LocalDate): Date = Date.from(mapToInstant(date)) - private fun mapToInstant(date: LocalDate): Instant = + protected open fun mapToInstant(date: LocalDate): Instant = date.atStartOfDay(ZoneId.systemDefault()).toInstant() @JvmName("mapNullable") - private fun map(date: Date?) = + protected fun map(date: Date?) = date?.let { map(it) } - private fun map(date: Date): LocalDate = + protected open fun map(date: Date): LocalDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() } \ No newline at end of file diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/reader/EInvoiceReader.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/reader/EInvoiceReader.kt index 0f0ce6a..e2e6e9f 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/reader/EInvoiceReader.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/reader/EInvoiceReader.kt @@ -6,39 +6,39 @@ import org.mustangproject.ZUGFeRD.ZUGFeRDInvoiceImporter import java.io.File import java.io.InputStream -class EInvoiceReader( - private val mapper: MustangMapper = MustangMapper() +open class EInvoiceReader( + protected open val mapper: MustangMapper = MustangMapper() ) { - fun extractFromXml(xmlFile: File) = extractFromXml(xmlFile.inputStream()) + open fun extractFromXml(xmlFile: File) = extractFromXml(xmlFile.inputStream()) - fun extractFromXml(stream: InputStream) = extractFromXml(stream.reader().readText()) + open fun extractFromXml(stream: InputStream) = extractFromXml(stream.reader().readText()) - fun extractFromXml(xml: String): Invoice { + open fun extractFromXml(xml: String): Invoice { val importer = ZUGFeRDInvoiceImporter() // XRechnungImporter only reads properties but not to a Invoice object importer.fromXML(xml) return extractInvoice(importer) } - fun extractFromPdf(pdfFile: File) = extractFromPdf(pdfFile.inputStream()) + open fun extractFromPdf(pdfFile: File) = extractFromPdf(pdfFile.inputStream()) - fun extractFromPdf(stream: InputStream): Invoice { + open fun extractFromPdf(stream: InputStream): Invoice { val importer = ZUGFeRDInvoiceImporter(stream) return extractInvoice(importer) } - fun extractXmlFromPdf(pdfFile: File) = extractXmlFromPdf(pdfFile.inputStream()) + open fun extractXmlFromPdf(pdfFile: File) = extractXmlFromPdf(pdfFile.inputStream()) - fun extractXmlFromPdf(stream: InputStream): String { + open fun extractXmlFromPdf(stream: InputStream): String { val importer = ZUGFeRDInvoiceImporter(stream) return String(importer.rawXML, Charsets.UTF_8) } - private fun extractInvoice(importer: ZUGFeRDInvoiceImporter): Invoice { + protected open fun extractInvoice(importer: ZUGFeRDInvoiceImporter): Invoice { val invoice = importer.extractInvoice() // TODO: the values LineTotalAmount, ChargeTotalAmount, AllowanceTotalAmount, TaxBasisTotalAmount, TaxTotalAmount, diff --git a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/validation/EInvoiceValidator.kt b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/validation/EInvoiceValidator.kt index 8371104..e50ae28 100644 --- a/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/validation/EInvoiceValidator.kt +++ b/e-invoice-domain/src/main/kotlin/net/codinux/invoicing/validation/EInvoiceValidator.kt @@ -1,27 +1,31 @@ package net.codinux.invoicing.validation +import net.codinux.log.logger import org.mustangproject.validator.ZUGFeRDValidator import java.io.File import java.lang.reflect.Field -class EInvoiceValidator { +open class EInvoiceValidator { companion object { private val SectionField = getPrivateField("section") private val CriterionField = getPrivateField("criterion") private val StacktraceField = getPrivateField("stacktrace") + private val log by logger() + private fun getPrivateField(fieldName: String): Field? = try { org.mustangproject.validator.ValidationResultItem::class.java.getDeclaredField(fieldName).apply { trySetAccessible() } } catch (e: Throwable) { + log.error(e) { "Could not access private field '$fieldName' of Mustang ValidationResultItem" } null } } - fun validate(fileToValidate: File, disableNotices: Boolean = false): InvoiceValidationResult { + open fun validate(fileToValidate: File, disableNotices: Boolean = false): InvoiceValidationResult { val validator = object : ZUGFeRDValidator() { fun getContext() = this.context } @@ -42,10 +46,10 @@ class EInvoiceValidator { return InvoiceValidationResult(validator.wasCompletelyValid(), isXmlValid, xmlValidationResults, report) } - private fun mapValidationResultItem(item: org.mustangproject.validator.ValidationResultItem) = + protected open fun mapValidationResultItem(item: org.mustangproject.validator.ValidationResultItem) = ValidationResultItem(mapSeverity(item), item.message, item.location, SectionField?.get(item) as? Int, CriterionField?.get(item) as? String, StacktraceField?.get(item) as? String) - private fun mapSeverity(item: org.mustangproject.validator.ValidationResultItem): ValidationResultSeverity { + protected open fun mapSeverity(item: org.mustangproject.validator.ValidationResultItem): ValidationResultSeverity { var name = item.severity.name name = name.first().uppercase() + name.substring(1)