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¶
- System Context
- Data Model
- API Design
- Multi-Tenant Strategy
- Offline & Sync Architecture
- Sequence Diagrams
- Security Architecture
- Performance Considerations
- Migration Strategy
- Integration Points
- Vertical Slice Decomposition
- Technical Risks & Mitigations
- Test Infrastructure
- 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:
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_idclaim → resolved byTenantResolutionFilter. - 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¶
- HMAC signing: QR payload HMAC-signed with tenant-specific 256-bit secret.
- Token invalidation: Regenerating QR invalidates previous token immediately.
- No QR-only auth: QR scan requires authenticated app session — QR code alone cannot record events.
- 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:
- Minimal validation path: QR validation → employee status check → event type determination → INSERT. No complex joins.
- 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.
- Prepared statements: All clock event queries use parameterized prepared statements.
- 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 statuson 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.attendancepackage 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.javabackend/src/main/java/com/altarys/papillon/modules/attendance/config/AttendanceModuleConfig.javabackend/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__*.sqlthroughV001_013__*.sqlbackend/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,PublicHolidayUpdatedin the shared events package. - Files:
backend/src/main/java/com/altarys/papillon/events/attendance/AttendanceRecorded.javabackend/src/main/java/com/altarys/papillon/events/attendance/WeeklyOvertimeComputed.javabackend/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_employeetable + event listeners forEmployeeCreated,EmployeeUpdated,EmployeeTerminated,ManagerChanged. Follows ABSMGT pattern. - Files:
backend/src/main/java/com/altarys/papillon/modules/attendance/employee/CachedEmployeeData.javabackend/src/main/java/com/altarys/papillon/modules/attendance/employee/CachedEmployeeRepository.javabackend/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
deviceIdHashon the same day. Create anomaly entry in DailyAttendanceanomaly_detailsJSONB. 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.javabackend/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/syncbatch endpoint. Process multiple offline events in one request. Per-event validation and response. Idempotency via client-generated UUIDs. - Files:
- Updates to
ClockEventController.javaandClockEventService.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-cacheendpoint returning valid {siteId, token} list for offline frontend cache. - Files:
- Updates to
SiteController.javaor newQrCacheController.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