Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré / juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts, sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle week-end + placeholder « en attente du quart », clustering + lasso + filtre-date sur la carte, accès rapide « À assigner » (badge). Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive + timeout fetch global + authFetch durci → fin des rechargements manuels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.7 KiB
JavaScript
62 lines
2.7 KiB
JavaScript
// P1 — Porte d'APPROBATION de facturation (chaîne installation → approbation → facturation).
|
|
// Quand le job terminal est complété (et que BILLING_APPROVAL_GATE=on), l'activation est placée
|
|
// « à approuver » au lieu d'activer/facturer directement. Un dispatcher/admin révise l'aperçu
|
|
// prorata (lecture seule) puis approuve → on ACTIVE les abonnements. La FACTURATION reste gérée
|
|
// par activateSubscriptionForJob (gatée PRORATION_WRITE) : en P1 (gate off) → active SANS facturer.
|
|
//
|
|
// Store : data/billing-approvals.json (même répertoire persistant que les autres données du hub).
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const STORE = path.join(__dirname, '..', 'data', 'billing-approvals.json')
|
|
|
|
function _read () { try { return JSON.parse(fs.readFileSync(STORE, 'utf8')) } catch (_) { return {} } }
|
|
function _write (o) { fs.mkdirSync(path.dirname(STORE), { recursive: true }); fs.writeFileSync(STORE, JSON.stringify(o, null, 2)) }
|
|
|
|
// Appelé à la complétion du job terminal (gaté). Marque « à approuver ». N'ACTIVE PAS, NE FACTURE PAS.
|
|
// Idempotent : ne rétrograde jamais un job déjà approuvé.
|
|
function markPending (job, completed_at) {
|
|
if (!job) return null
|
|
const all = _read()
|
|
if (all[job] && all[job].status === 'approved') return all[job]
|
|
all[job] = {
|
|
job,
|
|
completed_at: completed_at || (all[job] && all[job].completed_at) || new Date().toISOString(),
|
|
status: 'pending',
|
|
approved_by: null,
|
|
approved_at: null,
|
|
}
|
|
_write(all)
|
|
return all[job]
|
|
}
|
|
|
|
function listPending () { return Object.values(_read()).filter(a => a.status === 'pending') }
|
|
function get (job) { return _read()[job] || null }
|
|
|
|
// Approbation : enregistre la décision PUIS active les abonnements via dispatch.activateSubscriptionForJob.
|
|
// La facturation (création de la facture consolidée) est gérée DANS activateSubscriptionForJob et reste
|
|
// DOUBLE-GATÉE (PRORATION_WRITE) → en P1 (gate off) : active SANS écrire de facture.
|
|
async function approve (job, approver) {
|
|
if (!job) throw new Error('job requis')
|
|
const all = _read()
|
|
const rec = all[job]
|
|
if (!rec) throw new Error('aucune approbation en attente pour ' + job)
|
|
if (rec.status === 'approved') return { ...rec, already: true }
|
|
|
|
const result = await require('./dispatch').activateSubscriptionForJob(job)
|
|
|
|
rec.status = 'approved'
|
|
rec.approved_by = approver || 'inconnu'
|
|
rec.approved_at = new Date().toISOString()
|
|
rec.activation = {
|
|
activated: result.activated || [],
|
|
invoice: result.invoice || null,
|
|
written: !!result.written,
|
|
prorated_total: result.prorated_total != null ? result.prorated_total : null,
|
|
}
|
|
_write(all)
|
|
return rec
|
|
}
|
|
|
|
module.exports = { markPending, listPending, get, approve }
|