npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fin.cx/expenses

v3.0.0

Published

German travel and expense reimbursement as pure functions: the per diems, mileage and lodging flats of § 9 EStG and the BMF's foreign rates by year, the tax-free share of every line, whether a receipt's input tax may be deducted, and the business-meal spl

Readme

@fin.cx/expenses

German travel and expense reimbursement as pure functions: the per diems, mileage and lodging flats of § 9 EStG, the BMF's foreign rates by year, the tax-free and taxable share of every claim line, whether a receipt's input tax may be deducted, and the business-meal split. A claim is computed from what the person entered: the trip's days and places, the meals the company provided, the kilometres, the nights and the receipts. Every amount is a whole number of cents.

The package is pure: it reads no clock, keeps no state, has no dependencies and fetches nothing. Each year's rates ship with it.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

pnpm install @fin.cx/expenses

Usage

A three-day trip to Paris. The hotel's invoice to the company includes breakfast, and on the second evening the person attended a business dinner the company paid:

import { getTravelRates, perDiemsOf, lodgingFlatOf, mileageOf, taxTreatmentOf, type ITrip } from '@fin.cx/expenses';

const rates = getTravelRates(2026); // a year without rates throws ExpensesError 'travel_year_unknown'
const paris = { country: 'FR', city: 'Paris' };

const trip: ITrip = {
  startsAt: '2026-03-09T06:30', // leaving home or the first place of work, local time
  endsAt: '2026-03-11T21:15',
  days: [
    { date: '2026-03-09', placeAtMidnight: paris, placesOfWork: [paris], overnight: true },
    {
      date: '2026-03-10',
      placeAtMidnight: paris,
      placesOfWork: [paris],
      overnight: true,
      mealsProvided: [{ meal: 'breakfast' }, { meal: 'dinner' }],
    },
    { date: '2026-03-11', placesOfWork: [paris], mealsProvided: [{ meal: 'breakfast' }] },
  ],
  beyondThreeMonths: false, // the person's answer to "had you worked at this place for more than three months?"
};

const perDiems = perDiemsOf(trip, rates);
perDiems.days.map((day) => [day.date, day.kind, day.amountCents]);
// [['2026-03-09', 'arrival', 3900], ['2026-03-10', 'intermediate', 2320], ['2026-03-11', 'departure', 2740]]
// Paris: 58 € for 24 hours, 39 € for arrival and departure; breakfast −11.60 €, dinner −23.20 € (20 % and 40 % of 58 €)
perDiems.totalCents; // 8960

lodgingFlatOf({ nights: 2, place: paris }, rates).amountCents; // 31800: 159 € a night, for nights without a receipt
mileageOf({ km: 123.4, vehicle: 'car' }, rates).amountCents; // 3702: 0.30 € per km

taxTreatmentOf({ kind: 'per_diem', trip, paidCents: 12_000 }, rates);
// { allowedCents: 8960, paidCents: 12000, taxFreeCents: 8960, taxableCents: 3040, flatTaxableCents: 3040, basis: '§ 3 Nr. 16' }

Each day of the result says which place's rates it took and why (day.rate.rule), its rate before the meals (baseCents), every meal's reduction and the amount. A day that two trips share appears once, with the higher per diem: pass the trips together, perDiemsOf([firstTrip, secondTrip], rates).

Receipts:

import { inputVatOf, businessMealOf, EXPENSE_CATEGORIES } from '@fin.cx/expenses';

inputVatOf({ document: 'invoice', grossCents: 41_650, vatCents: 2_725, vatCountry: 'DE', recipient: 'person', supplyTo: 'company' });
// { deductible: false, inputVatCents: 0, reason: 'invoice_to_person' } — above 250 €: ask the hotel for an invoice to the company

inputVatOf({ document: 'ticket', grossCents: 8_990, vatCents: 588, vatCountry: 'DE', recipient: 'person', supplyTo: 'person' });
// { deductible: false, inputVatCents: 0, reason: 'supply_to_person' } — a candidate's own journey to the interview

const vat = inputVatOf({ document: 'invoice', grossCents: 18_445, vatCents: 2_945, vatCountry: 'DE', recipient: 'company', supplyTo: 'company' });
businessMealOf({
  grossCents: 18_445,
  inputVatCents: vat.inputVatCents,
  tipCents: 1_500,
  inRestaurant: true,
  participants: ['Mara Beispiel (Acme GmbH)', 'Dr. Jan Kunde (Kunde AG)'],
  occasion: 'Rahmenvertrag 2027',
});
// { expenseCents: 17000, deductibleCents: 11900, nondeductibleCents: 5100, inputVatCents: 2945, missing: [] }

