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
67 changes: 67 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Publish

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
automatic_release:
description: 'Automatically release the Central Portal deployment'
required: true
default: false
type: boolean

permissions:
contents: read

jobs:
publish:
name: Publish to Maven Central
runs-on: macos-latest
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_IN_MEMORY_KEY_ID }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }}
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: oracle
java-version: '26'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5

- name: Check release version
run: |
version="$(sed -n 's/^val releaseVersion = "\(.*\)"/\1/p' build.gradle.kts | head -n 1)"
test -n "$version"

if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
tag_version="${GITHUB_REF#refs/tags/v}"
if [[ "$tag_version" != "$version" ]]; then
echo "Tag version $tag_version does not match Gradle version $version" >&2
exit 1
fi
fi

if [[ "$version" == *-SNAPSHOT ]]; then
echo "Release publishing requires a non-SNAPSHOT version, found $version" >&2
exit 1
fi

echo "Publishing version $version"

- name: Publish
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.automatic_release }}" == "true" ]]; then
./gradlew publishAndReleaseToMavenCentral
else
./gradlew publishAndReleaseToMavenCentral
fi
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ Before submitting a change:
./gradlew :benchmark:jmhJar
```

Changes to generated XML require independent current-release validation evidence and focused fixtures. Performance changes require JMH measurements with JDK, hardware, command, throughput, and allocation results. Dependency additions require an architectural rationale and must not affect `brev-core` or `brev-billing` runtime classpaths.
Changes to generated XML require independent current-release validation evidence and focused fixtures. Performance changes require JMH measurements with JDK, hardware, command, throughput, and allocation results. Dependency additions require an architectural rationale and must not affect published module runtime classpaths.

Do not include real customer invoices, personal data, certificates, private keys, or production endpoint credentials in issues or fixtures.
26 changes: 14 additions & 12 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,22 @@ Brev can therefore coexist with and contribute to those projects. Its initial tr
application domain
│
▼
brev-core ── brev-billing ── brev-validation
│ │
├── brev-reader └── reference PHIVE adapter
│
└── brev-sbdh
│
▼
brev-transport-api
│
phase4 adapter first
│
Peppol network
brev-core
│
├── brev-documents (Billing write + read)
│ │
│ └── conformance (test-only PHIVE/Saxon)
│
├── brev-smp (types now, JDK client later)
│
└── brev-ap (types now, Phase4 adapter later)
│
Peppol network
```

All of the above live in this repository. Consumers depend on one artifact.
There is no `ph-commons` equivalent that every module must drag in.

The document layers must not depend on transport. The transport SPI must not leak Phase4 types.

## Model strategy
Expand Down
45 changes: 45 additions & 0 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Publishing

Brev publishes `brev-core`, `brev-documents`, `brev-smp`, and `brev-ap` to
Maven Central under `no.beint.brev`.

## Secrets

Do not put tokens or the private signing key in the repository. Export them as
environment variables (see `~/.config/skald/maven-central.env` on a release
machine; the same Central namespace and signing key are used):

```text
MAVEN_CENTRAL_USERNAME
MAVEN_CENTRAL_PASSWORD
SIGNING_IN_MEMORY_KEY
SIGNING_IN_MEMORY_KEY_ID
SIGNING_IN_MEMORY_KEY_PASSWORD
```

Gradle also accepts the same values as `ORG_GRADLE_PROJECT_mavenCentralUsername`,
`ORG_GRADLE_PROJECT_mavenCentralPassword`, `ORG_GRADLE_PROJECT_signingInMemoryKey`,
`ORG_GRADLE_PROJECT_signingInMemoryKeyId`, and
`ORG_GRADLE_PROJECT_signingInMemoryKeyPassword`.

Generate a Central Portal user token at
<https://central.sonatype.com/usertoken>.

## Release

Set `releaseVersion` in `build.gradle.kts`, then:

```shell
./gradlew clean build
./gradlew publishAndReleaseToMavenCentral
```

The Central Portal typically takes 10–30 minutes after a successful deployment
before the artifacts are downloadable from Maven Central.

For a CI release, push a matching tag:

```shell
git tag v0.1.0
git push origin v0.1.0
```
144 changes: 64 additions & 80 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,108 +1,93 @@
# Brev

**Current Peppol documents, compiled into small and explicit Java.**
**Current Peppol, as one modular JDK 26 library.**

Brev is an experimental, dependency-free JVM library for constructing, writing, reading, and validating current Peppol business documents. It deliberately does not preserve compatibility with obsolete Peppol releases or expose the complete UBL schema as an object model.
Brev is a dependency-free JVM library for constructing, writing, reading, and
later transporting current Peppol business documents. It lives in **one
repository**. Applications take only the module they need.

The name is Norwegian for “letter.”

> [!WARNING]
> Brev is an early vertical slice, not yet a conformant replacement for a production Peppol Billing generator or validator. The current code writes a deliberately narrow positive-invoice subset. Authoritative validation remains required before transmission.
> `0.1.0` replaces ReAI's Digipost + `ph-ubl` document layer. `brev-smp` and
> `brev-ap` publish types only. Keep official PHIVE validation on the send path
> until generated-rule parity exists.

