feat(absences) : fondation backend du module de gestion des absences

Module type Payfit (étapes 1+2 de la spec V1) : demande d'absence, validation
admin, soldes à jour.

- Enums : AbsenceType, AbsenceStatus, HalfDay, ContractType, FamilySituation
- Entités : AbsencePolicy, AbsenceBalance, AbsenceRequest + champs RH sur User
- Services : PublicHolidayProvider (fériés FR métropole en PHP pur, Computus),
  AbsenceDayCalculator (décompte jours ouvrés/ouvrables + demi-journées, TDD),
  AbsenceBalanceService (périodes + pending/taken/recrédit)
- API Platform : providers/processors (création, approve/reject/cancel) + RBAC
  me/admin, contrôleurs preview (dry-run), upload/download justificatif, calendrier
- Migrations : une par table + colonnes RH user (DEFAULT puis DROP DEFAULT)
- Fixtures : 5 policies par défaut, salariés démo, soldes et demandes
- Tests unitaires : PublicHolidayProvider, AbsenceDayCalculator (12 tests)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-05-21 14:45:14 +02:00
parent 325a7b07f9
commit de98924fd3
32 changed files with 2554 additions and 3 deletions

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Absence management: create the absence_policy table.
*/
final class Version20260521123520 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create absence_policy table';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE absence_policy (
id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
type VARCHAR(32) NOT NULL,
days_per_year DOUBLE PRECISION DEFAULT NULL,
days_per_event DOUBLE PRECISION DEFAULT NULL,
justification_required BOOLEAN NOT NULL,
notice_days INT NOT NULL,
count_working_days_only BOOLEAN NOT NULL,
active BOOLEAN NOT NULL,
PRIMARY KEY (id)
)
SQL);
$this->addSql('CREATE UNIQUE INDEX uniq_absence_policy_type ON absence_policy (type)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE absence_policy');
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Absence management: create the absence_balance table.
*/
final class Version20260521123521 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create absence_balance table';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE absence_balance (
id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
type VARCHAR(32) NOT NULL,
period VARCHAR(16) NOT NULL,
acquired DOUBLE PRECISION NOT NULL,
taken DOUBLE PRECISION NOT NULL,
pending DOUBLE PRECISION NOT NULL,
user_id INT NOT NULL,
PRIMARY KEY (id)
)
SQL);
$this->addSql('CREATE INDEX IDX_65723A76A76ED395 ON absence_balance (user_id)');
$this->addSql('CREATE UNIQUE INDEX uniq_absence_balance_user_type_period ON absence_balance (user_id, type, period)');
$this->addSql(<<<'SQL'
ALTER TABLE
absence_balance
ADD
CONSTRAINT FK_65723A76A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE
SQL);
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE absence_balance DROP CONSTRAINT FK_65723A76A76ED395');
$this->addSql('DROP TABLE absence_balance');
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Absence management: create the absence_request table.
*/
final class Version20260521123522 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create absence_request table';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE absence_request (
id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
type VARCHAR(32) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
start_half_day VARCHAR(16) DEFAULT NULL,
end_half_day VARCHAR(16) DEFAULT NULL,
counted_days DOUBLE PRECISION NOT NULL,
reason TEXT DEFAULT NULL,
justification_file_name VARCHAR(255) DEFAULT NULL,
status VARCHAR(16) NOT NULL,
rejection_reason TEXT DEFAULT NULL,
created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
reviewed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
user_id INT NOT NULL,
reviewed_by_id INT DEFAULT NULL,
PRIMARY KEY (id)
)
SQL);
$this->addSql('CREATE INDEX IDX_F211AA17A76ED395 ON absence_request (user_id)');
$this->addSql('CREATE INDEX IDX_F211AA17FC6B21F1 ON absence_request (reviewed_by_id)');
$this->addSql(<<<'SQL'
ALTER TABLE
absence_request
ADD
CONSTRAINT FK_F211AA17A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE
absence_request
ADD
CONSTRAINT FK_F211AA17FC6B21F1 FOREIGN KEY (reviewed_by_id) REFERENCES "user" (id) ON DELETE SET NULL NOT DEFERRABLE
SQL);
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE absence_request DROP CONSTRAINT FK_F211AA17A76ED395');
$this->addSql('ALTER TABLE absence_request DROP CONSTRAINT FK_F211AA17FC6B21F1');
$this->addSql('DROP TABLE absence_request');
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Absence management: add HR fields to the user table.
*
* Columns are created with DEFAULTs so the migration applies cleanly on an
* already-populated user table (production).
*/
final class Version20260521123523 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add HR / absence fields to user';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE "user" ADD is_employee BOOLEAN NOT NULL DEFAULT false');
$this->addSql('ALTER TABLE "user" ADD hire_date DATE DEFAULT NULL');
$this->addSql('ALTER TABLE "user" ADD end_date DATE DEFAULT NULL');
$this->addSql('ALTER TABLE "user" ADD contract_type VARCHAR(16) DEFAULT NULL');
$this->addSql('ALTER TABLE "user" ADD work_time_ratio DOUBLE PRECISION NOT NULL DEFAULT 1.0');
$this->addSql('ALTER TABLE "user" ADD annual_leave_days DOUBLE PRECISION NOT NULL DEFAULT 25.0');
$this->addSql("ALTER TABLE \"user\" ADD reference_period_start VARCHAR(5) NOT NULL DEFAULT '06-01'");
$this->addSql('ALTER TABLE "user" ADD initial_leave_balance DOUBLE PRECISION NOT NULL DEFAULT 0');
$this->addSql('ALTER TABLE "user" ADD family_situation VARCHAR(16) DEFAULT NULL');
$this->addSql('ALTER TABLE "user" ADD nb_children INT NOT NULL DEFAULT 0');
// Defaults were only needed to backfill existing rows; the ORM mapping
// carries no DB default, so drop them to keep the schema in sync.
$this->addSql('ALTER TABLE "user" ALTER is_employee DROP DEFAULT');
$this->addSql('ALTER TABLE "user" ALTER work_time_ratio DROP DEFAULT');
$this->addSql('ALTER TABLE "user" ALTER annual_leave_days DROP DEFAULT');
$this->addSql('ALTER TABLE "user" ALTER reference_period_start DROP DEFAULT');
$this->addSql('ALTER TABLE "user" ALTER initial_leave_balance DROP DEFAULT');
$this->addSql('ALTER TABLE "user" ALTER nb_children DROP DEFAULT');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE "user" DROP is_employee');
$this->addSql('ALTER TABLE "user" DROP hire_date');
$this->addSql('ALTER TABLE "user" DROP end_date');
$this->addSql('ALTER TABLE "user" DROP contract_type');
$this->addSql('ALTER TABLE "user" DROP work_time_ratio');
$this->addSql('ALTER TABLE "user" DROP annual_leave_days');
$this->addSql('ALTER TABLE "user" DROP reference_period_start');
$this->addSql('ALTER TABLE "user" DROP initial_leave_balance');
$this->addSql('ALTER TABLE "user" DROP family_situation');
$this->addSql('ALTER TABLE "user" DROP nb_children');
}
}