EXPENSE_CATEGORIES.find((category) => category.category === 'lodging');
// { category: 'lodging', accountRole: 'travel_employee_lodging', travel: true, taxFreeBasis: '§ 3 Nr. 16', requirements: ['receipt', 'trip', 'breakfast_stated'], … }

What it computes

Per diems (perDiemsOf)

| Day | Rule | Source | | --- | --- | --- | | A day of 24 hours away | 28 € in Germany; abroad the place's full-day rate | § 9 Abs. 4a Satz 3 Nr. 1, Satz 5 EStG | | The arrival and departure day of a trip with a night away | 14 €, or the place's partial rate, without a minimum of hours | § 9 Abs. 4a Satz 3 Nr. 2 EStG; BMF 25.11.2020, Rz. 49, 51 | | A day of more than 8 hours without a night away | 14 €, or the partial rate; 8 hours or less give nothing | § 9 Abs. 4a Satz 3 Nr. 3 EStG | | An absence over midnight without a night away | Its hours added, the 14 € on the day of the greater part (on equal parts, the first day) | § 9 Abs. 4a Satz 3 Nr. 3 second half-sentence; Rz. 47 | | Meals provided | −20 % for breakfast, −40 % each for lunch and dinner of the day's full-day rate, less what the person paid for that meal, the day never below 0 | § 9 Abs. 4a Satz 8 to 10 EStG; Rz. 73 to 78 | | Two trips meeting on a day | Only the higher per diem; the day's meals reduce it by its own full-day rate | BMF 5.12.2025, p. 2; Rz. 49 | | More than three months at the place | Nothing, with the warning three_month_rule | § 9 Abs. 4a Satz 6 and 7 EStG |

The place whose rate a day takes abroad:

  • the arrival day and each day between: the place reached last before 24:00 local time; where that place is in Germany, the last place of work abroad that day (§ 9 Abs. 4a Satz 5 EStG; R 9.6 Abs. 3 Satz 3 LStR);
  • the departure day: the trip's last place of work (BMF 5.12.2025, p. 1; BMF 25.11.2020, Rz. 52);
  • a one-day trip: its last place of work abroad (BMF 5.12.2025, p. 1).

The person gives, per day, the place at 24:00 (placeAtMidnight), where they worked (placesOfWork, in order), whether they spent the night away (overnight) and the meals provided. A meal counts as provided where the employer, or a third at its instigation, gave it: a breakfast on a hotel invoice to the company, a lunch at a seminar the company booked, a business meal, a meal the company reimbursed (Rz. 64, 75, 82). It reduces the day whether it was eaten or not.

The rate of a place (rateOfPlace)

A place is a country by its ISO 3166-1 alpha-2 code and, where it matters, a city. DE is Germany.

  • A city the letter lists for its country takes the city's rate; any other city the country's rate "im Übrigen". Paris includes the départements 77, 78 and 91 to 95: give Paris for a place there. Cities are compared without regard to case, as the letter writes them (New York City, Washington, D. C., Kanarische Inseln, Hongkong).
  • Micronesia takes the Philippines' rates, and Antigua and Barbuda, Dominica, Grenada, Guyana, St. Kitts and Nevis, St. Lucia, St. Vincent and the Grenadines and Suriname take Trinidad and Tobago's (BMF 5.12.2025, p. 2).
  • An overseas or outlying territory the letter does not list takes its mother country's rate: Réunion France's, Puerto Rico that of the United States, Hong Kong (HK) China's city rate for Hongkong (p. 2; R 9.6 Abs. 3 Satz 2 LStR).
  • Any other country the letter does not list takes Luxembourg's rate.

Mileage and lodging (mileageOf, lodgingFlatOf)

  • Mileage in the person's own vehicle: 0.30 € per km by car, 0.20 € per km by another motor vehicle, the highest Wegstreckenentschädigung of the BRKG (§ 9 Abs. 1 Satz 3 Nr. 4a Satz 2 EStG; § 5 Abs. 1 and 2 BRKG; Rz. 37). Kilometres take one decimal; the amount is rounded to the cent. A bicycle gives no flat: § 5 BRKG sets none for it. A company car gives none either (R 9.5 Abs. 2 Satz 3 LStR).
  • Lodging without a receipt: 20 € a night in Germany, the BMF's flat of the place abroad. Only the employer may pay these flats, and only for a night the person paid for. They do not apply to lodging provided free, a night in a vehicle, or a sleeping car or ship's cabin unless the night began or ended elsewhere (R 9.7 Abs. 3 LStR; Rz. 128).

Tax-free and taxable (taxTreatmentOf)

