Aller au contenu

Story QR-002: Site Management + QR Code Generation

@assignee: @angeisaac-ui

Module: qr-attendance Slice: QR-T1-001 + QR-T0-004 (partial) Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 2 Depends on: QR-001 Can develop concurrently with: QR-003, QR-004, QR-005 Merge order: Before QR-006, QR-008 Estimated complexity: M PRD User Stories: US-QR-008


Objective

Allow HR to manage attendance sites (create, update, deactivate) and generate QR codes for each site. Each site gets a unique HMAC-signed QR code that employees will scan for clock-in/out. QR codes can be regenerated (invalidating the previous one) and downloaded/printed with brand labeling. This story also creates the module configuration table (with HMAC secret), the cached employee data table, and the site table.


Backend Scope

Flyway Migrations

V001_001__attendance_module_config.sql:

CREATE TABLE attendance_module_config (
    id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL,
    qr_hmac_secret VARCHAR(255) NOT NULL,
    default_late_tolerance_minutes INTEGER NOT NULL DEFAULT 5,
    overtime_week_start_day SMALLINT NOT NULL DEFAULT 1,
    min_remote_work_advance_days INTEGER NOT NULL DEFAULT 2,
    max_remote_work_per_week INTEGER,
    max_remote_work_per_month INTEGER,
    auto_close_time TIME NOT NULL DEFAULT '23:59',
    delegues_consultation_done BOOLEAN NOT NULL DEFAULT false,
    delegues_consultation_date DATE,
    delegues_pv_document_id UUID,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(tenant_id)
);

V001_002__attendance_cached_employee.sql:

CREATE TABLE attendance_cached_employee (
    id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL,
    employee_id UUID NOT NULL,
    employee_matricule VARCHAR(50),
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    department_id UUID,
    current_manager_id UUID,
    hire_date DATE,
    employee_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
    synced_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(tenant_id, employee_id)
);
CREATE INDEX idx_cached_employee_tenant ON attendance_cached_employee(tenant_id, employee_id);

V001_003__attendance_site.sql:

CREATE TABLE attendance_site (
    id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL,
    name VARCHAR(100) NOT NULL,
    address VARCHAR(500),
    qr_token VARCHAR(255) NOT NULL,
    qr_generated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(tenant_id, name)
);
CREATE INDEX idx_site_tenant ON attendance_site(tenant_id);

Entities & Records

AttendanceModuleConfig (tenant DB):

Field Type Constraints
id UUID PK
tenantId UUID NOT NULL, UNIQUE
qrHmacSecret String NOT NULL, 256-bit hex, auto-generated
defaultLateToleranceMinutes int NOT NULL, default 5
overtimeWeekStartDay short NOT NULL, default 1 (Monday)
minRemoteWorkAdvanceDays int NOT NULL, default 2
maxRemoteWorkPerWeek Integer Nullable
maxRemoteWorkPerMonth Integer Nullable
autoCloseTime LocalTime NOT NULL, default 23:59
deleguesConsultationDone boolean NOT NULL, default false
deleguesConsultationDate LocalDate Nullable
deleguesPvDocumentId UUID Nullable
createdAt OffsetDateTime NOT NULL
updatedAt OffsetDateTime NOT NULL

CachedEmployeeData (tenant DB):

Field Type Constraints
id UUID PK
tenantId UUID NOT NULL
employeeId UUID NOT NULL, UNIQUE with tenantId
employeeMatricule String Nullable, max 50
firstName String NOT NULL, max 100
lastName String NOT NULL, max 100
departmentId UUID Nullable
currentManagerId UUID Nullable
hireDate LocalDate Nullable
employeeStatus String NOT NULL, default "ACTIVE"
syncedAt OffsetDateTime NOT NULL

Site (tenant DB):

Field Type Constraints
id UUID PK
tenantId UUID NOT NULL
name String NOT NULL, max 100, UNIQUE with tenantId
address String Nullable, max 500
qrToken String NOT NULL
qrGeneratedAt OffsetDateTime NOT NULL
isActive boolean NOT NULL, default true
createdAt OffsetDateTime NOT NULL
updatedAt OffsetDateTime NOT NULL

DTOs:

public record CreateSiteRequest(
    @NotBlank @Size(max = 100) String name,
    @Size(max = 500) String address
) {}

