Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sixt → Digital Planet e-Invoice Automation

English · Türkçe

A Windows desktop application that turns car-rental agreements into legally valid Turkish e-invoices. It pulls billable rental records from the Sixt Dealer API, converts the ones the operator selects into UBL-TR 1.2 documents, and issues them as e-Fatura or e-Arşiv through the Digital Planet integrator's SOAP service.

The hard requirement behind every design decision: the same rental is never invoiced twice. Duplicate invoices are not a cosmetic bug in this domain — they are a tax filing that has to be cancelled, explained, and corrected. Four independent layers prevent it, and they hold even if the machine loses power mid-send.

Built with .NET 8 · WPF · EF Core + SQLite, with a hand-written SOAP 1.1 client.

Dashboard


Table of contents


Why it exists

A Sixt franchise station closes dozens of rental agreements a week. Each one has to become an invoice, and in Turkey that invoice must be an electronic document in a government-mandated format:

  • The receiver decides the document type. If they are registered for e-Fatura, the invoice must be sent through the e-Fatura network; if not, it must be issued as an e-Arşiv invoice. Getting this wrong means the invoice is rejected.
  • The document itself is UBL-TR 1.2 XML — a specific Turkish profile of OASIS UBL, with its own mandatory fields, tax breakdown rules and identifier schemes.
  • Sealing (mali mühür) and delivery to the revenue administration (GİB) are done by a licensed integrator, not by the merchant.

Doing this by hand means copying agreement numbers, plates, dates and amounts into a portal, dozens of times a week, with a tax penalty waiting behind every typo. This application does the whole run in one pass and keeps a full audit trail of what was sent, when, and what came back.


Screenshots

The application UI is Turkish. Customer names, plates, company details and licence keys in these captures are demo data.

Invoice run UBL preview
Fetch → select → issue — billable agreements from the Sixt API, with per-row status and a live detail panel UBL preview — inspect the exact UBL-TR 1.2 document before anything is sent
History Settings
History & audit trail — every attempt kept with its step-by-step log, filterable and exportable Connections — Sixt, Digital Planet and licence settings, each with its own "test connection"
More screens
Screen File
Dashboard dashboard.png
History — successful run history-success-log.png
Settings — seller identity (UBL mandatory fields) settings-seller.png
Guided tour for first-time operators guided-tour.png
About / licence status about.png

How a rental becomes an invoice

sequenceDiagram
    autonumber
    actor Op as Operator
    participant App as Desktop app
    participant Sixt as Sixt Dealer API
    participant DB as SQLite
    participant DP as Digital Planet (SOAP)

    Op->>App: "Fetch list"
    App->>Sixt: POST /login  → Bearer JWT
    App->>Sixt: GET /dealer-invoice-items
    Sixt-->>App: billable agreements + extras
    Op->>App: select rows → "Issue selected"

    loop for each agreement
        App->>DB: already processed?  (unique index)
        App->>DP: CheckCustomerTaxId → e-Fatura payer?
        App->>DP: GetNewInvoiceId → invoice no + UUID
        App->>DB: persist no + UUID BEFORE sending (outbox)
        App->>App: build UBL-TR 1.2 (VAT, seller, buyer, FX rate)
        App->>DP: SendUBLInvoice / SendEArchiveData
        DP-->>App: accepted (DP seals it and forwards to GİB)
        App->>Sixt: POST /dealer-invoice-number-update
        App->>DB: mark Sent
    end
Loading

The operator is in control the whole way: the confirmation dialog states which mode the run is in, a live step panel shows where each record is, and Stop performs a soft cancel that finishes the record in flight rather than abandoning it half-sent.


Never invoice the same rental twice

This is the part of the system worth reading the code for. Four layers, each of which would be sufficient on a good day, arranged so that no single failure can produce a duplicate:

# Layer What it stops
1 Unique index on ProcessedDocuments.SourceDocumentId The same agreement being inserted twice, even by two concurrent runs
2 Outbox — the invoice number and UUID are written to the database before the send call A crash between "reserve a number" and "send". On restart the run continues with the reserved identifiers instead of drawing new ones
3 Target-side idempotencyreconciliationId = AgreementId A retry that reaches the integrator: Digital Planet rejects a second document carrying the same UUID/ETTN
4 Source-side markingdealer-invoice-number-update back to Sixt The record reappearing in tomorrow's list at all

State machine: New → Pending → Sent, plus Failed. Sent is terminal — nothing in the code path can move a record out of it, so a "retry" of a sent invoice is structurally impossible rather than merely discouraged.

Why the outbox matters more than it looks: GetNewInvoiceId has a side effect at the integrator — it consumes an invoice number from the seller's official series. Asking for a second number because the first one was lost in a crash produces a gap in a numbered series that the tax authority expects to be contiguous. Writing it down before use is what keeps the series clean.


Features

Invoice run

  • Filter billable agreements by pickup/return date, station and invoice type before fetching
  • Per-row selection with a detail panel: agreement, vehicle, period, extras and computed total
  • UBL preview — read the generated XML before committing to anything
  • Live step panel per record, plus a soft Stop that never leaves a half-sent document
  • Automatic routing: CheckCustomerTaxId decides e-Fatura vs e-Arşiv per receiver

Correctness

  • UBL-TR 1.2 generation: VAT breakdown, VAT-inclusive price handling, seller/buyer parties, person vs. company identification (TCKN/VKN), rental line plus per-extra lines
  • TCMB daily rate for foreign-currency invoices, with a manual override
  • Turkish tax-number validation and party-type detection

Operations

  • Test mode (dry-run) — fetches, builds UBL and writes to the database but issues nothing; an amber banner makes the mode impossible to miss
  • History screen with filters, per-record step log, PDF archive and Excel export
  • Dashboard: today's issue count, failures, monthly totals, 7-day trend, live FX rates
  • Guided tour for first-time operators
  • File logging via Serilog; SQLite means no database server to install on the operator's PC

Licensing

  • Machine-bound licence verified against a panel, with an offline grace window
  • Signature verification is ECDSA P-256 with an embedded public key; the vendor-side panel that issues these licences is a separate project

Architecture

        WPF (App.Desktop)  ── Material Design, MVVM, LiveCharts
                 │
                 ▼
        App.Application    ── InvoiceProcessor / InvoiceBatchRunner
                 │            dashboard + history queries, TCMB rates
       ┌─────────┼─────────┐
       ▼         ▼         ▼
   App.Sixt  App.Data   App.DigitalPlanet
   (HTTP)    (EF Core)  (hand-written SOAP 1.1)
       └─────────┼─────────┘
                 ▼
             App.Core       ── models, interfaces, UBL builder, options

App.Core depends on nothing; everything depends on App.Core. There is no server component — the engine lives in App.Application and is driven by the desktop UI.

Layer Choice
Runtime .NET 8, net8.0-windows
UI WPF, MaterialDesignThemes, CommunityToolkit.Mvvm, LiveChartsCore, WebView2
Data EF Core 8 + SQLite — a single file, no server to install
Sixt HttpClient + Bearer JWT
Digital Planet Hand-written SOAP 1.1 client over plain text/xml
Export ClosedXML (Excel), PDF archive from the integrator
Hosting Generic Host + DI, Serilog file logging

Why a hand-written SOAP client. The generated WCF client wanted MTOM; the integrator's MTOM-enabled endpoint answers a plain text/xml body with 415 Unsupported Media Type. Rather than fight the generated stack, the client is about 200 lines that build the envelope directly and post it to the non-MTOM endpoint. Two details cost real debugging time and are now pinned by tests: the endpoint must be the withoutmtom host, and the credential order is CorporateCode then LoginName — reversed, the service returns an empty ticket instead of an error.

Tests. 202 tests across two projects, concentrated on the invariants that matter: deduplication and idempotency, UBL generation, tax-number and party-type rules, currency conversion, and the desktop view-models.

dotnet test

Project layout