| Line | Tax-free up to | Basis | | --- | --- | --- | | per_diem | the per diems computed | § 3 Nr. 16 EStG | | mileage, lodging_flat | the flat | § 3 Nr. 16 EStG | | receipt, replacement of a travel category (tickets, taxi, rental car, parking, lodging) | the receipt's gross | § 3 Nr. 16 EStG | | receipt, replacement of any other category | the receipt's gross (Auslagenersatz) | § 3 Nr. 50 EStG |

Whatever the company pays above that is wages (taxableCents). Per diems paid above the tax-free amount may be taxed at a flat 25 % up to 100 % of the per diems before meal reductions (flatTaxableCents), but not after the three months (§ 40 Abs. 2 Satz 1 Nr. 4 EStG; Rz. 58, 59, Beispiele 42, 50, 51).

Input tax (inputVatOf)

recipient is whom the document names; supplyTo is whom the supply was made to. An employee's business travel and an outlay for the company are supplies to the company (company). A guest's own travel, such as a candidate's journey to the interview, is a supply to the guest (person), which the company only reimburses: its input tax is deducted only where the invoice names the company.

| Receipt | Input tax | Reason | | --- | --- | --- | | An invoice naming the company | deductible | invoice_to_company (§ 15 Abs. 1 Satz 1 Nr. 1, § 14 Abs. 4 UStG) | | Up to 250 € gross, naming anyone or no one | deductible | small_amount_invoice (§ 33 UStDV) | | A ticket | deductible | ticket (§ 34 Abs. 1 UStDV) | | A cross-border ticket without the carrier's statement of the domestic share | not deductible | cross_border_ticket (§ 34 Abs. 2 UStDV) | | Above 250 € naming the person | not deductible | invoice_to_person | | Above 250 € naming no one | not deductible | recipient_missing | | A supply to the person, the invoice not naming the company, even a ticket or up to 250 € | not deductible | supply_to_person (§ 15 Abs. 1 Satz 1 Nr. 1 UStG) | | Another country's VAT | not deductible here | foreign_vat | | No VAT shown | none | no_vat_shown | | An Ersatzbeleg; a per diem, mileage or lodging flat | none | replacement, flat_amount |

Whether an invoice carries every detail of § 14 Abs. 4 UStG is not checked.

Business meals (businessMealOf)

70 % of the expense is deductible and 30 % is not; the tip is part of the expense, and so are the host's and the employees' shares (§ 4 Abs. 5 Satz 1 Nr. 2 EStG; R 4.10 Abs. 5 Satz 4, Abs. 6 Satz 7 EStR). The expense is the gross less the deductible input tax, plus the tip. The input tax is deducted in full (§ 15 Abs. 1a Satz 2 UStG). The deductible part is rounded down to the cent, so it never exceeds 70 %; the rest is the non-deductible part. missing names the details the Bewirtungsbeleg still lacks:

  • always the participants and the occasion;
  • outside a restaurant also the place and the day;
  • for a restaurant invoice above 250 €, the host named on the invoice (§ 4 Abs. 5 Satz 1 Nr. 2 Satz 2 and 3 EStG; R 4.10 Abs. 8 and 9 EStR).

The restaurant's invoice must be machine-made, registered and itemised (R 4.10 Abs. 8 Satz 8 and 9 EStR). This is not checked.

A business meal on a trip day is a meal provided, which reduces that day's per diem (Rz. 82).

Categories (EXPENSE_CATEGORIES)

Tickets, taxi, rental car, fuel (of a company car), parking and tolls, lodging, business meal, office supplies, postage, literature, training, software, other. Each comes with:

  • the @fin.cx/chartdata account role it books on (TExpenseAccountRole), and the role of a business meal's non-deductible part;
  • whether it is a travel cost;
  • its tax-free basis;
  • what a line must carry (receipt, trip, breakfast stated, vehicle, participants, occasion, place and day).

The allowances book on travel_employee_meals, mileage_employee and travel_employee_lodging (ALLOWANCE_ACCOUNT_ROLES).

The roles are those of @fin.cx/chartdata from 3.2.0 (TExpenseRole), which gives a role's account in a chart:

import { getExpenseAccount } from '@fin.cx/chartdata';

getExpenseAccount('SKR03', 2026, 'travel_employee_meals')?.accountNumber; // '4664'
getExpenseAccount('SKR04', 2026, 'travel_employee_meals')?.accountNumber; // '6664'

This package does not depend on chartdata at runtime; a test holds every role against both standard charts.

What is refused

Every input is checked. A refusal throws an ExpensesError with a code and the field it is about (days[2].placeAtMidnight, [1].days[0].date for the second of several trips):