public record UpdateSiteRequest(
    @Size(max = 100) String name,
    @Size(max = 500) String address,
    Boolean isActive
) {}

public record SiteResponse(
    UUID id,
    String name,
    String address,
    boolean isActive,
    OffsetDateTime qrGeneratedAt,
    OffsetDateTime createdAt,
    OffsetDateTime updatedAt
) {}

public record QrCodePayload(
    UUID siteId,
    UUID tenantId,
    String token
) {}

Repository Layer

  • SiteRepository.java:
  • findByTenantId(UUID tenantId) -> List<Site>
  • findByTenantIdAndId(UUID tenantId, UUID id) -> Optional<Site>
  • existsByTenantIdAndName(UUID tenantId, String name) -> boolean
  • CachedEmployeeRepository.java:
  • findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> Optional<CachedEmployeeData>
  • findByTenantId(UUID tenantId) -> List<CachedEmployeeData>
  • EmployeeCacheEventListener.java:
  • Listens for EmployeeCreated, EmployeeUpdated, EmployeeTerminated, ManagerChanged (Spring Modulith Application Events)
  • Upserts CachedEmployeeData on each event

Service Layer

SiteService.java: - createSite(UUID tenantId, CreateSiteRequest) -> SiteResponse — Validates name uniqueness, generates HMAC token via QrCodeGenerator, creates site, publishes audit event - listSites(UUID tenantId) -> CursorPage<SiteResponse> — Returns all sites for tenant, cursor-paginated - getSite(UUID tenantId, UUID siteId) -> SiteResponse — Returns single site - updateSite(UUID tenantId, UUID siteId, UpdateSiteRequest) -> SiteResponse — Partial update, validates name uniqueness if changed - regenerateQr(UUID tenantId, UUID siteId) -> SiteResponse — Generates new HMAC token, updates qrToken and qrGeneratedAt, invalidates old token, publishes audit event - deactivateSite(UUID tenantId, UUID siteId) -> SiteResponse — Sets isActive=false

QrCodeGenerator.java: - generateQrCodeImage(QrCodePayload payload) -> byte[] — Generates QR code PNG using ZXing library. Payload is JSON: {"siteId":"uuid","tenantId":"uuid","token":"hmac-token"} - generateHmacToken(UUID tenantId, UUID siteId, String hmacSecret) -> String — HMAC-SHA256 of tenantId:siteId signed with tenant's qr_hmac_secret

QrCodeValidator.java: - validate(QrCodePayload payload) -> ValidationResult — Validates HMAC signature against tenant's secret, checks tenantId matches, siteId exists and is active, token matches site's current qrToken - Returns ValidationResult with status (VALID, INVALID_SIGNATURE, SITE_INACTIVE, TOKEN_EXPIRED, SITE_NOT_FOUND)

HMAC secret is auto-generated (256-bit hex string via SecureRandom) when AttendanceModuleConfig is first created for a tenant.

API Endpoints

Method Path Request Body Response Auth
POST /api/v1/qrcontr/sites CreateSiteRequest ApiResponse HR (P2)
GET /api/v1/qrcontr/sites ApiResponse> HR, Manager
GET /api/v1/qrcontr/sites/{siteId} ApiResponse HR, Manager
PUT /api/v1/qrcontr/sites/{siteId} UpdateSiteRequest ApiResponse HR (P2)
POST /api/v1/qrcontr/sites/{siteId}/regenerate-qr ApiResponse HR (P2)
GET /api/v1/qrcontr/sites/{siteId}/qr-code PNG image (byte[]) HR (P2)

Validation Rules

  • name required, max 100 chars, unique within tenant. Duplicate name error: "Un site avec ce nom existe deja."
  • address optional, max 500 chars
  • Site must exist and belong to tenant for update/regenerate operations
  • QR regeneration logs audit event with optional reason

Multi-Tenant Considerations

  • All tables include tenant_id column
  • All queries MUST filter by tenant_id
  • HMAC secret is per-tenant (stored in attendance_module_config)
  • Site name uniqueness is scoped to tenant

Frontend Scope

Pages & Routes

  • /qrcontr/sites — Site list page (HR only)
  • /qrcontr/sites/:siteId — Site detail + QR view
  • /qrcontr/sites/new — Create site form

Components