src/
├── App.Core            models, interfaces, UBL-TR builder, options, TrClock
├── App.Data            EF Core DbContext, entities, migrations
├── App.Sixt            Sixt Dealer API client (login, items, number-update)
├── App.DigitalPlanet   SOAP 1.1 client (ticket, tax-id check, invoice id, send, status, PDF)
├── App.Application     invoicing engine, dashboard/history queries, TCMB rate provider
└── App.Desktop         WPF UI: Dashboard · Issue Invoices · History · Settings
tests/
├── App.Tests           dedupe, idempotency, UBL, tax-id, party type, currency
└── App.Desktop.Tests   view-model and desktop service tests

Getting started

Prerequisites: Windows, .NET 8 SDK.

git clone <this repo>
cd einvoice-automation
dotnet run --project src/App.Desktop

The database is created and migrated on first start — it is a single SQLite file next to the executable, so there is nothing to install.

To actually issue invoices you need credentials that only the two vendors can give you:

  1. Sixt Dealer API — dealer e-mail and password, provided by Sixt to franchise partners.
  2. Digital Planet — corporate code, login name and password for the integration service.
  3. Seller identity — your own VKN, title, tax office and address. These are UBL-mandatory; the app refuses to issue an invoice while VKN or title is empty.

Fill them in from the Settings screen (each section has its own test connection button) or in src/App.Desktop/appsettings.json. The committed file contains placeholders only.

Start in test mode. Sync:SendEnabled = false runs the full pipeline — fetch, dedupe check, UBL generation, database writes — and stops short of issuing anything. Confirm the generated UBL looks right for your seller profile before turning it on.

# publish a single self-contained folder
dotnet publish src/App.Desktop -c Release -r win-x64 --self-contained

Configuration

Section Key Meaning
ConnectionStrings AppDb SQLite file, e.g. Data Source=sixt-fatura.db
Sixt BaseUrl, Email, Password, PageSize Dealer API endpoint and credentials
DigitalPlanet ServiceUrl, CorporateCode, LoginName, Password Integrator service — must be the non-MTOM URL
Seller Vkn, Title, TaxOffice, City, District, StreetAddress, … Seller party written into every UBL document
Invoice DefaultKdvRate, PricesIncludeVat, PaymentMeansCode, AutoEmailEArchive Invoice defaults; payment means is mandatory for GİB e-Arşiv reporting
ExchangeRates UseLiveRates, Manual.USD, Manual.EUR TCMB live rates, or fixed values
Sync SendEnabled, AutoRouteInvoiceKind Dry-run switch and automatic e-Fatura/e-Arşiv routing
License PanelUrl, Key Licence panel address and key
Archive PdfPath Where issued invoice PDFs are stored

Settings saved from the UI take effect on the next run — no restart required.


Notes & limitations

  • Windows only. It is a WPF application; the engine underneath is plain .NET, but the UI is not portable.
  • It cannot run standalone. Without Sixt dealer credentials and a Digital Planet integrator account there is nothing to fetch and nowhere to send. This repository is published as a portfolio and reference implementation, not as a product you can deploy against your own data.
  • Vendor documentation is deliberately not included. The Sixt Dealer API specification and the Digital Planet integration manual belong to those companies. What is described here is this project's own integration, written from scratch.
  • Sealing and GİB delivery are not done here. The application produces UBL and hands it to the integrator, which applies the financial seal and forwards the document. Content-level GİB schematron validation is an ongoing area.
  • The licence check is client-side. Anyone who can modify the binary can remove it — true of any offline licensing scheme. What the signature does guarantee is that a licence cannot be forged.
  • No automated UI tests. The invariants are covered by unit tests; the WPF surface is covered only through view-model tests.

License

MIT © Furkan Paşaoğlu

Screenshots and sample data in this repository are fabricated for demonstration and do not represent any real customer, vehicle or company.

About

Windows desktop app that turns Sixt car-rental agreements into Turkish UBL-TR 1.2 e-invoices (e-Fatura / e-Arşiv) via the Digital Planet integrator, with four independent layers preventing duplicate invoicing

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages