From 70dd59b03ec9fd25d0c75789c21aba15d61dbcd6 Mon Sep 17 00:00:00 2001
From: Alexis Burnaz <48258099+alxsbrz@users.noreply.github.com>
Date: Mon, 27 Apr 2026 12:08:22 +0200
Subject: [PATCH] feat(advalo): integrate module in etv2 and restore v1 AXA
scripts flow
---
ecole/public/css/global.css | 57 +
ecole/public/js/advalo-module.js | 955 +++++++++
ecole/src/controllers/advaloController.js | 174 ++
ecole/src/routes.js | 4 +-
ecole/src/services/advaloService.js | 1754 +++++++++++++++++
ecole/src/templates/advalo/Avenant.docx | Bin 0 -> 28038 bytes
.../templates/advalo/Avenant_Ponctuel.docx | Bin 0 -> 27917 bytes
.../advalo/Certificat_Assurance_Advalo.docx | Bin 0 -> 71465 bytes
ecole/vbs/script_cl063/Connexion.xlsm | Bin 0 -> 107221 bytes
ecole/vbs/script_cl063/ECFDDEViva.exe | Bin 0 -> 20480 bytes
ecole/vbs/script_cl063/config/.gitkeep | 0
ecole/vbs/script_cl063/extract.vbs | 24 +
ecole/vbs/script_pa025/Attestation.xlsm | Bin 0 -> 97784 bytes
ecole/vbs/script_pa025/ECFDDEViva.exe | Bin 0 -> 20480 bytes
ecole/vbs/script_pa025/attestation.vbs | 24 +
ecole/vbs/script_pa025/config/.gitkeep | 0
ecole/vbs/script_qt550/Bordereau.xlsm | Bin 0 -> 100269 bytes
ecole/vbs/script_qt550/ECFDDEViva.exe | Bin 0 -> 20480 bytes
ecole/vbs/script_qt550/bordereau.vbs | 24 +
ecole/vbs/script_qt550/config/.gitkeep | 0
ecole/views/advalo.ejs | 326 +++
ecole/views/partials/navbar.ejs | 4 +-
22 files changed, 3343 insertions(+), 3 deletions(-)
create mode 100644 ecole/public/js/advalo-module.js
create mode 100644 ecole/src/controllers/advaloController.js
create mode 100644 ecole/src/services/advaloService.js
create mode 100644 ecole/src/templates/advalo/Avenant.docx
create mode 100644 ecole/src/templates/advalo/Avenant_Ponctuel.docx
create mode 100644 ecole/src/templates/advalo/Certificat_Assurance_Advalo.docx
create mode 100644 ecole/vbs/script_cl063/Connexion.xlsm
create mode 100644 ecole/vbs/script_cl063/ECFDDEViva.exe
create mode 100644 ecole/vbs/script_cl063/config/.gitkeep
create mode 100644 ecole/vbs/script_cl063/extract.vbs
create mode 100644 ecole/vbs/script_pa025/Attestation.xlsm
create mode 100644 ecole/vbs/script_pa025/ECFDDEViva.exe
create mode 100644 ecole/vbs/script_pa025/attestation.vbs
create mode 100644 ecole/vbs/script_pa025/config/.gitkeep
create mode 100644 ecole/vbs/script_qt550/Bordereau.xlsm
create mode 100644 ecole/vbs/script_qt550/ECFDDEViva.exe
create mode 100644 ecole/vbs/script_qt550/bordereau.vbs
create mode 100644 ecole/vbs/script_qt550/config/.gitkeep
create mode 100644 ecole/views/advalo.ejs
diff --git a/ecole/public/css/global.css b/ecole/public/css/global.css
index af6c9276..eb8fe6fb 100644
--- a/ecole/public/css/global.css
+++ b/ecole/public/css/global.css
@@ -240,6 +240,63 @@ hr.form {
background: #26a69a;
}
+#advaloNavSelect .active a {
+ background: #26a69a;
+}
+
+#advaloNavSelect li a {
+ font-weight: 500;
+ letter-spacing: 0.2px;
+}
+
+.advalo-panel .input-field > label {
+ color: #1a237e !important;
+ transition: transform .18s ease, color .18s ease, font-size .18s ease;
+}
+
+.advalo-panel .input-field > label.active {
+ transform: translateY(-24px) scale(0.82) !important;
+ transform-origin: 0 0;
+}
+
+.advalo-loader-wrap {
+ min-height: 46px;
+}
+
+.advalo-ring-loader {
+ width: 34px;
+ height: 34px;
+ border: 3px solid rgba(0, 0, 139, 0.18);
+ border-top-color: #00008b;
+ border-right-color: #26a69a;
+ border-radius: 50%;
+ margin: 0 auto;
+ animation: advalo-spin 0.9s cubic-bezier(0.5, 0.1, 0.5, 0.9) infinite;
+}
+
+.advalo-cumul-hist,
+.advalo-cumul-fact {
+ margin-right: 6px;
+ margin-bottom: 6px;
+}
+
+.advalo-panel .btn,
+.advalo-panel .btn-flat {
+ color: #fff !important;
+}
+
+@keyframes advalo-spin {
+ 0% {
+ transform: rotate(0deg) scale(1);
+ }
+ 40% {
+ transform: rotate(180deg) scale(1.04);
+ }
+ 100% {
+ transform: rotate(360deg) scale(1);
+ }
+}
+
.border {
border-radius: 10px !important;
}
diff --git a/ecole/public/js/advalo-module.js b/ecole/public/js/advalo-module.js
new file mode 100644
index 00000000..210dcecc
--- /dev/null
+++ b/ecole/public/js/advalo-module.js
@@ -0,0 +1,955 @@
+document.addEventListener('DOMContentLoaded', function () {
+ if (window.location.pathname !== '/advalo') return;
+
+ const token = localStorage.getItem('jwtToken');
+ const pageSize = 20;
+ const state = {
+ historique: { page: 1, totalPages: 1 },
+ reporting: { page: 1, totalPages: 1 },
+ cumul: { page: 1, totalPages: 1 },
+ loaded: { historique: false, reporting: false, cumul: false },
+ confirmation: { demandId: '', summary: '' },
+ batchConfirmation: { batchId: '', summary: '' },
+ facturationRows: [],
+ removedFacturationIds: new Set(),
+ openHistoriqueId: null
+ };
+ const requestControllers = {};
+ const loadingState = {};
+ const TRANSPORT_MODES = ['Terrestre', 'Aérien', 'Fluvial', 'Maritime', 'Postal'];
+
+ const toast = (message, classes = 'red') => M.toast({ html: message, classes });
+
+ const authHeaders = () => {
+ if (!token) throw new Error('Session expirée.');
+ return { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` };
+ };
+
+ const authOnlyHeaders = () => {
+ if (!token) throw new Error('Session expirée.');
+ return { Authorization: `Bearer ${token}` };
+ };
+
+ const isAbortError = (error) => error && (error.name === 'AbortError' || String(error.message || '').toLowerCase().includes('aborted'));
+
+ const api = async (url, options = {}) => {
+ const res = await fetch(url, { ...options, headers: options.headers || authHeaders() });
+ const contentType = res.headers.get('content-type') || '';
+ if (contentType.includes('text/csv')) {
+ if (!res.ok) throw new Error('Erreur export CSV');
+ return res.text();
+ }
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data.valid === false) throw new Error(data.message || 'Erreur serveur');
+ return data;
+ };
+
+ const apiLatest = async (key, url, options = {}) => {
+ if (requestControllers[key]) requestControllers[key].abort();
+ const controller = new AbortController();
+ requestControllers[key] = controller;
+ try {
+ return await api(url, { ...options, signal: controller.signal });
+ } finally {
+ if (requestControllers[key] === controller) delete requestControllers[key];
+ }
+ };
+
+ const abortDataRequests = () => {
+ ['historique', 'reporting', 'cumul', 'facturation'].forEach((k) => {
+ if (requestControllers[k]) requestControllers[k].abort();
+ });
+ };
+
+ const parseAmount = (value) => {
+ const n = Number(String(value || '').replace(',', '.').replace(/\s/g, ''));
+ return Number.isFinite(n) ? n : 0;
+ };
+
+ const fmt = (n) => Number(n || 0).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+
+ const sourceLabel = (source) => (source === 'deleguee' ? 'Grille déléguée' : 'Hors grille');
+
+ const setIndicator = (id, page, totalPages, totalRows) => {
+ const el = document.getElementById(id);
+ if (!el) return;
+ const pages = Math.max(1, Number(totalPages || 1));
+ const current = Math.min(Math.max(1, Number(page || 1)), pages);
+ el.textContent = `Page ${current} / ${pages} - ${Number(totalRows || 0)} lignes`;
+ };
+
+ const setLoading = (section, isLoading) => {
+ const el = document.getElementById(`advalo-loading-${section}`);
+ if (!el) return;
+ if (isLoading) {
+ const marker = Symbol(section);
+ loadingState[section] = { marker, startedAt: Date.now() };
+ el.style.display = 'block';
+ return;
+ }
+ const current = loadingState[section];
+ if (!current) {
+ el.style.display = 'none';
+ return;
+ }
+ const elapsed = Date.now() - current.startedAt;
+ setTimeout(() => {
+ const latest = loadingState[section];
+ if (latest && latest.marker === current.marker) {
+ el.style.display = 'none';
+ delete loadingState[section];
+ }
+ }, Math.max(0, 280 - elapsed));
+ };
+
+ const syncTextFields = (scope = document) => {
+ if (window.M && typeof window.M.updateTextFields === 'function') window.M.updateTextFields();
+ const root = scope && scope.querySelectorAll ? scope : document;
+ const fields = root.querySelectorAll('.advalo-panel .input-field input:not([type="hidden"]), .advalo-panel .input-field textarea');
+ fields.forEach((field) => {
+ if (!field.id) return;
+ const label = document.querySelector(`label[for="${field.id}"]`);
+ if (!label) return;
+ const shouldFloat = String(field.value || '').trim().length > 0 || document.activeElement === field;
+ label.classList.toggle('active', shouldFloat);
+ });
+ };
+
+ const refreshTextFields = (scope = document) => {
+ syncTextFields(scope);
+ requestAnimationFrame(() => syncTextFields(scope));
+ setTimeout(() => syncTextFields(scope), 60);
+ };
+
+ const initSelects = (scope = document) => {
+ const selects = scope.querySelectorAll('select');
+ selects.forEach((select) => {
+ const instance = M.FormSelect.getInstance(select);
+ if (instance) instance.destroy();
+ M.FormSelect.init(select);
+ });
+ };
+
+ const formatDateInput = (raw) => {
+ const digits = String(raw || '').replace(/\D/g, '').slice(0, 8);
+ if (digits.length <= 2) return digits;
+ if (digits.length <= 4) return `${digits.slice(0, 2)}/${digits.slice(2)}`;
+ return `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4)}`;
+ };
+
+ const initDateFields = () => {
+ document.querySelectorAll('input.advalo-date').forEach((input) => {
+ if (input.dataset.dateMaskBound === '1') return;
+ input.addEventListener('input', () => {
+ const formatted = formatDateInput(input.value);
+ if (formatted !== input.value) input.value = formatted;
+ refreshTextFields(input.closest('.advalo-panel') || document);
+ });
+ input.dataset.dateMaskBound = '1';
+ });
+ };
+
+ const downloadDocx = async (url, options = {}, fallbackName = 'document_advalo.docx') => {
+ const res = await fetch(url, { ...options, headers: { ...authOnlyHeaders(), ...(options.headers || {}) } });
+ if (!res.ok) {
+ const payload = await res.json().catch(() => ({}));
+ throw new Error(payload.message || 'Erreur génération document');
+ }
+ const disposition = res.headers.get('content-disposition') || '';
+ const filenameMatch = disposition.match(/filename=\"?([^\";]+)\"?/i);
+ const filename = filenameMatch ? filenameMatch[1] : fallbackName;
+ const blob = await res.blob();
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = filename;
+ link.click();
+ URL.revokeObjectURL(link.href);
+ };
+
+ const ensureContract16 = (value, { required = false } = {}) => {
+ const digits = String(value || '').replace(/\D/g, '');
+ if (!digits) return required ? null : '';
+ if (digits.length !== 16) return null;
+ return digits;
+ };
+
+ const validateContractField = (id, required = false) => {
+ const input = document.getElementById(id);
+ if (!input) return true;
+ const v = ensureContract16(input.value, { required });
+ if (v === null) {
+ input.classList.add('invalid');
+ return false;
+ }
+ if (v) input.value = v;
+ input.classList.remove('invalid');
+ return true;
+ };
+
+ const requireFields = (ids) => {
+ let ok = true;
+ ids.forEach((id) => {
+ const input = document.getElementById(id);
+ if (!input) return;
+ if (!String(input.value || '').trim()) {
+ input.classList.add('invalid');
+ ok = false;
+ } else {
+ input.classList.remove('invalid');
+ }
+ });
+ return ok;
+ };
+
+ const applyNumericGuards = () => {
+ const integerOnlyIds = ['p-numContrat', 'p-numClient', 'p-numAgent', 'h-numClient', 'h-numContrat', 'c-numContrat', 'c-numClient', 'f-numContrat', 'r-numClient', 'r-numContrat', 'r-actorMatricule'];
+ integerOnlyIds.forEach((id) => {
+ const input = document.getElementById(id);
+ if (!input) return;
+ input.addEventListener('input', () => {
+ const cleaned = String(input.value || '').replace(/\D/g, '');
+ if (cleaned !== input.value) input.value = cleaned;
+ });
+ });
+
+ const decimalIds = ['p-capital', 'p-taux', 'p-primeMin', 'p-coutActe', 'p-cotisationHT', 'p-cotisationTTC'];
+ decimalIds.forEach((id) => {
+ const input = document.getElementById(id);
+ if (!input) return;
+ input.addEventListener('input', () => {
+ let cleaned = String(input.value || '').replace(/[^0-9.,]/g, '');
+ const comma = cleaned.indexOf(',');
+ const dot = cleaned.indexOf('.');
+ const splitAt = comma >= 0 ? comma : dot;
+ if (splitAt >= 0) cleaned = cleaned.slice(0, splitAt + 1) + cleaned.slice(splitAt + 1).replace(/[.,]/g, '');
+ if (cleaned !== input.value) input.value = cleaned;
+ });
+ });
+ };
+
+ const bindFloatingLabels = () => {
+ const inputs = document.querySelectorAll('#advalo-tab-ponctuel input, #advalo-tab-facturation input, #advalo-tab-historique input, #advalo-tab-cumul input, #advalo-tab-reporting input');
+ inputs.forEach((input) => {
+ if (input.dataset.floatBound === '1') return;
+ const resync = () => requestAnimationFrame(() => refreshTextFields(input.closest('.advalo-panel') || document));
+ input.addEventListener('focus', resync);
+ input.addEventListener('input', resync);
+ input.addEventListener('change', resync);
+ input.addEventListener('blur', resync);
+ input.dataset.floatBound = '1';
+ });
+ };
+
+ const navLinks = document.querySelectorAll('#advaloNavSelect a[data-target]');
+ const panels = document.querySelectorAll('.advalo-panel');
+ const confirmModal = M.Modal.init(document.getElementById('advalo-confirm-modal'), { dismissible: true });
+ const batchModal = M.Modal.init(document.getElementById('advalo-batch-modal'), { dismissible: true });
+
+ const getSelectedModes = () => [...document.querySelectorAll('.p-mode-check:checked')].map((el) => el.value).filter((v) => TRANSPORT_MODES.includes(v));
+ const syncModes = () => {
+ const modes = getSelectedModes();
+ document.getElementById('p-mode').value = modes.join(', ');
+ };
+
+ const recalcPonctuelPricing = () => {
+ const capital = parseAmount(document.getElementById('p-capital').value);
+ const taux = parseAmount(document.getElementById('p-taux').value);
+ const primeMin = parseAmount(document.getElementById('p-primeMin').value);
+ const coutActe = parseAmount(document.getElementById('p-coutActe').value);
+ if (!capital && !taux && !primeMin) return;
+ const cotisationHT = Math.max((capital * taux) / 100, primeMin);
+ const cotisationTTC = cotisationHT + coutActe;
+ document.getElementById('p-cotisationHT').value = cotisationHT.toFixed(2);
+ document.getElementById('p-cotisationTTC').value = cotisationTTC.toFixed(2);
+ document.getElementById('p-tarif').value = cotisationTTC.toFixed(2);
+ refreshTextFields();
+ };
+
+ const fillContractInfo = (info) => {
+ document.getElementById('p-numContrat').value = info.numContrat || document.getElementById('p-numContrat').value;
+ document.getElementById('p-numClient').value = info.numClient || '';
+ document.getElementById('p-nomClient').value = info.nomClient || '';
+ document.getElementById('p-numAgent').value = info.numAgent || '';
+ document.getElementById('p-nomAgent').value = info.nomAgent || '';
+ refreshTextFields();
+ };
+
+ const lookupContract = async () => {
+ if (!validateContractField('p-numContrat', true)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ const contract = document.getElementById('p-numContrat').value.trim();
+ const params = new URLSearchParams({ numContrat: contract });
+ const data = await api(`/advalo/lookup-contract?${params.toString()}`, { method: 'GET' });
+ fillContractInfo(data.info || {});
+ if (data.info?.source && data.info.source !== 'none') toast(`Informations chargées (${data.info.source}).`, 'green');
+ else toast('Aucune donnée trouvée pour ce contrat. Saisie manuelle possible.', 'orange');
+ };
+
+ const renderHistoriqueDetail = (typedId, row, details) => {
+ const detailId = `h-detail-${typedId.replace(/[^a-zA-Z0-9_-]/g, '-')}`;
+ return `
+
+
+
+
+ Marchandise: ${details.marchandise || '-'}
+ Modes: ${details.mode || '-'}
+ Départ: ${details.depart || '-'}
+ Arrivée: ${details.arrivee || '-'}
+
+
+ Valeur assurée: ${details.valeurAssuree || '-'}
+ Taux: ${details.taux || '-'}
+ Prime mini: ${details.primeMinimum || '-'}
+ Cotisation HT: ${details.cotisationHT || '-'}
+ Coût acte: ${details.coutActe || '-'}
+ Cotisation TTC: ${details.cotisationTTC || '-'}
+
+
+ Acteur: ${(details.actorPrenom || '')} ${(details.actorNom || '')} ${details.actorMatricule ? `(${details.actorMatricule})` : ''}
+
+ ${row.source === 'hors_grille' && String(row.statutFacturation || '').toLowerCase().includes('non') ? `` : ''}
+
+
+
+ |
+
`;
+ };
+
+ const loadHistorique = async () => {
+ setLoading('historique', true);
+ try {
+ const statut = document.getElementById('h-statut').value || 'all';
+ const params = new URLSearchParams({
+ numClient: document.getElementById('h-numClient').value || '',
+ numContrat: document.getElementById('h-numContrat').value || '',
+ dateDebut: document.getElementById('h-dateDebut').value || '',
+ dateFin: document.getElementById('h-dateFin').value || '',
+ sourceType: document.getElementById('h-sourceType').value || 'all',
+ statutFacturation: statut === 'all' ? '' : statut,
+ page: String(state.historique.page),
+ pageSize: String(pageSize),
+ sort: 'dateDebutIso',
+ order: 'desc'
+ });
+ const data = await apiLatest('historique', `/advalo/historique?${params.toString()}`, { method: 'GET' });
+ const body = document.getElementById('historique-body');
+ body.innerHTML = '';
+
+ (data.rows || []).forEach((row) => {
+ const typedId = `${row.source === 'deleguee' ? 'g' : 'd'}:${row.id}`;
+ body.insertAdjacentHTML('beforeend', `
+
+ |
+ ${sourceLabel(row.source)} |
+ ${row.numDemande || ''} |
+ ${row.numClient || ''} |
+ ${row.numContrat || ''} |
+ ${row.dateDebut || ''} |
+ ${row.dateFin || ''} |
+ ${row.tarif || ''} |
+ ${row.statutFacturation || ''} |
+
+
+
+ |
+
+ `);
+ });
+
+ document.querySelectorAll('.advalo-h-expand').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ const typedId = btn.dataset.id;
+ const existing = document.getElementById(`h-detail-${typedId.replace(/[^a-zA-Z0-9_-]/g, '-')}`);
+ if (existing) {
+ existing.remove();
+ state.openHistoriqueId = null;
+ btn.textContent = '▸';
+ return;
+ }
+
+ if (state.openHistoriqueId) {
+ const old = document.getElementById(`h-detail-${state.openHistoriqueId.replace(/[^a-zA-Z0-9_-]/g, '-')}`);
+ if (old) old.remove();
+ const oldBtn = document.querySelector(`.advalo-h-expand[data-id="${state.openHistoriqueId}"]`);
+ if (oldBtn) oldBtn.textContent = '▸';
+ }
+
+ try {
+ const detailData = await api(`/advalo/historique/${encodeURIComponent(typedId)}`, { method: 'GET' });
+ const row = (data.rows || []).find((r) => `${r.source === 'deleguee' ? 'g' : 'd'}:${r.id}` === typedId);
+ const details = detailData.row?.details || row?.details || {};
+ const currentRow = btn.closest('tr');
+ currentRow.insertAdjacentHTML('afterend', renderHistoriqueDetail(typedId, row || {}, details));
+ state.openHistoriqueId = typedId;
+ btn.textContent = '▾';
+ document.querySelectorAll('.advalo-h-delete').forEach((deleteBtn) => {
+ deleteBtn.addEventListener('click', async () => {
+ if (!confirm('Supprimer cette demande hors grille ?')) return;
+ try {
+ await api(`/advalo/demande/${encodeURIComponent(deleteBtn.dataset.id)}`, { method: 'DELETE' });
+ toast('Demande supprimée.', 'green');
+ await loadHistorique();
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+ });
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+ });
+
+ document.querySelectorAll('.advalo-doc-avenant').forEach((button) => {
+ button.addEventListener('click', async () => {
+ try {
+ await downloadDocx(`/advalo/demande/${encodeURIComponent(button.dataset.id)}/avenant`, { method: 'POST' }, 'Avenant_Advalo.docx');
+ toast('Avenant généré.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+ });
+
+ document.querySelectorAll('.advalo-doc-attestation').forEach((button) => {
+ button.addEventListener('click', async () => {
+ try {
+ await downloadDocx(`/advalo/demande/${encodeURIComponent(button.dataset.id)}/attestation`, { method: 'POST' }, 'Attestation_Advalo.docx');
+ toast('Attestation générée.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+ });
+
+ state.historique.totalPages = Number(data.meta?.totalPages || 1);
+ state.historique.page = Number(data.meta?.page || 1);
+ setIndicator('h-page-indicator', state.historique.page, state.historique.totalPages, data.meta?.totalRows || 0);
+ refreshTextFields();
+ } catch (error) {
+ if (isAbortError(error)) return;
+ throw error;
+ } finally {
+ setLoading('historique', false);
+ }
+ };
+
+ const updateFacturationInfo = () => {
+ const selected = [...document.querySelectorAll('.advalo-fact-check')].filter((input) => input.checked).length;
+ const total = [...document.querySelectorAll('.advalo-fact-check')].length;
+ document.getElementById('f-selection-info').textContent = `${selected} / ${total} ligne(s) sélectionnée(s)`;
+ };
+
+ const renderFacturationRows = () => {
+ const body = document.getElementById('facturation-body');
+ body.innerHTML = '';
+ state.facturationRows.forEach((row) => {
+ const typedId = `${row.source === 'deleguee' ? 'g' : 'd'}:${row.id}`;
+ body.insertAdjacentHTML('beforeend', `
+
+ |
+ ${sourceLabel(row.source)} |
+ ${row.numDemande || ''} |
+ ${row.numClient || ''} |
+ ${row.numContrat || ''} |
+ ${row.dateDebut || ''} |
+ ${row.tarif || ''} |
+ ${row.statutFacturation || ''} |
+
+ `);
+ });
+ document.querySelectorAll('.advalo-fact-check').forEach((input) => input.addEventListener('change', updateFacturationInfo));
+ updateFacturationInfo();
+
+ const first = state.facturationRows[0];
+ document.getElementById('facturation-client-agent-row').style.display = first ? 'block' : 'none';
+ document.getElementById('f-client-recap').textContent = first ? `${first.nomClient || '-'} (${first.numClient || '-'})` : '-';
+ document.getElementById('f-agent-recap').textContent = first ? (first.souscripteur || '-') : '-';
+ };
+
+ const loadFacturationCandidates = async () => {
+ if (!validateContractField('f-numContrat', false)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ setLoading('facturation', true);
+ try {
+ const mode = document.getElementById('f-sourceMode').value || 'hors_grille';
+ const params = new URLSearchParams({
+ numContrat: document.getElementById('f-numContrat').value || '',
+ dateDebut: document.getElementById('f-dateDebut').value || '',
+ dateFin: document.getElementById('f-dateFin').value || '',
+ facture: 'false',
+ nonFacture: 'true',
+ deleguee: mode === 'mixte' ? 'true' : 'false',
+ nonDeleguee: 'true',
+ sourceType: mode === 'mixte' ? 'all' : 'hors_grille',
+ page: '1',
+ pageSize: '200',
+ sort: 'dateDebutIso',
+ order: 'desc'
+ });
+ const data = await apiLatest('facturation', `/advalo/historique?${params.toString()}`, { method: 'GET' });
+ state.facturationRows = (data.rows || []);
+ state.removedFacturationIds = new Set();
+ renderFacturationRows();
+ if ((data.meta?.totalRows || 0) === 0) toast('Aucune ligne non facturée sur la période/contrat.', 'orange');
+ else toast(`${data.meta?.totalRows || 0} ligne(s) facturable(s).`, 'green');
+ } catch (error) {
+ if (isAbortError(error)) return;
+ throw error;
+ } finally {
+ setLoading('facturation', false);
+ }
+ };
+
+ const loadCumul = async () => {
+ setLoading('cumul', true);
+ try {
+ if (!validateContractField('c-numContrat', false)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ const params = new URLSearchParams({
+ numContrat: document.getElementById('c-numContrat').value || '',
+ numClient: document.getElementById('c-numClient').value || '',
+ dateDebut: document.getElementById('c-dateDebut').value || '',
+ dateFin: document.getElementById('c-dateFin').value || '',
+ page: String(state.cumul.page),
+ pageSize: String(pageSize),
+ sort: 'totalNonFacture',
+ order: 'desc'
+ });
+ const data = await apiLatest('cumul', `/advalo/cumul?${params.toString()}`, { method: 'GET' });
+ document.getElementById('k-total-advalo').textContent = fmt(data.totalAdvalo);
+ document.getElementById('k-total-facture').textContent = fmt(data.totalFacture);
+ document.getElementById('k-total-nonfacture').textContent = fmt(data.totalNonFacture);
+ document.getElementById('k-total-lignes').textContent = String(data.totalLignes || 0);
+
+ const body = document.getElementById('cumul-body');
+ body.innerHTML = '';
+ (data.rows || []).forEach((row) => {
+ body.insertAdjacentHTML('beforeend', `
+
+ | ${row.numContrat || ''} |
+ ${row.nomClient || ''} |
+ ${row.region || ''} |
+ ${row.dpt || ''} |
+ ${row.souscripteur || ''} |
+ ${fmt(row.totalAdvalo)} |
+ ${fmt(row.totalFacture)} |
+ ${fmt(row.totalNonFacture)} |
+
+
+
+ |
+
+ `);
+ });
+
+ state.cumul.totalPages = Number(data.meta?.totalPages || 1);
+ state.cumul.page = Number(data.meta?.page || 1);
+ setIndicator('c-page-indicator', state.cumul.page, state.cumul.totalPages, data.meta?.totalRows || 0);
+
+ document.querySelectorAll('.advalo-cumul-hist').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ document.getElementById('h-numContrat').value = btn.dataset.contrat || '';
+ state.historique.page = 1;
+ state.loaded.historique = true;
+ await activatePanel('advalo-tab-historique');
+ await loadHistorique();
+ });
+ });
+
+ document.querySelectorAll('.advalo-cumul-fact').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ document.getElementById('f-numContrat').value = btn.dataset.contrat || '';
+ await activatePanel('advalo-tab-facturation');
+ refreshTextFields();
+ });
+ });
+ refreshTextFields();
+ } catch (error) {
+ if (isAbortError(error)) return;
+ throw error;
+ } finally {
+ setLoading('cumul', false);
+ }
+ };
+
+ const loadReporting = async () => {
+ setLoading('reporting', true);
+ try {
+ if (!validateContractField('r-numContrat', false)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ const statut = document.getElementById('r-statut').value || 'all';
+ const params = new URLSearchParams({
+ numClient: document.getElementById('r-numClient').value || '',
+ numContrat: document.getElementById('r-numContrat').value || '',
+ souscripteur: document.getElementById('r-souscripteur').value || '',
+ region: document.getElementById('r-region').value || '',
+ dateDebut: document.getElementById('r-dateDebut').value || '',
+ dateFin: document.getElementById('r-dateFin').value || '',
+ actorMatricule: document.getElementById('r-actorMatricule').value || '',
+ actorNom: document.getElementById('r-actorNom').value || '',
+ actionType: document.getElementById('r-actionType').value || '',
+ sourceType: document.getElementById('r-sourceType').value || 'all',
+ statutFacturation: statut === 'all' ? '' : statut,
+ sort: document.getElementById('r-sort').value || 'totalAdvalo',
+ order: document.getElementById('r-order').value || 'desc',
+ page: String(state.reporting.page),
+ pageSize: String(pageSize)
+ });
+ const data = await apiLatest('reporting', `/advalo/reporting?${params.toString()}`, { method: 'GET' });
+ document.getElementById('r-total-advalo').textContent = fmt(data.totaux?.totalAdvalo || 0);
+ document.getElementById('r-total-facture').textContent = fmt(data.totaux?.totalFacture || 0);
+ document.getElementById('r-total-nonfacture').textContent = fmt(data.totaux?.totalNonFacture || 0);
+ document.getElementById('r-total-lignes').textContent = String(data.totaux?.totalLignes || 0);
+
+ const body = document.getElementById('reporting-body');
+ body.innerHTML = '';
+ (data.rows || []).forEach((row) => {
+ body.insertAdjacentHTML('beforeend', `
+
+ | ${row.numContrat || ''} |
+ ${row.nomClient || ''} |
+ ${row.region || ''} |
+ ${row.souscripteur || ''} |
+ ${fmt(row.totalAdvalo)} |
+ ${fmt(row.totalFacture)} |
+ ${fmt(row.totalNonFacture)} |
+ ${row.totalLignes || 0} |
+
`);
+ });
+
+ const actorBody = document.getElementById('reporting-actors-body');
+ actorBody.innerHTML = '';
+ (data.actorStats || []).forEach((a) => {
+ actorBody.insertAdjacentHTML('beforeend', `
+
+ | ${a.actorMatricule || ''} |
+ ${a.actorName || ''} |
+ ${a.actionType || ''} |
+ ${a.actionsCount || 0} |
+
+ `);
+ });
+
+ state.reporting.totalPages = Number(data.meta?.totalPages || 1);
+ state.reporting.page = Number(data.meta?.page || 1);
+ setIndicator('r-page-indicator', state.reporting.page, state.reporting.totalPages, data.meta?.totalRows || 0);
+ refreshTextFields();
+ } catch (error) {
+ if (isAbortError(error)) return;
+ throw error;
+ } finally {
+ setLoading('reporting', false);
+ }
+ };
+
+ const activatePanel = async (targetId) => {
+ abortDataRequests();
+ panels.forEach((panel) => { panel.style.display = panel.id === targetId ? 'block' : 'none'; });
+ navLinks.forEach((link) => {
+ const li = link.parentElement;
+ if (!li) return;
+ if (link.dataset.target === targetId) li.classList.add('active');
+ else li.classList.remove('active');
+ });
+
+ initDateFields();
+ refreshTextFields();
+
+ if (targetId === 'advalo-tab-historique' && !state.loaded.historique) {
+ state.historique.page = 1;
+ await loadHistorique();
+ state.loaded.historique = true;
+ }
+ if (targetId === 'advalo-tab-reporting' && !state.loaded.reporting) {
+ state.reporting.page = 1;
+ await loadReporting();
+ state.loaded.reporting = true;
+ }
+ if (targetId === 'advalo-tab-cumul' && !state.loaded.cumul) {
+ state.cumul.page = 1;
+ await loadCumul();
+ state.loaded.cumul = true;
+ }
+ };
+
+ navLinks.forEach((link) => {
+ link.addEventListener('click', async (event) => {
+ event.preventDefault();
+ try { await activatePanel(link.dataset.target); } catch (error) { toast(error.message); }
+ });
+ });
+
+ document.querySelectorAll('.p-mode-check').forEach((input) => input.addEventListener('change', () => { syncModes(); refreshTextFields(); }));
+ ['p-capital', 'p-taux', 'p-primeMin', 'p-coutActe'].forEach((id) => {
+ const input = document.getElementById(id);
+ if (input) input.addEventListener('input', recalcPonctuelPricing);
+ });
+
+ document.getElementById('p-numContrat').addEventListener('blur', async () => {
+ const value = String(document.getElementById('p-numContrat').value || '').trim();
+ if (value.length === 16) {
+ try { await lookupContract(); } catch (error) { toast(error.message); }
+ }
+ });
+ document.getElementById('p-numContrat').addEventListener('keydown', async (e) => {
+ if (e.key !== 'Enter') return;
+ e.preventDefault();
+ try { await lookupContract(); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-load-contract').addEventListener('click', async (event) => {
+ event.preventDefault();
+ try { await lookupContract(); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('advalo-ponctuel-form').addEventListener('submit', async (event) => {
+ event.preventDefault();
+ const errorSlot = document.getElementById('p-form-error');
+ if (errorSlot) errorSlot.textContent = '';
+
+ syncModes();
+ const required = ['p-numContrat', 'p-marchandise', 'p-depart', 'p-arrivee', 'p-dateDebut', 'p-dateFin', 'p-capital', 'p-taux', 'p-primeMin', 'p-cotisationHT', 'p-cotisationTTC'];
+ if (!validateContractField('p-numContrat', true) || !requireFields(required)) {
+ if (errorSlot) errorSlot.textContent = 'Complète les champs obligatoires (contrat 16 chiffres).';
+ toast('Complète les champs obligatoires (contrat 16 chiffres).');
+ return;
+ }
+ if (!document.getElementById('p-mode').value.trim()) {
+ if (errorSlot) errorSlot.textContent = 'Sélectionne au moins un mode de transport.';
+ toast('Sélectionne au moins un mode de transport.');
+ return;
+ }
+
+ try {
+ const payload = {
+ typeFacturation: document.querySelector('input[name="p-typeFacturation"]:checked')?.value || 'ponctuel',
+ numClient: document.getElementById('p-numClient').value.trim(),
+ nomClient: document.getElementById('p-nomClient').value.trim(),
+ numContrat: document.getElementById('p-numContrat').value.trim(),
+ marchandise: document.getElementById('p-marchandise').value.trim(),
+ mode: document.getElementById('p-mode').value.trim(),
+ capital: document.getElementById('p-capital').value.trim(),
+ depart: document.getElementById('p-depart').value.trim(),
+ arrivee: document.getElementById('p-arrivee').value.trim(),
+ dateDebut: document.getElementById('p-dateDebut').value.trim(),
+ dateFin: document.getElementById('p-dateFin').value.trim(),
+ taux: document.getElementById('p-taux').value.trim(),
+ primeMinimum: document.getElementById('p-primeMin').value.trim(),
+ coutActe: document.getElementById('p-coutActe').value.trim(),
+ cotisationHT: document.getElementById('p-cotisationHT').value.trim(),
+ cotisationTTC: document.getElementById('p-cotisationTTC').value.trim(),
+ tarif: document.getElementById('p-cotisationTTC').value.trim(),
+ facturer: document.getElementById('p-facturer').checked
+ };
+ const data = await api('/advalo/ponctuel', { method: 'POST', body: JSON.stringify(payload) });
+ toast(`Demande créée: ${data.row.numDemande || data.row.id}`, 'green');
+ state.confirmation.demandId = `d:${data.row.id}`;
+ state.confirmation.summary = `Demande ${data.row.numDemande || data.row.id} - Contrat ${data.row.numContrat || ''}`;
+ const summary = document.getElementById('advalo-confirm-summary');
+ if (summary) summary.textContent = state.confirmation.summary;
+ confirmModal.open();
+ event.target.reset();
+ document.querySelectorAll('.p-mode-check').forEach((i) => { i.checked = false; });
+ document.getElementById('p-coutActe').value = '36';
+ document.getElementById('p-taux').value = '0.3';
+ document.getElementById('p-primeMin').value = '15';
+ document.querySelector('input[name="p-typeFacturation"][value="ponctuel"]').checked = true;
+ recalcPonctuelPricing();
+ refreshTextFields();
+ state.loaded.historique = false;
+ state.loaded.reporting = false;
+ state.loaded.cumul = false;
+ } catch (error) {
+ if (errorSlot) errorSlot.textContent = error.message;
+ toast(error.message);
+ }
+ });
+
+ document.getElementById('btn-f-load').addEventListener('click', async (event) => {
+ event.preventDefault();
+ try { await loadFacturationCandidates(); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('btn-f-remove').addEventListener('click', (event) => {
+ event.preventDefault();
+ const selected = [...document.querySelectorAll('.advalo-fact-check:checked')].map((i) => i.value);
+ if (!selected.length) {
+ toast('Sélectionne au moins une ligne à retirer.');
+ return;
+ }
+ selected.forEach((id) => state.removedFacturationIds.add(id));
+ state.facturationRows = state.facturationRows.filter((row) => {
+ const typedId = `${row.source === 'deleguee' ? 'g' : 'd'}:${row.id}`;
+ return !state.removedFacturationIds.has(typedId);
+ });
+ renderFacturationRows();
+ toast('Lignes retirées de la liste.', 'green');
+ });
+
+ document.getElementById('btn-facturer').addEventListener('click', async (event) => {
+ event.preventDefault();
+ const ids = [...document.querySelectorAll('.advalo-fact-check:checked')].map((i) => i.value).filter(Boolean);
+ if (!ids.length) {
+ toast('Sélectionne au moins une ligne.');
+ return;
+ }
+ try {
+ const sourceMode = document.getElementById('f-sourceMode').value || 'hors_grille';
+ const includeTransportDetails = document.querySelector('input[name="f-include-details"]:checked')?.value !== 'false';
+ const data = await api('/advalo/facturation/batch', {
+ method: 'POST',
+ body: JSON.stringify({
+ demandeIds: ids,
+ sourceMode,
+ includeTransportDetails,
+ removedDemandeIds: [...state.removedFacturationIds]
+ })
+ });
+ if (data.idempotent) toast(`Lot déjà traité: ${data.batch.id}`, 'blue');
+ else toast(`Facturation OK: lot ${data.batch.id}`, 'green');
+ state.batchConfirmation.batchId = data.batch.id;
+ state.batchConfirmation.summary = `Lot ${data.batch.id} - Contrat ${data.batch.numContrat || ''} - ${data.batch.dateDebut || ''} / ${data.batch.dateFin || ''}`;
+ const batchSummary = document.getElementById('advalo-batch-summary');
+ if (batchSummary) batchSummary.textContent = state.batchConfirmation.summary;
+ batchModal.open();
+ await loadFacturationCandidates();
+ state.loaded.reporting = false;
+ state.loaded.cumul = false;
+ state.loaded.historique = false;
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+
+ document.getElementById('btn-h-search').addEventListener('click', async () => {
+ if (!validateContractField('h-numContrat', false)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ state.historique.page = 1;
+ try { await loadHistorique(); toast('Historique chargé.', 'green'); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('btn-h-export').addEventListener('click', async () => {
+ if (!validateContractField('h-numContrat', false)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ try {
+ const statut = document.getElementById('h-statut').value || 'all';
+ const params = new URLSearchParams({
+ numClient: document.getElementById('h-numClient').value || '',
+ numContrat: document.getElementById('h-numContrat').value || '',
+ dateDebut: document.getElementById('h-dateDebut').value || '',
+ dateFin: document.getElementById('h-dateFin').value || '',
+ sourceType: document.getElementById('h-sourceType').value || 'all',
+ statutFacturation: statut === 'all' ? '' : statut,
+ sort: 'dateDebutIso',
+ order: 'desc'
+ });
+ const csv = await api(`/advalo/export?${params.toString()}`, { method: 'GET', headers: { Authorization: `Bearer ${token}` } });
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'advalo_export.csv';
+ link.click();
+ URL.revokeObjectURL(url);
+ toast('Export CSV généré.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+
+ document.getElementById('btn-h-prev').addEventListener('click', async () => {
+ if (state.historique.page <= 1) return;
+ state.historique.page -= 1;
+ try { await loadHistorique(); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-h-next').addEventListener('click', async () => {
+ if (state.historique.page >= state.historique.totalPages) return;
+ state.historique.page += 1;
+ try { await loadHistorique(); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('btn-cumul').addEventListener('click', async (event) => {
+ event.preventDefault();
+ state.cumul.page = 1;
+ try { await loadCumul(); toast('Cumul calculé.', 'green'); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-c-prev').addEventListener('click', async () => {
+ if (state.cumul.page <= 1) return;
+ state.cumul.page -= 1;
+ try { await loadCumul(); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-c-next').addEventListener('click', async () => {
+ if (state.cumul.page >= state.cumul.totalPages) return;
+ state.cumul.page += 1;
+ try { await loadCumul(); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('btn-reporting').addEventListener('click', async (event) => {
+ event.preventDefault();
+ state.reporting.page = 1;
+ try { await loadReporting(); toast('Reporting chargé.', 'green'); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-r-prev').addEventListener('click', async () => {
+ if (state.reporting.page <= 1) return;
+ state.reporting.page -= 1;
+ try { await loadReporting(); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-r-next').addEventListener('click', async () => {
+ if (state.reporting.page >= state.reporting.totalPages) return;
+ state.reporting.page += 1;
+ try { await loadReporting(); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('advalo-confirm-avenant').addEventListener('click', async () => {
+ if (!state.confirmation.demandId) return;
+ try {
+ await downloadDocx(`/advalo/demande/${encodeURIComponent(state.confirmation.demandId)}/avenant`, { method: 'POST' }, 'Avenant_Advalo.docx');
+ toast('Avenant généré.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+
+ document.getElementById('advalo-confirm-attestation').addEventListener('click', async () => {
+ if (!state.confirmation.demandId) return;
+ try {
+ await downloadDocx(`/advalo/demande/${encodeURIComponent(state.confirmation.demandId)}/attestation`, { method: 'POST' }, 'Attestation_Advalo.docx');
+ toast('Attestation générée.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+
+ document.getElementById('advalo-batch-avenant').addEventListener('click', async () => {
+ if (!state.batchConfirmation.batchId) return;
+ try {
+ await downloadDocx(`/advalo/facturation/batch/${encodeURIComponent(state.batchConfirmation.batchId)}/avenant`, { method: 'POST' }, 'Avenant_Periodique_Advalo.docx');
+ toast('Avenant périodique généré.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+
+ applyNumericGuards();
+ bindFloatingLabels();
+ initDateFields();
+ initSelects(document);
+ syncModes();
+ recalcPonctuelPricing();
+ refreshTextFields();
+
+ activatePanel('advalo-tab-accueil').catch((error) => toast(error.message));
+});
diff --git a/ecole/src/controllers/advaloController.js b/ecole/src/controllers/advaloController.js
new file mode 100644
index 00000000..805a80ab
--- /dev/null
+++ b/ecole/src/controllers/advaloController.js
@@ -0,0 +1,174 @@
+const express = require('express');
+const jwt = require('jsonwebtoken');
+const renderPage = require('../utils/renderHelper');
+const advaloService = require('../services/advaloService');
+const logger = require('../utils/logger');
+
+const router = express.Router();
+
+function getActor(req) {
+ const authHeader = req.headers.authorization || '';
+ const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
+ if (!token) {
+ const err = new Error('Session invalide, reconnectez-vous.');
+ err.status = 401;
+ throw err;
+ }
+ return jwt.verify(token, 'no-mdp');
+}
+
+function handleError(res, error) {
+ logger.log('error', `Advalo error: ${error.message}`, {
+ status: error.status || 500,
+ stack: error.stack,
+ data: error.data,
+ originalError: error.originalError ? String(error.originalError) : undefined
+ });
+ return res.status(error.status || 500).json({
+ valid: false,
+ message: error.message || 'Erreur serveur Advalorem'
+ });
+}
+
+router.get('/', (_req, res) => {
+ renderPage('advalo.ejs', res);
+});
+
+router.get('/lookup-contract', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const info = await advaloService.lookupContract(req.query.numContrat, actor);
+ return res.json({ valid: true, info });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.get('/historique', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const data = await advaloService.getHistorique(req.query, actor);
+ return res.json({
+ valid: true,
+ rows: data.rows,
+ meta: {
+ totalRows: data.totalRows,
+ totalPages: data.totalPages,
+ page: data.page,
+ pageSize: data.pageSize
+ }
+ });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.get('/historique/:id', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const data = await advaloService.getHistoriqueDetail(req.params.id, actor);
+ return res.json({ valid: true, row: data });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.get('/cumul', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const cumul = await advaloService.getCumul(req.query, actor);
+ return res.json({ valid: true, ...cumul });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.post('/ponctuel', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const row = await advaloService.createPonctuel(req.body, actor);
+ return res.status(201).json({ valid: true, row });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.delete('/demande/:id', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const row = await advaloService.softDeleteDemande(req.params.id, actor);
+ return res.json({ valid: true, row });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.post('/facturation/batch', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const result = await advaloService.facturerBatch(req.body, actor);
+ return res.json({ valid: true, ...result });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.post('/demande/:id/avenant', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const doc = await advaloService.generateDemandeDocument(req.params.id, 'avenant', actor);
+ res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
+ res.setHeader('Content-Disposition', `attachment; filename="${doc.filename}"`);
+ return res.send(doc.buffer);
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.post('/demande/:id/attestation', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const doc = await advaloService.generateDemandeDocument(req.params.id, 'attestation', actor);
+ res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
+ res.setHeader('Content-Disposition', `attachment; filename="${doc.filename}"`);
+ return res.send(doc.buffer);
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.post('/facturation/batch/:id/avenant', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const doc = await advaloService.generateBatchAvenant(req.params.id, actor);
+ res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
+ res.setHeader('Content-Disposition', `attachment; filename="${doc.filename}"`);
+ return res.send(doc.buffer);
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.get('/reporting', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const data = await advaloService.getReporting(req.query, actor);
+ return res.json({ valid: true, ...data });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.get('/export', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const csv = await advaloService.exportHistorique(req.query, actor);
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
+ res.setHeader('Content-Disposition', 'attachment; filename="advalo_export.csv"');
+ return res.send(csv);
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+module.exports = router;
diff --git a/ecole/src/routes.js b/ecole/src/routes.js
index 3b64bb33..f2a08288 100644
--- a/ecole/src/routes.js
+++ b/ecole/src/routes.js
@@ -21,6 +21,7 @@ const historiqueParcoursController = require('./controllers/historiqueParcoursCo
const userController = require('./controllers/userController');
const regionController = require('./controllers/regionController');
const downloadController = require('./controllers/utilsController');
+const advaloController = require('./controllers/advaloController');
// Association des contrôleurs aux routes
router.use('/', rootController);
@@ -38,5 +39,6 @@ router.use('/historiqueParcours', historiqueParcoursController);
router.use('/user', userController);
router.use('/region', regionController);
router.use('/download', downloadController);
+router.use('/advalo', advaloController);
-module.exports = router;
\ No newline at end of file
+module.exports = router;
diff --git a/ecole/src/services/advaloService.js b/ecole/src/services/advaloService.js
new file mode 100644
index 00000000..6b6b0a9b
--- /dev/null
+++ b/ecole/src/services/advaloService.js
@@ -0,0 +1,1754 @@
+const crypto = require('crypto');
+const { spawn, execFile } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const { promisify } = require('util');
+const PizZip = require('pizzip');
+require('dotenv').config();
+const { db } = require('../db/db-connect');
+const logger = require('../utils/logger');
+
+const execFileAsync = promisify(execFile);
+
+const JWT_ROLES = ['SOUSCRIPTEUR', 'MANAGER', 'ADMIN'];
+const TRANSPORT_MODES = ['Terrestre', 'Aérien', 'Fluvial', 'Maritime', 'Postal'];
+const DEFAULT_PAGE_SIZE = 20;
+const MAX_PAGE_SIZE = 100;
+const PB_SQLITE_PATH = path.resolve(__dirname, '..', 'db', 'pb_data', 'data.db');
+const ADVALO_TEMPLATE_DIR = path.resolve(__dirname, '..', 'templates', 'advalo');
+const ADVALO_DOCUMENTS_DIR = path.resolve(process.cwd(), 'documents', 'advalorem');
+const AXA_SCRIPTS_ROOT = path.resolve(process.cwd(), 'vbs');
+const AXA_TIMEOUT_MS = Number(process.env.AXA_TIMEOUT_MS || 65000);
+const AXA_RETRY_COUNT = Number(process.env.AXA_RETRY_COUNT || 1);
+const AXA_RETRY_DELAY_MS = Number(process.env.AXA_RETRY_DELAY_MS || 1200);
+
+if (!db.baseUrl || db.baseUrl === '/') {
+ db.baseUrl = process.env.DB_URL || db.baseUrl;
+}
+
+function ensureRole(actor) {
+ if (!actor || !JWT_ROLES.includes(actor.userAuthGroupe)) {
+ const err = new Error('Droits insuffisants pour accéder au module Advalorem.');
+ err.status = 403;
+ throw err;
+ }
+}
+
+function ensureReportingRole(actor) {
+ ensureRole(actor);
+ if (!['MANAGER', 'ADMIN'].includes(actor.userAuthGroupe)) {
+ const err = new Error('Le reporting Advalorem est réservé aux managers/admins.');
+ err.status = 403;
+ throw err;
+ }
+}
+
+function padContract(value) {
+ const digits = String(value || '').replace(/\D/g, '');
+ if (!digits) return '';
+ return digits.padStart(16, '0').slice(-16);
+}
+
+function normalizeContractStrict(value, { required = false } = {}) {
+ const digits = String(value || '').replace(/\D/g, '');
+ if (!digits) {
+ if (required) {
+ const err = new Error('Numéro de contrat obligatoire.');
+ err.status = 400;
+ throw err;
+ }
+ return '';
+ }
+ if (digits.length !== 16) {
+ const err = new Error('Le numéro de contrat doit contenir exactement 16 chiffres.');
+ err.status = 400;
+ throw err;
+ }
+ return digits;
+}
+
+function parseFrDate(value) {
+ const v = String(value || '').trim();
+ const match = v.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
+ if (!match) return null;
+ const date = new Date(`${match[3]}-${match[2]}-${match[1]}T00:00:00.000Z`);
+ return Number.isNaN(date.getTime()) ? null : date;
+}
+
+function formatFrDate(date) {
+ const d = new Date(date);
+ const day = String(d.getUTCDate()).padStart(2, '0');
+ const month = String(d.getUTCMonth() + 1).padStart(2, '0');
+ const year = d.getUTCFullYear();
+ return `${day}/${month}/${year}`;
+}
+
+function parseAmount(value) {
+ const normalized = String(value || '')
+ .replace(/€/g, '')
+ .replace(/\u202F/g, '')
+ .replace(/\u00A0/g, '')
+ .replace(/\s/g, '')
+ .replace(',', '.');
+ const out = Number(normalized);
+ return Number.isFinite(out) ? out : 0;
+}
+
+function prettyNumber(value) {
+ const amount = parseAmount(value);
+ return amount.toLocaleString('fr-FR', {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+}
+
+function collapseSpaces(value) {
+ return String(value || '').replace(/\s+/g, ' ').trim();
+}
+
+function escapeRegex(value) {
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+function safeDocValue(value) {
+ return String(value ?? '')
+ .trim()
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+}
+
+function ensureDirectory(dirPath) {
+ fs.mkdirSync(dirPath, { recursive: true });
+}
+
+function sanitizeFilePart(value) {
+ return String(value || '')
+ .replace(/[^a-zA-Z0-9-_]+/g, '_')
+ .replace(/_+/g, '_')
+ .replace(/^_+|_+$/g, '');
+}
+
+function isAllZeroDigits(value) {
+ const digits = String(value || '').replace(/\D/g, '');
+ return Boolean(digits) && /^0+$/.test(digits);
+}
+
+function buildActorDisplayName(actor = {}) {
+ const first = String(actor.userFirstName || '').trim();
+ const last = String(actor.userLastName || '').trim();
+ const full = `${first} ${last}`.trim();
+ if (full) return full;
+ return String(actor.userMatricule || '').trim();
+}
+
+function withMatriculeSuffix(name, actor = {}) {
+ const matricule = String(actor.userMatricule || '').trim();
+ const safeName = String(name || '').trim();
+ if (!matricule) return safeName;
+ if (!safeName) return matricule;
+ return `${safeName} (${matricule})`;
+}
+
+function replaceTokens(text, replacements) {
+ let output = text;
+ Object.entries(replacements).forEach(([token, value]) => {
+ const val = safeDocValue(value);
+ output = output.replace(new RegExp(escapeRegex(token), 'g'), val);
+ });
+ return output;
+}
+
+function buildDocxFromTemplate(templatePath, replacements) {
+ const content = fs.readFileSync(templatePath);
+ const zip = new PizZip(content);
+ const documentXml = zip.file('word/document.xml').asText();
+ const replacedXml = replaceTokens(documentXml, replacements);
+ zip.file('word/document.xml', replacedXml);
+ return zip.generate({ type: 'nodebuffer' });
+}
+
+async function saveAdvaloDocumentTrace({ type, filePath, buffer, demandeId = '', batchId = '' }) {
+ const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
+ await db.records.create('advalo_document', {
+ type,
+ path: filePath,
+ sha256,
+ demandeId,
+ batchId,
+ createdAt: new Date().toISOString()
+ });
+}
+
+function toCsv(rows) {
+ const headers = [
+ 'source',
+ 'numDemande',
+ 'numClient',
+ 'nomClient',
+ 'numContrat',
+ 'dateDebut',
+ 'dateFin',
+ 'tarif',
+ 'statutFacturation'
+ ];
+ const escape = (value) => `"${String(value ?? '').replace(/"/g, '""')}"`;
+ const lines = rows.map((row) => headers.map((h) => escape(row[h])).join(';'));
+ return `${headers.join(';')}\n${lines.join('\n')}`;
+}
+
+function hashFingerprint(payload) {
+ return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
+}
+
+function escapeFilterValue(value) {
+ return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+}
+
+function normalizeStatus(status) {
+ const v = String(status || '').toLowerCase();
+ return v.includes('facturé') && !v.includes('non') ? 'facture' : 'non_facture';
+}
+
+async function logAudit(eventType, actor = {}, data = {}) {
+ const payload = {
+ actorMatricule: String(actor.userMatricule || ''),
+ actorNom: String(actor.userLastName || ''),
+ actorPrenom: String(actor.userFirstName || ''),
+ actorGroupe: String(actor.userAuthGroupe || ''),
+ timestamp: new Date().toISOString(),
+ ...data
+ };
+ try {
+ await db.records.create('advalo_audit', {
+ eventType,
+ createdAt: new Date().toISOString(),
+ data: payload
+ });
+ } catch (error) {
+ logger.log('warn', 'advalo_audit write skipped', { eventType, error: error.message });
+ }
+}
+
+function normalizeTransportModes(value) {
+ const toTokens = (v) => {
+ if (Array.isArray(v)) return v;
+ return String(v || '')
+ .split(',')
+ .map((x) => x.trim())
+ .filter(Boolean);
+ };
+ const unique = [...new Set(toTokens(value).map((token) => {
+ const normalized = token.toLowerCase();
+ return TRANSPORT_MODES.find((mode) => mode.toLowerCase() === normalized) || null;
+ }).filter(Boolean))];
+ return unique;
+}
+
+function toSourceLabel(source) {
+ return source === 'deleguee' ? 'Grille déléguée' : 'Hors grille';
+}
+
+function parsePagination(filters = {}) {
+ const page = Math.max(1, Number.parseInt(filters.page, 10) || 1);
+ const requestedPageSize = Number.parseInt(filters.pageSize, 10) || DEFAULT_PAGE_SIZE;
+ const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, requestedPageSize));
+ return { page, pageSize };
+}
+
+function parseDateRange(filters = {}) {
+ const rawFrom = String(filters.dateDebut || '').trim();
+ const rawTo = String(filters.dateFin || '').trim();
+
+ if (!rawFrom && !rawTo) {
+ return { fromIso: null, toIso: null };
+ }
+
+ const from = rawFrom ? parseFrDate(rawFrom) : null;
+ const to = rawTo ? parseFrDate(rawTo) : null;
+
+ if ((rawFrom && !from) || (rawTo && !to)) {
+ const err = new Error('Format de date invalide (attendu: JJ/MM/AAAA).');
+ err.status = 400;
+ throw err;
+ }
+
+ if (from && to && from > to) {
+ const err = new Error('La date de début doit être antérieure à la date de fin.');
+ err.status = 400;
+ throw err;
+ }
+
+ return {
+ fromIso: from ? from.toISOString().slice(0, 10) : null,
+ toIso: to ? to.toISOString().slice(0, 10) : null
+ };
+}
+
+function resolveSourceType(filters = {}) {
+ const value = String(filters.sourceType || '').toLowerCase();
+ if (['deleguee', 'grille_deleguee', 'grille'].includes(value)) return 'deleguee';
+ if (['hors_grille', 'demande', 'ponctuel'].includes(value)) return 'hors_grille';
+ return 'all';
+}
+
+function sqlQuote(value) {
+ return `'${String(value || '').replace(/'/g, "''")}'`;
+}
+
+function parseSqlJson(stdout) {
+ const raw = String(stdout || '').trim();
+ if (!raw) return [];
+ try {
+ const parsed = JSON.parse(raw);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch (_error) {
+ return [];
+ }
+}
+
+async function runSqlJson(sql) {
+ const { stdout } = await execFileAsync('sqlite3', ['-json', PB_SQLITE_PATH, sql], {
+ maxBuffer: 1024 * 1024 * 30
+ });
+ return parseSqlJson(stdout);
+}
+
+function sqlTarifNumberExpr(columnName = 'tarif') {
+ return `CAST(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(${columnName}, ''), '€', ''), char(160), ''), char(8239), ''), ' ', ''), ',', '.') AS REAL)`;
+}
+
+const MERGED_CTE_SQL = `
+WITH merged AS (
+ SELECT
+ id,
+ 'deleguee' AS source,
+ numDemande,
+ numClient,
+ nomClient,
+ numContrat,
+ dateDebut,
+ dateFin,
+ dateDebutIso,
+ dateFinIso,
+ tarif,
+ statutFacturation,
+ marchandise,
+ mode,
+ capital,
+ depart,
+ arrivee,
+ '' AS region,
+ '' AS dpt,
+ '' AS souscripteur,
+ '' AS createdBy,
+ 0 AS isDeleted
+ FROM advalo_deleguee
+
+ UNION ALL
+
+ SELECT
+ id,
+ 'hors_grille' AS source,
+ numDemande,
+ numClient,
+ nomClient,
+ numContrat,
+ dateDebut,
+ dateFin,
+ dateDebutIso,
+ dateFinIso,
+ tarif,
+ statutFacturation,
+ marchandise,
+ mode,
+ capital,
+ depart,
+ arrivee,
+ COALESCE(region, '') AS region,
+ '' AS dpt,
+ '' AS souscripteur,
+ COALESCE(createdBy, '') AS createdBy,
+ COALESCE(isDeleted, 0) AS isDeleted
+ FROM advalo_demande
+),
+normalized AS (
+ SELECT
+ id,
+ source,
+ CASE WHEN source = 'deleguee' THEN 'Grille déléguée' ELSE 'Hors grille' END AS sourceLabel,
+ COALESCE(numDemande, '') AS numDemande,
+ COALESCE(numClient, '') AS numClient,
+ COALESCE(nomClient, '') AS nomClient,
+ COALESCE(numContrat, '') AS numContrat,
+ COALESCE(dateDebut, '') AS dateDebut,
+ COALESCE(dateFin, '') AS dateFin,
+ COALESCE(dateDebutIso, '') AS dateDebutIso,
+ COALESCE(dateFinIso, '') AS dateFinIso,
+ COALESCE(tarif, '') AS tarif,
+ COALESCE(statutFacturation, '') AS statutFacturation,
+ COALESCE(marchandise, '') AS marchandise,
+ COALESCE(mode, '') AS mode,
+ COALESCE(capital, '') AS capital,
+ COALESCE(depart, '') AS depart,
+ COALESCE(arrivee, '') AS arrivee,
+ CASE
+ WHEN lower(COALESCE(statutFacturation, '')) LIKE '%factur%' AND lower(COALESCE(statutFacturation, '')) NOT LIKE '%non%'
+ THEN 'facture'
+ ELSE 'non_facture'
+ END AS statutNormalized,
+ COALESCE(region, '') AS region,
+ COALESCE(dpt, '') AS dpt,
+ COALESCE(souscripteur, '') AS souscripteur,
+ COALESCE(createdBy, '') AS createdBy,
+ ${sqlTarifNumberExpr('tarif')} AS tarifNum,
+ COALESCE(
+ date(substr(COALESCE(dateDebutIso, ''), 1, 10)),
+ date(substr(COALESCE(dateDebut, ''), 7, 4) || '-' || substr(COALESCE(dateDebut, ''), 4, 2) || '-' || substr(COALESCE(dateDebut, ''), 1, 2))
+ ) AS dateDebutSql,
+ COALESCE(
+ date(substr(COALESCE(dateFinIso, ''), 1, 10)),
+ date(substr(COALESCE(dateFin, ''), 7, 4) || '-' || substr(COALESCE(dateFin, ''), 4, 2) || '-' || substr(COALESCE(dateFin, ''), 1, 2))
+ ) AS dateFinSql
+ FROM merged
+ WHERE source = 'deleguee' OR isDeleted = 0
+)
+`;
+
+function buildCommonWhere(filters = {}, actor) {
+ const where = [];
+ const dateRange = parseDateRange(filters);
+ const contract = normalizeContractStrict(filters.numContrat, { required: false });
+ const numClient = String(filters.numClient || '').trim();
+ const region = String(filters.region || '').trim();
+ const souscripteur = String(filters.souscripteur || '').trim();
+ const actorMatricule = String(filters.actorMatricule || '').trim();
+ const actorNom = String(filters.actorNom || '').trim();
+ const actionType = String(filters.actionType || '').trim().toLowerCase();
+ const sourceType = resolveSourceType(filters);
+
+ if (contract) where.push(`numContrat = ${sqlQuote(contract)}`);
+ if (numClient) where.push(`numClient LIKE ${sqlQuote(`%${numClient}%`)}`);
+ if (region) where.push(`region LIKE ${sqlQuote(`%${region}%`)}`);
+ if (souscripteur) where.push(`souscripteur LIKE ${sqlQuote(`%${souscripteur}%`)}`);
+
+ if (dateRange.fromIso) where.push(`dateDebutSql >= date(${sqlQuote(dateRange.fromIso)})`);
+ if (dateRange.toIso) where.push(`dateDebutSql <= date(${sqlQuote(dateRange.toIso)})`);
+
+ if (sourceType === 'deleguee') where.push(`source = 'deleguee'`);
+ if (sourceType === 'hors_grille') where.push(`source = 'hors_grille'`);
+
+ const statusFilter = String(filters.statutFacturation || '').toLowerCase();
+ if (statusFilter === 'facture') where.push(`statutNormalized = 'facture'`);
+ if (statusFilter === 'non_facture') where.push(`statutNormalized = 'non_facture'`);
+
+ const facture = String(filters.facture || '').toLowerCase();
+ const nonFacture = String(filters.nonFacture || '').toLowerCase();
+ const deleguee = String(filters.deleguee || '').toLowerCase();
+ const nonDeleguee = String(filters.nonDeleguee || '').toLowerCase();
+
+ if (facture === 'false') where.push(`statutNormalized != 'facture'`);
+ if (nonFacture === 'false') where.push(`statutNormalized != 'non_facture'`);
+ if (deleguee === 'false') where.push(`source != 'deleguee'`);
+ if (nonDeleguee === 'false') where.push(`source != 'hors_grille'`);
+
+ if (actor?.userAuthGroupe === 'SOUSCRIPTEUR') {
+ where.push(`(source = 'deleguee' OR createdBy = ${sqlQuote(actor.userMatricule || '')})`);
+ }
+
+ if (actorMatricule || actorNom || actionType) {
+ const auditClauses = [];
+ if (actorMatricule) {
+ auditClauses.push(`json_extract(a.data, '$.actorMatricule') LIKE ${sqlQuote(`%${actorMatricule}%`)}`);
+ }
+ if (actorNom) {
+ auditClauses.push(`(
+ json_extract(a.data, '$.actorNom') LIKE ${sqlQuote(`%${actorNom}%`)}
+ OR json_extract(a.data, '$.actorPrenom') LIKE ${sqlQuote(`%${actorNom}%`)}
+ )`);
+ }
+ if (actionType) {
+ auditClauses.push(`a.eventType = ${sqlQuote(actionType)}`);
+ }
+ where.push(`EXISTS (
+ SELECT 1
+ FROM advalo_audit a
+ WHERE json_extract(a.data, '$.numContrat') = normalized.numContrat
+ ${auditClauses.length ? `AND ${auditClauses.join(' AND ')}` : ''}
+ )`);
+ }
+
+ return where;
+}
+
+function normalizeSqlRow(row) {
+ return {
+ ...row,
+ sourceType: row.source,
+ sourceLabel: row.sourceLabel || toSourceLabel(row.source),
+ statutNormalized: row.statutNormalized || normalizeStatus(row.statutFacturation),
+ numContrat: padContract(row.numContrat),
+ tarif: String(row.tarif ?? ''),
+ tarifNum: Number(row.tarifNum || 0)
+ };
+}
+
+function normalizeSort(sort, fallback, allowList) {
+ const value = String(sort || fallback);
+ return allowList.includes(value) ? value : fallback;
+}
+
+function normalizeOrder(order) {
+ return String(order || 'desc').toLowerCase() === 'asc' ? 'ASC' : 'DESC';
+}
+
+function buildPaginationMeta(totalRows, page, pageSize) {
+ const totalPages = Math.max(1, Math.ceil(totalRows / pageSize));
+ const safePage = Math.min(Math.max(1, page), totalPages);
+ return {
+ totalRows,
+ totalPages,
+ page: safePage,
+ pageSize,
+ offset: (safePage - 1) * pageSize
+ };
+}
+
+async function runQt550Axa(payload) {
+ if (process.platform !== 'win32') {
+ const err = new Error('Pont AXA indisponible sur cet environnement (Windows requis).');
+ err.status = 502;
+ throw err;
+ }
+
+ const matricule = sanitizeFilePart(payload?.matricule || 'SYSTEM');
+ const configPath = path.join(AXA_SCRIPTS_ROOT, 'script_qt550', 'config', `bordereau_${matricule}.txt`);
+ ensureDirectory(path.dirname(configPath));
+ const content = Object.entries({
+ numContrat: payload?.numContrat || '',
+ totalCapitaux: payload?.totalCapitaux || payload?.capital || '',
+ totalCotisation: payload?.totalCotisation || payload?.tarif || '',
+ dateDebut: payload?.dateDebut || '',
+ dateFin: payload?.dateFin || ''
+ })
+ .map(([key, value]) => `${key}:${value}`)
+ .join('##');
+ fs.writeFileSync(configPath, content, 'utf8');
+
+ await runWScriptScript('script_qt550/bordereau.vbs', { payload, errorLabel: 'QT550' });
+}
+
+function readAxaKeyValueFile(filePath) {
+ if (!fs.existsSync(filePath)) return {};
+ const raw = fs.readFileSync(filePath, 'utf8');
+ return raw.split(/\r?\n/).reduce((acc, line) => {
+ if (!line || !line.includes(':')) return acc;
+ const [key, ...rest] = line.split(':');
+ const normalizedKey = String(key || '').trim();
+ if (!normalizedKey) return acc;
+ acc[normalizedKey] = rest.join(':').trim();
+ return acc;
+ }, {});
+}
+
+async function runWScriptScript(relativeScriptPath, { payload = {}, errorLabel = 'AXA' } = {}) {
+ const scriptPath = path.join(AXA_SCRIPTS_ROOT, relativeScriptPath);
+ if (!fs.existsSync(scriptPath)) {
+ const err = new Error(`Script AXA manquant: ${relativeScriptPath}`);
+ err.status = 500;
+ throw err;
+ }
+
+ let lastError = null;
+ for (let attempt = 0; attempt <= AXA_RETRY_COUNT; attempt += 1) {
+ try {
+ await new Promise((resolve, reject) => {
+ const child = spawn('wscript.exe', [scriptPath], { cwd: process.cwd(), stdio: 'ignore' });
+ const timeout = setTimeout(() => {
+ child.kill('SIGTERM');
+ const err = new Error(`Timeout lors de l'appel AXA ${errorLabel}.`);
+ err.status = 504;
+ reject(err);
+ }, AXA_TIMEOUT_MS);
+
+ child.on('error', (error) => {
+ clearTimeout(timeout);
+ const err = new Error(`Erreur technique AXA ${errorLabel}: ${error.message}`);
+ err.status = 502;
+ reject(err);
+ });
+ child.on('exit', (code) => {
+ clearTimeout(timeout);
+ if (code === 0) return resolve();
+ const err = new Error(`AXA ${errorLabel} en échec (code ${code}).`);
+ err.status = 502;
+ return reject(err);
+ });
+ });
+ return;
+ } catch (error) {
+ lastError = error;
+ logger.log('warn', `AXA ${errorLabel} failed`, {
+ attempt: attempt + 1,
+ retries: AXA_RETRY_COUNT + 1,
+ error: error.message,
+ payload
+ });
+ if (attempt < AXA_RETRY_COUNT) await new Promise((resolve) => setTimeout(resolve, AXA_RETRY_DELAY_MS));
+ }
+ }
+ throw lastError || new Error(`AXA ${errorLabel} failed`);
+}
+
+function normalizeLookupInfo(raw = {}, fallbackContract = '') {
+ const numContrat = normalizeContractStrict(raw.numContrat || fallbackContract, { required: true });
+ return {
+ numContrat,
+ numClient: String(raw.numClient || '').replace(/\D/g, '').slice(0, 16),
+ nomClient: collapseSpaces(raw.nomClient || ''),
+ adresseClient: collapseSpaces(raw.adresseClient || ''),
+ codePostalClient: collapseSpaces(raw.codePostal || raw.codePostalClient || ''),
+ nomAgent: collapseSpaces(raw.nomAgent || ''),
+ numAgent: String(raw.numAgent || '').replace(/\D/g, '').slice(0, 16)
+ };
+}
+
+async function lookupContractFromAxaScripts(numContrat, actor) {
+ if (process.platform !== 'win32') return null;
+ const matricule = sanitizeFilePart(actor?.userMatricule || 'SYSTEM');
+ const pa025ConfigPath = path.join(AXA_SCRIPTS_ROOT, 'script_pa025', 'config', `client_${matricule}.txt`);
+ const cl063ConfigPath = path.join(AXA_SCRIPTS_ROOT, 'script_cl063', 'config', `client_${matricule}.txt`);
+ ensureDirectory(path.dirname(pa025ConfigPath));
+ ensureDirectory(path.dirname(cl063ConfigPath));
+
+ const pa025Payload = [
+ `numContrat:${numContrat}`,
+ 'dateDebutBordereau:',
+ 'dateFinBordereau:'
+ ].join('\r\n');
+
+ fs.writeFileSync(pa025ConfigPath, pa025Payload, 'utf8');
+ await runWScriptScript('script_pa025/attestation.vbs', { payload: { numContrat }, errorLabel: 'PA025' });
+ const pa025Data = readAxaKeyValueFile(pa025ConfigPath);
+
+ let cl063Data = {};
+ const pa025NumClient = String(pa025Data.numClient || '').replace(/\D/g, '');
+ if (pa025NumClient) {
+ fs.writeFileSync(cl063ConfigPath, `numClient:${pa025NumClient}`, 'utf8');
+ await runWScriptScript('script_cl063/extract.vbs', { payload: { numClient: pa025NumClient }, errorLabel: 'CL063' });
+ cl063Data = readAxaKeyValueFile(cl063ConfigPath);
+ }
+
+ const merged = { ...pa025Data, ...cl063Data };
+ return {
+ ...normalizeLookupInfo(merged, numContrat),
+ source: 'axa scripts'
+ };
+}
+
+async function lookupContract(numContrat, actor) {
+ ensureRole(actor);
+ const formatted = normalizeContractStrict(numContrat, { required: true });
+
+ try {
+ const scriptInfo = await lookupContractFromAxaScripts(formatted, actor);
+ if (scriptInfo?.numContrat) {
+ await logAudit('lookup_contrat', actor, {
+ sourceType: 'axa_scripts',
+ numContrat: scriptInfo.numContrat,
+ numClient: scriptInfo.numClient || ''
+ });
+ return scriptInfo;
+ }
+ } catch (error) {
+ logger.log('warn', 'lookupContract AXA scripts failed, fallback DB', {
+ numContrat: formatted,
+ actor: actor?.userMatricule || '',
+ error: error.message
+ });
+ }
+
+ const contratList = await db.records.getList('contrat', 1, 1, {
+ filter: `numContrat = "${escapeFilterValue(formatted)}"`,
+ expand: 'client,intermediaire'
+ });
+
+ if (contratList.totalItems > 0) {
+ const contrat = contratList.items[0];
+ await logAudit('lookup_contrat', actor, {
+ sourceType: 'base_contrat',
+ numContrat: formatted,
+ numClient: contrat?.['@expand']?.client?.numClient || ''
+ });
+ return {
+ numContrat: contrat.numContrat || formatted,
+ numClient: contrat?.['@expand']?.client?.numClient || '',
+ nomClient: contrat?.['@expand']?.client?.nom || '',
+ adresseClient: contrat?.['@expand']?.client?.adresse || '',
+ codePostalClient: contrat?.['@expand']?.client?.codePostal || '',
+ nomAgent: contrat?.['@expand']?.intermediaire?.nom || '',
+ numAgent: contrat?.['@expand']?.intermediaire?.numPortefeuille || '',
+ source: 'base contrat'
+ };
+ }
+
+ const delegate = await db.records.getList('advalo_deleguee', 1, 1, {
+ filter: `numContrat = "${escapeFilterValue(formatted)}"`,
+ sort: '-dateDebutIso'
+ });
+ if (delegate.totalItems > 0) {
+ const row = delegate.items[0];
+ await logAudit('lookup_contrat', actor, {
+ sourceType: 'grille_deleguee',
+ numContrat: formatted,
+ numClient: row.numClient || ''
+ });
+ return {
+ numContrat: row.numContrat || formatted,
+ numClient: row.numClient || '',
+ nomClient: row.nomClient || '',
+ adresseClient: '',
+ codePostalClient: '',
+ nomAgent: row.souscripteur || '',
+ numAgent: row.numPortefeuille || '',
+ source: 'grille déléguée'
+ };
+ }
+
+ const demande = await db.records.getList('advalo_demande', 1, 1, {
+ filter: `numContrat = "${escapeFilterValue(formatted)}" && isDeleted = false`,
+ sort: '-dateDebutIso'
+ });
+ if (demande.totalItems > 0) {
+ const row = demande.items[0];
+ await logAudit('lookup_contrat', actor, {
+ sourceType: 'hors_grille',
+ numContrat: formatted,
+ numClient: row.numClient || ''
+ });
+ return {
+ numContrat: row.numContrat || formatted,
+ numClient: row.numClient || '',
+ nomClient: row.nomClient || '',
+ adresseClient: '',
+ codePostalClient: '',
+ nomAgent: row.souscripteur || '',
+ numAgent: row.numPortefeuille || '',
+ source: 'hors grille'
+ };
+ }
+
+ const result = {
+ numContrat: formatted,
+ numClient: '',
+ nomClient: '',
+ adresseClient: '',
+ codePostalClient: '',
+ nomAgent: '',
+ numAgent: '',
+ source: 'none'
+ };
+ await logAudit('lookup_contrat', actor, {
+ sourceType: 'none',
+ numContrat: formatted,
+ numClient: ''
+ });
+ return result;
+}
+
+async function getHistorique(filters, actor) {
+ ensureRole(actor);
+ const { page, pageSize } = parsePagination(filters);
+ const sort = normalizeSort(filters.sort, 'dateDebutSql', [
+ 'dateDebutSql',
+ 'dateDebutIso',
+ 'dateDebut',
+ 'dateFinSql',
+ 'dateFinIso',
+ 'dateFin',
+ 'numContrat',
+ 'numClient',
+ 'tarifNum',
+ 'tarif'
+ ]);
+ const sqlSort = ['dateDebut', 'dateDebutIso'].includes(sort)
+ ? 'dateDebutSql'
+ : ['dateFin', 'dateFinIso'].includes(sort)
+ ? 'dateFinSql'
+ : ['tarif', 'tarifNum'].includes(sort)
+ ? 'tarifNum'
+ : sort;
+ const order = normalizeOrder(filters.order);
+
+ const where = buildCommonWhere(filters, actor);
+ const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
+
+ const countRows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT COUNT(*) AS totalRows FROM normalized ${whereClause};
+`);
+ const totalRows = Number(countRows[0]?.totalRows || 0);
+ const meta = buildPaginationMeta(totalRows, page, pageSize);
+
+ const rows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ id,
+ source,
+ sourceLabel,
+ numDemande,
+ numClient,
+ nomClient,
+ numContrat,
+ dateDebut,
+ dateFin,
+ dateDebutIso,
+ dateFinIso,
+ marchandise,
+ mode,
+ capital,
+ depart,
+ arrivee,
+ tarif,
+ tarifNum,
+ statutFacturation,
+ statutNormalized,
+ region,
+ dpt,
+ souscripteur,
+ createdBy
+FROM normalized
+${whereClause}
+ORDER BY ${sqlSort} ${order}, id ASC
+LIMIT ${meta.pageSize} OFFSET ${meta.offset};
+`);
+
+ const normalizedRows = rows.map(normalizeSqlRow);
+ const detailRows = normalizedRows.filter((row) => row.source === 'hors_grille');
+ let detailById = {};
+ if (detailRows.length) {
+ const idsSql = detailRows.map((row) => sqlQuote(row.id)).join(', ');
+ const audits = await runSqlJson(`
+SELECT
+ json_extract(data, '$.demandeId') AS demandeId,
+ json_extract(data, '$.actorMatricule') AS actorMatricule,
+ json_extract(data, '$.actorNom') AS actorNom,
+ json_extract(data, '$.actorPrenom') AS actorPrenom,
+ json_extract(data, '$.pricing.valeurAssuree') AS valeurAssuree,
+ json_extract(data, '$.pricing.taux') AS taux,
+ json_extract(data, '$.pricing.primeMinimum') AS primeMinimum,
+ json_extract(data, '$.pricing.cotisationHT') AS cotisationHT,
+ json_extract(data, '$.pricing.coutActe') AS coutActe,
+ json_extract(data, '$.pricing.cotisationTTC') AS cotisationTTC,
+ created
+FROM advalo_audit
+WHERE eventType IN ('create','update')
+ AND json_extract(data, '$.demandeId') IN (${idsSql})
+ORDER BY created DESC;
+`);
+ audits.forEach((audit) => {
+ const key = String(audit.demandeId || '');
+ if (!key || detailById[key]) return;
+ detailById[key] = {
+ actorMatricule: String(audit.actorMatricule || ''),
+ actorNom: String(audit.actorNom || ''),
+ actorPrenom: String(audit.actorPrenom || ''),
+ valeurAssuree: String(audit.valeurAssuree || ''),
+ taux: String(audit.taux || ''),
+ primeMinimum: String(audit.primeMinimum || ''),
+ cotisationHT: String(audit.cotisationHT || ''),
+ coutActe: String(audit.coutActe || ''),
+ cotisationTTC: String(audit.cotisationTTC || '')
+ };
+ });
+ }
+
+ return {
+ rows: normalizedRows.map((row) => ({
+ ...row,
+ details: {
+ marchandise: row.marchandise || '',
+ mode: row.mode || '',
+ depart: row.depart || '',
+ arrivee: row.arrivee || '',
+ valeurAssuree: detailById[row.id]?.valeurAssuree || row.capital || '',
+ taux: detailById[row.id]?.taux || '',
+ primeMinimum: detailById[row.id]?.primeMinimum || '',
+ cotisationHT: detailById[row.id]?.cotisationHT || '',
+ coutActe: detailById[row.id]?.coutActe || '',
+ cotisationTTC: detailById[row.id]?.cotisationTTC || row.tarif || '',
+ actorMatricule: detailById[row.id]?.actorMatricule || row.createdBy || '',
+ actorNom: detailById[row.id]?.actorNom || '',
+ actorPrenom: detailById[row.id]?.actorPrenom || ''
+ }
+ })),
+ totalRows: meta.totalRows,
+ totalPages: meta.totalPages,
+ page: meta.page,
+ pageSize: meta.pageSize
+ };
+}
+
+async function getCumul(filters, actor) {
+ ensureRole(actor);
+ const { page, pageSize } = parsePagination(filters);
+ const sort = normalizeSort(filters.sort, 'totalNonFacture', [
+ 'numContrat',
+ 'nomClient',
+ 'region',
+ 'dpt',
+ 'souscripteur',
+ 'totalAdvalo',
+ 'totalFacture',
+ 'totalNonFacture',
+ 'totalLignes'
+ ]);
+ const order = normalizeOrder(filters.order || 'desc');
+
+ const where = buildCommonWhere({
+ ...filters,
+ facture: 'true',
+ nonFacture: 'true',
+ deleguee: 'true',
+ nonDeleguee: 'true'
+ }, actor);
+ const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
+
+ const totalsRows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause};
+`);
+ const totals = {
+ totalAdvalo: Number(totalsRows[0]?.totalAdvalo || 0),
+ totalFacture: Number(totalsRows[0]?.totalFacture || 0),
+ totalNonFacture: Number(totalsRows[0]?.totalNonFacture || 0),
+ totalLignes: Number(totalsRows[0]?.totalLignes || 0)
+ };
+
+ const contractsCountRows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT COUNT(*) AS totalRows FROM (
+ SELECT numContrat
+ FROM normalized
+ ${whereClause}
+ GROUP BY numContrat
+) x;
+`);
+ const totalRows = Number(contractsCountRows[0]?.totalRows || 0);
+ const meta = buildPaginationMeta(totalRows, page, pageSize);
+
+ const rows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ numContrat,
+ MAX(nomClient) AS nomClient,
+ MAX(region) AS region,
+ MAX(dpt) AS dpt,
+ MAX(souscripteur) AS souscripteur,
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause}
+GROUP BY numContrat
+ORDER BY ${sort} ${order}, numContrat ASC
+LIMIT ${meta.pageSize} OFFSET ${meta.offset};
+`);
+
+ return {
+ ...totals,
+ rows: rows.map((row) => ({
+ numContrat: padContract(row.numContrat),
+ nomClient: row.nomClient || '',
+ region: row.region || '',
+ dpt: row.dpt || '',
+ souscripteur: row.souscripteur || '',
+ totalAdvalo: Number(row.totalAdvalo || 0),
+ totalFacture: Number(row.totalFacture || 0),
+ totalNonFacture: Number(row.totalNonFacture || 0),
+ totalLignes: Number(row.totalLignes || 0)
+ })),
+ meta: {
+ totalRows: meta.totalRows,
+ totalPages: meta.totalPages,
+ page: meta.page,
+ pageSize: meta.pageSize
+ }
+ };
+}
+
+function toGroupRows(rows) {
+ return rows.map((row) => ({
+ label: row.label || 'Non renseigné',
+ totalAdvalo: Number(row.totalAdvalo || 0),
+ totalFacture: Number(row.totalFacture || 0),
+ totalNonFacture: Number(row.totalNonFacture || 0),
+ totalLignes: Number(row.totalLignes || 0)
+ }));
+}
+
+async function getReporting(filters, actor) {
+ ensureReportingRole(actor);
+
+ const { page, pageSize } = parsePagination(filters);
+ const sort = normalizeSort(filters.sort, 'totalAdvalo', [
+ 'numContrat',
+ 'nomClient',
+ 'region',
+ 'souscripteur',
+ 'totalAdvalo',
+ 'totalFacture',
+ 'totalNonFacture',
+ 'totalLignes'
+ ]);
+ const order = normalizeOrder(filters.order || 'desc');
+
+ const where = buildCommonWhere(filters, actor);
+ const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
+
+ const totalsRows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause};
+`);
+ const totaux = {
+ totalAdvalo: Number(totalsRows[0]?.totalAdvalo || 0),
+ totalFacture: Number(totalsRows[0]?.totalFacture || 0),
+ totalNonFacture: Number(totalsRows[0]?.totalNonFacture || 0),
+ totalLignes: Number(totalsRows[0]?.totalLignes || 0)
+ };
+
+ const contractsCountRows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT COUNT(*) AS totalRows FROM (
+ SELECT numContrat
+ FROM normalized
+ ${whereClause}
+ GROUP BY numContrat
+) x;
+`);
+ const totalRows = Number(contractsCountRows[0]?.totalRows || 0);
+ const meta = buildPaginationMeta(totalRows, page, pageSize);
+
+ const rows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ numContrat,
+ MAX(nomClient) AS nomClient,
+ MAX(region) AS region,
+ MAX(souscripteur) AS souscripteur,
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause}
+GROUP BY numContrat
+ORDER BY ${sort} ${order}, numContrat ASC
+LIMIT ${meta.pageSize} OFFSET ${meta.offset};
+`);
+
+ const groupedClients = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ CASE WHEN trim(nomClient) = '' THEN 'Non renseigné' ELSE trim(nomClient) END AS label,
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause}
+GROUP BY label
+ORDER BY totalAdvalo DESC
+LIMIT 20;
+`);
+
+ const groupedSouscripteurs = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ CASE WHEN trim(souscripteur) = '' THEN 'Non renseigné' ELSE trim(souscripteur) END AS label,
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause}
+GROUP BY label
+ORDER BY totalAdvalo DESC
+LIMIT 20;
+`);
+
+ const groupedRegions = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ CASE WHEN trim(region) = '' THEN 'Non renseigné' ELSE trim(region) END AS label,
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause}
+GROUP BY label
+ORDER BY totalAdvalo DESC
+LIMIT 20;
+`);
+
+ const actorStats = await runSqlJson(`
+SELECT
+ trim(
+ COALESCE(json_extract(data, '$.actorPrenom'), '') ||
+ CASE WHEN trim(COALESCE(json_extract(data, '$.actorPrenom'), '')) != '' THEN ' ' ELSE '' END ||
+ COALESCE(json_extract(data, '$.actorNom'), '')
+ ) AS actorName,
+ COALESCE(json_extract(data, '$.actorMatricule'), '') AS actorMatricule,
+ COALESCE(eventType, '') AS actionType,
+ COUNT(*) AS actionsCount
+FROM advalo_audit
+WHERE eventType IN ('create','update','delete_soft','facturation_batch','doc_avenant','doc_attestation','lookup_contrat')
+GROUP BY actorMatricule, actorName, actionType
+ORDER BY actionsCount DESC
+LIMIT 100;
+`);
+
+ return {
+ rows: rows.map((row) => ({
+ numContrat: padContract(row.numContrat),
+ nomClient: row.nomClient || '',
+ region: row.region || '',
+ souscripteur: row.souscripteur || '',
+ totalAdvalo: Number(row.totalAdvalo || 0),
+ totalFacture: Number(row.totalFacture || 0),
+ totalNonFacture: Number(row.totalNonFacture || 0),
+ totalLignes: Number(row.totalLignes || 0)
+ })),
+ meta: {
+ totalRows: meta.totalRows,
+ totalPages: meta.totalPages,
+ page: meta.page,
+ pageSize: meta.pageSize
+ },
+ totaux,
+ groupes: {
+ clients: toGroupRows(groupedClients),
+ souscripteurs: toGroupRows(groupedSouscripteurs),
+ regions: toGroupRows(groupedRegions)
+ },
+ actorStats: actorStats.map((row) => ({
+ actorMatricule: String(row.actorMatricule || ''),
+ actorName: String(row.actorName || '').trim() || 'Non renseigné',
+ actionType: String(row.actionType || ''),
+ actionsCount: Number(row.actionsCount || 0)
+ }))
+ };
+}
+
+async function createPonctuel(payload, actor) {
+ ensureRole(actor);
+ const required = [
+ 'typeFacturation',
+ 'numContrat',
+ 'marchandise',
+ 'capital',
+ 'depart',
+ 'arrivee',
+ 'dateDebut',
+ 'dateFin',
+ 'tarif',
+ 'taux',
+ 'primeMinimum',
+ 'cotisationHT',
+ 'cotisationTTC'
+ ];
+ const missing = required.filter((key) => !String(payload[key] || '').trim());
+ if (missing.length) {
+ const err = new Error(`Champs obligatoires manquants: ${missing.join(', ')}`);
+ err.status = 400;
+ throw err;
+ }
+
+ const typeFacturation = String(payload.typeFacturation || '').toLowerCase();
+ if (!['ponctuel', 'periodique'].includes(typeFacturation)) {
+ const err = new Error('Type de facturation invalide (ponctuel|periodique).');
+ err.status = 400;
+ throw err;
+ }
+ const numContrat = normalizeContractStrict(payload.numContrat, { required: true });
+ const modes = normalizeTransportModes(payload.mode);
+ if (!modes.length) {
+ const err = new Error('Sélectionne au moins un mode de transport.');
+ err.status = 400;
+ throw err;
+ }
+
+ const now = new Date();
+ const numDemande =
+ payload.numDemande || `${actor.userMatricule}_${now.toISOString().slice(0, 19).replace(/[T:]/g, '-')}`;
+
+ const row = {
+ sourceType: typeFacturation,
+ numDemande,
+ numClient: String(payload.numClient || '').trim(),
+ nomClient: String(payload.nomClient || '').trim(),
+ numContrat,
+ dateDemande: formatFrDate(now),
+ marchandise: String(payload.marchandise).trim(),
+ mode: modes.join(', '),
+ capital: String(payload.capital).trim(),
+ depart: String(payload.depart).trim(),
+ arrivee: String(payload.arrivee).trim(),
+ dateDebut: String(payload.dateDebut).trim(),
+ dateFin: String(payload.dateFin).trim(),
+ dateDebutIso: parseFrDate(payload.dateDebut)?.toISOString() || null,
+ dateFinIso: parseFrDate(payload.dateFin)?.toISOString() || null,
+ nomDevis: payload.nomDevis || 'Pas de nom de devis',
+ proprietaire: payload.proprietaire || 'Proprietaires multiple',
+ tarif: String(payload.tarif).trim(),
+ statutCommande: 'Validé',
+ statutFacturation: 'Non facturé',
+ isDeleted: false,
+ createdBy: actor.userMatricule,
+ region: actor.userRegion || ''
+ };
+
+ if (payload.facturer === true) {
+ await runQt550Axa({
+ matricule: actor.userMatricule,
+ numContrat: row.numContrat,
+ capital: row.capital,
+ tarif: row.tarif,
+ dateDebut: row.dateDebut,
+ dateFin: row.dateFin
+ });
+ row.statutFacturation = `Facturé ${formatFrDate(new Date())}`;
+ }
+ const created = await db.records.create('advalo_demande', row);
+ await logAudit('create', actor, {
+ sourceType: 'hors_grille',
+ demandeId: created.id,
+ numContrat: created.numContrat,
+ numClient: created.numClient || '',
+ pricing: {
+ valeurAssuree: String(payload.capital || ''),
+ taux: String(payload.taux || ''),
+ primeMinimum: String(payload.primeMinimum || ''),
+ cotisationHT: String(payload.cotisationHT || ''),
+ coutActe: String(payload.coutActe || ''),
+ cotisationTTC: String(payload.cotisationTTC || '')
+ },
+ mode: created.mode,
+ marchandise: created.marchandise,
+ depart: created.depart,
+ arrivee: created.arrivee,
+ statutFacturation: created.statutFacturation
+ });
+ return created;
+}
+
+function parseSourceId(composedId) {
+ const raw = String(composedId || '');
+ if (raw.startsWith('d:')) return { source: 'advalo_demande', id: raw.slice(2) };
+ if (raw.startsWith('g:')) return { source: 'advalo_deleguee', id: raw.slice(2) };
+ return { source: 'advalo_demande', id: raw };
+}
+
+async function getDemandByTypedId(composedId) {
+ const parsed = parseSourceId(composedId);
+ const record = await db.records.getOne(parsed.source, parsed.id);
+ return {
+ ...record,
+ id: parsed.id,
+ source: parsed.source === 'advalo_deleguee' ? 'deleguee' : 'hors_grille',
+ typedId: `${parsed.source === 'advalo_deleguee' ? 'g' : 'd'}:${parsed.id}`
+ };
+}
+
+async function getHistoriqueDetail(composedId, actor) {
+ ensureRole(actor);
+ const row = await getDemandByTypedId(composedId);
+ const [lastAudit] = await runSqlJson(`
+SELECT
+ json_extract(data, '$.actorMatricule') AS actorMatricule,
+ json_extract(data, '$.actorNom') AS actorNom,
+ json_extract(data, '$.actorPrenom') AS actorPrenom,
+ json_extract(data, '$.pricing.valeurAssuree') AS valeurAssuree,
+ json_extract(data, '$.pricing.taux') AS taux,
+ json_extract(data, '$.pricing.primeMinimum') AS primeMinimum,
+ json_extract(data, '$.pricing.cotisationHT') AS cotisationHT,
+ json_extract(data, '$.pricing.coutActe') AS coutActe,
+ json_extract(data, '$.pricing.cotisationTTC') AS cotisationTTC
+FROM advalo_audit
+WHERE json_extract(data, '$.demandeId') = ${sqlQuote(row.id)}
+ AND eventType IN ('create','update')
+ORDER BY created DESC
+LIMIT 1;
+`);
+ return {
+ ...row,
+ details: {
+ marchandise: row.marchandise || '',
+ mode: row.mode || '',
+ depart: row.depart || '',
+ arrivee: row.arrivee || '',
+ valeurAssuree: String(lastAudit?.valeurAssuree || row.capital || ''),
+ taux: String(lastAudit?.taux || ''),
+ primeMinimum: String(lastAudit?.primeMinimum || ''),
+ cotisationHT: String(lastAudit?.cotisationHT || ''),
+ coutActe: String(lastAudit?.coutActe || ''),
+ cotisationTTC: String(lastAudit?.cotisationTTC || row.tarif || ''),
+ actorMatricule: String(lastAudit?.actorMatricule || row.createdBy || ''),
+ actorNom: String(lastAudit?.actorNom || ''),
+ actorPrenom: String(lastAudit?.actorPrenom || '')
+ }
+ };
+}
+
+async function softDeleteDemande(composedId, actor) {
+ ensureRole(actor);
+ const row = await getDemandByTypedId(composedId);
+ if (row.source !== 'hors_grille') {
+ const err = new Error('Suppression autorisée uniquement sur les demandes hors grille.');
+ err.status = 400;
+ throw err;
+ }
+ if (normalizeStatus(row.statutFacturation) === 'facture') {
+ const err = new Error('Une demande facturée ne peut pas être supprimée.');
+ err.status = 400;
+ throw err;
+ }
+ const updated = await db.records.update('advalo_demande', row.id, { isDeleted: true });
+ await logAudit('delete_soft', actor, {
+ sourceType: 'hors_grille',
+ demandeId: row.id,
+ numContrat: row.numContrat || '',
+ numClient: row.numClient || ''
+ });
+ return updated;
+}
+
+function getDocDateStamp() {
+ const now = new Date();
+ const yyyy = now.getUTCFullYear();
+ const mm = String(now.getUTCMonth() + 1).padStart(2, '0');
+ const dd = String(now.getUTCDate()).padStart(2, '0');
+ const hh = String(now.getUTCHours()).padStart(2, '0');
+ const mi = String(now.getUTCMinutes()).padStart(2, '0');
+ const ss = String(now.getUTCSeconds()).padStart(2, '0');
+ return `${yyyy}-${mm}-${dd}-${hh}-${mi}-${ss}`;
+}
+
+function buildCommonDocContext(row, contractInfo = {}, actor = {}) {
+ const dateEmission = formatFrDate(new Date());
+ const rawContrat = row.numContrat || contractInfo.numContrat || '';
+ const normalizedContrat = padContract(rawContrat);
+ const numContrat = isAllZeroDigits(normalizedContrat) ? '' : normalizedContrat;
+ const rawClient = String(contractInfo.numClient || row.numClient || '').trim();
+ const numClient = safeDocValue(isAllZeroDigits(rawClient) ? '' : rawClient);
+ const nomClient = safeDocValue(contractInfo.nomClient || row.nomClient);
+ const adresseClient = safeDocValue(contractInfo.adresseClient || '');
+ const codePostal = safeDocValue(contractInfo.codePostalClient || '');
+ const actorDisplayName = buildActorDisplayName(actor);
+ const nomAgentSource = actorDisplayName || contractInfo.nomAgent || row.souscripteur || '';
+ const nomAgent = safeDocValue(withMatriculeSuffix(nomAgentSource, actor));
+ const numAgent = safeDocValue(contractInfo.numAgent || '');
+
+ return {
+ nomAgent,
+ numAgent,
+ adresseAgent: '',
+ postalAgent: '',
+ telAgent: '',
+ faxAgent: '',
+ nomClient,
+ adresseClient,
+ codePostal,
+ numContrat,
+ numClient,
+ dateEmission,
+ dateDebut: safeDocValue(row.dateDebut),
+ dateFin: safeDocValue(row.dateFin),
+ capitaux: prettyNumber(row.capital),
+ capitauxField: prettyNumber(row.capital),
+ cotisation: prettyNumber(row.tarif),
+ cotisationField: prettyNumber(row.tarif),
+ cotisationTTC: prettyNumber(row.tarif),
+ coutActeField: '36,00',
+ tauxField: '0,30',
+ taux: '0,30',
+ intervalle: `${safeDocValue(row.dateDebut)} au ${safeDocValue(row.dateFin)}`,
+ typeMarchandise: safeDocValue(row.marchandise),
+ marchandise: safeDocValue(row.marchandise),
+ modes: safeDocValue(row.mode),
+ mode: safeDocValue(row.mode),
+ depart: safeDocValue(row.depart),
+ arrivee: safeDocValue(row.arrivee),
+ primeMinimum: '15,00',
+ numDemande: safeDocValue(row.numDemande),
+ matricule: safeDocValue(actor.userMatricule || ''),
+ prenomSouscripteur: safeDocValue(actor.userFirstName || ''),
+ nomSouscripteur: safeDocValue(actor.userLastName || ''),
+ souscripteur: safeDocValue(withMatriculeSuffix(actorDisplayName, actor)),
+ regionSouscripteur: safeDocValue(actor.userRegion || ''),
+ listeDemandes: safeDocValue(row.numDemande || '')
+ };
+}
+
+async function generateDemandeDocument(composedId, type, actor) {
+ ensureRole(actor);
+ const row = await getDemandByTypedId(composedId);
+ const contractInfo = await lookupContract(row.numContrat, actor);
+ const context = buildCommonDocContext(row, contractInfo, actor);
+ const nowStamp = getDocDateStamp();
+ const clientSlug = sanitizeFilePart(context.nomClient || 'client');
+ const contractSlug = sanitizeFilePart(context.numContrat || 'contrat');
+
+ let templateName = '';
+ let docType = '';
+ let outputFolder = '';
+ let filename = '';
+ let replacements = {};
+
+ if (type === 'attestation') {
+ templateName = 'Certificat_Assurance_Advalo.docx';
+ docType = 'attestation';
+ outputFolder = path.join(ADVALO_DOCUMENTS_DIR, 'attestations');
+ filename = `Attestation_Advalo_${clientSlug}_${contractSlug}_${nowStamp}.docx`;
+ replacements = {
+ '«numContrat»': context.numContrat,
+ '«nomClient»': context.nomClient,
+ '«dateDebut»': context.dateDebut,
+ '«dateFin»': context.dateFin,
+ '«capital»': context.capitauxField,
+ '«mode»': context.modes,
+ '«depart»': context.depart,
+ '«arrivee»': context.arrivee,
+ '«marchandise»': context.marchandise,
+ '«numDemande»': context.numDemande,
+ '«dateEmission»': context.dateEmission
+ };
+ } else {
+ templateName = 'Avenant_Ponctuel.docx';
+ docType = 'avenant_ponctuel';
+ outputFolder = path.join(ADVALO_DOCUMENTS_DIR, 'ponctuel');
+ filename = `Avenant_Advalo_${clientSlug}_${contractSlug}_${nowStamp}.docx`;
+ replacements = {
+ nomAgent: context.nomAgent,
+ numAgent: context.numAgent,
+ adresseAgent: context.adresseAgent,
+ postalAgent: context.postalAgent,
+ telAgent: context.telAgent,
+ faxAgent: context.faxAgent,
+ nomClient: context.nomClient,
+ adresseClient: context.adresseClient,
+ codePostal: context.codePostal,
+ numContrat: context.numContrat,
+ numClient: context.numClient,
+ typeMarchandise: context.typeMarchandise,
+ modes: context.modes,
+ depart: context.depart,
+ arrivee: context.arrivee,
+ dateDebut: context.dateDebut,
+ dateFin: context.dateFin,
+ capitauxField: context.capitauxField,
+ tauxField: context.tauxField,
+ primeMinimum: context.primeMinimum,
+ cotisationField: context.cotisationField,
+ coutActeField: context.coutActeField,
+ cotisationTTC: context.cotisationTTC,
+ dateEmission: context.dateEmission,
+ numDemande: context.numDemande
+ };
+ }
+
+ const templatePath = path.join(ADVALO_TEMPLATE_DIR, templateName);
+ if (!fs.existsSync(templatePath)) {
+ const err = new Error(`Template introuvable: ${templateName}`);
+ err.status = 500;
+ throw err;
+ }
+
+ ensureDirectory(outputFolder);
+ const buffer = buildDocxFromTemplate(templatePath, replacements);
+ const filePath = path.join(outputFolder, filename);
+ fs.writeFileSync(filePath, buffer);
+ await saveAdvaloDocumentTrace({
+ type: docType,
+ filePath,
+ buffer,
+ demandeId: row.id,
+ batchId: ''
+ });
+ await logAudit(type === 'attestation' ? 'doc_attestation' : 'doc_avenant', actor, {
+ sourceType: row.source,
+ demandeId: row.id,
+ numContrat: row.numContrat || '',
+ numClient: row.numClient || '',
+ filename
+ });
+
+ return { filename, filePath, buffer };
+}
+
+async function generateBatchAvenant(batchId, actor) {
+ ensureRole(actor);
+ const batch = await db.records.getOne('advalo_facturation_batch', batchId);
+ const [batchAudit] = await runSqlJson(`
+SELECT json_extract(data, '$.includeTransportDetails') AS includeTransportDetails
+FROM advalo_audit
+WHERE eventType = 'facturation_batch'
+ AND json_extract(data, '$.batchId') = ${sqlQuote(batch.id)}
+ORDER BY created DESC
+LIMIT 1;
+`);
+ const includeTransportDetails = String(batchAudit?.includeTransportDetails || 'true') !== 'false';
+ const contractInfo = await lookupContract(batch.numContrat, actor);
+ const nowStamp = getDocDateStamp();
+ const clientSlug = sanitizeFilePart(contractInfo.nomClient || 'client');
+ const contractSlug = sanitizeFilePart(batch.numContrat || 'contrat');
+ const templatePath = path.join(ADVALO_TEMPLATE_DIR, 'Avenant.docx');
+
+ if (!fs.existsSync(templatePath)) {
+ const err = new Error('Template Avenant.docx introuvable.');
+ err.status = 500;
+ throw err;
+ }
+
+ const outputFolder = path.join(ADVALO_DOCUMENTS_DIR, 'periodique');
+ ensureDirectory(outputFolder);
+ const filename = `Avenant_Periodique_${clientSlug}_${contractSlug}_${nowStamp}.docx`;
+ const filePath = path.join(outputFolder, filename);
+ const actorDisplayName = buildActorDisplayName(actor);
+ const interlocuteur = withMatriculeSuffix(
+ actorDisplayName || contractInfo.nomAgent || '',
+ actor
+ );
+ const replacements = {
+ nomAgent: safeDocValue(interlocuteur),
+ adresseAgent: '',
+ postalAgent: '',
+ telAgent: '',
+ faxAgent: '',
+ nomClient: safeDocValue(contractInfo.nomClient || ''),
+ adresseClient: safeDocValue(contractInfo.adresseClient || ''),
+ codePostal: safeDocValue(contractInfo.codePostalClient || ''),
+ numContrat: padContract(batch.numContrat || ''),
+ numClient: safeDocValue(contractInfo.numClient || ''),
+ intervalle: `${safeDocValue(batch.dateDebut)} au ${safeDocValue(batch.dateFin)}`,
+ cotisationTTC: prettyNumber(batch.totalCotisation),
+ dateEmission: formatFrDate(new Date()),
+ capitauxField: prettyNumber(batch.totalCapitaux),
+ tauxField: '0,30',
+ cotisationField: prettyNumber(batch.totalCotisation),
+ coutActeField: '36,00',
+ listeDemandes: includeTransportDetails ? (Array.isArray(batch.demandeIds) ? batch.demandeIds.join(', ') : '') : ''
+ };
+
+ const buffer = buildDocxFromTemplate(templatePath, replacements);
+ fs.writeFileSync(filePath, buffer);
+ await saveAdvaloDocumentTrace({
+ type: 'avenant_periodique',
+ filePath,
+ buffer,
+ demandeId: '',
+ batchId: batch.id
+ });
+ await logAudit('doc_avenant', actor, {
+ sourceType: 'batch',
+ batchId: batch.id,
+ numContrat: batch.numContrat || '',
+ filename,
+ includeTransportDetails
+ });
+
+ return { filename, filePath, buffer };
+}
+
+async function facturerBatch(payload, actor) {
+ ensureRole(actor);
+ const sourceMode = String(payload.sourceMode || 'hors_grille').toLowerCase();
+ const includeTransportDetails = payload.includeTransportDetails !== false;
+ const removed = new Set(Array.isArray(payload.removedDemandeIds) ? payload.removedDemandeIds.filter(Boolean) : []);
+ const ids = (Array.isArray(payload.demandeIds) ? payload.demandeIds.filter(Boolean) : [])
+ .filter((id) => !removed.has(id));
+
+ if (!ids.length) {
+ const err = new Error('Aucune ligne sélectionnée pour la facturation.');
+ err.status = 400;
+ throw err;
+ }
+
+ const parsedIds = ids.map(parseSourceId);
+ if (sourceMode === 'hors_grille' && parsedIds.some((item) => item.source !== 'advalo_demande')) {
+ const err = new Error('Le mode Hors grille ne permet pas de facturer des lignes grille déléguée.');
+ err.status = 400;
+ throw err;
+ }
+
+ const rows = await Promise.all(
+ parsedIds.map(async (entry) => {
+ const row = await db.records.getOne(entry.source, entry.id);
+ return { ...row, _collection: entry.source, _typedId: `${entry.source === 'advalo_demande' ? 'd' : 'g'}:${entry.id}` };
+ })
+ );
+
+ const active = rows.filter((row) => row && !row.isDeleted && normalizeStatus(row.statutFacturation) !== 'facture');
+ if (!active.length) {
+ const err = new Error('Aucune ligne non facturée sur la période/contrat.');
+ err.status = 400;
+ throw err;
+ }
+
+ const numContrat = padContract(active[0].numContrat);
+ const dateDebut = active[0].dateDebut;
+ const dateFin = active[0].dateFin;
+ const fingerprint = hashFingerprint({
+ numContrat,
+ dateDebut,
+ dateFin,
+ sourceMode,
+ ids: active.map((row) => row._typedId).sort()
+ });
+
+ const existing = await db.records.getList('advalo_facturation_batch', 1, 1, {
+ filter: `fingerprint="${fingerprint}" && status="DONE"`
+ });
+ if (existing.totalItems > 0) {
+ return { idempotent: true, batch: existing.items[0] };
+ }
+
+ const totalCapitaux = active.reduce((acc, row) => acc + parseAmount(row.capital), 0);
+ const totalCotisation = active.reduce((acc, row) => acc + parseAmount(row.tarif), 0);
+
+ const batch = await db.records.create('advalo_facturation_batch', {
+ numContrat,
+ dateDebut,
+ dateFin,
+ sourceMode,
+ demandeIds: active.map((row) => row._typedId),
+ totalCapitaux,
+ totalCotisation,
+ fingerprint,
+ status: 'IN_PROGRESS',
+ createdBy: actor.userMatricule,
+ createdAt: new Date().toISOString()
+ });
+
+ try {
+ await runQt550Axa({
+ matricule: actor.userMatricule,
+ numContrat,
+ totalCapitaux,
+ totalCotisation,
+ dateDebut,
+ dateFin
+ });
+ const factDate = `Facturé ${formatFrDate(new Date())}`;
+
+ await Promise.all(
+ active.map((row) =>
+ db.records.update(row._collection, row.id, {
+ statutFacturation: factDate
+ })
+ )
+ );
+
+ const done = await db.records.update('advalo_facturation_batch', batch.id, {
+ status: 'DONE',
+ finishedAt: new Date().toISOString()
+ });
+ await logAudit('facturation_batch', actor, {
+ sourceType: sourceMode,
+ batchId: done.id,
+ numContrat,
+ numClient: active[0].numClient || '',
+ includeTransportDetails,
+ demandeIds: active.map((row) => row._typedId),
+ totals: { totalCapitaux, totalCotisation }
+ });
+
+ return { idempotent: false, batch: done };
+ } catch (error) {
+ await db.records.update('advalo_facturation_batch', batch.id, {
+ status: 'FAILED',
+ errorMessage: error.message,
+ finishedAt: new Date().toISOString()
+ });
+ throw error;
+ }
+}
+
+async function exportHistorique(filters, actor) {
+ ensureRole(actor);
+ const sort = normalizeSort(filters.sort, 'dateDebutSql', [
+ 'dateDebutSql',
+ 'dateDebutIso',
+ 'dateDebut',
+ 'dateFinSql',
+ 'dateFinIso',
+ 'dateFin',
+ 'numContrat',
+ 'numClient',
+ 'tarifNum',
+ 'tarif'
+ ]);
+ const sqlSort = ['dateDebut', 'dateDebutIso'].includes(sort)
+ ? 'dateDebutSql'
+ : ['dateFin', 'dateFinIso'].includes(sort)
+ ? 'dateFinSql'
+ : ['tarif', 'tarifNum'].includes(sort)
+ ? 'tarifNum'
+ : sort;
+ const order = normalizeOrder(filters.order);
+
+ const where = buildCommonWhere(filters, actor);
+ const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
+
+ const rows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ id,
+ source,
+ sourceLabel,
+ numDemande,
+ numClient,
+ nomClient,
+ numContrat,
+ dateDebut,
+ dateFin,
+ dateDebutIso,
+ dateFinIso,
+ tarif,
+ statutFacturation,
+ statutNormalized
+FROM normalized
+${whereClause}
+ORDER BY ${sqlSort} ${order}, id ASC;
+`);
+
+ return toCsv(rows.map(normalizeSqlRow));
+}
+
+module.exports = {
+ lookupContract,
+ getHistorique,
+ getHistoriqueDetail,
+ getCumul,
+ createPonctuel,
+ softDeleteDemande,
+ facturerBatch,
+ generateDemandeDocument,
+ generateBatchAvenant,
+ getReporting,
+ exportHistorique
+};
diff --git a/ecole/src/templates/advalo/Avenant.docx b/ecole/src/templates/advalo/Avenant.docx
new file mode 100644
index 0000000000000000000000000000000000000000..e3646f2100e0a06029185da016332d5202657e95
GIT binary patch
literal 28038
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 p*(G*ww5290BYWvm>4)Gvff
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 FImC&
z0ini1^FpJxrmW~9IY+Qf6P9b^h-dO&ACw
zmqUwOfquJdwNKxxeaL>a?wQ9!G7w1OGe1&dlY~K|T%XOe
znxfQ&CUkI4Vx4ZZ#u0Eo1-a{6@f#v#T8*J
zRWWTMEl19}`FcypF$wl$BvfA&szlXN#iRPc*4D25RV>#*N~Cm5bQb{Ay`6n2VK!lu
zc7cb#{F6Ubakh}Qg+Clb*%#-WeWnEp5(yVUB`TuR$!EOZ6q^s#V0cs{TiWW~!7-O9
zC!CWv9k;?UA{v2Jf0;JI%mO!!UR}gjW9D93u3+&ow_2I@6pn~N>Bjxbth|G8s$hIM
zj^5ZPqLV6~y)_Y}j069G;VADCsf*+=ig3O_yd2_;{>go^W
zLipqMnF!>SbAJC5_w0SAoYTE7#p_Y0Tp^?HLAW~h>@qdtt~ro1$}n=YSq$0A46Zb7
z9#@Kjpd)R(-;p{l-elb~Q--0v5?IMj-XtuX>2|Z5r-17n=I+oYFKLTCM=X%d#w7H1
zbURmfyFEvVBgf^@`Nhy?R^`oxMLF6=7^8IL><{+i4heU`z<{opl6d7><;kN^3P`>T
z_tQd1ndH^`@4Ol4$PeDXLrV#r38rB+7QwynCsvix!xeq4`psXHhw@M0B~YPr#P!4?
znSYxN43@BPmte^X|Cz7*5Hu#kEHq}m+sTx^J%|>aANl=ik)K*P!`V=pi@_|B23aB}
zk#=(+lRr3wi#7kdqWg<0#buzIS
zQ2GF6i~8%nh(q?S@E--IrdeEdfO;_Vcg*IlrRyqzSfoAme%jPInE=6eYjs
zviO;|%aiwUnhc^5Djq5i8r`u^^OBhmaz6@p&zi|5MKm6HGn@wZ)4FFy2yXI5+zu^G
zMy{w5zrnnW8?_R0DK#p>GI^EO)J#L>jyO1lUK;i(3b?dyNI^w{`m%(b4a+z8lKdQF
zR-zt4xQ@aAm+xSDklNYy$@|^?X1-HK88FWw-}%4Vy9&1`o3BkPAf+IUfaC&F(zw!6
z(kTrR3rL5wfOL0C3hWZnUD5(l0;`lDNJvT70)A`!USL1p|KNpd&b9mO-uKMF%(F9R
z&$;KpewFZR!{zrK)T%MhzlPA|r?8spcPn+lu6d=|$4Yuz9FmsUgfN2{=TT)6$6t4d
zTZc3q(e6YAO+?S1$#XG$4DduB&m~SSw@yx~Gyw4f$7iv1<|Zpo$YVhsuEmg$-s(+*)8lEk~omRJ+#t-RFc_
zsLh}<40lzUsD8QuuLG@ZVHd4!&%=+o{Y)7_^a5Ql8WXts1XD7*9K3MD3prD=CTe)A
zc+ZQ7*2h1MW`UnuDhdgQf%lU0(V(X4RiJq+;U0l6*;=zVDE(T
z&+g;lqD2|C>mX$`yfCx!*-m*_m68l}-(Ajn7qd|fSmHDIf&L&ws#tzCuzglV{xAre
zCCCQzj`eN*$8=9(gHY+e2o$8zYsP&|{5m2c^L)@2L$;9PnGDsTw0`t@{U|}?$!P)e
zHtV44*s^4V-7*M90f||6Xv+n%TL#G5kp=D;BUcTUC71W(VN*2gvrW~$;lugy_+vyeU!DMtpa86+RHwQk
zTrbLhG=h?o|NAEb|69?NoJ_a0yOf!G6-d+5)`Qz*`XWWlS@>z^Dv08HOj($|$Cglc
z(d$Gcqr=}`fa-!Ks6>N83BGHF<$?xK@~uxTn2DmYAC^yd;SP5dJJqSwCO+
zS=CS8V3D6RC`7?R=z!N3bS2)P6)Lb26){-pGrbZ`7rdZf2K=cPEP{?$VRbu!FV>Qc}U;PL-y*8G*)nlet%u^noH&0goF%@ZKnrzM~R}4ZUfS3eJUoe&M
zFndGa&MwTqk!fLicVvFcTZTJz_lP9|rB$z4a^C@#w|~IWtMR??Ru8e5`3~|*_fSI4
zDsmYa7`G`TwN_2WXd+gm6;Iw)0|%O9HNMPB%u@|CGdKS+zqO-gFkRLBz29@{qv&oP
z?3uv;%V;=pGRSic-E4nfZ2{eEe5wziapul7-iu=iBHVr#8or4lS^T*@PeI_W*pG*@
zjmSKCJD8D_Q+aF9siL^_xZ=s`Q1A)TEADl`tN@VzflbkA%7?{Q{PmLzt=IW--14fs
z$+(Zdaz8_@4%3W^Yd9n0+fq<&uF#U-QYJaRPv>)>GZyYFVGx74UxUL5VtPMtrl80S>AL@i?#n5|JZW
zS+_&8{e>Yx;WTGgbR4>ppvF}GBfWc@QH63K=|J2>FiDx=U~>~3WLLbAsXi1})n{(G
zWQ1;AORnm*aTv3TbL%9LBn+z9UnUP?4b+;}_x4Tc`>7aP1oZ+DxN2U$xPJ0@yg_f%91DGn3t7+8V
zoIY_*TaWpZkdL!!pO!?$IR;xg8902=xE2I!IJh6FEH35vZd<|mV=)C8p>5@dCzZh&
z$p+5edT|aztv}Rnea;vf`R-bZR9Dd$?a-
zg97CN9AXgQj1^VtdJU9)@`-BQ2g?HuFa-^&o#Vb&d+@)a%E-{GajyefXT&8BRA^|A
zPlAIUnS@c`e(V{MRnMWFXJ0kx`_4VM{KP-vTo91!%YLnt7e{4RX-IdxTV&ipv21NU
zx}mC8z$7W}Jr~TU75}_LT$;G;ZN>fpF1GGtyaG)@<<)7xa)v{N82i1ng+f#fk4@;I
zL~E#-SK_y7fat)h@4UOs_+2_eFO;}vpGX#eG43L4>+;w=H^5$UAk+j
zEIINut+xu|O*~4lFWMamP29;&a7m>nW}Gk+=$2{FFSfsF)_$F!nTqkeGVOV_%^0}V
ziWG7l?{oO~y|17XNlF^rzkCm#Wl#9Oy|1gOvFfGhg7~;8c#7bS!0*`Wls9JhT$<=D
zA{fy9a>RXenB1QuY3JWEie(Cxo!;&@T7HE{O_70Se{QB#WfzIdg
zA!zCCvW<)KO|3Hq`XMiKoD%M@7&6p|%)6W~%;z(cF&WTE0c9{%DTp)%IssA!ubCl4
zS~7R{=J>Tu%D8rTSyfp&GOJtFESkd`#-w_lrJ7Hu?ImwfVnM4l?1C(xwRH_CpR7iOiz&?aNpE|RHhe2rQ=bWkCnD4oYQRw|=f
zRlR(y4(xs7ZUAMTK!INJMi=A1oR=DMwJ
zk^6j)pUSF@{KR^w+BXU7L@Qe~Lkb?QA)Xx97bwu*3Lsm;DR_lfp5K%2`o((#hnQ8d!9MM6jVZ`9fpY`En8iv^v_Q8{4-$1!U+1+n5(DB?+xQ}V$
zlGte4f;FVn4@h_`pTs2jeQ{f|@@vI%nnw#w6iF%rrAxWy#k9Y}p%hbLjZOa&zTB@4|0e;wD&=x~jpQL78%$sYafxqVY+hENX=|2(eyL2nq}Uz36Q#+pZJ
zv#+)LFs{)<9O-zVfr1fpT4;^|=nMVT>MXvdD;CN0`;+G{c^t8C=midBnbOX~oO-6ofvq(V9P`olkQc7D#`mG{BQP$vGjOdkt++nCMY%cn-~2
z{Hh*5;6T10sFF$Xvws&Kzt42N)xlHQwUBp&!nLb+WaDX;taBzZqDhXG?DAgDG3AK0
zvnF%4(ujsu90DTxYOHW^{j`cP52l6M4M3cj&vUf7FCc
zKfiji_Hajo$Z?zUZ+^NbpJ_S$;rA+3c$7Z@T(AbNQuFIeFm@ALhhL)`E*<=zN)7zZ
z;2GQdQ>7;Gmr9L?rI0UH&i!CX-7J(4T6{heyb>Dyr{rR@RE!{1+Qcc=*;A??E^a%J
z)vu`ONm4`jukhnvEJ;Z>r)R^uOx<7Jypw#pn3v4V0*C6A8q9;f7M%~*=2iKeA6lja
z{RXy;xdo9Q8^?hsdg!qXos)G7Nr6@{M8~-Qh+GF{@Op|dx-aH!5*u_?A*P&&*K_`_
zy?qDi5oj-`)AY6+HY!Z!Ijn`z)1)FMpBxrD35@kX$*zE!Xp_;|^zi!k^lGbB5=
zR$}dpyqJ99Wt3npt1*hO>xaOjoHOQJvDa2h9m2rvFf{v`^R>5`hC*v|;x#^bIoGs%
zZUi`(c(t01g(MO+w@nR-dwZAHNvKP$*vsOoa_K3l41+}9sWyJI@MZ+_vYZwr$HY?<
zOtkqeNoGV5)uJ?OFWBmM!N=$S_b@B+ejXWzzw!w_a`7*ZTq8TXpSR`z1}=Qm!hh0L
ze~A$^6R$kH-qf>qlLR8l%|t;cP!)m9(wmYe#j6}j_b!Csa7`%QIKalEP{FpQhcB#i
z!^*tqZs*e`oyv))eE_>jzV3~W>*ePT&K-p>`*`}?uUWbHb=oZU;2D#9y$HroktDoV
zGJ(ul%&!RFsL0;J5(=#Xx?uH&du&cYE$Vhp@=$|3fv@ffG||>!q!w}9tnjsW?)*Xj
ztZDG%gQ^!+C!-{eUKy6C_fUey)(DMdR39`S)vY;M#qfTY#s*Dq${Z^G5Rg~q9y3V9
z*ilIt^!i(`f^DzVg}Q!}T%-de2(d&Wh&?9WqZ!M>C$NfujlA%*w5G#6IR
z>gQ0Yjg&WXv4Z%VbLm~4Xn#^#t5E0lo%@b!j|V7qE|Q13c8(L=KhdJ!4ZAI5K8}|_
zjb}X&2_RdH#sT?{n2_I;(k2h@d@%WdbyU{+P31deU4{dxQs1pSLH9N>cMAyLY0xKa
z=9-Tj(rjPj`vz@K+Zwj@lp>ez-s4yBv?6Pn3;K(P&$LQhZbs^@Uz
zpwsXsY)s3u-4qI)Q4k7t?HNbPW&MQaY!1`Ugi*=?;~E|HvTs#skUqf4LB__@VO;LxGLWl%3StN&O!(WcaUd+qiP3+S*mL(dAKuR#y_0vdXuA^p`ohw)v2A
zp(R;o(Qqa&TpKVS6Q8_$zfD{U{k9)cBsT9$KZbPEc?O0+ug9LAABJ6ox1NAVt~;I%
zvYba|9%otGwx{oTKexl)%o2Yr(^-}&_d!phTbVa<0XdITZhoXTZMeYe+ruLr%Dq#o
zNR7&CK#6
zkIpVx%{VurVNd7Qg5&J{LBZ{?M;G4<*;4e$JA;J-*
zw=UrK;5(CF9l0dLg@{I!T)IFz!JROd(7z;05YdP^@fYY5noC{(Bb
zND(OzQ*ACN`1$@&Tu!_}gkKG5y#T`lOGJ?V7T}6Va5d)hG6B0NBEdxjC?X4D=;H;C
zjo6=FTnU6k#3M$;T;P+W|Myq;Cq4!djObFl0LN+m0V6mV5g8EuSQiY2+J6|#;3tRu
zLMZiKequQ{OWG)GW=No68z8J4H12HfqNN^W_$_#`@{X`au<={
k>N4Om!5!1fL*Q?V0wpg8zWqQ~HAXG>fN0Y>!$1Po~ZKg<8Y9_UXuuvue5AJ~F^{UTmVroQ?cn@pZ)
z_1uZJDx{NU){V3@K$6q$_S^%5!rWEre1hGNdAaQpn+q
zyI5nj8t-B6V(g5p5*4m2^!f{Kb4Y(py2BRTg*&LSm~{qjvk*c)mx436dhsf>W8>_L
zCFHD~?k({?V?C5VcYeV3EDuBk>o}opYfF&2Euwlp(TNiYto=mAsd7dUZmHek(a~Em
zX|R+6IVnU@iJi+$8ya+AS#b2B)?8JY0ni%5`O(>Hv|i
zs;*aSQ*l*3fcy8!n!3#K-3q-ZBMHSf3Y$On190c@Z)IHj90dS@>K)wQo8m;X1C}|H
zY_CG1h-hz)ucfqa6ZxiIWZC0z+IFd%>%^6vVG1yci`<
z`MxEn)9+!!Hdk--nXeaNk^@nT7(y%I4aid_pd`{fulcw<7n2epef5OxX-
z?3qb|=9C?GT%%#_t9yq58Q^gqq6jRxi!0<&7$8Cu0mmA~v
zeljV0>38JhQS=O&W3gTW<5@l(=`f_T8@baWijkpZBjR^qg1rhl2c{2mK|Z63>Yv2+
z6By-($WQ3a4r@ZUol87p^6|Vu9a!$+6Qup?L}tTt<>AId%0bsJ%o%9PFk<51gq+&B
z_IHnsmRiG!$ks$&n@42Kl#RSc(WOyD*_=d(S+zGg6ZhP9cMMU0EKV+v?Di1g&oH
zf7e}_Y9aXofgNd49|Qyi=pzpYXH!N~2NO3t;OP0+uz99yW536R`qAC9DRdP8xcA4-
zMKE2FhCY^pmMZo5q{ohm50fkuoN;G~fjq?Z2_DEX{8Goc0)K|lEGgA&M%#Qwz9ChM
zcZ{h&uaVU(TZlsw`R#3#@3dS3n*lWiG6VR`=3a2&}x~2T2r|v@?r25m@rwuqQrb
z3nwMp+0O>j7J$(pJ8nu$wA|T5tlzuH%{x^h&8{cgu<
zoZgSI#jBz%pHVhDSLb-3Y>0O+^GWGc@a!HHy-rH1+g~Vwhw;ZmJ&QcB_n-&5)qJ2;
zdJIAtR*@4TQ)nJ_HZ8U{+YsqaHz{TBy&pR*^Nm?c{@%;|k+(5NiDe>vw(v-b$6)fE
z+aT;QQLWJ4?d$%O%At2KizKYnv_Q8EtQdgV5&}CJ1S&Za5LodG=XGj%gg73{fOXgUl#nW5*M@
z8Fw8NH#oddH#wM^K&us@n|B$J4~0eT*>El1OWN$(9}Nqv7eoY^rAD>)L3E0zWH7XG
zy@gr3NdfK?%joLJ!-Z5IG=U$1QLm>6kb+c1Q`EtYDX1JhOi+i#-?O36z}n1?<&nRh
zLQoZ>4={P&qGXj?-F!vW&6fhPN$T}$IN^e#?HB^?Miojh7K7qAU{W6`*k4X>8eipsD`T
z-6XUNAau*m^Dd1&pq$DgYohrZJ5Tu|2SZn(N1iCN&8{h2FZcZu+5Ox&?NeJxt-a9G
zw-L7*L)3s&3%j`lql-_xT5ooH=@YxXfmOqk8I)YCJt
zMT!%c(@W^n$MEggeS`ry5c>*SNUG7%hgbwfM}t4QH((uDIlI3fGa-leKPrCx;I@uu
zxx=zKyC`2+&X3e~pCB~RJ#qPs6lL71c&7)6ejtRtVH;YD`R5jIs*I{Lq3ABQP72+(
z3ujl9Nsi0IS
zB;q6A%7R$n+Cr5BTb&`Xfcp6n0B
z@2&ZSb
z+pxeYn{cC=+F=Fo*2#}P@D(x@Tq7F2?dL@hhqLs3WP|98tcXA_7f9Nn
z>VkDn#5n`7Hpc`D+8XM*)_CWoMTw9Xq0Nh}V2+DEg9~XctEc>}5AK)KiR0aux3Mrbd?||yjOi(f*@@$$F-DDx=Y{g{^1-*Gmh@8w
z;Z7F`Wedi{9qoAK@rKg3GR+5L3FDu&tz=FJO{+1UVDlwY^C<8QE3D6;Mw_sFvh=YX7VIlcpm)w+
z;lE(t7#@3n`E2D52<(e=YB1(r73A=}$gJv~mhna|;#FVp8sv;b`P@F36V}U5-Y*7s
zamXqb^Voi7Aw0^=#cj4*jt2GDDNzT6{m5FbwJ~NDogw()+`yn!Pp>n#ZM$;i1UIgDGKsH9Yl&?w4+OkQMJBC>}-?ua_%1{|WKe-L18fci(8;M06|<5?I6L
zD5847)adF-GxR#@0p)A5iz*`B@yQlQrERQVw|8WJ;IpOS^2xpKLFYlYHqi|_iu<#B
zVtW{>)Iez|2|R|Huk(5SM3<8oO9p2hfGBD0FegG)`!_4XYfq)8PZATZoBM;;EECp5
z|2z(Eff94&=xirfl0HO>74~-~&C$;wwYRZeA5=P&a2l7`wu^djavrl_lU0&jNsL79
zd+QgF{(i+A_~bG7xcb1&tsJebrbtImhES`mYAw3V^K-k?C`-X5
zo&_E0S2>*5nnIB}e`AVFImT%vlNyz(URZQz@7Ed|`4HMGb%jQp0lLhqLu^^blg>2g
zUIm$@t9E?n7FV86oIh+-V`Z^07QC@my|q8Kwf$+CUK@X+AuUQ?-Iv
zu8^spI^*RH8#W__3ywDD;O=5E#tMbU;EkG~p?9&evOd;ff?RCjAn1>)-&}KBTP>d6
zQ1W?A6&LtWs+$DSi(3l61hqft^2C_1NxR(`pW+6`yaFi{UIkaM*?>M%0e^m*VqXl0
z2nyq^*iqKYgkmj|Hpfn_XICW8}+3#<2q+rqZZrXnvFOw6r1J5~u7SFx2Q89$!-9fu>Tiu|!oQR$z*
zK4I{tG~zZ#SLTBooH?2lj4fmHxY^Kc-j^Hm9uj8OP&>;eqxDJrX`hQROCPN;6lgWL
zrX;h-uJZ%lUOh%)%#~tGkdJ05C`>+Z_@q?ukCeI6?0eu0%jlJpcPba_-nBGy-(mh-
zrpG?;_VKEh4gI-WiMIPGzvb@qA^;3XKR?ej_3U?9fqjE{Y?T}czMDs{UoQ;hfZmdzuiI6zS|SJt3;qKnlA{aS2%P36c}yWrsVR@#oK
zHxrureKQxwK5|;}fF+5%OaxYXtmj_mym>|e{INsr)`^RT6}ROsNEEkw2r^1=8{p(k
zErrsedn^OTUyefm!YdtyupT_FnAnBM3t?ZTWRtlA$D0dak_^`NAk7N7r_tk}XF=Dc
zRV)xa=`tpFbf64TMCG5t?ne1$HnXv>9NAirMx8s0FFcNeb>ovytFD#3%ZmD)X-y0P
zfyKslzP73Hfer_|R;Pgzg$<;7i$~DyrP`lpE~c=6x8RZF4(gBP)9)kw%&x;4Sx6kg
z#^hsxj1J4Pw(}Tedzll20M-pbT`<*7U(7eGKGS8q4%J_M}yNyX@Kw#LLk~{*YP;
zBU;8=zfiu^2pd8j__Bv5v$Z|iAM)qd^MnbDgt-yCrxkFo!pdR?!EK$dED<&>j)k#G
zHMvLz0|4pk0P!8zv7gQtC$l(FIai!Q{LW=>5b1;=Ma$?(Q2`rh00-yvvXFRAm?YNA
zmM5(P#5_!s&e3CUaQA!vbYu)>juk1h0nw&_L%|D=>wGXx?j#pce9Q)HY+E`<urDXOB2n|S#8#;h|KNOlX!v>|SS+?WqIxy?z
zh&ak(FcyHg`K&Zt0XyLWWbCXp|JfT`^!iA9a#q!#RpAI(iPBh3r;JQt#S_b%h;0Rr;)P5}oawQ&%3OR}Z57nFkp>)gf24+|8CI?9;7kmUk5fJ9&y(s(
z=~Tzsv1*7gGE7y;qjOvq6>v;ZFsv%9+eQ`kf<1(1wy&b#u_4@wclT^=aUWoje;k~a
z+H~;WUcZ?J5X=#xYvB3?cA^RoQ!?YwoWFI@9-m6V5La}sjv`_|4t{|NL6ZB$pCKhvl2_sm?%uvF5m@zv`L+(94A%m%nD#wZKj@l`c
z@&jvsg7|mfg#^5*3Ik=iE?5u{R3Ik*2Cun;gR8xRtC`DRVnbE>T6{JWP?@2v_XDYA{;lnsXxDpq&Xb5dn=BrBH@MyB`(eRrqRHnQxxU71(wfW+
zH^_gJSxmFzj8|k(yCp8OuO(qB>`b8*`!HS@aet&x$s(QRtjulBXDEr6mNiJ_A}@ih
zlf^7_rs-&%s`e94g`!ScMIMJCP0H~Y+W);Yu5es?`6_9cp#C_ri0Iuiii84NqldZ_
zhdJJ_F?QR07$M;I7y}hP2pCe|kC?3W*sMKiHE1>h2rxB*SzGLrV3%8?;yM{^1Twj{
z;sYjS%3M*--)ZKkUscQ?oTk}z=pwxs?_heexIPA5E*^UBju68gpZNt*G;?8$lhje05;pc`vLq^1BgqcpPr|ONk`-P>lo%|{snNy)s
zw7^&NT#PAi%=LbTr$%lXH_ty}|Tl1|p#6Ce~!dE>kO{-e)G=`zPShY|NtwZe#Yzukon)
zVeAj9FZgMu+fka~CE9H=3Zh2vkC|I_8j($BMF{SFAzaUUSXKI*XblVk?fiFIPqY08
z+;pANC=z)b2$6-6gkbN2aE7;7asiY5*AJ7YMqPE;|bQ5*NvL8-HXU7
z>}3R3-eITAmTnsy)(kX7(%%?jUfR5$bxJ<-#q`(DvbKN4>wQA~JH)wx(>(u$xE3Jd
z{%>{5%--~W5pYhyO;5yYp%ZH!#Qy;}(6!qMfjyjvg}#X44nEd&TAU)oGqMD!3RDIU
z=9l{yyGGGGH_@-;D#-)KJ#^lh0UwPtq+ii~;{PHfE{=nfGfDkcMixsi_np|Cj*ES?
zqWUkut=e;&wJ3i5T|o4-kirqoV)J{8EXl`MDefPDgT&4N0*?B>11=cmKLMxw&wvwl
z{I7s3%Mk_i00D;y7=_v0UO1~GKIB^>1F_kC!kcR00h!C^#O^(K1ralJBaM`DW;F^5
zlh)*r!i|nrvNu&-OOSskxjVKr_+-N@Rd|y~i7RY1oK;IQ>0=oVOXfIuncp#{Ld2Yd
zYUsx7*EgN$5{BkmJ1|(9JIox#T$b?94M-wL`v>5tQT_(p-3k?oVp#Q_&`)j|`{`RS
zD-a`Srm_wTDb83E4idsT-aSosJo81vGoi+6{o@V4g`16XT>zUu(Ru6nd*K^aSCE><
zh8N}XYkx;R^0H%%^L@w_ZpL-gW@5x^`9dpA{x
zNsaK0s{FyqGfW|A-y+s`D*z!seau;cy~#)?*`lWXrYJ|7mmYJj7NbA4Zv{9MSaAPGj;ea4Gy}l
zo7=k7gFL^UtS!_`|9K`Cb2MFas#Zpq6G3MZa~oXX7wZnNqm)7CQcx8SRi_iPIGnzH
z*%_;lycvi3q0T^!Ag6TEDaGW@W|7?TbAR5t%)?*0soxv0m%1aohA`DjRmG1}N$v6_
z-J9f(Bz}d{-F!m2mU3VV1ix_>+Ud$qGtm$d_%{s3_8%)S$f8^^ZRO8~soG4~`tVHgM{vfQK;_9Sl^thnRnsVP_J9qlrMM{=8!J)f)}HBn!kn9xA*cAt5$
z*7JjK27&B$&)kbrUIW2B1%;MZH;;pMTUBt3PEaLn{k~tpSWSuL$HxY@M)?tAT67}B
za!@&3ZwBUEkT%xZ>fVAc6Ee~fgf9TUOC@%zZ&a9fe$%~Yb-hf`plBT13yeky2}
zIK8JBHCt3ee_$UI?}`2`io%(+Of>&O5sgyUFovF;;Yk%S}k6`0-`o5p=ORWJp=%
zGz{nbLV<1~N*i=d2~+WwWI8cQ^O=pqSHK#}2rF7(Q=`;Rt7zvV*WmgRZnS=RF`>8y
z>!VUt*edt-U>04PlpQVXuzN;3bhE|aN@&uA{z6QNt%w;G5oxBHKo?mTV|*V0*1}2>
zc;ApEziC?t7?vC{KUH6L>!`|d?;p=p$f65m^n*$KlJ?y3Y^N#edy-E1jf4_^no^EA
zTV`d09m`^gzkFozVfJ?r+(GnYwd^)+hw*^2>Utwk@2^Eh_EeBXagY{2z&vVU8*P*
zNAUZAW?P9b>uSm8el44mo%_$mmEpYE-d;|WUl@|fcBo%SC?}aiX`{$$oAo*~oyBD#
z=iwUi7q``e6THvewd(6q$bVdSAJEsOgYc7ql
zyZTZF
zBODN${*)vnRSh~}j!~dTug}@nvDbf|sl*%&{|lEf{5LM6{EN$80czj(H~;1`L^-9M
zzqqW?@IScBMW966IQJ-rMb|p}ZxB+Nu$xW(R%R3>h!jDLCj^2}MjBb82U-^1n2*Oj
z_Sk$FOkl^1hz$=k7{)@0h`k#JV{cRq>5d5wqQ<*;fNNGDIA$ox+;4V|Yqmo{JD%)*
zRO$g<7q8TiWl0?s$>98pjIqF@Ihdgc@8@>#D;iT@%K&l>zXTW9LKyMU%0#FHo!ekP
zuMPD#F`vq0ae>xj+S3uOErjacFHxU#E0%2;vd`a~ZNCTVS{}gJGY~Kg%^EgXl0?)7
zb4kv0X=GU%w!d9^uI8w$fJQ7k+%OKTJTC!3SVxAjTZnDEbIGnliCR&oIg3Pb>T5W&LPELZjEeW4<@9wIPKnFv1
zHII$Ixa<=0Ke#Nld0*r|xy<8-H;~KJ{)5ZD{ueG=erq)1UV#r_SE1ds7P^
zGv{&jRMAP?z|GARCNKPDE~Z&lgivsqg=^+#a^Tg2-ZR8JH}>45YbmE@y1T%;cD4H9
z+!3XCF1yk<6yN<9i@Z~0!f2|Um?R&8cB|a>c#ceoOgMS*i*(D!+o$(-g={0+RPaq*
zBu*CmRvD9hWD)`f*uIxoiUL&a{zE!+geWd~;b0D7vGVh0{ncdYP*Iyq>)>Zx8_uCH
zg?r1SUOdO<9tOA!O`MxXlP3ugZ_B{R${+6F$W?Xc8cMlEN`9tlYJwZ$#ENPp_gP7K
zTDsjhKlLaeCnR%ePC9UYCd*f4Z40A*38_~^!1Gjt@2~UL9l;Ew;27a8h*|-gJH^7z
zxf+6s?x`fNlXqn!5tJ1TbuSDi6~B+vQiD$|
zOoADFYDPMSHkmbR=%^*QSW9m_VQBbGzOv)e1pOJk&72T?=C&N_o
zo=0_>`t@xqOPQv
zxrPJvsI>K&;Zb-32kuSwTDHnn7
zVR}wy1ujEZX+OwP1WHFa&XyhHPKM7RH-x565nmV@z>{Fg6kWQfh#ofWnM*|N;4`T8
zk&o7*Cw4*xo*k)UQN~f~OtTFUtTZG|-*_rRMZSKN-+lJw?jg(nm=@)0Rx2dd`MRRJZF&e6v
zXmFeCx`<|MxoJIHGg?~Bio#-PCL`ROW|O`De(7^vy%yIzYD6NNGK$=MR*T6{R_6GX
z$1X1hQ~oiyqHGbY?v(sWmK_E0ktt{xJ)lZLx`o49Ja637+sVfc+MTZqeim6YOb&Mt
zVCW#>u>PSEM9S+erQAm^`Z@U3zx~tt@%o~BUDD@kjIt7D*xCKS#g_HpM1TA7LtZ6L
z1ewYjhAye~b=GmaAa*fzagTRLtCM-s&-q
z-jh%cI(2;!71zi1gHNKoc^x`r~e;e!GEIYf5L+Q2@C#T!UAA^?%xXu
zNOM!_w_IRY@K?bA9{XDjv@tBdclA_8WAfO?@
zZC)5)TnYkzE~Erpfx0M52!qs2;-3I_AS{IBgg`**6X4&BfFuY4>nN$^0?ZIz_^W_I
zYs0%%C8!NL|kuthRF_w$3)Q0na&{T98%F5Dn@J_WvivBq~g&9~iiCsGyM|
zCjhdFJx<^ioh6PuHu5O{v7`NZkbb?tLy`|XHHvrHpA1BeJM(Mc&A{a9**rcz7IMY_
z&nkLw)i8JOn@5;M1iV(%dErc5e!DaupEQJEmz-LATT@6*MjX&?IOB8fw&Mr1Xdm!&
z$#xjA{?j2O%s{KsjY4+|hHg6nR^U#S?C&bJb$@~B_l6UoopW)+pjFA1&UJ?^FR(WL
z9j$ZeG^()*=pDh}>g0_&^)6mE915_yn#X8ArBk1R$+5vAQIN%kg5xa)TgGJ_f*4?1
ztITT8T+0IVFs*^k^K6e+VO`>)_4&(tvsWEex~CBSfU{QM)r@SZ#2;qA_^tVy0}ZkB
zqwVZgIE_LQP(fk#YMU1)8omRKD!sDjrX0q%w+P`_q#+C;9U8>DXwGk@O;cfd;F{sn
z&7yoI|2nm*^o$LnPQS<>pu+@NS`VkRRW1$+M*iB1r&)b*aH9i0o4WPM`kBU}(Z3#R
z531FIt!1gDI2*79=~RmpTfg#wF1^YLAu8RgxjJfa1RlqHajasOlb9zR1>VYsa!t?Jo^i<`dm2D;ZT-MU=xoD`s7eM+q^!UZ;}JN9I@{>PZs7#<%10HnZ*{ncs~zDfBx-#!+l`##mhhZ
zF^!}Kpt#HsA`y=Y%&@1eUCKI-6jGP?-$uQXo=d@}f1uhQa?e@fPszQ@##VVMmGosXx0~n%S8#{{8#6{&l7$6N4v#
z+y#9@6y7@KJ#k#GX;YlEzf@z_(GZf*scchPoo6+(vD(BGZkX>33z3s3g3wBVER4Y1
zzn?7PKN>8z`xdGCD4AvT6Qg_4u(!r=A)&USyQZRsVIrs7k+F0Vv
zK_myz(wzE{`*lT9T7FTjFjOA#$abxD(~$&7S9~Ym&|zWG=mEu_>$?}vyV8-P^bOpt
zT^~Bb^bq1H(dm0TJJQp)zO{?`CrcnSoKBtIwTqS2cUB;{UyRDAu~aNNQ9a>pzoc@|
zx3%BfH`#8xyj0>nHP`Y(>4(gg0)j3dL&n3o6<6;#ul&naPO@Iusq7XxuI`oFPZtn7
zpu|$iQxIK8YSlsx>@^aZi!!RNN-ytE(;#_8FkEKRMk$?jss@f3_WVFBk4U9ZB__)}
zW5WaCktrf_xO~WCiLXkjjKIQW(v-VZ!#@A)ouofdt^TH>%>9BmFqpWz7fC{?vf^AD
zsLl!Pb^Z1{(q}=56u{$kcf7yD7}Lagz)(OwKGylNQyGxwd-3*u`dCXCU^f)Q`*AR3
z(DC-3@Tts*sJ+GO_k4B|nmtXmt*s^+&W^s{KE{?cX&(Q7?~tF
z!;-Q<5LTH5(^Wwn>SYyQ$@I5doz>79{%jbcr<3~pGDOIR0SX-MMDye%+tB>6o@QaXMTSBK2;{nXcfx)aW7@Z!PmTY?;UhN-d0
z1ZD=|F6DWnwhF`W}-$t##8h{va1zdDu
zyyaZd&^&L5n}Ez#W%IB;$#^YCv#5v_K5lxmFSF2ut#`TAZ6(}7rSA~ugcij#O=j6V
zn|3%>Z9$=~C!WEcE}o(nW(6F-f!_Dw$R^4nJ{kMkE~{2f@1pqWRWWeI!SX#Ud?MZy#KhMeDkd
z2$Y`a&XWFmv5enD_#`MtrGwISg>3sQ;f1N3N89o~(F#|{r9LMJo_p3Vg7q0EX+|it
zv`$v57CAcXk0O`K*B_3ved-(5!IQgw9MTeF2L~>4$bu|*5xq9tU(B*(C2a1`KsTj3
zFT!4gPLl(Z{qRs*2V_yz;GG#zutxd~dldi6veq
zE|wTo`1v;5UUqR}H^>IitGBKF*^(DuhKSng$SSqp%M?6j;`fy5w=u43J*`r@T&(2a
z3u918-jI%GY5b65!sVhyG>}^eT+t9{y>gkzJuv)}K
z;GGcLH}@|mv0I{sRNeaOQ1~b$8jJWSdGHtG(KaX6PUvxu^eZT0uDAsQamfmY)Vcr-
zSLfGh*(DFN2dr#vgtU~+*G0e1#H?Y833{x^Hum%9y*|AgZgs6qPR){^K732;8Ljqf
z^w%F)!tvV8nL={R&5C2{S7tqopD%eXIBiO{#X?Y
zIBCKvVFvX0kYgIjR$r%!#?G7-zOl+{U%V%8Jp@M+p-UrnYg|?6|D3*BTM%A*tUSmw
zU~uC}gEmCR888#jYey1zIPtyS)lYEf=t#&Yd1xhxiIH9UEk<8uu$e(SIQH5o?+#_X
z=?0^D=wChSpq`zpU7%(62(J&AqhbruE}O1~ytnVhdx|sv3_8WPs#~go(yv~knjaNX
zyt1Yc>z<*T@02b%Us#LFTk@}~V?XnK?qsVyYY3k9%
z%+=M(-s11M?nZ6BgtaylKgQ|Lx+@n>TK^b(4yTWs3D5gprjc&Ht1y!L%K&n>?CQaOxO;1kw*qMs}%$@z4CbDel8-)&&v)LR3z
z8=-3f+Vxtg<3H(9Hvm;UF)i*cK%Wgqirp-Kf2Z*W13$inovBush+PSvY!++
z#yX%e{rcRAR_$MR~
z**LpJJegcIKVG%|4zT;&{DMa24_RI9<8>^|=prpjC8VaLOEuJ~5;`i8f{JiC)TVIzQ(m*aY`+HBcu;OA9%
zG&UnPffejR|IM%M`zdY|o0nYuTcLWV6+qRKa519h&3!fwEG1 |