Components follow design spec wireframes 3.9, 3.10:

  • SiteListPage.tsx — List of site cards with QR status, active/inactive badge
  • SiteCreateForm.tsx — Name + address fields, submit button
  • SiteDetailPage.tsx — QR code display, download/print buttons, regenerate button
  • QrCodeCard.tsx — Displays QR code image with brand label ("PAPILLON HR" + site name)

UI States (ALL REQUIRED)

State Behavior French Copy
Loading Skeleton cards for site list
Empty Onboarding illustration + guidance "Creez votre premier site de pointage."
Error Inline retry card "Une erreur est survenue. Reessayez."
Offline Cached site list, create/edit disabled "Disponible en ligne uniquement"
Success (create) Site card slides in, QR appears "Site cree avec succes."

Responsive Behavior

  • 360px (mobile): Full-width stacked site cards, full-width QR display
  • 768px (tablet): 2-column site cards grid, centered QR at 50% width
  • 1024px+ (desktop): Sidebar nav, 3-column grid, QR at 33% width

Interactions

  • QR regeneration: confirmation bottom sheet with message "L'ancien QR code sera invalide. Vous devrez imprimer et afficher le nouveau. Continuer ?" Two buttons: "Annuler" + "Regenerer" (warning/amber style)
  • After regeneration: toast "Nouveau QR code genere. Pensez a l'imprimer."
  • QR download: browser download trigger for PNG file
  • QR print: browser print dialog with QR + site name + brand label

Acceptance Criteria

AC-001: Site creation with automatic QR code generation
Given I am HR and logged in
When I create a new site with name "Siege Abidjan" and address "Plateau, Rue du Commerce"
Then the site is saved with a QR code containing the site ID, tenant ID, and a static HMAC-signed token
And the QR code image is generated server-side and available for download/print

AC-002: QR code regeneration invalidates previous code
Given a site "Entrepot Yopougon" exists with an active QR code
When I regenerate the site's QR code as HR
Then the previous QR code is immediately invalidated
And a new HMAC token is generated
And any scan using the old QR code will fail validation

AC-003: QR code download and print
Given a QR code has been generated for a site
When I view the QR code detail page
Then I can download the QR code as a PNG image
And I can print the QR code with the site name and brand label

OHADA & Regulatory Rules

None specific to site management.


Standards & Conventions

  • docs/standards/java-spring-guidelines.md — Entity records, repository patterns
  • docs/standards/api-guidelines.md — ApiResponse envelope, cursor-based pagination (ADR-12)
  • docs/standards/database-guidelines.md — Multi-tenant table design, tenant_id in all queries
  • docs/standards/react-typescript-guidelines.md — Component structure, hooks

Testing Requirements

Unit Tests

  • QrCodeGenerator produces valid PNG byte array
  • QrCodeValidator accepts valid HMAC, rejects invalid/expired tokens
  • SiteService.createSite generates unique HMAC token per site
  • SiteService.regenerateQr invalidates old token

Integration Tests

  • POST /sites -> 201, site created with QR token
  • POST /sites with duplicate name -> 409
  • POST /sites/{id}/regenerate-qr -> new token, old token invalid
  • GET /sites/{id}/qr-code -> returns PNG with correct content-type
  • Tenant isolation: tenant A cannot see tenant B's sites

What QA Will Validate

  • Site list page displays correctly on all breakpoints (360px, 768px, 1024px)
  • QR code displays on site detail page
  • QR download produces a valid PNG
  • QR print opens browser print dialog with brand label
  • QR regeneration shows confirmation, generates new code
  • Create site shows success toast and card appears
  • Empty state displays onboarding illustration
  • Offline state shows cached list, blocks create/edit

Out of Scope

  • Employee site assignment (QR-008)
  • Clock event recording (QR-006)
  • Module configuration CRUD beyond HMAC secret (QR-004)
  • Offline QR scanning (QR-006)

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses
  • [ ] All frontend pages render correctly at 360px, 768px, 1024px
  • [ ] All 5 UI states implemented (loading, empty, error, offline, success)
  • [ ] French micro-copy matches design spec exactly
  • [ ] All acceptance criteria pass manually
  • [ ] Unit tests written and passing
  • [ ] Integration tests written and passing
  • [ ] Multi-tenant isolation verified
  • [ ] Offline write+sync: N/A (site management is online-only)
  • [ ] No TypeScript any, no Java raw types
  • [ ] API responses follow standard envelope format
  • [ ] Audit trail entries for site creation, update, QR regeneration