Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ plugins {

dependencies {
implementation(project(":brev-documents"))
implementation("no.digipost:peppol-bis-billing-3-generator-api:v0.5-billing-3.0.18")
testImplementation("com.helger.phive.rules:phive-rules-peppol:4.5.3")
testRuntimeOnly("org.glassfish.jaxb:jaxb-runtime:4.0.9")
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
package no.beint.brev.benchmark;

import no.beint.brev.CountryCode;
import no.beint.brev.CurrencyCode;
import no.beint.brev.EndpointId;
import no.beint.brev.SchemeId;
import no.beint.brev.UnitCode;
import no.beint.brev.documents.BillingDocument;
import no.beint.brev.documents.BillingLine;
import no.beint.brev.documents.Documents;
import no.beint.brev.documents.Party;
import no.beint.brev.documents.PaymentInstruction;
import no.beint.brev.documents.PostalAddress;
import no.beint.brev.documents.Quantity;
import no.beint.brev.documents.UnitPrice;
import no.beint.brev.documents.VatCategory;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
Expand All @@ -23,9 +11,9 @@
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import peppol.bis.invoice3.api.PeppolBillingApi;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

@State(Scope.Benchmark)
Expand All @@ -35,42 +23,17 @@
@Measurement(iterations = 5)
@Fork(2)
public class BillingWriterBenchmark {
private static final CurrencyCode NOK = new CurrencyCode("NOK");
private final BillingDocument invoice = createInvoice();
private final BillingDocument brevInvoice = BillingBenchmarkFixture.createBrevInvoice();
private final peppol.bis.invoice3.domain.Invoice digipostInvoice =
BillingBenchmarkFixture.createDigipostInvoice();

@Benchmark
public byte[] writeInvoice() {
return Documents.toByteArray(invoice);
public byte[] brevSerialization() {
return Documents.toByteArray(brevInvoice);
}

private static BillingDocument createInvoice() {
PostalAddress address = new PostalAddress("Testveien 1", "Oslo", "0150", new CountryCode("NO"));
Party seller = Party.withVat(
new EndpointId(SchemeId.NORWEGIAN_ORGANIZATION, "913341464"),
"Seller AS",
"913341464",
"NO913341464MVA",
address);
Party buyer = Party.withoutVat(
new EndpointId(SchemeId.NORWEGIAN_ORGANIZATION, "987654321"),
"Buyer AS",
"987654321",
address);
return BillingDocument.invoice()
.id("BENCH-1")
.issueDate(LocalDate.of(2026, 8, 17))
.dueDate(LocalDate.of(2026, 9, 1))
.currency(NOK)
.buyerReference("benchmark")
.seller(seller)
.buyer(buyer)
.payment(new PaymentInstruction("NO9386011117947", "1234567890123456789012345"))
.line(new BillingLine(
"1",
"Consulting",
new Quantity(new BigDecimal("10"), UnitCode.HOUR),
new UnitPrice(NOK, new BigDecimal("1250")),
VatCategory.standard(new BigDecimal("25"))))
.build();
@Benchmark
public byte[] digipostSerialization() throws IOException {
return PeppolBillingApi.create(digipostInvoice).inputStream().readAllBytes();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package no.beint.brev.benchmark;

import no.beint.brev.CountryCode;
import no.beint.brev.CurrencyCode;
import no.beint.brev.EndpointId;
import no.beint.brev.SchemeId;
import no.beint.brev.UnitCode;
import no.beint.brev.documents.BillingDocument;
import no.beint.brev.documents.BillingLine;
import no.beint.brev.documents.Party;
import no.beint.brev.documents.PaymentInstruction;
import no.beint.brev.documents.PostalAddress;
import no.beint.brev.documents.Quantity;
import no.beint.brev.documents.UnitPrice;
import no.beint.brev.documents.VatCategory;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;

final class BillingBenchmarkFixture {
private static final CurrencyCode NOK = new CurrencyCode("NOK");

private BillingBenchmarkFixture() {
}

static BillingDocument createBrevInvoice() {
PostalAddress address = new PostalAddress("Testveien 1", "Oslo", "0150", new CountryCode("NO"));
Party seller = Party.withVat(
new EndpointId(SchemeId.NORWEGIAN_ORGANIZATION, "922989451"),
"Seller AS",
"922989451",
"NO922989451MVA",
address);
Party buyer = Party.withoutVat(
new EndpointId(SchemeId.NORWEGIAN_ORGANIZATION, "987654325"),
"Buyer AS",
"987654325",
address);
return BillingDocument.invoice()
.id("BENCH-1")
.issueDate(LocalDate.of(2026, 8, 17))
.dueDate(LocalDate.of(2026, 9, 1))
.currency(NOK)
.buyerReference("benchmark")
.seller(seller)
.buyer(buyer)
.payment(new PaymentInstruction("NO9386011117947", "1234567890123456789012345"))
.line(new BillingLine(
"1",
"Consulting",
new Quantity(new BigDecimal("10"), UnitCode.HOUR),
new UnitPrice(NOK, new BigDecimal("1250")),
VatCategory.standard(new BigDecimal("25"))))
.build();
}

static peppol.bis.invoice3.domain.Invoice createDigipostInvoice() {
peppol.bis.invoice3.domain.AccountingSupplierParty seller = new peppol.bis.invoice3.domain.AccountingSupplierParty(
createDigipostParty("922989451", "Seller AS", true));
peppol.bis.invoice3.domain.AccountingCustomerParty buyer = new peppol.bis.invoice3.domain.AccountingCustomerParty(
createDigipostParty("987654325", "Buyer AS", false));
peppol.bis.invoice3.domain.TaxScheme vatScheme = new peppol.bis.invoice3.domain.TaxScheme("VAT");
peppol.bis.invoice3.domain.TaxCategory taxCategory = new peppol.bis.invoice3.domain.TaxCategory("S", vatScheme)
.withPercent("25");
peppol.bis.invoice3.domain.TaxTotal taxTotal = new peppol.bis.invoice3.domain.TaxTotal(
new peppol.bis.invoice3.domain.TaxAmount("3125.00", "NOK"))
.withTaxSubtotal(new peppol.bis.invoice3.domain.TaxSubtotal(
new peppol.bis.invoice3.domain.TaxableAmount("12500.00", "NOK"),
new peppol.bis.invoice3.domain.TaxAmount("3125.00", "NOK"),
taxCategory));
peppol.bis.invoice3.domain.LegalMonetaryTotal totals = new peppol.bis.invoice3.domain.LegalMonetaryTotal(
new peppol.bis.invoice3.domain.LineExtensionAmount("12500.00", "NOK"),
new peppol.bis.invoice3.domain.TaxExclusiveAmount("12500.00", "NOK"),
new peppol.bis.invoice3.domain.TaxInclusiveAmount("15625.00", "NOK"),
new peppol.bis.invoice3.domain.PayableAmount("15625.00", "NOK"));
peppol.bis.invoice3.domain.ClassifiedTaxCategory lineTax =
new peppol.bis.invoice3.domain.ClassifiedTaxCategory("S", new peppol.bis.invoice3.domain.TaxScheme("VAT"))
.withPercent("25");
peppol.bis.invoice3.domain.InvoiceLine line = new peppol.bis.invoice3.domain.InvoiceLine(
"1",
new peppol.bis.invoice3.domain.InvoicedQuantity("10", "HUR"),
new peppol.bis.invoice3.domain.LineExtensionAmount("12500.00", "NOK"),
new peppol.bis.invoice3.domain.Item("Consulting", lineTax),
new peppol.bis.invoice3.domain.Price(new peppol.bis.invoice3.domain.PriceAmount("1250", "NOK"))
.withBaseQuantity(new peppol.bis.invoice3.domain.BaseQuantity("1").withUnitCode("HUR")));
peppol.bis.invoice3.domain.PaymentMeans payment = new peppol.bis.invoice3.domain.PaymentMeans(
new peppol.bis.invoice3.domain.PaymentMeansCode("30"))
.withPaymentID("1234567890123456789012345")
.withPayeeFinancialAccount(new peppol.bis.invoice3.domain.PayeeFinancialAccount("NO9386011117947"));
return new peppol.bis.invoice3.domain.Invoice(
"BENCH-1", "2026-08-17", "NOK", seller, buyer, taxTotal, totals, List.of(line))
.withDueDate("2026-09-01")
.withBuyerReference("benchmark")
.withPaymentMeans(payment);
}

private static peppol.bis.invoice3.domain.Party createDigipostParty(
String organizationNumber, String name, boolean vatRegistered) {
peppol.bis.invoice3.domain.PostalAddress address = new peppol.bis.invoice3.domain.PostalAddress(
new peppol.bis.invoice3.domain.Country("NO"))
.withStreetName("Testveien 1")
.withCityName("Oslo")
.withPostalZone("0150");
peppol.bis.invoice3.domain.Party party = new peppol.bis.invoice3.domain.Party(
new peppol.bis.invoice3.domain.EndpointID(organizationNumber).withSchemeID("0192"),
address,
new peppol.bis.invoice3.domain.PartyLegalEntity(name)
.withCompanyID(new peppol.bis.invoice3.domain.CompanyID(organizationNumber).withSchemeID("0192")))
.withPartyName(new peppol.bis.invoice3.domain.PartyName(name));
if (vatRegistered) {
party.withPartyTaxScheme(new peppol.bis.invoice3.domain.PartyTaxScheme(
"NO" + organizationNumber + "MVA", new peppol.bis.invoice3.domain.TaxScheme("VAT")));
}
return party;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package no.beint.brev.benchmark;

import com.helger.phive.api.execute.ValidationExecutionManager;
import com.helger.phive.api.executorset.IValidationExecutorSetRegistry;
import com.helger.phive.api.executorset.ValidationExecutorSetRegistry;
import com.helger.phive.api.validity.IValidityDeterminator;
import com.helger.phive.peppol.PeppolValidation2026_05;
import com.helger.phive.xml.source.IValidationSourceXML;
import com.helger.phive.xml.source.ValidationSourceXML;
import no.beint.brev.documents.Documents;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
import peppol.bis.invoice3.api.PeppolBillingApi;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.ByteArrayInputStream;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

final class BillingWriterBenchmarkTest {
private static ValidationExecutorSetRegistry<IValidationSourceXML> registry;

@BeforeAll
static void registerCurrentBillingRules() {
registry = new ValidationExecutorSetRegistry<>();
try {
PeppolValidation2026_05.class.getMethod("initBilling", IValidationExecutorSetRegistry.class)
.invoke(null, registry);
} catch (ReflectiveOperationException exception) {
PeppolValidation2026_05.init(registry);
}
}

@Test
void benchmarkInvoicesContainTheSameBusinessValues() throws Exception {
Document brev = parse(Documents.toByteArray(BillingBenchmarkFixture.createBrevInvoice()));
Document digipost = parse(PeppolBillingApi.create(BillingBenchmarkFixture.createDigipostInvoice())
.inputStream().readAllBytes());

for (String element : List.of(
"ID",
"IssueDate",
"DueDate",
"DocumentCurrencyCode",
"BuyerReference",
"EndpointID",
"RegistrationName",
"StreetName",
"CityName",
"PostalZone",
"IdentificationCode",
"TaxAmount",
"TaxableAmount",
"LineExtensionAmount",
"TaxExclusiveAmount",
"TaxInclusiveAmount",
"PayableAmount",
"InvoicedQuantity",
"Name",
"PriceAmount",
"BaseQuantity",
"PaymentMeansCode",
"PaymentID")) {
assertEquals(values(brev, element), values(digipost, element), element);
}

assertValid(brev);
assertValid(digipost);
}

private static void assertValid(Document document) {
var executorSet = registry.getOfID(PeppolValidation2026_05.VID_OPENPEPPOL_INVOICE_UBL_V3);
if (executorSet == null) {
throw new IllegalStateException("current Peppol Billing rules were not registered");
}
var result = ValidationExecutionManager.executeValidation(
IValidityDeterminator.createDefault(),
executorSet,
ValidationSourceXML.create(null, document));
assertFalse(
result.containsAtLeastOneError(),
() -> result.getAllErrors().stream()
.map(error -> error.getErrorText(Locale.ENGLISH))
.collect(Collectors.joining("\n")));
}

private static List<String> values(Document document, String localName) throws Exception {
var expression = XPathFactory.newInstance().newXPath()
.compile("//*[local-name()='" + localName + "']/text()");
var nodes = (org.w3c.dom.NodeList) expression.evaluate(document, XPathConstants.NODESET);
return java.util.stream.IntStream.range(0, nodes.getLength())
.mapToObj(index -> nodes.item(index).getNodeValue())
.toList();
}

private static Document parse(byte[] xml) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
return factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml));
}
}
Loading