gigafibre-fsm/services/targo-hub/lib/project-templates.js
louispaulb 2aee8f31df fix(contracts): create pending Service Subscription on signing + test templates
Root cause of CTR-00008: _createBuiltInInstallChain only created Issue +
Dispatch Jobs. It never created a pending Service Subscription, so when
the chain's terminal job Completed, activateSubscriptionForJob found
nothing matching customer+service_location+status='En attente' to flip.
Result: 4/4 tasks done, no sub activation, no prorated invoice.

Changes:
- contracts.js: after chain creation, create Service Subscription with
  status='En attente' (plan_name + service_category inferred from the
  contract). Back-link it on Service Contract.service_subscription (a
  new custom field — the stock 'subscription' field on Service Contract
  points at the built-in ERPNext Subscription doctype, not ours).
- project-templates.js: add test_single (1-step) and test_parallel
  (diamond: step0 → step1 ∥ step2) for faster lifecycle testing.
  Extract chooseTemplate(contract) with precedence:
    contract.install_template → contract_type mapping → fiber_install.
- contracts.js: chain builder now uses chooseTemplate instead of
  hardcoded fiber_install, logs the chosen template per contract.
- _inferServiceCategory/_inferPlanName helpers map contract metadata
  into the Service Subscription's required fields.

Companion changes on ERPNext (custom fields, no code):
  Service Contract.service_subscription  Link → Service Subscription
  Service Contract.install_template       Select (fiber_install,
    phone_service, move_service, repair_service, test_single,
    test_parallel)

Retroactive repair for CTR-00008 applied directly on prod:
  → SUB-0000100003 (Actif), SINV-2026-700014 (Draft, $9.32 prorata).

Smoke test of test_single path on prod (CTR-00010 synthetic, cleaned up):
  template=test_single ✓  sub created ✓  activated on completion ✓
  prorated invoice emitted ✓

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 10:03:49 -04:00

94 lines
6.7 KiB
JavaScript

'use strict'
// ─────────────────────────────────────────────────────────────────────────────
// Install chain templates for Service Contracts.
//
// Each template is an ordered array of steps. Steps chain via `depends_on_step`
// (index into the same array, or null for "run as soon as chain starts").
// `null` → the step is born "open" immediately; otherwise born "On Hold"
// until the parent step completes (see dispatch.unblockDependents).
//
// Multiple steps with depends_on_step=null run in PARALLEL (tech sees both
// at once, either can be completed first). Likewise multiple steps pointing
// at the same parent run in parallel after the parent. The terminal-node
// detector (_isChainTerminal) only fires when ALL siblings are Completed.
// ─────────────────────────────────────────────────────────────────────────────
const TEMPLATES = {
// ── Production templates ────────────────────────────────────────────────
fiber_install: [
{ subject: 'Vérification pré-installation (éligibilité & OLT)', job_type: 'Autre', priority: 'medium', duration_h: 0.5, assigned_group: 'Admin', depends_on_step: null },
{ subject: 'Installation fibre chez le client', job_type: 'Installation', priority: 'high', duration_h: 3, assigned_group: 'Tech Targo', depends_on_step: 0 },
{ subject: 'Activation du service & configuration ONT', job_type: 'Installation', priority: 'high', duration_h: 0.5, assigned_group: 'Admin', depends_on_step: 1 },
{ subject: 'Test de débit & validation client', job_type: 'Dépannage', priority: 'medium', duration_h: 0.5, assigned_group: 'Tech Targo', depends_on_step: 2 },
],
phone_service: [
{ subject: 'Importer le numéro de téléphone', job_type: 'Autre', priority: 'medium', duration_h: 0.5, assigned_group: 'Admin', depends_on_step: null },
{ subject: 'Installation fibre (pré-requis portage)', job_type: 'Installation', priority: 'high', duration_h: 2, assigned_group: 'Tech Targo', depends_on_step: 0 },
{ subject: 'Portage du numéro vers Gigafibre', job_type: 'Autre', priority: 'medium', duration_h: 0.5, assigned_group: 'Admin', depends_on_step: 1 },
{ subject: 'Validation et test du service téléphonique', job_type: 'Dépannage', priority: 'medium', duration_h: 0.5, assigned_group: 'Tech Targo', depends_on_step: 2 },
],
move_service: [
{ subject: 'Préparation déménagement (vérifier éligibilité nouveau site)', job_type: 'Autre', priority: 'medium', duration_h: 0.5, assigned_group: 'Admin', depends_on_step: null },
{ subject: 'Retrait équipement ancien site', job_type: 'Retrait', priority: 'medium', duration_h: 1, assigned_group: 'Tech Targo', depends_on_step: 0 },
{ subject: 'Installation au nouveau site', job_type: 'Installation', priority: 'high', duration_h: 3, assigned_group: 'Tech Targo', depends_on_step: 1 },
{ subject: 'Transfert abonnement & mise à jour adresse', job_type: 'Autre', priority: 'medium', duration_h: 0.5, assigned_group: 'Admin', depends_on_step: 2 },
],
repair_service: [
{ subject: 'Diagnostic à distance', job_type: 'Dépannage', priority: 'high', duration_h: 0.5, assigned_group: 'Admin', depends_on_step: null },
{ subject: 'Intervention terrain', job_type: 'Réparation', priority: 'high', duration_h: 2, assigned_group: 'Tech Targo', depends_on_step: 0 },
{ subject: 'Validation & suivi client', job_type: 'Dépannage', priority: 'medium', duration_h: 0.5, assigned_group: 'Admin', depends_on_step: 1 },
],
// ── Test templates ──────────────────────────────────────────────────────
// For end-to-end testing the contract → chain → activation → invoice
// lifecycle without running through a 4-step production chain.
// One-step: tech completes one job, subscription activates immediately.
// Fastest path to verify the full lifecycle.
test_single: [
{ subject: 'Test — Activation immédiate', job_type: 'Installation', priority: 'medium', duration_h: 0.25, assigned_group: 'Admin', depends_on_step: null },
],
// Three steps with a parallel middle: step 0 → (step 1 ∥ step 2).
// Both middle jobs are born "open" after step 0 completes (same depends_on).
// Tech picks either one first; subscription activates when BOTH are Completed.
// Exercises _isChainTerminal's sibling-completion check.
test_parallel: [
{ subject: 'Test — Étape initiale', job_type: 'Autre', priority: 'medium', duration_h: 0.25, assigned_group: 'Admin', depends_on_step: null },
{ subject: 'Test — Branche A (parallèle)', job_type: 'Installation', priority: 'medium', duration_h: 0.25, assigned_group: 'Admin', depends_on_step: 0 },
{ subject: 'Test — Branche B (parallèle)', job_type: 'Installation', priority: 'medium', duration_h: 0.25, assigned_group: 'Admin', depends_on_step: 0 },
],
}
// Pick a template by explicit id, then by contract_type mapping, else fall back
// to the universal fiber_install safety net.
//
// chooseTemplate({ install_template: 'test_single' }) → test_single
// chooseTemplate({ contract_type: 'Résidentiel' }) → fiber_install
// chooseTemplate({ contract_type: 'Test-Simple' }) → test_single
// chooseTemplate({ contract_type: 'Test-Parallèle' }) → test_parallel
// chooseTemplate({}) → fiber_install
function chooseTemplate (contract = {}) {
const explicit = contract.install_template
if (explicit && TEMPLATES[explicit]) return explicit
const byType = {
'Résidentiel': 'fiber_install',
'Commercial': 'fiber_install',
'Déménagement': 'move_service',
'Téléphonie': 'phone_service',
'Réparation': 'repair_service',
'Test-Simple': 'test_single',
'Test-Parallèle': 'test_parallel',
'Test-Parallele': 'test_parallel', // tolerate missing accent
}
const mapped = byType[contract.contract_type]
return mapped || 'fiber_install'
}
function getTemplateSteps (templateId) {
return TEMPLATES[templateId] || []
}
module.exports = { TEMPLATES, getTemplateSteps, chooseTemplate }