Aller au contenu

QR Attendance (QRCONTR) — Technical Architecture

Module: ATTMGT (Application Plane) Product: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Country Scope: CI (Côte d'Ivoire) — MVP Version: 1.0 Date: 2026-03-31 Author: ARCHITECT (Claude Code) Input documents: - docs/prd/qr-attendance-prd.md (PRD+FSD v1.0) - docs/architecture/platform-saas-blueprint.md (Blueprint v2) - docs/architecture/platform-vision-arch.md (Shared Infrastructure v1.0) - docs/architecture/absence-management-arch.md (ABSMGT patterns reference) - docs/architecture/PROGRESS.md (Cross-module decisions, ADR-01 through ADR-17)


Table of Contents

  1. System Context
  2. Data Model
  3. API Design
  4. Multi-Tenant Strategy
  5. Offline & Sync Architecture
  6. Sequence Diagrams
  7. Security Architecture
  8. Performance Considerations
  9. Migration Strategy
  10. Integration Points
  11. Vertical Slice Decomposition
  12. Technical Risks & Mitigations
  13. Test Infrastructure
  14. Local Dev Environment

1. System Context

1.1 Module Positioning

QRCONTR is an Application Plane module in com.altarys.papillon.modules.attendance. It handles tenant business data: sites, QR codes, clock events, schedules, daily attendance, regularizations, remote work, overtime classification, and reporting. It is not a Control Plane module.

┌──────────────────────────────────────────────────────────────────────────┐
│                         ALTARYS PLATFORM                                │
│                                                                         │
│  ┌────────────────────────────┐    ┌─────────────────────────────────┐  │
│  │    CONTROL PLANE            │    │     APPLICATION PLANE            │  │
│  │                             │    │                                  │  │
│  │  tenant/                    │    │  attendance/ ◀── THIS MODULE    │  │
│  │    ModuleActivated     │    │    REST API endpoints            │  │
│  │    TenantSettings           │    │    AttendanceRecorded event     │  │
│  │                             │    │    WeeklyOvertimeComputed event │  │
│  │  organization/ ◀───────────────  │                                  │  │
│  │    EmployeeCreated     │    │  absence/ ◀─────────────────   │  │
│  │    EmployeeUpdated     │    │    Publishes: LeaveApproved     │  │
│  │    EmployeeTerminated  │    │    Publishes: LeaveCancelled    │  │
│  │    ManagerChanged      │    │    Publishes: PublicHolidayUpdated│ │
│  │                             │    │                                  │  │
│  │  notification/ ◀───────────────  │  payroll/ (future)              │  │
│  │    Clock anomaly alerts     │    │    Consumes: AttendanceRecorded │  │
│  │    Regularization submitted │    │    Consumes: WeeklyOvertime     │  │
│  │    Remote work updates      │    │                                  │  │
│  │                             │    │                                  │  │
│  │  audit/                     │    │                                  │  │
│  │    All mutations logged     │    │                                  │  │
│  └────────────────────────────┘    └─────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────┘

1.2 Key Constraints (Locked by ADRs & CLAUDE.md)

Constraint Source Implication for QRCONTR
ScopedValue for TenantContext ADR-01 Every service method accesses TenantContextHolder.current()
Spring Data JDBC only CLAUDE.md No lazy loading, explicit tenant_id in all queries
Event-driven read models (not Java interfaces) ADR-14 Own CachedEmployeeData table synced via CP events
BigDecimal for financials CLAUDE.md Overtime hours stored as INTEGER (minutes), no monetary calc
Application Events for cross-module ADR-14 Publish AttendanceRecorded, WeeklyOvertimeComputed
Cursor-based pagination ADR-12 List endpoints use keyset pagination
Audit trail immutable ADR-10 Clock events never modified, regularizations are additive
Client-generated UUID for offline sync Decision Idempotency key for deduplication on sync
ZXing for QR generation Decision Server-side QR code image generation
HMAC secret in tenant config Decision qr_hmac_secret column in AttendanceModuleConfig
Single module, package-based organization Decision com.altarys.papillon.modules.attendance.* with internal packages
@Scheduled + tenant iteration for batch Decision Daily auto-close + weekly overtime classification
Stored DailyAttendance table Decision Pre-computed, refreshed on clock event / regularization change
Public holidays via events from ABSMGT Decision Own cache table, new PublicHolidayUpdated

1.3 Dual Frontend Support (Design from Day One)

  • Papillon HR Suite (frontend/): SME-focused, mobile-first, warm brand (amber). QR scanning is primary UX.
  • ALTARYS ENTERPRISE (frontend-enterprise/, V2): Enterprise-focused, desktop-first, corporate brand (navy). Bulk operations priority.
  • Single backend API: Both frontends consume the same REST endpoints
  • Design tokens: Theme injection enables both brands from one codebase

2. Data Model

2.1 Entity-Relationship Diagram

erDiagram
    Site ||--o{ Clock : "records_at"
    Site ||--o{ EmployeeSiteAssignment : "assigned_to"
    WorkSchedule ||--o{ WorkScheduleDay : "has_days"
    WorkSchedule ||--o{ EmployeeScheduleAssignment : "assigned_to_employee"
    WorkSchedule ||--o{ DepartmentScheduleAssignment : "assigned_to_department"
    Clock ||--o| DailyAttendance : "aggregated_into"
    DailyAttendance ||--o| RegularizationRequest : "corrected_by"
    DailyAttendance ||--o| WeeklyOvertimeSummary : "contributes_to"
    CachedEmployeeData ||--o{ Clock : "clocks"
    CachedEmployeeData ||--o{ DailyAttendance : "daily_record"
    CachedEmployeeData ||--o{ RegularizationRequest : "requests"
    CachedEmployeeData ||--o{ RemoteWorkRequest : "requests"
    AttendanceModuleConfig ||--o{ OvertimeConfig : "configures"
    AttendanceModuleConfig ||--o{ DepartmentAttendanceConfig : "department_overrides"
    AttendancePublicHoliday ||--o{ DailyAttendance : "affects"
    CompanyNonWorkingDay ||--o{ DailyAttendance : "affects"

    Site {
        uuid id PK
        uuid tenant_id FK
        varchar name "unique within tenant"
        varchar address "optional"
        varchar qr_token "current valid HMAC token"
        timestamp qr_generated_at
        boolean is_active "default true"
        timestamp created_at
        timestamp updated_at
    }

    Clock {
        uuid id PK "client-generated UUID"
        uuid tenant_id FK
        uuid employee_id FK
        uuid site_id FK
        varchar event_type "IN, OUT, BREAK_OUT, BREAK_IN"
        timestamptz timestamp "device clock"
        timestamptz sync_timestamp "server receive time"
        varchar device_id_hash "SHA-256"
        varchar source "EMPLOYEE_SCAN, SYSTEM, HR_REGULARIZATION"
        boolean is_duplicate "default false"
        varchar auto_close_reason "nullable"
        timestamp created_at
    }

    DailyAttendance {
        uuid id PK
        uuid tenant_id FK
        uuid employee_id FK
        date attendance_date
        varchar status "PRESENT, RETARD, ABSENT, etc."
        timestamptz first_clock_in
        timestamptz last_clock_out
        int worked_minutes
        int scheduled_minutes
        int overtime_minutes
        int late_minutes
        int early_departure_minutes
        boolean is_auto_closed "default false"
        boolean has_anomaly "default false"
        jsonb anomaly_details
        uuid regularization_id "nullable FK"
        timestamp created_at
        timestamp updated_at
    }

    WorkSchedule {
        uuid id PK
        uuid tenant_id FK
        varchar name "e.g. Bureau, Entrepot"
        boolean is_active "default true"
        timestamp created_at
        timestamp updated_at
    }

    WorkScheduleDay {
        uuid id PK
        uuid schedule_id FK
        smallint day_of_week "1=Mon, 7=Sun"
        boolean is_working_day
        time start_time "conditional"
        time end_time "conditional"
        time break_start_time "optional"
        time break_end_time "optional"
        boolean break_tracking_enabled "default false"
    }

    EmployeeScheduleAssignment {
        uuid id PK
        uuid tenant_id FK
        uuid employee_id FK
        uuid schedule_id FK
        date effective_from
        date effective_to "nullable"
    }

    DepartmentScheduleAssignment {
        uuid id PK
        uuid tenant_id FK
        uuid department_id FK
        uuid schedule_id FK
        date effective_from
        date effective_to "nullable"
    }

    EmployeeSiteAssignment {
        uuid id PK
        uuid tenant_id FK
        uuid employee_id FK
        uuid site_id FK
        boolean is_primary "default true"
        date effective_from
        date effective_to "nullable"
    }

    RegularizationRequest {
        uuid id PK
        uuid tenant_id FK
        uuid employee_id FK
        date request_date
        varchar reason "OUBLI_POINTAGE, PROBLEME_TECHNIQUE, etc."
        text reason_detail "conditional"
        time corrected_clock_in "conditional"
        time corrected_clock_out "conditional"
        varchar status "PENDING, APPROVED, REJECTED"
        uuid requested_by FK
        uuid approved_by "nullable FK"
        timestamp approved_at "nullable"
        boolean is_direct_create "default false"
        uuid override_of "nullable FK"
        timestamp created_at
        timestamp updated_at
    }

    RemoteWorkRequest {
        uuid id PK
        uuid tenant_id FK
        uuid employee_id FK
        date request_date
        text reason "conditional"
        varchar status "PENDING, APPROVED, REJECTED, CANCELLED"
        uuid approved_by "nullable FK"
        timestamp approved_at "nullable"
        text rejection_reason "nullable"
        timestamp created_at
        timestamp updated_at
    }

    AttendancePublicHoliday {
        uuid id PK
        varchar country_code "CI"
        int year
        date holiday_date
        varchar name_fr
        varchar holiday_code
        boolean is_fixed
        varchar source "PLATFORM, TENANT_OVERRIDE"
        uuid tenant_id "nullable for platform-level"
        timestamp synced_at
    }

    CompanyNonWorkingDay {
        uuid id PK
        uuid tenant_id FK
        date day_date
        varchar name
        boolean is_recurring "annual repeat"
        timestamp created_at
    }

    OvertimeConfig {
        uuid id PK
        uuid tenant_id FK
        varchar band_code "BAND_1, BAND_2, BAND_3, NIGHT, SUNDAY_HOLIDAY"
        decimal weekly_threshold_start "nullable"
        decimal weekly_threshold_end "nullable"
        time night_start_time "nullable"
        time night_end_time "nullable"
        varchar country_code "CI"
        timestamp created_at
        timestamp updated_at
    }

    AttendanceModuleConfig {
        uuid id PK
        uuid tenant_id FK "one per tenant"
        varchar qr_hmac_secret "256-bit hex"
        int default_late_tolerance_minutes "default 5"
        smallint overtime_week_start_day "default 1 (Monday)"
        int min_remote_work_advance_days "default 2"
        int max_remote_work_per_week "nullable"
        int max_remote_work_per_month "nullable"
        time auto_close_time "default 23:59"
        boolean delegues_consultation_done
        date delegues_consultation_date
        uuid delegues_pv_document_id
        timestamp created_at
        timestamp updated_at
    }

    DepartmentAttendanceConfig {
        uuid id PK
        uuid tenant_id FK
        uuid department_id FK
        int late_tolerance_minutes "nullable"
        timestamp created_at
        timestamp updated_at
    }

    WeeklyOvertimeSummary {
        uuid id PK
        uuid tenant_id FK
        uuid employee_id FK
        date week_start_date
        date week_end_date
        int total_worked_minutes
        int band_1_minutes "default 0"
        int band_2_minutes "default 0"
        int band_3_minutes "default 0"
        int night_minutes "default 0"
        int sunday_holiday_minutes "default 0"
        int total_overtime_minutes
        timestamp created_at
        timestamp updated_at
    }

    CachedEmployeeData {
        uuid id PK
        uuid tenant_id FK
        uuid employee_id FK
        varchar employee_matricule
        varchar first_name
        varchar last_name
        uuid department_id "nullable"
        uuid current_manager_id "nullable"
        date hire_date
        varchar employee_status "ACTIVE, TERMINATED, etc."
        timestamp synced_at
    }

2.2 Key Indexing Strategy

Table Index Purpose
clock_event (tenant_id, employee_id, timestamp) Employee daily clock lookup
clock_event (tenant_id, site_id, timestamp) Site activity view
clock_event (tenant_id, device_id_hash, timestamp) Shared device anomaly detection
daily_attendance (tenant_id, attendance_date, status) Dashboard: today's attendance by status
daily_attendance (tenant_id, employee_id, attendance_date) UNIQUE One record per employee per day
weekly_overtime_summary (tenant_id, employee_id, week_start_date) UNIQUE One per employee per week
regularization_request (tenant_id, status, created_at) Pending regularizations list
remote_work_request (tenant_id, employee_id, request_date) Employee's remote work for date
remote_work_request (tenant_id, status, request_date) Pending requests for approvers
attendance_public_holiday (country_code, year, holiday_date) Holiday lookup by date
company_non_working_day (tenant_id, day_date) Non-working day check
employee_schedule_assignment (tenant_id, employee_id, effective_from, effective_to) Current schedule lookup
department_schedule_assignment (tenant_id, department_id, effective_from, effective_to) Department schedule lookup
employee_site_assignment (tenant_id, employee_id, is_primary) Primary site lookup
attendance_cached_employee (tenant_id, employee_id) UNIQUE Employee data cache lookup

2.3 Tenant Isolation per Entity

Entity tenant_id Column Isolation Notes
Site YES Unique name within tenant
Clock YES High volume, partition candidate for Shared profile
DailyAttendance YES One per employee per day per tenant
WorkSchedule YES Tenant-specific schedules
WorkScheduleDay Inherited via schedule_id No direct tenant_id needed (FK to WorkSchedule)
EmployeeScheduleAssignment YES Prevents cross-tenant assignment
DepartmentScheduleAssignment YES Prevents cross-tenant assignment
EmployeeSiteAssignment YES Prevents cross-tenant assignment
RegularizationRequest YES Employee data isolation
RemoteWorkRequest YES Employee data isolation
AttendancePublicHoliday NULLABLE NULL = platform-level (all tenants), non-null = tenant override
CompanyNonWorkingDay YES Tenant-specific non-working days
OvertimeConfig YES Tenant-specific overtime bands
AttendanceModuleConfig YES One row per tenant
DepartmentAttendanceConfig YES Per-department tolerance
WeeklyOvertimeSummary YES Per-employee per-week overtime
CachedEmployeeData YES Event-driven read model

3. API Design

3.1 API Namespace

All QRCONTR endpoints live under /api/v1/qrcontr/ (per PROGRESS.md namespace convention).

3.2 Endpoints

3.2.1 — Sites

Method Path Description Auth
POST /api/v1/qrcontr/sites Create site + auto-generate QR HR (P2)
GET /api/v1/qrcontr/sites List sites (paginated) HR, Manager
GET /api/v1/qrcontr/sites/{siteId} Get site detail HR, Manager
PUT /api/v1/qrcontr/sites/{siteId} Update site (name, address, active) HR (P2)
POST /api/v1/qrcontr/sites/{siteId}/regenerate-qr Regenerate QR code (invalidates old) HR (P2)
GET /api/v1/qrcontr/sites/{siteId}/qr-code Download QR code image (PNG) HR (P2)

Create Site Request:

{
  "name": "Siège Abidjan",
  "address": "Plateau, Rue du Commerce, Abidjan"
}

Create Site Response:

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Siège Abidjan",
    "address": "Plateau, Rue du Commerce, Abidjan",
    "qrGeneratedAt": "2026-03-31T08:00:00Z",
    "isActive": true,
    "createdAt": "2026-03-31T08:00:00Z"
  }
}

3.2.2 — Clock Events

Method Path Description Auth
POST /api/v1/qrcontr/clock-events Record a clock event (single) Employee (P4)
POST /api/v1/qrcontr/clock-events/sync Batch sync offline events Employee (P4)
GET /api/v1/qrcontr/clock-events/me Own clock events (paginated) Employee (P4)
GET /api/v1/qrcontr/clock-events All clock events (HR only, paginated) HR (P2)

Record Clock Event Request:

{
  "id": "client-generated-uuid",
  "siteId": "550e8400-...",
  "qrToken": "hmac-signed-token-value",
  "timestamp": "2026-03-31T08:02:00+00:00",
  "deviceIdHash": "a1b2c3d4e5f6...64chars"
}

Record Clock Event Response:

{
  "success": true,
  "data": {
    "id": "client-generated-uuid",
    "eventType": "IN",
    "timestamp": "2026-03-31T08:02:00+00:00",
    "siteName": "Siège Abidjan",
    "status": "RECORDED"
  }
}

Batch Sync Request:

{
  "events": [
    {
      "id": "uuid-1",
      "siteId": "...",
      "qrToken": "...",
      "timestamp": "2026-03-31T08:02:00+00:00",
      "deviceIdHash": "..."
    },
    {
      "id": "uuid-2",
      "siteId": "...",
      "qrToken": "...",
      "timestamp": "2026-03-31T17:30:00+00:00",
      "deviceIdHash": "..."
    }
  ]
}

Batch Sync Response:

{
  "success": true,
  "data": {
    "synced": 2,
    "duplicatesSkipped": 0,
    "failed": 0,
    "results": [
      { "id": "uuid-1", "status": "SYNCED", "eventType": "IN" },
      { "id": "uuid-2", "status": "SYNCED", "eventType": "OUT" }
    ]
  }
}

3.2.3 — Work Schedules

Method Path Description Auth
POST /api/v1/qrcontr/schedules Create work schedule HR (P2)
GET /api/v1/qrcontr/schedules List schedules HR (P2)
GET /api/v1/qrcontr/schedules/{scheduleId} Get schedule with days HR (P2)
PUT /api/v1/qrcontr/schedules/{scheduleId} Update schedule HR (P2)
POST /api/v1/qrcontr/schedules/{scheduleId}/assign-employee Assign to employee HR (P2)
POST /api/v1/qrcontr/schedules/{scheduleId}/assign-department Assign to department HR (P2)
GET /api/v1/qrcontr/employees/{employeeId}/schedule Get effective schedule for employee HR, Manager, Employee (own)

Create Schedule Request:

{
  "name": "Bureau Standard",
  "days": [
    { "dayOfWeek": 1, "isWorkingDay": true, "startTime": "08:00", "endTime": "17:00", "breakStartTime": "12:00", "breakEndTime": "13:00", "breakTrackingEnabled": false },
    { "dayOfWeek": 2, "isWorkingDay": true, "startTime": "08:00", "endTime": "17:00", "breakStartTime": "12:00", "breakEndTime": "13:00", "breakTrackingEnabled": false },
    { "dayOfWeek": 6, "isWorkingDay": true, "startTime": "08:00", "endTime": "12:00" },
    { "dayOfWeek": 7, "isWorkingDay": false }
  ]
}

3.2.4 — Daily Attendance

Method Path Description Auth
GET /api/v1/qrcontr/attendance/today Today's dashboard data HR, Manager
GET /api/v1/qrcontr/attendance/daily Daily attendance list (by date, department, status) HR, Manager
GET /api/v1/qrcontr/attendance/me Own attendance history Employee (P4)
GET /api/v1/qrcontr/employees/{employeeId}/attendance Single employee's attendance HR, Manager (team), Employee (own)

Today's Dashboard Response:

{
  "success": true,
  "data": {
    "date": "2026-03-31",
    "summary": {
      "total": 45,
      "present": 32,
      "late": 5,
      "absent": 3,
      "remote work": 3,
      "enConge": 1,
      "ferie": 0,
      "incomplet": 1,
      "absentPending": 0
    },
    "anomalies": [
      { "employeeId": "...", "employeeName": "KONÉ Aminata", "type": "SHARED_DEVICE", "details": "Appareil partagé avec DIALLO Moussa" }
    ],
    "autoClosedYesterday": [
      { "employeeId": "...", "employeeName": "TOURÉ Ibrahim", "date": "2026-03-30", "autoClosedAt": "17:00" }
    ]
  }
}

Filtering Parameters (common to list endpoints): - ?date=2026-03-31 — specific date - ?dateFrom=2026-03-01&dateTo=2026-03-31 — date range - ?departmentId=uuid — filter by department - ?siteId=uuid — filter by site - ?status=RETARD,ABSENT — filter by status (comma-separated) - ?cursor=<encoded>&size=20 — keyset pagination (ADR-12)

3.2.5 — Regularizations

Method Path Description Auth
POST /api/v1/qrcontr/regularizations Create regularization request Employee, Manager
POST /api/v1/qrcontr/regularizations/direct HR direct-create (auto-approved) HR (P2)
GET /api/v1/qrcontr/regularizations List regularizations (filterable by status) HR, Manager (team)
GET /api/v1/qrcontr/regularizations/{id} Get regularization detail HR, Manager, Employee (own)
POST /api/v1/qrcontr/regularizations/{id}/approve Approve regularization Manager (team), HR
POST /api/v1/qrcontr/regularizations/{id}/reject Reject regularization Manager (team), HR
POST /api/v1/qrcontr/regularizations/{id}/override HR override a manager-approved one HR (P2)

Create Regularization Request:

{
  "employeeId": "uuid",
  "date": "2026-03-30",
  "reason": "OUBLI_POINTAGE",
  "reasonDetail": null,
  "correctedClockIn": "08:05",
  "correctedClockOut": "17:30"
}

3.2.6 — Remote work

Method Path Description Auth
POST /api/v1/qrcontr/remote-work Request remote work Employee (P4)
GET /api/v1/qrcontr/remote-work List requests (filterable) HR, Manager (team), Employee (own)
GET /api/v1/qrcontr/remote-work/{id} Get request detail HR, Manager, Employee (own)
POST /api/v1/qrcontr/remote-work/{id}/approve Approve request Manager (N+1), HR
POST /api/v1/qrcontr/remote-work/{id}/reject Reject request Manager (N+1), HR
POST /api/v1/qrcontr/remote-work/{id}/cancel Cancel own pending request Employee (own)

3.2.7 — Configuration

Method Path Description Auth
GET /api/v1/qrcontr/config Get module config for tenant HR (P2)
PUT /api/v1/qrcontr/config Update module config HR (P2)
GET /api/v1/qrcontr/config/overtime Get overtime band configuration HR (P2)
PUT /api/v1/qrcontr/config/overtime Update overtime bands HR (P2)
GET /api/v1/qrcontr/config/departments/{deptId} Get department-level config HR (P2)
PUT /api/v1/qrcontr/config/departments/{deptId} Update department config HR (P2)

3.2.8 — Public Holidays & Non-Working Days

Method Path Description Auth
GET /api/v1/qrcontr/holidays List public holidays (by year) HR, Manager
POST /api/v1/qrcontr/company-non-working-days Add company non-working day HR (P2)
GET /api/v1/qrcontr/company-non-working-days List company non-working days HR, Manager
DELETE /api/v1/qrcontr/company-non-working-days/{id} Remove company non-working day HR (P2)

3.2.9 — Reports & Export

Method Path Description Auth
GET /api/v1/qrcontr/reports/monthly-attendance Monthly attendance sheet HR (P2), Comptable (P3)
GET /api/v1/qrcontr/reports/department-summary Monthly department summary HR, Manager (own dept)
GET /api/v1/qrcontr/reports/overtime Weekly overtime report HR (P2), Comptable (P3)
GET /api/v1/qrcontr/reports/export Export as Excel/CSV HR (P2), Comptable (P3)

Report Parameters: - ?employeeId=uuid — single employee (for monthly attendance) - ?departmentId=uuid — department filter - ?month=2026-03 — month (YYYY-MM) - ?weekStart=2026-03-24 — week start date (for overtime) - ?format=xlsx|csv — export format (for /export)

3.2.10 — Employee Site Assignments

Method Path Description Auth
POST /api/v1/qrcontr/site-assignments Assign employee to site HR (P2)
GET /api/v1/qrcontr/employees/{employeeId}/site-assignments Get employee's site assignments HR, Manager, Employee (own)
DELETE /api/v1/qrcontr/site-assignments/{id} Remove site assignment HR (P2)

3.2.11 — QR Validation Cache (for offline frontend)

Method Path Description Auth
GET /api/v1/qrcontr/qr-cache Get valid {siteId, tenantId, token} list for offline cache Employee (P4)

Response:

{
  "success": true,
  "data": {
    "tenantId": "uuid",
    "sites": [
      { "siteId": "uuid", "token": "hmac-token", "name": "Siège Abidjan" },
      { "siteId": "uuid", "token": "hmac-token", "name": "Entrepôt Yopougon" }
    ],
    "generatedAt": "2026-03-31T08:00:00Z"
  }
}

3.2.12 — Module Activation Compliance

Method Path Description Auth
POST /api/v1/qrcontr/compliance/delegues Record délégués consultation acknowledgement HR (P2)
GET /api/v1/qrcontr/compliance/delegues Get consultation status HR (P2)

4. Multi-Tenant Strategy

4.1 All 4 DB Profiles

QRCONTR follows the platform standard from platform-vision-arch.md Section 13:

Profile How tenant_id is Used Schema Strategy
Shared WHERE tenant_id = ? in every query Single schema, single DB. RLS as safety net. All clock_event rows in one table.
Schema-per-tenant tenant_id still in queries + separate schema per tenant Flyway per-schema. SET search_path at request start.
DB-per-tenant tenant_id still in queries + separate database Dynamic DataSource routing.
BYO-DB tenant_id still present (for consistency) + tenant's own database Connection details stored encrypted in platform DB. Tenant provides JDBC URL.

4.2 Design-for-BYO-DB-First

Per the mandate, every design decision assumes BYO-DB: - No cross-tenant queries: Reporting never aggregates across tenants. - No foreign keys to platform tables: tenant_id is a logical reference, never a DB FK constraint. - No shared sequences: All IDs are UUIDs (client-generated for Clock, server-generated for others). - Module config is tenant-local: AttendanceModuleConfig lives in the tenant DB, not the platform DB. The HMAC secret is in the tenant DB. - Public holidays: AttendancePublicHoliday is seeded into each tenant's DB at module activation. Platform-level holidays have tenant_id = NULL (only in Shared profile); in BYO-DB, all holidays are copied into the tenant's DB.

4.3 QR Code Tenant Isolation

The QR payload includes tenantId. At scan time: 1. Client decodes QR → extracts tenantId, siteId, token 2. Client verifies tenantId matches authenticated user's tenant 3. Server validates HMAC using tenant's secret 4. Server verifies siteId belongs to the authenticated tenant

A Tenant A employee scanning a Tenant B QR code is rejected at step 2 (client) or step 4 (server). Zero cross-tenant leakage.

4.4 Scheduled Batch Tenant Iteration

For daily auto-close and weekly overtime classification:

@Scheduled(cron = "0 59 23 * * *") // Daily at 23:59
void runAutoCloseForAllTenants() {
    List<TenantInfo> tenants = platformJdbcClient.sql(
        "SELECT t.id, t.country_code, t.db_profile FROM tenant t " +
        "JOIN tenant_module tm ON t.id = tm.tenant_id " +
        "WHERE tm.module_code = 'QRCONTR' AND tm.status = 'ACTIVE'"
    ).query(TenantInfo.class).list();

    for (TenantInfo tenant : tenants) {
        TenantContextHolder.setFallback(
            new TenantContext(tenant.id(), tenant.countryCode(), tenant.dbProfile()));
        try {
            autoCloseService.processAutoClose(tenant.id(), LocalDate.now());
        } finally {
            TenantContextHolder.clearFallback();
        }
    }
}

5. Offline & Sync Architecture

5.1 What's Available Offline

Feature Offline? Storage Notes
QR scanning + clock event YES IndexedDB Client-generated UUID, queued FIFO
QR validation YES IndexedDB cache Cached {siteId, token} tuples
Own attendance history YES IndexedDB cache Last-synced data
Regularization request YES IndexedDB Drafted offline, synced
Remote work request YES IndexedDB Drafted offline, synced
Dashboard (HR/Manager) NO Requires server aggregation
Reports/Export NO Server-generated
Config changes NO HR operations require connectivity

5.2 IndexedDB Schema

// attendance-offline-store.ts
interface OfflineClock {
  id: string;           // Client-generated UUID
  siteId: string;
  qrToken: string;
  timestamp: string;    // ISO 8601 with timezone
  deviceIdHash: string;
  isSynced: boolean;
  syncAttempts: number;
  lastSyncError?: string;
  createdAt: string;
}

interface OfflineRegularization {
  id: string;
  employeeId: string;
  date: string;
  reason: string;
  reasonDetail?: string;
  correctedClockIn?: string;
  correctedClockOut?: string;
  isSynced: boolean;
  createdAt: string;
}

interface OfflineRemoteWorkRequest {
  id: string;
  date: string;
  reason?: string;
  isSynced: boolean;
  createdAt: string;
}

interface QrValidationCache {
  tenantId: string;
  sites: Array<{ siteId: string; token: string; name: string }>;
  lastRefreshed: string;
}

interface AttendanceHistoryCache {
  employeeId: string;
  records: DailyAttendanceRecord[];
  lastSynced: string;
}

5.3 Sync Protocol

sequenceDiagram
    participant App as Papillon App
    participant SW as Service Worker
    participant IDB as IndexedDB
    participant API as Backend API

    Note over App: Employee scans QR code
    App->>App: Generate UUID, capture timestamp
    App->>IDB: Store OfflineClock (isSynced: false)
    App->>App: Show "Pointage enregistré — 08:02"

    alt Online
        App->>API: POST /clock-events {id, siteId, ...}
        API-->>App: 200 OK {eventType: IN}
        App->>IDB: Update isSynced: true
    else Offline
        Note over App: Event stays in IndexedDB queue
    end

    Note over SW: Connectivity restored
    SW->>IDB: Query WHERE isSynced = false ORDER BY createdAt
    loop For each pending event (FIFO)
        SW->>API: POST /clock-events/sync {events: [...]}
        alt Success
            API-->>SW: 200 OK {synced: N}
            SW->>IDB: Update all isSynced: true
        else Server error
            API-->>SW: 500
            SW->>SW: Exponential backoff (max 3 retries)
        else Validation failure
            API-->>SW: 422 {error: "QR code invalide"}
            SW->>IDB: Mark syncFailed, store error
            SW->>App: Notify user
        end
    end

5.4 Conflict Resolution

Scenario Resolution
Same event ID already on server Server returns success, no duplicate created (idempotency)
QR token expired while offline Server rejects event. Employee sees "Pointage non synchronisé, contactez les RH." HR creates direct regularization.
Device clock drift Server records both timestamp (device) and syncTimestamp (server receive). Audit trail preserves both. No auto-correction — HR reviews if drift > threshold.
Offline regularization conflicts with server state Server applies validation at sync time. If date already has an approved regularization, sync returns 409 Conflict.

5.5 QR Validation Cache Refresh

  • Cache refreshed on every successful sync
  • Cache refreshed on app launch (if online)
  • Cache TTL: 7 days (covers extended offline periods)
  • If QR not in cache (new site added while offline): reject with "QR code non reconnu hors ligne"

6. Sequence Diagrams

6.1 Employee Clock-In (Online)

sequenceDiagram
    participant Emp as Employee App
    participant API as Clock API
    participant QR as QR Validator
    participant CE as Clock Service
    participant DA as DailyAttendance Service
    participant Audit as Audit Module

    Emp->>Emp: Scan QR code, decode JSON payload
    Emp->>Emp: Client-side: verify tenantId matches own
    Emp->>API: POST /clock-events {id, siteId, qrToken, timestamp, deviceIdHash}

    API->>QR: validateQrToken(tenantId, siteId, qrToken)
    QR->>QR: Verify HMAC signature with tenant secret
    QR->>QR: Verify siteId exists and is active
    QR->>QR: Verify token matches current site token

    alt Validation fails
        QR-->>API: Invalid
        API-->>Emp: 422 "QR code invalide. Contactez les RH."
    end

    API->>CE: recordClockEvent(event)
    CE->>CE: Check employee status (CachedEmployeeData)

    alt Employee TERMINATED
        CE-->>API: Rejected
        API-->>Emp: 403 "Compte désactivé — contactez les RH."
    end

    CE->>CE: Determine event type (IN or OUT) based on current state
    CE->>CE: Check duplicate (same type within 15 min → discard)
    CE->>CE: Check shared device anomaly (same deviceIdHash, different employee)
    CE->>CE: Save Clock to DB

    CE->>DA: recalculateDailyAttendance(employeeId, date)
    DA->>DA: Compute status, worked hours, late/early flags
    DA->>DA: Upsert DailyAttendance record

    CE->>Audit: logClockEvent(event)

    API-->>Emp: 200 OK {eventType: "IN", timestamp: "08:02"}

6.2 Missed Clock-Out Auto-Close (Batch)

sequenceDiagram
    participant Sched as @Scheduled Job
    participant PDB as Platform DB
    participant ACS as AutoClose Service
    participant DA as DailyAttendance Service
    participant Audit as Audit Module

    Sched->>PDB: Get tenants with QRCONTR active
    loop For each tenant
        Sched->>Sched: Set TenantContext (ThreadLocal fallback)
        Sched->>ACS: processAutoClose(tenantId, today)
        ACS->>ACS: Query employees with open clock-in (IN without OUT)
        loop For each employee with open clock-in
            ACS->>ACS: Get employee's schedule for today
            alt Has schedule
                ACS->>ACS: Create synthetic clock-out at scheduled end time
                ACS->>ACS: Save Clock (source=SYSTEM, autoCloseReason=MISSED_CLOCK_OUT)
                ACS->>DA: recalculateDailyAttendance (is_auto_closed=true, no overtime)
                ACS->>Audit: logAutoClose(employeeId, date, scheduledEndTime)
            else No schedule
                ACS->>DA: Flag as INCOMPLET "Pas d'horaire configuré"
            end
        end
        Sched->>Sched: Clear TenantContext
    end

6.3 Regularization Approval

sequenceDiagram
    participant HR as HR / Manager
    participant API as Regularization API
    participant RS as Regularization Service
    participant DA as DailyAttendance Service
    participant Notif as Notification Module
    participant Audit as Audit Module

    HR->>API: POST /regularizations/{id}/approve
    API->>RS: approveRegularization(id)
    RS->>RS: Validate approver authority (manager for team, HR for all)
    RS->>RS: Load RegularizationRequest, verify PENDING status
    RS->>RS: Update status → APPROVED, set approver + approved_at

    RS->>DA: recalculateWithRegularization(employeeId, date, correctedIn, correctedOut)
    DA->>DA: Recompute DailyAttendance with corrected times
    DA->>DA: Recalculate late/early/overtime flags
    DA->>DA: Update DailyAttendance record

    RS->>Notif: RegularizationApproved(employeeId, date)
    RS->>Audit: logRegularizationApproval(id, approver, before, after)

    API-->>HR: 200 OK

6.4 Weekly Overtime Classification (Batch)

sequenceDiagram
    participant Sched as @Scheduled Job (Monday 00:30)
    participant OCS as Overtime Classification Service
    participant DA as DailyAttendance Repository
    participant OC as OvertimeConfig Repository
    participant WOS as WeeklyOvertimeSummary Repository
    participant Event as Application Events

    Sched->>Sched: Set TenantContext for each active tenant
    Sched->>OCS: classifyWeeklyOvertime(tenantId, previousWeek)
    OCS->>OC: Load overtime band config for tenant
    OCS->>DA: Get all DailyAttendance for previous week

    loop For each employee
        OCS->>OCS: Sum weekly worked minutes
        OCS->>OCS: Classify excess into BAND_1, BAND_2, BAND_3
        OCS->>OCS: Identify NIGHT hours (21:00-05:00 window)
        OCS->>OCS: Identify SUNDAY_HOLIDAY hours
        OCS->>WOS: Upsert WeeklyOvertimeSummary

        OCS->>Event: publish WeeklyOvertimeComputed
    end

6.5 Remote work Request Flow

sequenceDiagram
    participant Emp as Employee
    participant API as Remote work API
    participant TS as Remote work Service
    participant Config as ModuleConfig
    participant Notif as Notification Module
    participant DA as DailyAttendance Service

    Emp->>API: POST /remote work {date, reason?}
    API->>TS: createRequest(date, reason)
    TS->>Config: Get minAdvanceNoticeDays, maxPerWeek, maxPerMonth
    TS->>TS: Validate advance notice (reason required if short-notice)
    TS->>TS: Check weekly/monthly limits (warn if exceeded, don't block)
    TS->>TS: Save RemoteWorkRequest (PENDING)
    TS->>Notif: RemoteWorkRequestSubmitted(employeeId, date)

    API-->>Emp: 201 Created {status: PENDING, limitWarning?: "Limite de 2 jours/semaine atteinte"}

    Note over Emp,DA: Later: Manager approves
    API->>TS: approveRequest(id)
    TS->>TS: Update status → APPROVED
    TS->>DA: upsertDailyAttendance(employeeId, date, status=REMOTE_WORK)
    TS->>Notif: RemoteWorkApproved(employeeId, date)

7. Security Architecture

7.1 Authentication

  • All API endpoints require a valid JWT from Keycloak.
  • JWT contains tenant_id claim → resolved by TenantResolutionFilter.
  • QR scan only valid from within the authenticated Papillon app session (FR-QR-090).

7.2 Authorization (RBAC)

Role Scope Capabilities
P1 (DG) All employees View dashboard (read-only), view reports
P2 (HR) All employees Full CRUD on sites, schedules, config. Approve/reject all regularizations. Direct-create regularizations. Override manager approvals. Export reports.
P3 (Comptable) All employees Read-only: attendance data, overtime reports, exports
P4 (Employee) Own data only Clock in/out, view own history, request regularization, request remote work
N+1 (Manager) Direct reports View team dashboard, approve team regularizations, approve team remote work, view team reports

7.3 Data Access Rules

// Authorization service pattern
@Service
public class AttendanceAuthorizationService {

    public boolean canViewEmployee(UUID requesterId, UUID targetEmployeeId) {
        // P2 (HR): all employees in tenant
        // N+1 (Manager): direct reports only (via CachedEmployeeData.current_manager_id)
        // P4 (Employee): own data only (requesterId == targetEmployeeId)
        // P3 (Comptable): all employees (read-only)
        // P1 (DG): all employees (read-only)
    }

    public boolean canApproveRegularization(UUID approverId, UUID employeeId) {
        // HR: any employee
        // Manager: direct reports only
    }

    public boolean canExportReports(UUID userId) {
        // HR (P2) and Comptable (P3) only
    }
}

7.4 QR Code Security

  1. HMAC signing: QR payload HMAC-signed with tenant-specific 256-bit secret.
  2. Token invalidation: Regenerating QR invalidates previous token immediately.
  3. No QR-only auth: QR scan requires authenticated app session — QR code alone cannot record events.
  4. Device ID hashing: Device UUID is SHA-256 hashed; raw UUID never stored.
// QR validation
public boolean validateQrPayload(UUID tenantId, UUID siteId, String token) {
    String secret = configRepository.getHmacSecret(tenantId);
    String expectedToken = HmacUtils.hmacSha256Hex(secret, siteId + ":" + tenantId);
    return MessageDigest.isEqual(
        expectedToken.getBytes(UTF_8),
        token.getBytes(UTF_8)
    ); // Constant-time comparison to prevent timing attacks
}

7.5 Audit Trail

Every mutation is logged via the platform Audit Module:

Action What's Logged
Clock event recorded employeeId, siteId, eventType, timestamp, source
Clock event rejected employeeId, siteId, reason (terminated, invalid QR)
Duplicate discarded employeeId, discardedEventId, keptEventId, reason
QR code regenerated siteId, regeneratedBy, reason (optional)
Regularization created/approved/rejected/overridden requestId, actor, before/after values
Remote work created/approved/rejected/cancelled requestId, actor
Config changed field, oldValue, newValue, changedBy
Report exported reportType, parameters, exportedBy
Auto-close triggered employeeId, date, scheduledEndTime

8. Performance Considerations

8.1 Clock Event Write Path (Critical: < 1s)

The clock-in response time target is < 1 second. Optimizations:

  1. Minimal validation path: QR validation → employee status check → event type determination → INSERT. No complex joins.
  2. Async DailyAttendance update: The DailyAttendance recalculation can be async (Application Event within the module) after returning 200 to the employee. But for simplicity in MVP, we do it synchronously — the recalculation is simple arithmetic.
  3. Prepared statements: All clock event queries use parameterized prepared statements.
  4. Connection pool: HikariCP with per-tenant connection limits for BYO-DB.

8.2 Dashboard (< 5s)

  • DailyAttendance is pre-computed (stored table). Dashboard reads are simple SELECT WHERE date = today AND tenant_id = ?.
  • No on-the-fly aggregation from Clock.
  • Summary counts use GROUP BY status on DailyAttendance.

8.3 Batch Jobs

Job Frequency Estimated Load Strategy
Auto-close Daily 23:59 ~50 employees per tenant with open clock-ins Single query per tenant. Process sequentially.
Weekly overtime Monday 00:30 ~200 employees per tenant × 7 days Batch query DailyAttendance for entire week. Compute in memory. Batch INSERT/UPDATE WeeklyOvertimeSummary.

8.4 Report Generation (< 5s single, < 15s department)

  • Monthly attendance: JOIN DailyAttendance + CachedEmployeeData for one employee, 30 rows. Trivial.
  • Department summary: Aggregate query on DailyAttendance grouped by employee.
  • Overtime report: Direct read from WeeklyOvertimeSummary.

8.5 Scale Targets

Metric Target Strategy
4M daily clock events (platform) Shared profile Partitioning by tenant_id (future), index on (tenant_id, employee_id, timestamp)
500 simultaneous clock-ins (per tenant) Shift change Simple INSERT — no contention. PostgreSQL handles this easily.
100K tenants × 20 employees Platform total Tenant iteration for batch jobs is O(N_tenants). Acceptable at this scale.

8.6 Caching

Data Cache Strategy TTL
AttendanceModuleConfig Application-level cache (Caffeine) per tenant 5 min
OvertimeConfig Application-level cache per tenant 5 min
Public holiday dates Application-level cache per country+year 24 hours
Employee schedule (effective) Application-level cache per employee 5 min (invalidated on assignment change)
QR HMAC secrets Application-level cache per tenant 5 min (invalidated on regeneration)
@Bean
public CacheManager attendanceCacheManager() {
    CaffeineCacheManager manager = new CaffeineCacheManager();
    manager.setCaffeine(Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(Duration.ofMinutes(5)));
    return manager;
}

9. Migration Strategy

9.1 Flyway Migration Naming

V{module_version}_{sequence}__{description}.sql

Example:
V001_001__create_attendance_site_table.sql
V001_002__create_attendance_work_schedule_tables.sql
V001_003__create_attendance_clock_event_table.sql
V001_004__create_attendance_daily_attendance_table.sql
V001_005__create_attendance_regularization_table.sql
V001_006__create_attendance_remote_work_table.sql
V001_007__create_attendance_overtime_tables.sql
V001_008__create_attendance_config_tables.sql
V001_009__create_attendance_cached_employee_table.sql
V001_010__create_attendance_public_holiday_table.sql
V001_011__create_attendance_company_non_working_day_table.sql
V001_012__seed_ci_overtime_defaults.sql
V001_013__seed_ci_public_holidays_2026_2027.sql

9.2 Multi-Profile Migration

Profile Migration Path
Shared Run migrations once on the shared tenant DB. All tables include tenant_id. RLS policies added.
Schema-per-tenant Run migrations per schema via Flyway.configure().schemas(tenantSchema). Same SQL files.
DB-per-tenant Run migrations per database. Same SQL files.
BYO-DB Run migrations on the tenant-provided database. Same SQL files. Includes data seeding (overtime defaults, public holidays).

9.3 Seed Data

Overtime band defaults (CI — temporarily validated):

-- V001_012__seed_ci_overtime_defaults.sql
-- These are seeded at module activation, not at DB migration
-- The activation service calls this via application logic
INSERT INTO overtime_config (id, tenant_id, band_code, weekly_threshold_start, weekly_threshold_end, country_code)
VALUES
  (gen_random_uuid(), :tenantId, 'BAND_1', 40.00, 46.00, 'CI'),
  (gen_random_uuid(), :tenantId, 'BAND_2', 46.00, 48.00, 'CI'),
  (gen_random_uuid(), :tenantId, 'BAND_3', 48.00, NULL,   'CI');

INSERT INTO overtime_config (id, tenant_id, band_code, night_start_time, night_end_time, country_code)
VALUES (gen_random_uuid(), :tenantId, 'NIGHT', '21:00', '05:00', 'CI');

INSERT INTO overtime_config (id, tenant_id, band_code, country_code)
VALUES (gen_random_uuid(), :tenantId, 'SUNDAY_HOLIDAY', 'CI');

Public holidays (CI 2026-2027):

-- V001_013__seed_ci_public_holidays_2026_2027.sql
-- Fixed CI holidays (same every year)
-- Moveable Islamic holidays (year-specific dates)
-- Seeded into attendance_public_holiday with tenant_id = NULL (platform-level)


10. Integration Points

10.1 Events Consumed

Event Source Handler Effect
EmployeeCreated Control Plane EmployeeCacheEventListener Insert into attendance_cached_employee. Employee has no schedule yet — clock events recorded but no flags.
EmployeeUpdated Control Plane EmployeeCacheEventListener Update attendance_cached_employee (name, department, manager). May change schedule inheritance if department changed.
EmployeeTerminated Control Plane EmployeeCacheEventListener Update status to TERMINATED. Future scans rejected (FR-QR-013).
EmployeeOffboarded Control Plane EmployeeCacheEventListener Same as Terminated.
ManagerChanged Control Plane EmployeeCacheEventListener Update current_manager_id in cache. Affects regularization/remote work approval scope.
LeaveApproved Absence Module LeaveEventListener Set DailyAttendance status = EN_CONGE for leave dates. Suppress absence flag.
LeaveCancelled Absence Module LeaveEventListener Remove EN_CONGE status. Re-evaluate attendance for those dates.
PublicHolidayUpdated Absence Module HolidayEventListener Refresh attendance_public_holiday cache table. (NEW EVENT — must be added to shared event catalog)

10.2 Events Published

Event Payload Consumer Trigger
AttendanceRecorded { tenantId, employeeId, date, firstClockIn, lastClockOut, workedMinutes, overtimeMinutes, band1Minutes, band2Minutes, band3Minutes, nightMinutes, sundayHolidayMinutes, lateMinutes, earlyDepartureMinutes, status, isAutoClose } Payroll (future) End-of-day processing or after regularization approval changes a day's calculations
WeeklyOvertimeComputed { tenantId, employeeId, weekStartDate, weekEndDate, totalOvertimeMinutes, band1Minutes, band2Minutes, band3Minutes, nightMinutes, sundayHolidayMinutes } Payroll (future) End-of-week overtime classification

10.3 New Shared Event Definition

// com.altarys.papillon.events.attendance.AttendanceRecorded
public record AttendanceRecorded(
    UUID tenantId,
    UUID employeeId,
    LocalDate date,
    OffsetDateTime firstClockIn,
    OffsetDateTime lastClockOut,
    int workedMinutes,
    int overtimeMinutes,
    int band1Minutes,
    int band2Minutes,
    int band3Minutes,
    int nightMinutes,
    int sundayHolidayMinutes,
    int lateMinutes,
    int earlyDepartureMinutes,
    String status,
    boolean isAutoClose
) {}

// com.altarys.papillon.events.attendance.WeeklyOvertimeComputed
public record WeeklyOvertimeComputed(
    UUID tenantId,
    UUID employeeId,
    LocalDate weekStartDate,
    LocalDate weekEndDate,
    int totalOvertimeMinutes,
    int band1Minutes,
    int band2Minutes,
    int band3Minutes,
    int nightMinutes,
    int sundayHolidayMinutes
) {}

// com.altarys.papillon.events.holiday.PublicHolidayUpdated (NEW)
public record PublicHolidayUpdated(
    UUID tenantId,           // nullable for platform-wide
    String countryCode,
    int year,
    List<HolidayEntry> holidays
) {
    public record HolidayEntry(LocalDate date, String nameFr, String holidayCode, boolean isFixed) {}
}

10.4 Module Gating

Every QRCONTR endpoint checks that the module is active for the tenant:

@Component
public class ModuleGateInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, ...) {
        if (request.getRequestURI().startsWith("/api/v1/qrcontr/")) {
            UUID tenantId = TenantContextHolder.current().tenantId();
            boolean active = platformJdbcClient.sql(
                "SELECT 1 FROM tenant_module WHERE tenant_id = ? AND module_code = 'QRCONTR' AND status = 'ACTIVE'"
            ).param(tenantId).query(Boolean.class).optional().isPresent();
            if (!active) {
                throw new ModuleNotActiveException("QRCONTR");
            }
        }
        return true;
    }
}

10.5 Standalone Mode (without Absence Module)

QRCONTR is designed to work independently of ABSMGT: - If ABSMGT is not active: LeaveApproved is never received. Days without clock events are flagged as "Absent sans justification" (no "En congé" distinction). - If ABSMGT is later activated: events start flowing, and future days are correctly classified. - Public holidays: if ABSMGT is not active, QRCONTR seeds its own holiday data at module activation from platform defaults.


11. Vertical Slice Decomposition

Tier 0 (Serial — Must merge before Tier 1)

QR-T0-001: Module Skeleton + Test Infrastructure

  • Scope: Create the com.altarys.papillon.modules.attendance package structure. Spring Modulith module declaration. Add ZXing dependency to build.gradle. TestContainers setup for attendance-specific tests. Mock beans for Keycloak auth.
  • Files:
  • backend/src/main/java/com/altarys/papillon/modules/attendance/package-info.java
  • backend/src/main/java/com/altarys/papillon/modules/attendance/config/AttendanceModuleConfig.java
  • backend/build.gradle (add ZXing dependency)
  • backend/src/test/java/com/altarys/papillon/modules/attendance/AttendanceModuleIntegrationTest.java
  • Estimated Complexity: S
  • Dependencies: Control Plane (for TenantContext, module gating)

QR-T0-002: Flyway Migrations (All Tables)

  • Scope: Create all Flyway migration files for QRCONTR tables. Verify migrations run on all 4 DB profiles. Seed CI overtime defaults and public holidays.
  • Files:
  • backend/src/main/resources/db/migration/V001_001__*.sql through V001_013__*.sql
  • backend/src/test/java/com/altarys/papillon/modules/attendance/migration/AttendanceMigrationTest.java
  • Estimated Complexity: M
  • Dependencies: QR-T0-001

QR-T0-003: Shared Event Contracts

  • Scope: Define AttendanceRecorded, WeeklyOvertimeComputed, PublicHolidayUpdated in the shared events package.
  • Files:
  • backend/src/main/java/com/altarys/papillon/events/attendance/AttendanceRecorded.java
  • backend/src/main/java/com/altarys/papillon/events/attendance/WeeklyOvertimeComputed.java
  • backend/src/main/java/com/altarys/papillon/events/holiday/PublicHolidayUpdated.java
  • Estimated Complexity: S
  • Dependencies: None (shared events package exists)

QR-T0-004: CachedEmployeeData Event Listener

  • Scope: Own attendance_cached_employee table + event listeners for EmployeeCreated, EmployeeUpdated, EmployeeTerminated, ManagerChanged. Follows ABSMGT pattern.
  • Files:
  • backend/src/main/java/com/altarys/papillon/modules/attendance/employee/CachedEmployeeData.java
  • backend/src/main/java/com/altarys/papillon/modules/attendance/employee/CachedEmployeeRepository.java
  • backend/src/main/java/com/altarys/papillon/modules/attendance/employee/EmployeeCacheEventListener.java
  • Test file
  • Estimated Complexity: M
  • Dependencies: QR-T0-002

Tier 1 (Parallel — after Tier 0 merges)

QR-T1-001: Site CRUD + QR Code Generation

  • Scope: Full site lifecycle: create, list, get, update, deactivate. QR code generation (ZXing) with HMAC signing. QR regeneration (invalidates old token). QR image download (PNG).
  • Files:
  • .../attendance/site/Site.java, SiteRepository.java, SiteService.java, SiteController.java
  • .../attendance/site/QrCodeGenerator.java (ZXing wrapper)
  • .../attendance/site/QrCodeValidator.java (HMAC validation)
  • DTOs, test files
  • Estimated Complexity: M
  • Dependencies: QR-T0-002
  • Split candidate: Could split QR generation from site CRUD, but they're tightly coupled — keep together.

QR-T1-002: Work Schedule CRUD + Assignment

  • Scope: Create/list/update schedules with per-day configuration. Assign schedule to employee or department. Get effective schedule for an employee (employee override > department default).
  • Files:
  • .../attendance/schedule/WorkSchedule.java, WorkScheduleDay.java, WorkScheduleRepository.java
  • .../attendance/schedule/ScheduleAssignmentService.java
  • .../attendance/schedule/ScheduleController.java
  • DTOs, test files
  • Estimated Complexity: M
  • Dependencies: QR-T0-002, QR-T0-004

QR-T1-003: Module Configuration

  • Scope: Module config CRUD (late tolerance, week start day, remote work limits, auto-close time). Department-level config. Overtime band configuration CRUD. HMAC secret generation at module activation.
  • Files:
  • .../attendance/config/AttendanceConfig.java, OvertimeConfig.java, DepartmentConfig.java
  • .../attendance/config/ConfigRepository.java, ConfigService.java, ConfigController.java
  • DTOs, test files
  • Estimated Complexity: M
  • Dependencies: QR-T0-002

QR-T1-004: Public Holiday + Company Non-Working Day

  • Scope: Public holiday cache table + event listener for PublicHolidayUpdated. Company non-working day CRUD. Holiday lookup service for date checks.
  • Files:
  • .../attendance/holiday/AttendancePublicHoliday.java, CompanyNonWorkingDay.java
  • .../attendance/holiday/HolidayRepository.java, HolidayService.java, HolidayController.java
  • .../attendance/holiday/HolidayEventListener.java
  • Seed data for CI 2026-2027
  • Test files
  • Estimated Complexity: M
  • Dependencies: QR-T0-002, QR-T0-003

Tier 2 (After Tier 1 core is merged)

QR-T2-001: Clock Event Recording + QR Validation

  • Scope: Core clock-in/out flow. QR validation (HMAC). Event type determination (IN/OUT based on state). Duplicate detection (15-min window). Terminated employee rejection. Device ID capture. DailyAttendance upsert (basic: status + worked_minutes).
  • Files:
  • .../attendance/clockevent/Clock.java, ClockEventRepository.java, ClockEventService.java
  • .../attendance/clockevent/ClockEventController.java
  • .../attendance/clockevent/EventTypeDeterminer.java (IN vs OUT logic)
  • .../attendance/clockevent/DuplicateDetector.java
  • .../attendance/daily/DailyAttendance.java, DailyAttendanceRepository.java, DailyAttendanceService.java
  • DTOs, test files
  • Estimated Complexity: L
  • Dependencies: QR-T1-001 (site + QR validation), QR-T1-002 (schedule for worked hours calc), QR-T0-004 (employee status)
  • Split candidate: Yes — could split into:
  • QR-T2-001a: Clock event recording + QR validation (no daily calc)
  • QR-T2-001b: DailyAttendance computation (status, worked hours, late/early flags)

QR-T2-002: Employee Site Assignment

  • Scope: Assign employee to site (primary/secondary). List assignments. Remove assignment. Non-primary site scan flagging.
  • Files:
  • .../attendance/site/EmployeeSiteAssignment.java, SiteAssignmentRepository.java, SiteAssignmentService.java, SiteAssignmentController.java
  • DTOs, test files
  • Estimated Complexity: S
  • Dependencies: QR-T1-001, QR-T0-004

Tier 3 (After clock events work)

QR-T3-001: Late Arrival + Early Departure Detection

  • Scope: Late detection with tolerance (company default + department override). Early departure detection. Update DailyAttendance status (RETARD). Late/early minutes calculation.
  • Files:
  • .../attendance/daily/LateEarlyDetectionService.java
  • Updates to DailyAttendanceService.java
  • Test files
  • Estimated Complexity: M
  • Dependencies: QR-T2-001

QR-T3-002: Missed Clock-Out Auto-Close (Batch)

  • Scope: @Scheduled daily job. Iterate active tenants with QRCONTR. Find open clock-ins past scheduled end. Create synthetic SYSTEM clock-out. Mark DailyAttendance as auto-closed. No overtime for auto-closed periods.
  • Files:
  • .../attendance/batch/AutoCloseScheduledJob.java
  • .../attendance/batch/AutoCloseService.java
  • Test files (with fixed clock for testing)
  • Estimated Complexity: M
  • Dependencies: QR-T2-001, QR-T1-002

QR-T3-003: No-Show Detection

  • Scope: Daily batch: identify employees with no clock events on a working day. Cross-reference with holidays, leave (LeaveApproved), remote work. Set DailyAttendance status = ABSENT/EN_CONGE/FERIE/REMOTE_WORK/ABSENT_PENDING.
  • Files:
  • .../attendance/batch/NoShowDetectionService.java
  • .../attendance/daily/LeaveEventListener.java (LeaveApproved, LeaveCancelled)
  • Test files
  • Estimated Complexity: M
  • Dependencies: QR-T2-001, QR-T1-004 (holidays), QR-T0-003 (events)

QR-T3-004: Shared Device Anomaly Detection

  • Scope: Detect when two different employees clock in with the same deviceIdHash on the same day. Create anomaly entry in DailyAttendance anomaly_details JSONB. Display on dashboard.
  • Files:
  • .../attendance/clockevent/SharedDeviceDetector.java
  • Updates to ClockEventService.java
  • Test files
  • Estimated Complexity: S
  • Dependencies: QR-T2-001

Tier 4 (After detection rules work)

QR-T4-001: Regularization Workflow

  • Scope: Full regularization lifecycle: create request, approve, reject, HR direct-create, HR override. DailyAttendance recalculation on approval. Additive record (original preserved).
  • Files:
  • .../attendance/regularization/RegularizationRequest.java, RegularizationRepository.java
  • .../attendance/regularization/RegularizationService.java, RegularizationController.java
  • DTOs, test files
  • Estimated Complexity: L
  • Dependencies: QR-T2-001 (DailyAttendance recalc)
  • Split candidate: Yes — could split into:
  • QR-T4-001a: Create + approve/reject (employee/manager path)
  • QR-T4-001b: HR direct-create + override

QR-T4-002: Remote work Workflow

  • Scope: Full remote work lifecycle: request, approve, reject, cancel. Advance notice validation. Weekly/monthly limit warnings. Day-of status handling (ABSENT_PENDING for unapproved). DailyAttendance integration.
  • Files:
  • .../attendance/remotework/RemoteWorkRequest.java, RemoteWorkRepository.java
  • .../attendance/remotework/RemoteWorkService.java, RemoteWorkController.java
  • DTOs, test files
  • Estimated Complexity: L
  • Dependencies: QR-T2-001 (DailyAttendance), QR-T1-003 (config for limits)
  • Split candidate: Yes — could split into:
  • QR-T4-002a: Request + approve/reject + cancel
  • QR-T4-002b: Limit checks + day-of status handling

QR-T4-003: Weekly Overtime Classification (Batch)

  • Scope: @Scheduled weekly job (Monday 00:30). Calculate total weekly hours per employee. Classify into BAND_1/2/3 based on tenant config. Identify NIGHT hours. Identify SUNDAY_HOLIDAY hours. Publish WeeklyOvertimeComputed.
  • Files:
  • .../attendance/overtime/OvertimeClassificationService.java
  • .../attendance/overtime/OvertimeClassificationScheduledJob.java
  • .../attendance/overtime/WeeklyOvertimeSummary.java, WeeklyOvertimeSummaryRepository.java
  • Test files (with configurable bands)
  • Estimated Complexity: L
  • Dependencies: QR-T2-001 (DailyAttendance), QR-T1-003 (overtime config), QR-T1-004 (holidays)

Tier 5 (After all core logic)

QR-T5-001: Daily Attendance Dashboard API

  • Scope: Today's dashboard endpoint. Summary counts. Employee list with filtering (department, site, status). Anomaly list. Auto-closed from yesterday. Manager: team-only filter. Keyset pagination.
  • Files:
  • .../attendance/dashboard/DashboardController.java, DashboardService.java
  • DTOs, test files
  • Estimated Complexity: M
  • Dependencies: QR-T3-001, QR-T3-002, QR-T3-003, QR-T3-004

QR-T5-002: Monthly Attendance Report

  • Scope: Monthly attendance sheet (per employee). Department summary. Pre-formatted for display + export.
  • Files:
  • .../attendance/report/MonthlyAttendanceReportService.java
  • .../attendance/report/DepartmentSummaryReportService.java
  • .../attendance/report/ReportController.java
  • DTOs, test files
  • Estimated Complexity: M
  • Dependencies: QR-T2-001, QR-T3-001, QR-T4-001 (regularization info)

QR-T5-003: Overtime Report for Payroll

  • Scope: Weekly overtime report per employee. Band breakdown. Export-ready format.
  • Files:
  • .../attendance/report/OvertimeReportService.java
  • Updates to ReportController.java
  • Test files
  • Estimated Complexity: S
  • Dependencies: QR-T4-003

QR-T5-004: Report Export (Excel + CSV)

  • Scope: Export all reports to Excel (.xlsx) and CSV (.csv). UTF-8 with BOM. French date/time/decimal format. Apache POI for Excel.
  • Files:
  • .../attendance/report/ExcelExportService.java
  • .../attendance/report/CsvExportService.java
  • backend/build.gradle (add Apache POI dependency)
  • Test files
  • Estimated Complexity: M
  • Dependencies: QR-T5-002, QR-T5-003

QR-T5-005: Batch Sync Endpoint

  • Scope: /clock-events/sync batch endpoint. Process multiple offline events in one request. Per-event validation and response. Idempotency via client-generated UUIDs.
  • Files:
  • Updates to ClockEventController.java and ClockEventService.java
  • .../attendance/clockevent/BatchSyncService.java
  • Test files
  • Estimated Complexity: M
  • Dependencies: QR-T2-001

QR-T5-006: Module Activation Compliance

  • Scope: Délégués du personnel compliance reminder endpoints. Record consultation acknowledgement. Threshold awareness (11+ employees).
  • Files:
  • .../attendance/compliance/ComplianceController.java, ComplianceService.java
  • Test files
  • Estimated Complexity: S
  • Dependencies: QR-T1-003 (config), QR-T0-004 (employee count)

QR-T5-007: QR Validation Cache Endpoint

  • Scope: /qr-cache endpoint returning valid {siteId, token} list for offline frontend cache.
  • Files:
  • Updates to SiteController.java or new QrCacheController.java
  • Test files
  • Estimated Complexity: S
  • Dependencies: QR-T1-001

Vertical Slice Summary

Slice Tier Complexity Dependencies
QR-T0-001: Module Skeleton 0 S CP
QR-T0-002: Flyway Migrations 0 M T0-001
QR-T0-003: Shared Events 0 S
QR-T0-004: CachedEmployeeData 0 M T0-002
QR-T1-001: Site CRUD + QR Gen 1 M T0-002
QR-T1-002: Schedule CRUD + Assignment 1 M T0-002, T0-004
QR-T1-003: Module Config 1 M T0-002
QR-T1-004: Holidays + Non-Working Days 1 M T0-002, T0-003
QR-T2-001: Clock Event + QR Validation 2 L T1-001, T1-002, T0-004
QR-T2-002: Employee Site Assignment 2 S T1-001, T0-004
QR-T3-001: Late/Early Detection 3 M T2-001
QR-T3-002: Auto-Close Batch 3 M T2-001, T1-002
QR-T3-003: No-Show Detection 3 M T2-001, T1-004, T0-003
QR-T3-004: Shared Device Anomaly 3 S T2-001
QR-T4-001: Regularization Workflow 4 L T2-001
QR-T4-002: Remote work Workflow 4 L T2-001, T1-003
QR-T4-003: Weekly Overtime Classification 4 L T2-001, T1-003, T1-004
QR-T5-001: Dashboard API 5 M T3-*
QR-T5-002: Monthly Report 5 M T2-001, T3-001, T4-001
QR-T5-003: Overtime Report 5 S T4-003
QR-T5-004: Report Export 5 M T5-002, T5-003
QR-T5-005: Batch Sync 5 M T2-001
QR-T5-006: Compliance 5 S T1-003, T0-004
QR-T5-007: QR Cache Endpoint 5 S T1-001

Total: 24 slices (4× Tier 0, 4× Tier 1, 2× Tier 2, 4× Tier 3, 3× Tier 4, 7× Tier 5)


12. Technical Risks & Mitigations

# Risk Impact Probability Mitigation
R1 Device clock manipulation — Employees set phone clock forward/backward to fake timestamps HIGH MEDIUM Record both timestamp (device) and syncTimestamp (server). Dashboard flag for drift > 5 minutes. HR reviews flagged events. V2: NTP-validated timestamps.
R2 QR code screenshot sharing — Employee shares QR code photo for buddy punching MEDIUM HIGH Authenticated session required. Device ID hash tracks device. Shared device anomaly detection flags same device for different employees. V2: token rotation.
R3 Offline event replay attacks — Tampered local data synced to server LOW LOW Client-generated UUID prevents duplicate creation. HMAC validation still required at sync. Server-side re-validation of all business rules.
R4 Clock event volume at shift change — 500+ employees scan within 15 minutes MEDIUM MEDIUM Clock event INSERT is simple (no contention). PostgreSQL handles 500 concurrent INSERTs. DailyAttendance recalc can be async if needed.
R5 Overtime calculation correctness — CI overtime bands temporarily validated (TV-QR-002) HIGH MEDIUM Bands are fully configurable per tenant. Incorrect defaults can be corrected without code change. Document validation status prominently.
R6 Public holiday calendar accuracy — Islamic holidays are moveable, dates change yearly MEDIUM HIGH Admin-maintained calendar. Annual update process. HR can add custom holidays. Event-driven sync from ABSMGT.
R7 BYO-DB migration failures — Tenant provides a database that already has conflicting table names MEDIUM LOW Namespace all tables with attendance_ prefix. Migration scripts use IF NOT EXISTS. Health check at module activation.
R8 Offline data loss — IndexedDB cleared (user clears browser data) HIGH LOW Document that clearing browser data loses unsynced events. Auto-sync runs every 30s when online. Warning in app if unsynced events > 8 hours old.
R9 Weekly overtime recalculation after retroactive regularization — Regularization for a day in a past week changes overtime classification MEDIUM MEDIUM After regularization approval, check if the affected day's week has a WeeklyOvertimeSummary. If so, re-run weekly classification for that week.
R10 Concurrent regularization approval — Two approvers approve the same regularization simultaneously LOW LOW Optimistic locking via version field on RegularizationRequest. Second approval gets 409 Conflict.

13. Test Infrastructure

13.1 TestContainers

QRCONTR uses the existing TestContainers PostgreSQL setup from the platform. No additional containers needed.

// Reuse existing test infrastructure
@SpringBootTest
@Testcontainers
class AttendanceIntegrationTest extends AbstractTenantIntegrationTest {
    // Uses shared PostgreSQL TestContainer
    // Uses withTenant() for TenantContext setup
}

13.2 Mock/Stub Beans

Bean Mock Strategy Why
Keycloak JWT Decoder @MockBean JwtDecoder Tests don't need actual Keycloak. Auth is tested at integration level.
TenantContextHolder withTenant(tenantId) helper Set ThreadLocal fallback for test.
Notification Module @MockBean NotificationEventPublisher QRCONTR publishes events — verify publication, don't test notification delivery.
Audit Module @MockBean AuditEventPublisher or real (if available) Verify audit events are published.

13.3 Test Support Classes

// AttendanceTestFixtures.java
public class AttendanceTestFixtures {

    public static Site createSite(UUID tenantId, String name) { ... }
    public static WorkSchedule createStandardSchedule(UUID tenantId) { ... }  // Mon-Fri 08:00-17:00
    public static Clock createClockIn(UUID tenantId, UUID employeeId, UUID siteId, OffsetDateTime timestamp) { ... }
    public static Clock createClockOut(UUID tenantId, UUID employeeId, UUID siteId, OffsetDateTime timestamp) { ... }
    public static CachedEmployeeData createActiveEmployee(UUID tenantId, UUID employeeId, String name) { ... }
    public static OvertimeConfig createCiDefaults(UUID tenantId) { ... }
}

13.4 Sample Test Pattern

@SpringBootTest
@Testcontainers
class ClockEventServiceTest extends AbstractTenantIntegrationTest {

    @Autowired ClockEventService clockEventService;
    @Autowired DailyAttendanceRepository dailyAttendanceRepo;

    @Test
    void shouldRecordClockInAndCreateDailyAttendance() {
        withTenant(TEST_TENANT_ID, () -> {
            // Given
            var employee = fixtures.createActiveEmployee(TEST_TENANT_ID, empId, "KONÉ Aminata");
            var site = fixtures.createSite(TEST_TENANT_ID, "Siège");
            var schedule = fixtures.createStandardSchedule(TEST_TENANT_ID);
            fixtures.assignScheduleToEmployee(empId, schedule.id());

            // When
            var event = clockEventService.recordClockEvent(
                empId, site.id(), "valid-hmac-token",
                OffsetDateTime.parse("2026-03-31T08:02:00Z"),
                "device-hash-abc"
            );

            // Then
            assertThat(event.eventType()).isEqualTo("IN");
            var daily = dailyAttendanceRepo.findByEmployeeAndDate(TEST_TENANT_ID, empId, LocalDate.of(2026, 3, 31));
            assertThat(daily).isPresent();
            assertThat(daily.get().status()).isEqualTo("PRESENT");
        });
    }

    @Test
    void shouldRejectTerminatedEmployee() {
        withTenant(TEST_TENANT_ID, () -> {
            var employee = fixtures.createEmployee(TEST_TENANT_ID, empId, "TERMINATED");
            var site = fixtures.createSite(TEST_TENANT_ID, "Siège");

            assertThatThrownBy(() ->
                clockEventService.recordClockEvent(empId, site.id(), "valid-token",
                    OffsetDateTime.now(), "device-hash")
            ).isInstanceOf(EmployeeTerminatedException.class);
        });
    }

    @Test
    void shouldDeduplicateWithin15Minutes() {
        withTenant(TEST_TENANT_ID, () -> {
            var employee = fixtures.createActiveEmployee(TEST_TENANT_ID, empId, "Test");
            var site = fixtures.createSite(TEST_TENANT_ID, "Siège");
            var t1 = OffsetDateTime.parse("2026-03-31T08:00:00Z");
            var t2 = t1.plusMinutes(10);

            clockEventService.recordClockEvent(empId, site.id(), "token", t1, "device");
            clockEventService.recordClockEvent(empId, site.id(), "token", t2, "device");

            var events = clockEventRepo.findByEmployeeAndDate(TEST_TENANT_ID, empId, LocalDate.of(2026, 3, 31));
            assertThat(events).hasSize(2); // Both stored, but second marked as duplicate
            assertThat(events.get(1).isDuplicate()).isTrue();
        });
    }
}

13.5 Fixed Clock for Batch Tests

// Use java.time.Clock injection for testability
@Service
public class AutoCloseService {
    private final Clock clock;

    public AutoCloseService(Clock clock) { this.clock = clock; }

    public void processAutoClose(UUID tenantId, LocalDate date) {
        // Uses clock.instant() instead of Instant.now()
    }
}

// In tests:
@TestConfiguration
class TestClockConfig {
    @Bean
    Clock testClock() {
        return Clock.fixed(
            Instant.parse("2026-03-31T23:59:00Z"),
            ZoneId.of("Africa/Abidjan")
        );
    }
}

14. Local Dev Environment

14.1 Docker Compose (existing)

QRCONTR uses the existing docker-compose.yml services (PostgreSQL 16, Keycloak 26.1, pgAdmin 4). No additional Docker services needed.

14.2 Required Env Vars

All existing env vars from .env.example apply. No new env vars needed for QRCONTR.

14.3 Verification Command

# 1. Start infrastructure
docker compose up -d

# 2. Run migrations
./gradlew flywayMigrate

# 3. Start backend
./gradlew bootRun

# 4. Verify QRCONTR endpoints
# (Requires auth token from Keycloak)
TOKEN=$(curl -s -X POST http://localhost:8180/realms/altarys/protocol/openid-connect/token \
  -d "grant_type=password&client_id=papillon-app&[email protected]&password=dev" | jq -r '.access_token')

# Module config (should 404 or 403 if module not active — correct behavior)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/qrcontr/config

# 5. Run tests
./gradlew test --tests "*attendance*"

Appendix A: Package Structure

backend/src/main/java/com/altarys/papillon/modules/attendance/
├── package-info.java              # Spring Modulith module declaration
├── config/
│   ├── AttendanceConfig.java      # Module config entity
│   ├── OvertimeConfig.java        # Overtime band entity
│   ├── DepartmentConfig.java      # Dept-level overrides
│   ├── ConfigRepository.java
│   ├── ConfigService.java
│   └── ConfigController.java
├── site/
│   ├── Site.java
│   ├── SiteRepository.java
│   ├── SiteService.java
│   ├── SiteController.java
│   ├── EmployeeSiteAssignment.java
│   ├── SiteAssignmentRepository.java
│   ├── SiteAssignmentService.java
│   ├── SiteAssignmentController.java
│   ├── QrCodeGenerator.java       # ZXing wrapper
│   └── QrCodeValidator.java       # HMAC validation
├── schedule/
│   ├── WorkSchedule.java
│   ├── WorkScheduleDay.java
│   ├── EmployeeScheduleAssignment.java
│   ├── DepartmentScheduleAssignment.java
│   ├── ScheduleRepository.java
│   ├── ScheduleAssignmentService.java
│   └── ScheduleController.java
├── clockevent/
│   ├── Clock.java
│   ├── ClockEventRepository.java
│   ├── ClockEventService.java
│   ├── ClockEventController.java
│   ├── EventTypeDeterminer.java
│   ├── DuplicateDetector.java
│   ├── SharedDeviceDetector.java
│   └── BatchSyncService.java
├── daily/
│   ├── DailyAttendance.java
│   ├── DailyAttendanceRepository.java
│   ├── DailyAttendanceService.java
│   ├── LateEarlyDetectionService.java
│   └── LeaveEventListener.java
├── regularization/
│   ├── RegularizationRequest.java
│   ├── RegularizationRepository.java
│   ├── RegularizationService.java
│   └── RegularizationController.java
├── remote work/
│   ├── RemoteWorkRequest.java
│   ├── RemoteWorkRepository.java
│   ├── RemoteWorkService.java
│   └── RemoteWorkController.java
├── overtime/
│   ├── WeeklyOvertimeSummary.java
│   ├── WeeklyOvertimeSummaryRepository.java
│   ├── OvertimeClassificationService.java
│   └── OvertimeClassificationScheduledJob.java
├── holiday/
│   ├── AttendancePublicHoliday.java
│   ├── CompanyNonWorkingDay.java
│   ├── HolidayRepository.java
│   ├── HolidayService.java
│   ├── HolidayController.java
│   └── HolidayEventListener.java
├── employee/
│   ├── CachedEmployeeData.java
│   ├── CachedEmployeeRepository.java
│   └── EmployeeCacheEventListener.java
├── batch/
│   ├── AutoCloseScheduledJob.java
│   ├── AutoCloseService.java
│   └── NoShowDetectionService.java
├── report/
│   ├── MonthlyAttendanceReportService.java
│   ├── DepartmentSummaryReportService.java
│   ├── OvertimeReportService.java
│   ├── ExcelExportService.java
│   ├── CsvExportService.java
│   └── ReportController.java
├── compliance/
│   ├── ComplianceService.java
│   └── ComplianceController.java
├── dashboard/
│   ├── DashboardService.java
│   └── DashboardController.java
└── security/
    └── AttendanceAuthorizationService.java

Appendix B: ADR Summary (Decisions Made for QRCONTR)

ADR Decision Rationale
ADR-QR-01 Own CachedEmployeeData table Zero coupling to ABSMGT. Module works standalone. Follows ADR-14.
ADR-QR-02 Stored DailyAttendance table O(1) dashboard reads. Recomputed on clock event / regularization changes.
ADR-QR-03 Public holidays via events from ABSMGT New PublicHolidayUpdated. Standalone fallback with platform-seeded holidays.
ADR-QR-04 @Scheduled + tenant iteration for batch Same pattern as ABSMGT's monthly accrual. Idempotent, simple, MVP-appropriate.
ADR-QR-05 ZXing for QR generation Mature, Apache 2.0, widely used. Generates PNG/SVG from QR payloads.
ADR-QR-06 Client-generated UUID for sync Idempotency key. Proven offline-first pattern.
ADR-QR-07 HMAC secret in tenant config table Simple, works across all 4 DB profiles. 256-bit random value.
ADR-QR-08 Packages in single module Internal cohesion via packages. No sub-module ceremony.

End of Technical Architecture — QR Attendance (QRCONTR) v1.0