## Why Brev
## Why one repo, not ten

Existing JVM libraries solve important but different problems:
The Helger stack is the interoperability reference. It is also split across
many repositories because it preserves every UBL version, every Peppol
profile, and years of API compatibility.

- `ph-ubl` provides complete JAXB bindings for several UBL versions;
- the Digipost generator offers a convenient mutable Billing-domain API;
- PHIVE executes authoritative XSD and Schematron validation artefacts;
- Phase4 and Oxalis implement full AS4 access-point transport.
Brev occupies the opposite corner:

Brev targets the unoccupied space between an application's domain model and transport:
- one repo, independently published modules;
- current Peppol only;
- JDK 26 baseline, no older-Java tax;
- typed documents instead of the full UBL schema;
- zero third-party runtime dependencies in every published module.

- current Peppol profile semantics rather than every UBL element;
- distinct immutable Java types instead of interchangeable strings;
- required data at construction time;
- calculated totals rather than duplicated mutable values;
- direct buffered UTF-8 output without DOM, JAXB, reflection, or an intermediate XML string;
- validation rules compiled from exact official artefacts into ordinary Java;
- zero third-party runtime dependencies in the document core.
| Module | Take it when you need | Status |
|---|---|---|
| `brev-core` | identifiers, codes, release metadata | shipping |
| `brev-documents` | Billing invoice/credit note model, writer, reader | shipping |
| `brev-smp` | typed SMP lookup results | types only |
| `brev-ap` | typed send/receive messages | types only |

See [DESIGN.md](DESIGN.md) for the architectural rationale, [ROADMAP.md](ROADMAP.md) for the complete implementation plan, and [docs/performance.md](docs/performance.md) for the measured starting point.
See [docs/modules.md](docs/modules.md), [DESIGN.md](DESIGN.md), and
[ROADMAP.md](ROADMAP.md).

## Current target

The repository tracks one release target:
## Current document target

| Component | Target |
|---|---|
| Peppol BIS Billing | 3.0.21 |
| EN 16931 validation artefacts | 1.3.16 |
| Publication date | 2026-05-20 |
| Mandatory from | 2026-08-17 |
| UBL syntax | UBL 2.1 Invoice |
| UBL syntax | Invoice and CreditNote 2.1 |
| Process | Billing profile 01 |

Brev does not bundle historical rule sets. See [docs/version-policy.md](docs/version-policy.md).

## Modules

- `brev-core`: participant, endpoint, document, process, currency, country, and unit value types plus release metadata.
- `brev-billing`: immutable Billing model, derived totals, and direct UBL writer.
- `conformance`: test-only PHIVE/Saxon reference validation; never a production dependency.
- `benchmark`: JMH performance harness; never a production dependency.

Planned modules are added only after their conformance gates pass. They include validation, streaming input, SBDH, discovery, reporting, and a Phase4 transport adapter.
Historical rule sets are not bundled. When Billing 4 is mandatory, Brev will
replace this writer rather than keep a compatibility flag.

## Example

```java
var nok = new CurrencyCode("NOK");
var organization = new SchemeId("0192");
var address = new PostalAddress(
"Dokumentveien 1", "Oslo", "0150", new CountryCode("NO"));

var seller = Party.withVat(
new EndpointId(organization, "913341464"),
"Seller AS", "913341464", "NO913341464MVA", address);
var buyer = Party.withoutVat(
new EndpointId(organization, "987654321"),
"Buyer AS", "987654321", address);

var line = new InvoiceLine(
"1",
"Consulting",
new Quantity(new BigDecimal("10"), new UnitCode("HUR")),
new UnitPrice(nok, new BigDecimal("1250")),
new VatCategory(TaxCategoryCode.STANDARD_RATE, new BigDecimal("25")));

var invoice = new Invoice(
"INV-1",
LocalDate.of(2026, 8, 17),
LocalDate.of(2026, 9, 1),
nok,
"buyer-reference",
seller,
buyer,
new PaymentInstruction("NO9386011117947", "payment-reference"),
List.of(line));

PeppolBillingWriter.write(invoice, outputStream);
var invoice = BillingDocument.invoice()
.id("INV-1")
.issueDate(LocalDate.of(2026, 8, 17))
.dueDate(LocalDate.of(2026, 9, 1))
.currency(new CurrencyCode("NOK"))
.buyerReference("buyer-reference")
.seller(seller)
.buyer(buyer)
.payment(new PaymentInstruction("NO9386011117947", "payment-reference"))
.line(new BillingLine(
"1",
"Consulting",
new Quantity(new BigDecimal("10"), UnitCode.HOUR),
new UnitPrice(new CurrencyCode("NOK"), new BigDecimal("1250")),
VatCategory.standard(new BigDecimal("25"))))
.build();

Documents.write(invoice, outputStream);
BillingDocument parsed = Documents.read(Documents.toByteArray(invoice));
```

The writer emits compact XML straight to the supplied stream and does not close it.
The writer emits compact XML straight to the supplied stream and does not close
it. Attachments are Base64-encoded incrementally.

## Deliberately unsupported in the first slice
## What ReAI can generate today

- Credit notes and negative invoices.
- Allowances, charges, rounding adjustments, prepayments, and multiple payment means.
- Exempt, reverse-charge, intra-community, export, and out-of-scope VAT categories.
- Attachments and document references.
- Full current-profile validation.
- XML parsing, SBDH, SMP, reporting, and AS4.
- Older Peppol Billing releases.
- invoices and credit notes
- VAT categories S, Z, E, G, AE, O
- Norwegian supplier identity (0192, VAT, Foretaksregisteret)
- order reference, billing reference, IBAN/BIC
- embedded PDF and other attachments
- price-level allowances
- optional payment, due date, and address parts

Unsupported features fail through the absence of an API rather than silently producing approximate XML.
Unsupported Peppol Billing features fail through the absence of an API.

## Building

Expand All @@ -113,12 +98,11 @@ Brev requires JDK 26.
./gradlew :benchmark:jmh
```

The build also runs the PHIVE/Saxon Billing 3.0.21 conformance fixture and fails if either published module acquires a third-party runtime dependency.

## Status

Version `0.1.0-SNAPSHOT` establishes the API direction and provides an executable performance baseline. It must not be used to transmit invoices until the Phase 1 conformance gate in [ROADMAP.md](ROADMAP.md) is complete.
The build fails if a published module acquires a third-party runtime
dependency. Conformance runs official PHIVE/Saxon Billing 3.0.21 artefacts as
a test-only oracle.

## License

Apache License 2.0. Imported standards and validation artefacts retain their own licenses and provenance.
Apache License 2.0. Imported standards and validation artefacts retain their
own licenses and provenance.
29 changes: 15 additions & 14 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,31 @@ Status: in progress in `0.1.0-SNAPSHOT`.
- [x] Zero-runtime-dependency enforcement.
- [x] Typed participant, endpoint, currency, country, and unit values.
- [x] Exact release metadata for Billing 3.0.21 / validation artefacts 1.3.16.
- [x] Immutable positive-invoice vertical slice.
- [x] Immutable Billing invoice and credit-note model covering ReAI output.
- [x] Derived line, tax, and payable totals.
- [x] Direct buffered UTF-8 UBL writer.
- [x] Direct buffered UTF-8 UBL writer for Invoice and CreditNote.
- [x] Bounded StAX reader for inbound Invoice and CreditNote.
- [x] One-repo modules for documents, SMP types, and AP types.
- [x] XML escaping, Unicode, immutability, and invariant tests.
- [x] JMH benchmark entry point.
- [x] Independent XSD and Schematron validation of the emitted fixture through PHIVE/Saxon.
- [ ] Baseline benchmark against the ReAI Digipost generator path.

Exit gate: the fixture passes independent current-release validation and benchmark results are recorded with hardware, JDK, commands, throughput, and allocation.

## Phase 1 — complete current Billing model and writer
## Phase 1 — ReAI Billing surface

- Generate code-list types from the pinned 3.0.21 artefacts.
- Add credit notes and negative invoices as separate semantic variants.
- Add all current VAT categories with category-specific required fields.
- Add document and line allowances/charges.
- Add payment means, prepayments, rounding, tax-currency totals, periods, delivery, references, attachments, and notes.
- Add profile 02 without weakening profile 01 construction rules.
- Express mutually exclusive and conditional groups as sealed types.
- Stream Base64 attachments without whole-attachment copies.
- Add deterministic output snapshots for every model branch.
- Run every generated fixture through XSD, official Peppol/EN16931 Schematron, and at least one independent service.
Status: the ReAI-used subset is implemented in `brev-documents`. Remaining Billing terms that ReAI does not emit stay out of the API.

Exit gate: every business term supported by Peppol Billing 3.0.21 is either represented and tested or listed as an intentional, specification-cited exclusion. Every fixture passes the authoritative validator.
- [x] Credit notes as a document type, not a flag on a mutable invoice.
- [x] VAT categories S, Z, E, G, AE, O with category-specific fields.
- [x] Price allowances, order/billing references, IBAN/BIC, attachments.
- [x] Incremental Base64 for embedded documents.
- [ ] Generate remaining code-list types from the pinned 3.0.21 artefacts.
- [ ] Document-level allowances, prepayments, rounding, delivery, notes.
- [ ] Profile 02 without weakening profile 01 construction rules.

Exit gate: every business term ReAI emits is represented, tested, and independently validated. Unused Billing terms remain absent.

## Phase 2 — validation compiler

Expand Down
2 changes: 1 addition & 1 deletion benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ plugins {
}

dependencies {
implementation(project(":brev-billing"))
implementation(project(":brev-documents"))
}
Loading