gigafibre-fsm/docs/assets/build-billing-pptx.js
louispaulb beb6ddc5e5 docs: reorganize into architecture/features/reference/archive folders
All docs moved with git mv so --follow preserves history. Flattens the
single-folder layout into goal-oriented folders and adds a README.md index
at every level.

- docs/README.md — new landing page with "I want to…" intent table
- docs/architecture/ — overview, data-model, app-design
- docs/features/ — billing-payments, cpe-management, vision-ocr, flow-editor
- docs/reference/ — erpnext-item-diff, legacy-wizard/
- docs/archive/ — HANDOFF-2026-04-18, MIGRATION, status-snapshots/
- docs/assets/ — pptx sources, build scripts (fixed hardcoded path)
- roadmap.md gains a "Modules in production" section with clickable
  URLs for every ops/tech/portal route and admin surface
- Phase 4 (Customer Portal) flipped to "Largely Shipped" based on
  audit of services/targo-hub/lib/payments.js (16 endpoints, webhook,
  PPA cron, Klarna BNPL all live)
- Archive files get an "ARCHIVED" banner so stale links inside them
  don't mislead readers

Code comments + nginx configs rewritten to use new doc paths. Root
README.md documentation table replaced with intent-oriented index.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 11:51:33 -04:00

365 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Billing & Payments — Dev handoff presentation
// Regenerate with: cd docs && node build-billing-pptx.js
//
// Structure (per user feedback 2026-04-17):
// 1. Title
// 2. End-to-end flow (facture → paiement) with scripts listed under each stage
// 3. OPS app modules with scripts/components per module
// 4. PDF preview + pay-public preview (aspect ratios preserved)
const fs = require("fs");
const path = require("path");
const pptxgen = require("pptxgenjs");
const sharp = require("sharp");
function imgPath(rel) {
const p = path.join(__dirname, rel);
return fs.existsSync(p) ? p : null;
}
async function imgRatio(rel) {
const p = path.join(__dirname, rel);
if (!fs.existsSync(p)) return null;
const m = await sharp(p).metadata();
return { w: m.width, h: m.height, ratio: m.width / m.height, path: p };
}
// Fit an image inside a target box (tx, ty, tw, th) maintaining its native ratio
function fitImage(meta, tx, ty, tw, th) {
const boxR = tw / th;
let w, h;
if (meta.ratio > boxR) { w = tw; h = tw / meta.ratio; }
else { h = th; w = th * meta.ratio; }
const x = tx + (tw - w) / 2;
const y = ty + (th - h) / 2;
return { path: meta.path, x, y, w, h };
}
const C = {
darkBg: "0D0F18",
cardBg: "181C2E",
cardBg2: "111422",
purple: "5C59A8",
purpleDk: "3F3D7A",
accent: "818CF8",
green: "22C55E",
amber: "F59E0B",
red: "EF4444",
white: "FFFFFF",
text: "E2E4EF",
muted: "7B80A0",
indigo: "6366F1",
};
const shadow = () => ({ type: "outer", blur: 8, offset: 3, angle: 135, color: "000000", opacity: 0.3 });
let pres;
function header(s, title, subtitle) {
s.background = { color: C.darkBg };
s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } });
s.addText(title, { x: 0.6, y: 0.22, w: 9, h: 0.6, fontSize: 26, fontFace: "Arial Black", color: C.white, margin: 0 });
if (subtitle) s.addText(subtitle, { x: 0.6, y: 0.76, w: 9, h: 0.3, fontSize: 11, fontFace: "Calibri", color: C.muted, margin: 0 });
}
async function build() {
pres = new pptxgen();
pres.layout = "LAYOUT_16x9";
pres.author = "Targo";
pres.title = "Facturation & Paiements — Handoff dev";
// ═════════ SLIDE 1 — Title ═════════
let s = pres.addSlide();
s.background = { color: C.darkBg };
s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.purple } });
s.addShape(pres.shapes.RECTANGLE, { x: 0.8, y: 1.2, w: 0.12, h: 1.8, fill: { color: C.indigo } });
s.addText("Facturation", { x: 1.15, y: 1.2, w: 8, h: 0.7, fontSize: 42, fontFace: "Arial Black", color: C.white, bold: true, margin: 0 });
s.addText("& Paiements", { x: 1.15, y: 1.85, w: 8, h: 0.6, fontSize: 36, fontFace: "Arial Black", color: C.accent, margin: 0 });
s.addText("Handoff technique — pipeline end-to-end + modules OPS", { x: 1.15, y: 2.7, w: 8.5, h: 0.4, fontSize: 16, fontFace: "Calibri", color: C.muted, margin: 0 });
const heroStats = [
{ v: "80", l: "Scripts migration" },
{ v: "115 K", l: "Factures importées" },
{ v: "1,6 M", l: "Items (incl. backfill)" },
{ v: "15", l: "Pages OPS" },
];
heroStats.forEach((st, i) => {
const sx = 1.15 + i * 2.1;
s.addShape(pres.shapes.RECTANGLE, { x: sx, y: 3.7, w: 1.8, h: 1.0, fill: { color: C.cardBg }, shadow: shadow() });
s.addText(st.v, { x: sx, y: 3.72, w: 1.8, h: 0.55, fontSize: 22, fontFace: "Arial Black", color: C.accent, align: "center", valign: "middle", margin: 0 });
s.addText(st.l, { x: sx, y: 4.25, w: 1.8, h: 0.35, fontSize: 10, fontFace: "Calibri", color: C.muted, align: "center", margin: 0 });
});
s.addText("Avril 2026 • erp.gigafibre.ca + client.gigafibre.ca + ops", { x: 0.8, y: 5.1, w: 9, h: 0.3, fontSize: 10, fontFace: "Calibri", color: C.muted, margin: 0 });
// ═════════ SLIDE 2 — End-to-end flow with scripts per stage ═════════
s = pres.addSlide();
header(s, "Flux facture → paiement", "Scripts & fichiers utilisés à chaque étape");
// 6 stages laid out left-to-right, each with a card + scripts list below
const stages = [
{
title: "1. Import legacy",
tagline: "MariaDB → PostgreSQL",
color: C.purple,
scripts: [
"import_invoices.py",
"import_payments.py",
"import_payment_methods.py",
"import_payment_arrangements.py",
"import_expro_payments.py",
"backfill_service_location.py",
"fix_invoice_outstanding.py",
"fix_invoice_customer_names.py",
"add_missing_custom_fields.py",
],
},
{
title: "2. Facture",
tagline: "ERPNext Sales Invoice",
color: C.accent,
scripts: [
"setup_invoice_print_format.py",
"test_jinja_render.py",
"Print Format: « Facture TARGO »",
"Jinja template (inline)",
"pdf_generator=\"chrome\"",
"Doctype: Sales Invoice",
"Custom field: service_location",
],
},
{
title: "3. Rendu PDF",
tagline: "Chromium --print-to-pdf",
color: C.green,
scripts: [
"gigafibre_utils.api.invoice_pdf",
"Dockerfile (chromium install)",
"common_site_config:",
" chromium_path=/usr/bin/chromium",
"cairosvg → logo PNG",
"Producer: Skia/PDF m147",
"~1 s / facture",
],
},
{
title: "4. QR / Magic-link",
tagline: "Tokens HMAC-SHA256",
color: C.indigo,
scripts: [
"api.invoice_qr(invoice)",
"api.invoice_qr_datauri(invoice)",
"api._sign_pay_token",
"api.validate_pay_token",
"api.request_magic_link",
"api.pay_token (admin)",
"api.pay_redirect (portal)",
"Secret: gigafibre_pay_secret",
],
},
{
title: "5. Landing publique",
tagline: "client.gigafibre.ca/pay-public",
color: C.amber,
scripts: [
"gigafibre_utils/www/",
" pay-public.html",
"/opt/traefik/dynamic/",
" pay-public.yml",
"Traefik priority ≥ 250",
"(carve-out Authentik)",
"Vanilla JS, pas de build",
],
},
{
title: "6. Stripe + Payment Entry",
tagline: "Checkout → webhook → réconciliation",
color: C.red,
scripts: [
"api.create_checkout_session",
"api.stripe_webhook",
"api._stripe_post",
"api._stripe_verify_signature",
"Secret: gigafibre_stripe_secret_key",
"Secret: gigafibre_stripe_webhook_secret",
"Twilio: twilio_account_sid / token / from",
],
},
];
const cardW = 1.5, cardH = 0.7, gap = 0.08;
const totalW = stages.length * cardW + (stages.length - 1) * gap;
const startX = (10 - totalW) / 2;
const cardY = 1.3;
stages.forEach((st, i) => {
const x = startX + i * (cardW + gap);
s.addShape(pres.shapes.RECTANGLE, {
x, y: cardY, w: cardW, h: cardH,
fill: { color: C.cardBg }, line: { color: st.color, width: 1.5 }, shadow: shadow(),
});
s.addText(st.title, { x, y: cardY + 0.03, w: cardW, h: 0.32, fontSize: 10, fontFace: "Arial Black", color: C.white, align: "center", valign: "middle", margin: 0 });
s.addText(st.tagline, { x, y: cardY + 0.35, w: cardW, h: 0.3, fontSize: 7.5, fontFace: "Calibri", color: st.color, align: "center", valign: "middle", margin: 0 });
// Arrow to next stage
if (i < stages.length - 1) {
s.addText("→", { x: x + cardW - 0.05, y: cardY + 0.18, w: gap + 0.1, h: 0.35, fontSize: 14, color: C.muted, align: "center", valign: "middle", margin: 0, bold: true });
}
// Scripts list under the card
const listY = cardY + cardH + 0.15;
s.addShape(pres.shapes.RECTANGLE, {
x, y: listY, w: cardW, h: 3.3,
fill: { color: C.cardBg2 }, line: { color: st.color, width: 0.5 },
});
const txt = st.scripts.map(sc => "• " + sc).join("\n");
s.addText(txt, {
x: x + 0.04, y: listY + 0.05, w: cardW - 0.08, h: 3.2,
fontSize: 6.8, fontFace: "Courier New", color: C.text, valign: "top", margin: 0,
paraSpaceAfter: 2,
});
});
// Footer: common secrets
s.addShape(pres.shapes.RECTANGLE, { x: 0.4, y: 5.5, w: 9.2, h: 0.8, fill: { color: "1a1a2e" }, line: { color: C.purple, width: 0.5 } });
s.addText("Configuration (common_site_config.json)", { x: 0.55, y: 5.55, w: 9, h: 0.2, fontSize: 9, fontFace: "Calibri", color: C.accent, bold: true, margin: 0 });
s.addText("gigafibre_pay_secret · gigafibre_pay_host · gigafibre_stripe_secret_key · gigafibre_stripe_webhook_secret · twilio_account_sid · twilio_auth_token · twilio_from_number · chromium_path", {
x: 0.55, y: 5.8, w: 9, h: 0.45, fontSize: 8, fontFace: "Courier New", color: C.text, margin: 0,
});
// ═════════ SLIDE 3 — OPS modules with scripts/components ═════════
s = pres.addSlide();
header(s, "Modules OPS", "Pages & composants · apps/ops/src/");
const modules = [
{
title: "Clients",
color: C.purple,
files: [
"pages/ClientsPage.vue",
"pages/ClientDetailPage.vue",
"components/customer/*",
"components/shared/",
" detail-sections/",
" InvoiceDetail.vue",
" PaymentDetail.vue",
" SubscriptionDetail.vue",
" EquipmentDetail.vue",
" IssueDetail.vue",
],
},
{
title: "Dispatch",
color: C.amber,
files: [
"pages/DispatchPage.vue",
"components/dispatch/",
"pages/dispatch-styles.scss",
"mode sombre (exception)",
],
},
{
title: "Facturation",
color: C.green,
files: [
"shared/CreateInvoiceModal.vue",
"shared/detail-sections/",
" InvoiceDetail.vue",
" onglet Aperçu client",
" iframe → invoice_pdf",
"shared/detail-sections/",
" PaymentDetail.vue",
],
},
{
title: "Rapports",
color: C.indigo,
files: [
"pages/RapportsPage.vue",
"pages/ReportARPage.vue",
"pages/ReportRevenuPage.vue",
"pages/ReportTaxesPage.vue",
"pages/ReportVentesPage.vue",
],
},
{
title: "Network",
color: C.accent,
files: [
"pages/NetworkPage.vue",
"pages/TelephonyPage.vue",
"pages/OcrPage.vue",
],
},
{
title: "Équipe & Tickets",
color: C.red,
files: [
"pages/EquipePage.vue",
"pages/TicketsPage.vue",
"pages/AgentFlowsPage.vue",
"pages/DashboardPage.vue",
"pages/SettingsPage.vue",
],
},
];
const cols = 3, rows = 2;
const mW = 2.9, mH = 2.25;
const mGapX = 0.15, mGapY = 0.15;
const mStartX = (10 - (cols * mW + (cols - 1) * mGapX)) / 2;
const mStartY = 1.3;
modules.forEach((mod, i) => {
const cx = mStartX + (i % cols) * (mW + mGapX);
const cy = mStartY + Math.floor(i / cols) * (mH + mGapY);
s.addShape(pres.shapes.RECTANGLE, {
x: cx, y: cy, w: mW, h: mH,
fill: { color: C.cardBg }, line: { color: mod.color, width: 1.5 }, shadow: shadow(),
});
s.addShape(pres.shapes.RECTANGLE, { x: cx, y: cy, w: mW, h: 0.35, fill: { color: mod.color } });
s.addText(mod.title, { x: cx + 0.1, y: cy, w: mW - 0.2, h: 0.35, fontSize: 12, fontFace: "Arial Black", color: C.white, valign: "middle", margin: 0 });
s.addText(mod.files.map(f => "• " + f).join("\n"), {
x: cx + 0.1, y: cy + 0.42, w: mW - 0.2, h: mH - 0.5,
fontSize: 8.5, fontFace: "Courier New", color: C.text, valign: "top", margin: 0,
});
});
// Footer: deploy note
s.addShape(pres.shapes.RECTANGLE, { x: 0.4, y: mStartY + 2 * (mH + mGapY) + 0.1, w: 9.2, h: 0.5, fill: { color: "1a1a2e" }, line: { color: C.amber, width: 0.5 } });
s.addText("Déploiement: scp dist/spa/* → /opt/ops-app/ · nginx proxy API (token) · Authentik SSO · Traefik routing", {
x: 0.55, y: mStartY + 2 * (mH + mGapY) + 0.18, w: 9, h: 0.4, fontSize: 9, fontFace: "Calibri", color: C.text, margin: 0,
});
// ═════════ SLIDE 4 — Visuels : PDF facture + landing pay-public ═════════
s = pres.addSlide();
header(s, "Visuels", "PDF Facture TARGO (Chrome) + Landing /pay-public");
const invoiceMeta = await imgRatio("assets/screenshots/invoice-pdf.png");
const payMeta = await imgRatio("assets/screenshots/pay-public.png");
// Two equal 4.5" × 4.5" boxes, images fit maintaining ratio
const box = { w: 4.3, h: 4.4, y: 1.2 };
const leftX = 0.4;
const rightX = 5.3;
s.addShape(pres.shapes.RECTANGLE, { x: leftX, y: box.y, w: box.w, h: box.h, fill: { color: C.white }, line: { color: C.purple, width: 1 } });
if (invoiceMeta) {
const fit = fitImage(invoiceMeta, leftX + 0.05, box.y + 0.05, box.w - 0.1, box.h - 0.1);
s.addImage({ path: fit.path, x: fit.x, y: fit.y, w: fit.w, h: fit.h });
}
s.addText("PDF Facture TARGO", { x: leftX, y: box.y + box.h + 0.05, w: box.w, h: 0.3, fontSize: 11, fontFace: "Calibri", color: C.white, bold: true, align: "center", margin: 0 });
s.addText("Chromium --print-to-pdf · 1 page · ~1 s", { x: leftX, y: box.y + box.h + 0.32, w: box.w, h: 0.25, fontSize: 9, fontFace: "Calibri", color: C.muted, align: "center", margin: 0 });
s.addShape(pres.shapes.RECTANGLE, { x: rightX, y: box.y, w: box.w, h: box.h, fill: { color: C.white }, line: { color: C.accent, width: 1 } });
if (payMeta) {
const fit = fitImage(payMeta, rightX + 0.05, box.y + 0.05, box.w - 0.1, box.h - 0.1);
s.addImage({ path: fit.path, x: fit.x, y: fit.y, w: fit.w, h: fit.h });
}
s.addText("Landing /pay-public", { x: rightX, y: box.y + box.h + 0.05, w: box.w, h: 0.3, fontSize: 11, fontFace: "Calibri", color: C.white, bold: true, align: "center", margin: 0 });
s.addText("Bypass Authentik · Stripe Checkout · Magic-link SMS/email", { x: rightX, y: box.y + box.h + 0.32, w: box.w, h: 0.25, fontSize: 9, fontFace: "Calibri", color: C.muted, align: "center", margin: 0 });
// Save
const out = path.join(__dirname, "Gigafibre-Billing-Handoff.pptx");
await pres.writeFile({ fileName: out });
console.log("Wrote:", out);
}
build().catch(e => { console.error(e); process.exit(1); });