Skip to content

feat(tax): split_component rule type + Germany Berlin profile#91

Merged
telivity-otaip merged 1 commit into
mainfrom
feat/split-component-tax
Apr 23, 2026
Merged

feat(tax): split_component rule type + Germany Berlin profile#91
telivity-otaip merged 1 commit into
mainfrom
feat/split-component-tax

Conversation

@telivity-otaip
Copy link
Copy Markdown
Collaborator

Summary

Germany requires split-rate VAT on composite charges: 7% on the food portion of breakfast, 19% on the beverage portion. The existing tax engine applied one rate per charge type — this adds a `split_component` rule type that taxes a configurable percentage of the charge at its own rate, so multiple rules stack to the correct effective rate.

Example

€20 breakfast in Berlin:

Rule Type Rate Split% Tax
Food VAT split_component 7% 70% €20 × 0.70 × 0.07 = €0.98
Beverage VAT split_component 19% 30% €20 × 0.30 × 0.19 = €1.14
Total €2.12

Changes

  • Schema: `'split_component'` enum value + `splitPercentage numeric(5,2)` column
  • Migration: hand-authored `0001_split_component_tax.sql` (drizzle-kit generate broken in this repo; `push-schema.ts` mirrors the migration)
  • TaxService: `calculateTaxes` + `backCalculateFromInclusive` handle split_component via decimal.js (no parseFloat)
  • DTOs: `splitPercentage` with `@ValidateIf(type==='split_component')`, `@Min(0.01)`, `@Max(100)`
  • Seed: Germany Berlin profile with 5 rules (accommodation VAT, city tax w/ business exemption, food/beverage split, standard VAT)

Test plan

  • `pnpm -w build` green
  • `pnpm -w typecheck` green
  • `pnpm -w test` — 582/582 passing (+7 tests: split calc, back-calc, Berlin integration, business exemption, non-100% splits)
  • `pnpm --filter @telivityhaip/api lint` — 0 errors

Refs #87.

🤖 Generated with Claude Code

Germany requires split-rate VAT on composite charges — 7% on the food
portion of breakfast, 19% on the beverage portion. The existing tax
engine applied one rate per charge type; this change introduces a rule
type that taxes a percentage of the charge at its own rate, so multiple
split_component rules on the same charge type stack to produce the
correct effective rate.

Schema
- taxRuleTypeEnum: new value 'split_component'
- tax_rules: new splitPercentage numeric(5,2) column (nullable)
- Hand-authored migration 0001_split_component_tax.sql (drizzle-kit
  generate is broken in this repo; push-schema.ts mirrors the migration)

TaxService
- calculateTaxes: for split_component, tax = base × (splitPercentage/100)
  × (rate/100). Respects compounding (uses runningBase when isCompounding)
- backCalculateFromInclusive: split_component contributes
  (rate × splitPercentage / 100) to the effective percentage rate
- All math via decimal.js on string numerics (no parseFloat)

DTOs
- splitPercentage?: number with @ValidateIf(type==='split_component'),
  @min(0.01), @max(100), @IsNumber({maxDecimalPlaces: 2})

Seed
- Germany Berlin profile with 5 rules:
  1. Accommodation VAT 7% (room, room_upgrade)
  2. City Tax Übernachtungsteuer 5% (room, business-exempt)
  3. Food VAT 7% split_component 70% (breakfast/meal/half_board/full_board)
  4. Beverage VAT 19% split_component 30% (same)
  5. Standard VAT 19% (minibar/spa/parking/telephone/laundry)
- Seeded inactive (demo property is US-based, one active profile/property)

Tests: +7 in tax.service.spec.ts — split calc, back-calc, Berlin room +
breakfast + minibar, business exemption, non-100% splits.

582/582 passing, build + typecheck + lint clean.

Refs #87

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@telivity-otaip telivity-otaip merged commit c2de47b into main Apr 23, 2026
3 checks passed
@telivity-otaip telivity-otaip deleted the feat/split-component-tax branch April 23, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant