diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1b643..60519dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Toutes les modifications importantes du projet sont consignées ici. Le format s’inspire de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le projet suit le [versionnage sémantique](https://semver.org/lang/fr/). +## [1.0.0-rc.12] — 2026-09-08 + +### Tarifs d’adoption et environnement local + +- grille des frais d’adoption par espèce, avant et après stérilisation, avec possibilité de choisir un tarif libre (`#14`) ; +- reprise automatique des deux anciens montants pour toutes les espèces existantes ; +- affichage du tarif correspondant sur chaque fiche animale, quelle que soit l’espèce ; +- explication des tentatives SSL provoquées par le mode HTTPS uniquement d’un navigateur avec le serveur PHP local (`#18`). + ## [1.0.0-rc.11] — 2026-09-08 ### Configuration des salles et des tournées diff --git a/HISTORIQUE.md b/HISTORIQUE.md index 24e65ce..b04839e 100644 --- a/HISTORIQUE.md +++ b/HISTORIQUE.md @@ -53,3 +53,7 @@ Correction du champ de statut du formulaire animal, stabilisation des codes inte ## `1.0.0-rc.11` — Salles et tournées Ajout de critères d’accueil par salle et de quatre créneaux de tournée configurables sans modifier les réglages existants. + +## `1.0.0-rc.12` — Tarifs par espèce + +Remplacement des deux montants globaux par une grille d’adoption propre à chaque espèce, avec tarif fixe ou libre. diff --git a/README.md b/README.md index b26890b..827cdf9 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ Le dernier argument, `public/router.php`, est indispensable : sans lui, les rout Ouvrez ensuite `http://127.0.0.1:8000/setup`. L’assistant vérifie l’environnement, recueille l’identité de l’association et crée le premier compte administrateur. +Le serveur PHP intégré ne prend pas en charge HTTPS. Si le navigateur utilise un mode « HTTPS uniquement », autorisez HTTP pour l’adresse locale ; une tentative en `https://127.0.0.1:8000` apparaît dans le terminal comme une requête SSL non prise en charge. + Les migrations sont appliquées automatiquement. Elles peuvent aussi être exécutées explicitement : ```bash diff --git a/VERSION b/VERSION index 21254c9..884b1ae 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc.11 +1.0.0-rc.12 diff --git a/app/Controllers/SettingsController.php b/app/Controllers/SettingsController.php index 6a2be35..6496598 100644 --- a/app/Controllers/SettingsController.php +++ b/app/Controllers/SettingsController.php @@ -495,6 +495,7 @@ final class SettingsController ->query('SELECT id,name FROM ref_dewormers ORDER BY name COLLATE NOCASE') ->fetchAll(PDO::FETCH_ASSOC), 'settings' => AppSettings::all(), + 'adoptionFees' => PricingService::adoptionFees(), 'saved' => isset($_GET['saved']), 'error' => (string) ($_GET['error'] ?? ''), ]); @@ -616,19 +617,23 @@ final class SettingsController { self::admin(); self::postOnly(); - AppSettings::save( - [ - 'adoption_fee_sterilized' => (string) max( - 0, - min(2000, (int) ($_POST['adoption_fee_sterilized'] ?? 220)), - ), - 'adoption_fee_unsterilized' => (string) max( - 0, - min(2000, (int) ($_POST['adoption_fee_unsterilized'] ?? 200)), - ), - ], - Auth::id(), + $submitted = (array) ($_POST['fees'] ?? []); + $allowed = array_keys(PricingService::adoptionFees()); + $stmt = DB::pdo()->prepare( + "INSERT INTO adoption_fee_rules(species_code,fee_before_sterilization,fee_after_sterilization,is_free,updated_at) VALUES(?,?,?,?,datetime('now')) ON CONFLICT(species_code) DO UPDATE SET fee_before_sterilization=excluded.fee_before_sterilization,fee_after_sterilization=excluded.fee_after_sterilization,is_free=excluded.is_free,updated_at=datetime('now')", ); + foreach ($submitted as $species => $values) { + $species = SpeciesService::normalize((string) $species); + if (!in_array($species, $allowed, true) || !is_array($values)) { + continue; + } + $stmt->execute([ + $species, + max(0, min(2000, (int) ($values['before'] ?? 0))), + max(0, min(2000, (int) ($values['after'] ?? 0))), + isset($values['free']) ? 1 : 0, + ]); + } AuditService::log('adoption_fees_saved', '/settings/pricing/adoption', 'Tarifs d’adoption modifiés'); header('Location: /settings/pricing?saved=1'); exit(); diff --git a/app/Services/PricingService.php b/app/Services/PricingService.php index a556caa..7ab7ac8 100644 --- a/app/Services/PricingService.php +++ b/app/Services/PricingService.php @@ -37,6 +37,23 @@ final class PricingService { return max(0, (int) round((float) str_replace(',', '.', trim($v)) * 100)); } + public static function adoptionFees(): array + { + $rows = DB::pdo()->query( + "SELECT s.code,s.name,s.icon,COALESCE(r.fee_before_sterilization,200) fee_before_sterilization,COALESCE(r.fee_after_sterilization,220) fee_after_sterilization,COALESCE(r.is_free,0) is_free FROM ref_species s LEFT JOIN adoption_fee_rules r ON r.species_code=s.code WHERE s.active=1 ORDER BY s.sort_order,s.name COLLATE NOCASE", + )->fetchAll(PDO::FETCH_ASSOC); + return array_column($rows, null, 'code'); + } + + public static function adoptionFee(string $species, bool $sterilized): ?int + { + $fees = self::adoptionFees(); + $rule = $fees[SpeciesService::normalize($species)] ?? null; + if (!$rule || (int) $rule['is_free'] === 1) { + return null; + } + return (int) $rule[$sterilized ? 'fee_after_sterilization' : 'fee_before_sterilization']; + } public static function tariffs(bool $active = false): array { $w = $active ? 'WHERE ct.active=1 AND dc.deleted_at IS NULL' : ''; diff --git a/app/Views/_animal_show_modals.php b/app/Views/_animal_show_modals.php index 98be9d7..4792870 100644 --- a/app/Views/_animal_show_modals.php +++ b/app/Views/_animal_show_modals.php @@ -641,7 +641,7 @@ $today = date('Y-m-d'); AppSettings::int('adoption_fee_sterilized')])) ?> + class="form-hint mt-0"> $feeAfterSterilization]))) ?> diff --git a/app/Views/animal_show.php b/app/Views/animal_show.php index eb1867f..777d7ca 100644 --- a/app/Views/animal_show.php +++ b/app/Views/animal_show.php @@ -285,9 +285,9 @@ if ($felvPos) { - - + + + diff --git a/app/Views/evolutions.php b/app/Views/evolutions.php index fb2370d..4793e2e 100644 --- a/app/Views/evolutions.php +++ b/app/Views/evolutions.php @@ -31,9 +31,9 @@
- 1.0.0-rc.11 -

Salles et tournées configurables

-

Critères d’accueil par salle et créneaux de soins adaptés à l’organisation du refuge.

+ 1.0.0-rc.12 +

Tarifs d’adoption par espèce

+

Montants avant ou après stérilisation, avec possibilité de laisser le tarif libre.

diff --git a/app/Views/settings_pricing.php b/app/Views/settings_pricing.php index b76a6ee..bf0e7e2 100644 --- a/app/Views/settings_pricing.php +++ b/app/Views/settings_pricing.php @@ -6,7 +6,7 @@
✅
-
+
@@ -15,24 +15,19 @@
-
-
-
€
-
-
-
€
-
+
+ + $fee): ?> + + + + +
€
€
-
+
diff --git a/migrations/094_species_adoption_fees.sql b/migrations/094_species_adoption_fees.sql new file mode 100644 index 0000000..11ff77e --- /dev/null +++ b/migrations/094_species_adoption_fees.sql @@ -0,0 +1,14 @@ +CREATE TABLE adoption_fee_rules ( + species_code TEXT PRIMARY KEY, + fee_before_sterilization INTEGER NOT NULL DEFAULT 200, + fee_after_sterilization INTEGER NOT NULL DEFAULT 220, + is_free INTEGER NOT NULL DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY(species_code) REFERENCES ref_species(code) ON DELETE CASCADE +); + +INSERT OR IGNORE INTO adoption_fee_rules(species_code,fee_before_sterilization,fee_after_sterilization) +SELECT s.code, + COALESCE((SELECT CAST(setting_value AS INTEGER) FROM app_settings WHERE setting_key='adoption_fee_unsterilized'),200), + COALESCE((SELECT CAST(setting_value AS INTEGER) FROM app_settings WHERE setting_key='adoption_fee_sterilized'),220) +FROM ref_species s; diff --git a/resources/lang/en.php b/resources/lang/en.php index dff3907..df5939a 100644 --- a/resources/lang/en.php +++ b/resources/lang/en.php @@ -125,4 +125,5 @@ return [ 'settings.invalid_rna'=>'The RNA number must start with W followed by 9 digits.','settings.invalid_siret'=>'The SIRET number must contain 14 digits and be valid.','rooms.name_too_long'=>'A room name cannot exceed 100 characters.','rooms.box_name_too_long'=>'A box name cannot exceed 100 characters.','animal.name_too_long'=>'An animal name cannot exceed 150 characters.', 'pricing.adoption_fees_help'=>'Indicative amounts shared by all animals, according to their sterilisation status.','rooms.inactive_help'=>'An unchecked room becomes inactive, while its previous assignments remain in the history. Its internal code never changes.', 'care.noon'=>'noon','care.afternoon'=>'afternoon','rooms.admission_rules'=>'Optional admission criteria','rooms.care_periods'=>'Rounds scheduled for this room','rooms.period_required'=>'Select at least one care-round period per room.','rooms.animal_not_allowed'=>'This animal does not match the admission criteria of the selected room.', + 'pricing.before_sterilization'=>'Before sterilisation','pricing.after_sterilization'=>'After sterilisation','pricing.free_fee'=>'Open amount','pricing.free'=>'Open','pricing.adoption_fees_help'=>'Indicative amounts by species. Enable “Open amount” when no fixed fee is required.','animal.sterilization_auto_free'=>'The adoption fee for this species remains open after sterilisation.', ]; diff --git a/resources/lang/fr.php b/resources/lang/fr.php index 087870f..400f321 100644 --- a/resources/lang/fr.php +++ b/resources/lang/fr.php @@ -125,4 +125,5 @@ return [ 'settings.invalid_rna'=>'Le numéro RNA doit commencer par W et être suivi de 9 chiffres.','settings.invalid_siret'=>'Le numéro SIRET doit contenir 14 chiffres et être valide.','rooms.name_too_long'=>'Le nom d’une salle ne peut pas dépasser 100 caractères.','rooms.box_name_too_long'=>'Le nom d’un box ne peut pas dépasser 100 caractères.','animal.name_too_long'=>'Le nom d’un animal ne peut pas dépasser 150 caractères.', 'pricing.adoption_fees_help'=>'Montants indicatifs communs à tous les animaux, selon leur état de stérilisation.','rooms.inactive_help'=>'Une salle décochée devient inactive, mais ses anciennes affectations restent dans l’historique. Son code interne ne change jamais.', 'care.noon'=>'midi','care.afternoon'=>'après-midi','rooms.admission_rules'=>'Critères d’accueil facultatifs','rooms.care_periods'=>'Tournées prévues dans cette salle','rooms.period_required'=>'Sélectionnez au moins un créneau de tournée par salle.','rooms.animal_not_allowed'=>'Cet animal ne correspond pas aux critères d’accueil de la salle choisie.', + 'pricing.before_sterilization'=>'Avant stérilisation','pricing.after_sterilization'=>'Après stérilisation','pricing.free_fee'=>'Tarif libre','pricing.free'=>'Libre','pricing.adoption_fees_help'=>'Montants indicatifs par espèce. Activez « Tarif libre » lorsqu’aucun montant fixe n’est imposé.','animal.sterilization_auto_free'=>'Le tarif d’adoption de cette espèce reste libre après la stérilisation.', ]; diff --git a/tests/run.php b/tests/run.php index 4ba0a86..1fb2c48 100644 --- a/tests/run.php +++ b/tests/run.php @@ -27,6 +27,7 @@ foreach ( 'NotificationService', 'PrivacyService', 'StatisticsService', + 'PricingService', 'SpeciesService', 'LabReferenceService', 'PublicContentFormatter', @@ -133,6 +134,15 @@ try { $assert(AppSettings::get('app_language') === 'en'); I18n::setLocale('fr'); }); + $test('Les frais d’adoption sont configurables par espèce', function () use ($db, $assert): void { + $fees = PricingService::adoptionFees(); + $assert(isset($fees['chat']), 'Tarif du chat absent'); + $db->exec("UPDATE adoption_fee_rules SET fee_before_sterilization=175,fee_after_sterilization=225,is_free=0 WHERE species_code='chat'"); + $assert(PricingService::adoptionFee('chat', false) === 175); + $assert(PricingService::adoptionFee('chat', true) === 225); + $db->exec("UPDATE adoption_fee_rules SET is_free=1 WHERE species_code='chat'"); + $assert(PricingService::adoptionFee('chat', true) === null, 'Le tarif libre n’est pas appliqué'); + }); $test('Les identifiants RNA et SIRET sont contrôlés', function () use ($assert): void { $assert(Ids::validRna('W123456789')); $assert(!Ids::validRna('W1234'));