| Code | Refused | | --- | --- | | travel_year_unknown | A year without rates; a trip day in a year whose rates were not passed | | invalid_place | A country that is no ISO 3166-1 alpha-2 code in capitals (or XK); an empty city | | invalid_trip | Times that are not YYYY-MM-DDTHH:MM; days that are not the trip's calendar days in order; no place at 24:00 on an arrival or intermediate day; no place of work for the departure day; a night away after the last day; three or more days without a night away; a meal listed twice on a day | | trip_end_not_after_start | A trip's end at or before its start (endsAt) | | invalid_amount | Cents that are not whole numbers from 0; kilometres with more than one decimal; nights that are not whole; a tax amount above its gross | | invalid_line | A line kind, category or vehicle this package does not know | | invalid_receipt | A document kind, recipient, supply or VAT country this package does not know |

The years

| Year | Rates | Foreign table | | --- | --- | --- | | 2026 | 28 € / 14 €, 20 € lodging in Germany, 0.30 € / 0.20 € per km | BMF letter of 5 December 2025, GZ IV C 5 - S 2353/00094/007/012, DOK COO.7005.100.3.13672232: 166 countries, 48 cities |

TRAVEL_RATE_YEARS lists them. A trip over the turn of a year takes both years' rates: perDiemsOf(trip, [getTravelRates(2026), getTravelRates(2027)]), once 2027 exists. Each year's letter comes as a minor release.

How the foreign table is made

ts/data/travelrates.<year>.ts is generated from the letter's PDF, as published on bundesfinanzministerium.de:

  1. tools/extract-pdf-rows.mjs lays out the PDF's text with pdf.js (not a dependency: pass the path of any pdfjs-dist).
  2. tools/generate-travelrates.ts reads the table's rows. The text layer splits some cells, such as Chile's "29" into "2" and "9", and some names run over two lines ("Paris sowie die / Departments 77, 78, 91 bis 95", "Vereinigte Staaten von Amerika / (USA)"). The generator joins them and reports each one.
  3. tools/check-travelrates.ts reads the same PDF a second time through Ghostscript and compares every row's name and amounts.

For 2026, both readings agree on all 214 rows. Every row was also compared by hand with the rendered pages 1 to 7 of the letter's Anlage. The changes against the previous year, printed bold in the letter, are not recorded.

ts/data/countries.ts (the ISO codes and the mother country of each territory) is generated by tools/generate-countries.ts from Wikipedia's "List of ISO 3166 country codes".

Not covered

  • Double household (doppelte Haushaltsführung), the professional drivers' allowance for nights in a vehicle (§ 9 Abs. 1 Satz 3 Nr. 5b EStG), meal benefits outside trips, and the flat 25 % tax itself (flatTaxableCents says how much it may cover). These belong to payroll.
  • The three-month period itself: it is the person's answer, not computed from earlier trips.
  • Several separate absences on one day (BMF 25.11.2020, Rz. 47, Beispiel 29): give the day's longest trip, or add the times yourself.
  • Days in the air over more than two calendar days (Austria's rate) and sea voyages (Luxembourg's rate), R 9.6 Abs. 3 Satz 4 LStR: give the place yourself.
  • Meals above 60 € taxed as wages, which reduce nothing (Rz. 86): leave them out of mealsProvided.
  • The limit on actual lodging costs after 48 months at one place of work (§ 9 Abs. 1 Satz 3 Nr. 5a Satz 4 EStG).
  • Whether an expense is reasonable, whether a meal of the company's own employees is a business meal (it is not, R 4.10 Abs. 7 EStR), and whether an invoice meets § 14 Abs. 4 UStG.
  • Changes of daylight saving time: hours away are wall-clock hours.

Sources

Unconfirmed:

  • That a crown dependency (Guernsey, Jersey, Isle of Man) or another non-sovereign ISO territory is an "Übersee- oder Außengebiet" in the letter's sense, taking its mother country's rate, is this package's reading. The letter does not list them.
  • The worked example of BMF 25.11.2020, Beispiel 50 Abwandlung 1, states a payment of 5 € for breakfast but computes with 6 €; it is not used.
  • The Einkommensteuer-Handbuch was read in its 2024 edition, the newest found; R 4.10 EStR's text there gives the host's name above 150 €, with a note raising the limit to 250 € from 2017.
  • That a guest's own travel, such as a candidate's journey to the interview, is a supply to the guest and not to the company is this package's reading of § 15 Abs. 1 Satz 1 Nr. 1 UStG (a supply "für sein Unternehmen"); confirm it with a tax advisor.
  • The UStAE's section on input tax from employees' travel receipts was not read. Not deducting the input tax of an invoice above 250 € that names the person follows § 14 Abs. 4 Satz 1 Nr. 1 UStG (the recipient's name and address).

License and Legal Information

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license.md file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at [email protected].

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.