Files
CapaKraken/samples/Dispov2/plan-country-sah-fte.md
T

6.9 KiB

Plan: Country-Driven Standard Available Hours (SAH) and FTE

Date: 2026-03-13 Status: Draft Depends on: OrgUnit hierarchy, Resource model extensions

Problem

CapaKraken currently uses a flat hoursPerDay on allocations. The chargeability reporting model requires:

  • Country-specific daily working hours (8h Germany, 9h India, variable Spain)
  • Public holidays per country AND metro city
  • FTE as a multiplier that reduces available hours (FTE 0.5 = 4h/day in an 8h country)
  • SAH (Standard Available Hours) = net hours after deducting holidays and absences

Without SAH, FTE-based chargeability calculations cannot be accurate.

Concepts

Standard Available Hours (SAH)

"Die Standard Available Hrs (SAH) bezeichnen die Netto-Arbeitszeit, also die Zeit, die nach Abzug aller Fehlzeiten tatsaechlich fuer die produktive Arbeit zur Verfuegung steht."

Formula per resource per period:

grossHoursPerDay = country.dailyWorkingHours (with Spain schedule rules)
effectiveHoursPerDay = grossHoursPerDay * resource.fte
workingDaysInPeriod = calendarDays - weekends - publicHolidays(country, metroCity)
absenceDays = vacation + illness + other absence
SAH = effectiveHoursPerDay * (workingDaysInPeriod - absenceDays)

Daily Working Hours by Country

Country Daily Hours Special Rules
Costa Rica 8h -
Germany 8h -
Hungary 8h -
India 9h -
Italy 8h -
Portugal 8h -
Spain variable Fridays always 6.5h; Mon-Thu 6.5h during 1 Jul - 15 Sep, otherwise 9h
United Kingdom 8h -

FTE Impact

  • FTE is stored with at least 2 decimal places (e.g. 0.50, 0.80)
  • FTE can change per month (contract changes, joiners/leavers)
  • Part-time 50% in Germany: 8h * 0.50 = 4h effective daily hours
  • Part-time 80% in India: 9h * 0.80 = 7.2h effective daily hours

Schema Changes

New: Country model

model Country {
  id                String     @id @default(cuid())
  code              String     @unique       // ISO 3166-1 alpha-2 (DE, IN, ES, ...)
  name              String                   // Display name
  dailyWorkingHours Float      @default(8.0) // Base daily hours
  scheduleRules     Json?      @db.JsonB     // For Spain-style variable schedules
  createdAt         DateTime   @default(now())
  updatedAt         DateTime   @updatedAt

  metroCities       MetroCity[]
  resources         Resource[]
}

New: MetroCity model

model MetroCity {
  id        String   @id @default(cuid())
  name      String                        // e.g. "Munich", "Stuttgart"
  countryId String
  country   Country  @relation(fields: [countryId], references: [id])
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  resources Resource[]

  @@unique([countryId, name])
}

Resource model extensions

model Resource {
  // ... existing fields ...

  fte          Float     @default(1.0)   // Already exists, ensure 2+ decimals
  countryId    String?
  country      Country?  @relation(fields: [countryId], references: [id])
  metroCityId  String?
  metroCity    MetroCity? @relation(fields: [metroCityId], references: [id])
}

Note: Resource already has postalCode and federalState from Phase 9 (Vacation Pro). The new countryId + metroCityId are complementary — federalState stays for German public holiday derivation, countryId drives daily hours and SAH.

Engine: SAH Calculator

Location: packages/engine/src/sah/calculator.ts

interface SAHInput {
  country: { dailyWorkingHours: number; scheduleRules?: SpainScheduleRule };
  fte: number;
  periodStart: Date;
  periodEnd: Date;
  publicHolidays: Date[];   // from country + metro city
  absenceDays: Date[];       // vacation, illness, other
}

interface SAHResult {
  grossWorkingDays: number;
  publicHolidayDays: number;
  absenceDays: number;
  netWorkingDays: number;
  effectiveHoursPerDay: number;
  standardAvailableHours: number;
}

function calculateSAH(input: SAHInput): SAHResult;

Spain schedule handling:

interface SpainScheduleRule {
  type: 'spain';
  fridayHours: number;           // 6.5
  summerPeriod: { from: string; to: string }; // "07-01" to "09-15"
  summerHours: number;           // 6.5
  regularHours: number;          // 9.0
}

function getSpainDailyHours(date: Date, rule: SpainScheduleRule): number;

Public Holidays

Phase 9 already has computeEaster() and getPublicHolidays(year, state?) for German states (Bavaria). This needs extension:

  • Move to a country-aware public holiday system
  • Support metro city specific holidays (e.g. Munich Assumption Day)
  • Seed known holidays per country from a reference table
  • Admin UI to manage/override holidays per country+city

Existing Phase 9 German holiday logic becomes the "Germany" implementation within the broader system.

FTE Monthly Tracking

The chargeability report shows FTE can vary by month per resource. Options:

  1. Simple: Single fte field on Resource, changed when contract changes (current approach)
  2. Monthly: ResourceFTEHistory model tracking FTE per month with effective date

Recommendation: Start with option 1 (single FTE field). Add history tracking as a follow-up if month-over-month FTE change reporting is needed. The current Resource model already has fte.

UI Changes

Admin: Country + Metro City Management

  • /admin/countries page
  • CRUD for countries with daily working hours and schedule rules
  • Nested metro city management per country
  • Seed data for the 8 known countries + their metro cities

Resource: Country + Metro City Assignment

  • ResourceModal.tsx gets Country dropdown (required) and Metro City dropdown (filtered by country)
  • Auto-suggest metro city based on existing postalCode where possible

Dashboard/Reports: SAH Display

  • Resource detail page shows SAH for current period
  • Chargeability report uses SAH as the denominator

Migration

  1. Create Country + MetroCity tables with seed data
  2. Backfill existing resources: derive country from federalState (Germany) or leave null
  3. Admin assigns countries to remaining resources

Dependencies

  • Public holiday system must be extended to all countries (not just German states)
  • Vacation/absence system (Phase 9) feeds into SAH absence deduction
  • Chargeability report (separate plan) consumes SAH as primary metric

Acceptance Criteria

  • Country model with daily working hours and optional schedule rules
  • Metro City model linked to Country
  • Resource linked to Country + Metro City
  • SAH calculator in engine: calculateSAH() pure function
  • Spain variable schedule correctly handled
  • FTE reduces effective daily hours: effectiveHours = dailyHours * fte
  • Public holidays extended beyond Germany to all 8 countries
  • Admin UI for Country + Metro City CRUD
  • Resource modal with Country + Metro City selection
  • Seed data for all 8 countries and their metro cities