[FEAT] MktgLead getJsonBody + script import-feedback-to-nexus + seed demo agile-tech
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1d934e4e63
commit
9b53ca3ba1
@ -13,9 +13,12 @@ require_once APP_PATH . '/services/EmailService.php';
|
|||||||
|
|
||||||
class MktgLeadController extends BaseController
|
class MktgLeadController extends BaseController
|
||||||
{
|
{
|
||||||
private const WEBHOOK_URL = 'https://mktg.agile.software/api/webhook/leads';
|
// AgileHub CRM (mktg.agile.software dismesso 2026-04-19)
|
||||||
private const WEBHOOK_KEY = 'wh_nis2_2026_c1d2e3f4a5b6c7d8';
|
private const WEBHOOK_URL = 'https://agilehub.agile.software/api/leads/webhook';
|
||||||
|
private const WEBHOOK_KEY = 'agilehub_nis2_2026';
|
||||||
|
private const TENANT_ID = '7';
|
||||||
private const PRODUCT = 'NIS2 Agile';
|
private const PRODUCT = 'NIS2 Agile';
|
||||||
|
private const PRODUCT_CODE = 'NIS2';
|
||||||
private const SOURCE = 'nis2-landing';
|
private const SOURCE = 'nis2-landing';
|
||||||
private const NOTIFY_EMAIL = 'info@agile.software';
|
private const NOTIFY_EMAIL = 'info@agile.software';
|
||||||
|
|
||||||
@ -93,6 +96,8 @@ class MktgLeadController extends BaseController
|
|||||||
'email' => $email,
|
'email' => $email,
|
||||||
'phone' => $phone,
|
'phone' => $phone,
|
||||||
'company' => $company,
|
'company' => $company,
|
||||||
|
'product' => self::PRODUCT_CODE,
|
||||||
|
'productLabel' => self::PRODUCT,
|
||||||
'product_interest' => $interest ?: self::PRODUCT,
|
'product_interest' => $interest ?: self::PRODUCT,
|
||||||
'source' => $source,
|
'source' => $source,
|
||||||
'notes' => $fullNotes,
|
'notes' => $fullNotes,
|
||||||
@ -118,6 +123,7 @@ class MktgLeadController extends BaseController
|
|||||||
CURLOPT_HTTPHEADER => [
|
CURLOPT_HTTPHEADER => [
|
||||||
'Content-Type: application/json',
|
'Content-Type: application/json',
|
||||||
'X-Webhook-Key: ' . self::WEBHOOK_KEY,
|
'X-Webhook-Key: ' . self::WEBHOOK_KEY,
|
||||||
|
'X-Tenant-Id: ' . self::TENANT_ID,
|
||||||
],
|
],
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_TIMEOUT => 8,
|
CURLOPT_TIMEOUT => 8,
|
||||||
|
|||||||
128
docs/sql/seed_agile_tech_demo.sql
Normal file
128
docs/sql/seed_agile_tech_demo.sql
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- SEED DEMO: Agile Technology SRL (consulting firm) + 3 aziende clienti
|
||||||
|
-- Database : nis2_agile_db
|
||||||
|
-- Data : 2026-05-29
|
||||||
|
-- Scope : ambiente DEV/test. Idempotente (rieseguibile via ON DUPLICATE).
|
||||||
|
-- Rollback : vedi sezione finale (commentata) per cancellazione totale.
|
||||||
|
--
|
||||||
|
-- Tutti i dati sono INVENTATI ai fini test della pipeline
|
||||||
|
-- consulting_firm → consultant users → client organizations → KB visibility.
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
USE nis2_agile_db;
|
||||||
|
SET @now := NOW();
|
||||||
|
|
||||||
|
-- ── 1) CONSULTING FIRM ────────────────────────────────────────────────────
|
||||||
|
INSERT INTO consulting_firms
|
||||||
|
(name, vat_number, fiscal_code, forma_giuridica, address, city, province, cap,
|
||||||
|
phone, pec, website, plan, max_organizations, max_users, status, created_at)
|
||||||
|
VALUES
|
||||||
|
('Agile Technology SRL', 'IT12345670962', '12345670962', 'SRL',
|
||||||
|
'Via dei Consulenti 42', 'Milano', 'MI', '20121',
|
||||||
|
'+39 02 5500 0001', 'agiletech@pec.it', 'https://agile.software',
|
||||||
|
'enterprise', 100, 20, 'active', @now)
|
||||||
|
ON DUPLICATE KEY UPDATE name = VALUES(name);
|
||||||
|
|
||||||
|
-- Nota: la tabella non ha UNIQUE su vat_number, quindi gestiamo "upsert manuale"
|
||||||
|
SET @firm_id := (SELECT id FROM consulting_firms
|
||||||
|
WHERE vat_number = 'IT12345670962' ORDER BY id LIMIT 1);
|
||||||
|
|
||||||
|
-- ── 2) CONSULENTI (users con role=consultant, legati al firm) ─────────────
|
||||||
|
-- Password test: "Consultant2026!" (hash bcrypt cost=10, riusabile per tutti)
|
||||||
|
SET @pwd := '$2y$10$Z6QwT5qg5sP9ZdYy3mFvgeKv6L9DkH3o0c2.M0Y0PnCqVTwQHvz/i';
|
||||||
|
|
||||||
|
INSERT INTO users (email, password_hash, full_name, phone, role, consulting_firm_id, is_active, created_at)
|
||||||
|
VALUES
|
||||||
|
('marco.ferri@agiletech.demo', @pwd, 'Marco Ferri', '+39 333 1112201', 'consultant', @firm_id, 1, @now),
|
||||||
|
('laura.greco@agiletech.demo', @pwd, 'Laura Greco', '+39 333 1112202', 'consultant', @firm_id, 1, @now),
|
||||||
|
('paolo.rossi@agiletech.demo', @pwd, 'Paolo Rossi', '+39 333 1112203', 'consultant', @firm_id, 1, @now)
|
||||||
|
ON DUPLICATE KEY UPDATE consulting_firm_id = VALUES(consulting_firm_id),
|
||||||
|
role = VALUES(role);
|
||||||
|
|
||||||
|
SET @u_marco := (SELECT id FROM users WHERE email='marco.ferri@agiletech.demo');
|
||||||
|
SET @u_laura := (SELECT id FROM users WHERE email='laura.greco@agiletech.demo');
|
||||||
|
SET @u_paolo := (SELECT id FROM users WHERE email='paolo.rossi@agiletech.demo');
|
||||||
|
|
||||||
|
-- ── 3) ORGANIZATIONS CLIENTI (3 aziende, settori diversi) ─────────────────
|
||||||
|
INSERT INTO organizations
|
||||||
|
(name, vat_number, fiscal_code, sector, entity_type, voluntary_compliance,
|
||||||
|
employee_count, annual_turnover_eur, country, city, address, website,
|
||||||
|
contact_email, contact_phone, subscription_plan, consulting_firm_id, is_active, created_at)
|
||||||
|
VALUES
|
||||||
|
('Aurora Sanità S.p.A.', 'IT04501230965', '04501230965', 'health',
|
||||||
|
'essential', 0, 480, 92000000.00, 'IT', 'Bergamo', 'Via degli Ospedali 8',
|
||||||
|
'https://aurorasanita.demo', 'compliance@aurorasanita.demo', '+39 035 7001001',
|
||||||
|
'professional', @firm_id, 1, @now),
|
||||||
|
|
||||||
|
('NordWater Utilities S.r.l.', 'IT05512340963', '05512340963', 'water',
|
||||||
|
'essential', 0, 220, 41500000.00, 'IT', 'Brescia', 'Strada del Depuratore 14',
|
||||||
|
'https://nordwater.demo', 'security@nordwater.demo', '+39 030 4002002',
|
||||||
|
'professional', @firm_id, 1, @now),
|
||||||
|
|
||||||
|
('Logistica Veloce S.r.l.', 'IT06723450961', '06723450961', 'transport',
|
||||||
|
'important', 0, 95, 18700000.00, 'IT', 'Verona', 'Viale dei Trasporti 27',
|
||||||
|
'https://logisticaveloce.demo', 'it@logisticaveloce.demo', '+39 045 5003003',
|
||||||
|
'free', @firm_id, 1, @now)
|
||||||
|
ON DUPLICATE KEY UPDATE consulting_firm_id = VALUES(consulting_firm_id);
|
||||||
|
|
||||||
|
SET @org_aurora := (SELECT id FROM organizations WHERE vat_number='IT04501230965');
|
||||||
|
SET @org_nordwater := (SELECT id FROM organizations WHERE vat_number='IT05512340963');
|
||||||
|
SET @org_logistica := (SELECT id FROM organizations WHERE vat_number='IT06723450961');
|
||||||
|
|
||||||
|
-- ── 4) UTENTI INTERNI DELLE ORG CLIENTI (org_admin per ciascuna) ─────────
|
||||||
|
INSERT INTO users (email, password_hash, full_name, phone, role, is_active, created_at) VALUES
|
||||||
|
('admin@aurorasanita.demo', @pwd, 'Giulia Bianchi (Aurora)', '+39 035 7001010', 'org_admin', 1, @now),
|
||||||
|
('admin@nordwater.demo', @pwd, 'Stefano Conti (NordWater)', '+39 030 4002020', 'org_admin', 1, @now),
|
||||||
|
('admin@logisticaveloce.demo', @pwd, 'Elena Marini (Logistica)', '+39 045 5003030', 'org_admin', 1, @now)
|
||||||
|
ON DUPLICATE KEY UPDATE role = VALUES(role);
|
||||||
|
|
||||||
|
SET @u_admin_aur := (SELECT id FROM users WHERE email='admin@aurorasanita.demo');
|
||||||
|
SET @u_admin_nw := (SELECT id FROM users WHERE email='admin@nordwater.demo');
|
||||||
|
SET @u_admin_log := (SELECT id FROM users WHERE email='admin@logisticaveloce.demo');
|
||||||
|
|
||||||
|
-- ── 5) user_organizations: org_admin "interno" delle 3 org + consulenti ──
|
||||||
|
INSERT INTO user_organizations (user_id, organization_id, role, is_primary, joined_at) VALUES
|
||||||
|
(@u_admin_aur, @org_aurora, 'org_admin', 1, @now),
|
||||||
|
(@u_admin_nw, @org_nordwater, 'org_admin', 1, @now),
|
||||||
|
(@u_admin_log, @org_logistica, 'org_admin', 1, @now),
|
||||||
|
-- Marco è consulente delle 3 org clienti
|
||||||
|
(@u_marco, @org_aurora, 'consultant', 0, @now),
|
||||||
|
(@u_marco, @org_nordwater, 'consultant', 0, @now),
|
||||||
|
(@u_marco, @org_logistica, 'consultant', 0, @now),
|
||||||
|
-- Laura solo Aurora + NordWater
|
||||||
|
(@u_laura, @org_aurora, 'consultant', 0, @now),
|
||||||
|
(@u_laura, @org_nordwater, 'consultant', 0, @now),
|
||||||
|
-- Paolo solo Logistica
|
||||||
|
(@u_paolo, @org_logistica, 'consultant', 0, @now)
|
||||||
|
ON DUPLICATE KEY UPDATE role = VALUES(role);
|
||||||
|
|
||||||
|
-- ── 6) firm_org_assignments: mapping firm → org → consulente ─────────────
|
||||||
|
-- assigned_to = 0 → tutti i membri del firm vedono la org (default)
|
||||||
|
-- assigned_to = X → solo quel consulente è "lead"
|
||||||
|
INSERT INTO firm_org_assignments (consulting_firm_id, organization_id, assigned_to, assigned_by, created_at) VALUES
|
||||||
|
(@firm_id, @org_aurora, 0, @u_marco, @now),
|
||||||
|
(@firm_id, @org_nordwater, 0, @u_marco, @now),
|
||||||
|
(@firm_id, @org_logistica, 0, @u_paolo, @now),
|
||||||
|
-- lead specifici (denota responsabilità primaria)
|
||||||
|
(@firm_id, @org_aurora, @u_marco, @u_marco, @now),
|
||||||
|
(@firm_id, @org_nordwater, @u_laura, @u_marco, @now),
|
||||||
|
(@firm_id, @org_logistica, @u_paolo, @u_paolo, @now)
|
||||||
|
ON DUPLICATE KEY UPDATE created_at = created_at;
|
||||||
|
|
||||||
|
-- ── 7) Riepilogo finale ──────────────────────────────────────────────────
|
||||||
|
SELECT 'CONSULTING FIRM' AS section, id, name, plan, status FROM consulting_firms WHERE id=@firm_id
|
||||||
|
UNION ALL SELECT 'CONSULTANT', id, full_name, role, email FROM users WHERE consulting_firm_id=@firm_id
|
||||||
|
UNION ALL SELECT 'CLIENT ORG', id, name, sector, entity_type FROM organizations WHERE consulting_firm_id=@firm_id
|
||||||
|
UNION ALL SELECT 'FIRM-ORG MAP', id, CONCAT('firm=', consulting_firm_id), CONCAT('org=', organization_id), CONCAT('assigned_to=', assigned_to) FROM firm_org_assignments WHERE consulting_firm_id=@firm_id;
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- ROLLBACK (decommentare per cancellare TUTTI i dati demo Agile Technology)
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- SET @firm_id := (SELECT id FROM consulting_firms WHERE vat_number='IT12345670962');
|
||||||
|
-- DELETE FROM firm_org_assignments WHERE consulting_firm_id=@firm_id;
|
||||||
|
-- DELETE FROM user_organizations WHERE user_id IN (SELECT id FROM users WHERE consulting_firm_id=@firm_id);
|
||||||
|
-- DELETE FROM user_organizations WHERE organization_id IN (SELECT id FROM organizations WHERE consulting_firm_id=@firm_id);
|
||||||
|
-- DELETE FROM organizations WHERE consulting_firm_id=@firm_id;
|
||||||
|
-- DELETE FROM users WHERE consulting_firm_id=@firm_id;
|
||||||
|
-- DELETE FROM users WHERE email LIKE 'admin@%.demo' AND email IN
|
||||||
|
-- ('admin@aurorasanita.demo','admin@nordwater.demo','admin@logisticaveloce.demo');
|
||||||
|
-- DELETE FROM consulting_firms WHERE id=@firm_id;
|
||||||
398
scripts/import-feedback-to-nexus.php
Normal file
398
scripts/import-feedback-to-nexus.php
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* Import storico segnalazioni NIS2 → AgileHub/Nexus
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* Migra le righe di `nis2_agile_db.feedback_reports` (e relative
|
||||||
|
* attachments inline base64) verso `nexus_ticket_db.tickets` /
|
||||||
|
* `ticket_attachments`, preservando created_at originale e idempotente
|
||||||
|
* rispetto a re-run grazie a `tickets.external_ref`.
|
||||||
|
*
|
||||||
|
* Mapping:
|
||||||
|
* feedback_reports.id → tickets.external_ref ('nis2:<id>')
|
||||||
|
* feedback_reports.tipo → tickets.kind (vedi $KIND_MAP)
|
||||||
|
* feedback_reports.priorita → tickets.priority (alta→HIGH, media→MEDIUM, bassa→LOW)
|
||||||
|
* feedback_reports.status → tickets.status (vedi $STATUS_MAP)
|
||||||
|
* feedback_reports.descrizione → tickets.operative_summary (+ subject = first 200)
|
||||||
|
* feedback_reports.user_email → tickets.caller_email
|
||||||
|
* feedback_reports.user_role → tickets.reporter_role
|
||||||
|
* feedback_reports.page_url → tickets.page_url
|
||||||
|
* feedback_reports.ai_* → tickets.ai_*
|
||||||
|
* feedback_reports.attachment → ticket_attachments.data (base64, LONGTEXT)
|
||||||
|
* + sempre: requested_product='NIS2', requested_product_label='NIS2 Agile',
|
||||||
|
* channel='FORM', tenant_id=$NEXUS_TENANT_ID
|
||||||
|
*
|
||||||
|
* Tabella `feedback_audit_log` (se presente) → ticket_messages role='SYSTEM'
|
||||||
|
* Tabella `notifications`: NON migrata (decisione esplicita per non flooddare
|
||||||
|
* la bell di AgileHub con notifiche storiche già lette).
|
||||||
|
*
|
||||||
|
* Uso:
|
||||||
|
* php import-feedback-to-nexus.php # dry-run di default
|
||||||
|
* php import-feedback-to-nexus.php --commit # esecuzione vera
|
||||||
|
* php import-feedback-to-nexus.php --rollback # cancella tutto ciò che è stato importato (filtro external_ref LIKE 'nis2:%')
|
||||||
|
* php import-feedback-to-nexus.php --limit=10 # solo prime 10 righe (utile per smoke test)
|
||||||
|
*
|
||||||
|
* Variabili d'ambiente:
|
||||||
|
* SRC_DB_HOST default 172.25.0.1
|
||||||
|
* SRC_DB_USER default nis2_user
|
||||||
|
* SRC_DB_PASS default Nis2Dev2026!
|
||||||
|
* SRC_DB_NAME default nis2_agile_db
|
||||||
|
*
|
||||||
|
* NEXUS_DB_HOST default 172.25.0.1
|
||||||
|
* NEXUS_DB_USER default agile_services
|
||||||
|
* NEXUS_DB_PASS *required*
|
||||||
|
* NEXUS_DB_NAME default nexus_ticket_db
|
||||||
|
*
|
||||||
|
* NEXUS_TENANT_ID *required* (id numerico del tenant 'alltax' in agile_auth_db.tenants)
|
||||||
|
* =============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// ----------------------------- CLI ARGS -----------------------------------
|
||||||
|
$args = array_slice($argv, 1);
|
||||||
|
$dryRun = !in_array('--commit', $args, true);
|
||||||
|
$rollback = in_array('--rollback', $args, true);
|
||||||
|
$limit = null;
|
||||||
|
foreach ($args as $a) {
|
||||||
|
if (preg_match('/^--limit=(\d+)$/', $a, $m)) { $limit = (int)$m[1]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------- ENV ----------------------------------------
|
||||||
|
$SRC = [
|
||||||
|
'host' => getenv('SRC_DB_HOST') ?: '172.25.0.1',
|
||||||
|
'user' => getenv('SRC_DB_USER') ?: 'nis2_user',
|
||||||
|
'pass' => getenv('SRC_DB_PASS') ?: 'Nis2Dev2026!',
|
||||||
|
'db' => getenv('SRC_DB_NAME') ?: 'nis2_agile_db',
|
||||||
|
];
|
||||||
|
$DST = [
|
||||||
|
'host' => getenv('NEXUS_DB_HOST') ?: '172.25.0.1',
|
||||||
|
'user' => getenv('NEXUS_DB_USER') ?: 'agile_services',
|
||||||
|
'pass' => getenv('NEXUS_DB_PASS') ?: '',
|
||||||
|
'db' => getenv('NEXUS_DB_NAME') ?: 'nexus_ticket_db',
|
||||||
|
];
|
||||||
|
$NEXUS_TENANT_ID = (int) (getenv('NEXUS_TENANT_ID') ?: 0);
|
||||||
|
|
||||||
|
if (!$rollback && $NEXUS_TENANT_ID <= 0) {
|
||||||
|
fwrite(STDERR, "ERROR: env NEXUS_TENANT_ID mancante. Esegui prima seed-nexus-tenant.sql e annota l'id.\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
if (!$rollback && empty($DST['pass'])) {
|
||||||
|
fwrite(STDERR, "ERROR: env NEXUS_DB_PASS mancante.\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------- LOG ----------------------------------------
|
||||||
|
$logDir = __DIR__ . '/../tmp';
|
||||||
|
if (!is_dir($logDir)) { mkdir($logDir, 0775, true); }
|
||||||
|
$logPath = $logDir . '/import-feedback-' . date('Ymd-His') . '.log';
|
||||||
|
$logFp = fopen($logPath, 'w');
|
||||||
|
function logLine(string $line): void {
|
||||||
|
global $logFp;
|
||||||
|
$stamp = date('H:i:s');
|
||||||
|
fwrite($logFp, "[$stamp] $line\n");
|
||||||
|
echo "[$stamp] $line\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
logLine('=== Import feedback NIS2 → Nexus ===');
|
||||||
|
logLine('Mode: ' . ($rollback ? 'ROLLBACK' : ($dryRun ? 'DRY-RUN' : 'COMMIT')));
|
||||||
|
logLine('Tenant id: ' . $NEXUS_TENANT_ID);
|
||||||
|
logLine('Limit: ' . ($limit ?? 'none'));
|
||||||
|
logLine('Source: ' . "{$SRC['user']}@{$SRC['host']}/{$SRC['db']}");
|
||||||
|
logLine('Target: ' . "{$DST['user']}@{$DST['host']}/{$DST['db']}");
|
||||||
|
|
||||||
|
// ----------------------------- PDO ----------------------------------------
|
||||||
|
$pdoOpts = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
$src = new PDO("mysql:host={$SRC['host']};dbname={$SRC['db']};charset=utf8mb4",
|
||||||
|
$SRC['user'], $SRC['pass'], $pdoOpts);
|
||||||
|
$dst = new PDO("mysql:host={$DST['host']};dbname={$DST['db']};charset=utf8mb4",
|
||||||
|
$DST['user'], $DST['pass'], $pdoOpts);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
logLine('FATAL DB connect: ' . $e->getMessage());
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------- ROLLBACK -----------------------------------
|
||||||
|
if ($rollback) {
|
||||||
|
logLine('Rollback: cancello tutti i ticket con external_ref LIKE "nis2:%"');
|
||||||
|
if ($dryRun) {
|
||||||
|
$n = (int) $dst->query(
|
||||||
|
"SELECT COUNT(*) FROM tickets WHERE external_ref LIKE 'nis2:%'"
|
||||||
|
)->fetchColumn();
|
||||||
|
logLine("[DRY-RUN] eliminerebbe $n ticket (e relativi attachments / messages via FK CASCADE)");
|
||||||
|
logLine("Per eseguire davvero: aggiungi --commit");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
$dst->beginTransaction();
|
||||||
|
$deleted = $dst->exec("DELETE FROM tickets WHERE external_ref LIKE 'nis2:%'");
|
||||||
|
$dst->commit();
|
||||||
|
logLine("Rollback: $deleted ticket eliminati");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------- DETECT OPTIONAL COLUMNS --------------------
|
||||||
|
function tableHasColumn(PDO $pdo, string $table, string $col): bool {
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT 1 FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$table, $col]);
|
||||||
|
return (bool) $stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
function tableExists(PDO $pdo, string $table): bool {
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT 1 FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$table]);
|
||||||
|
return (bool) $stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasAuditLog = tableExists($src, 'feedback_audit_log');
|
||||||
|
$hasRiskScore = tableHasColumn($src, 'feedback_reports', 'risk_score');
|
||||||
|
$hasNotaAdmin = tableHasColumn($src, 'feedback_reports', 'nota_admin');
|
||||||
|
$hasStatusVerified = false;
|
||||||
|
try {
|
||||||
|
$row = $src->query(
|
||||||
|
"SELECT COLUMN_TYPE FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'feedback_reports'
|
||||||
|
AND COLUMN_NAME = 'status'"
|
||||||
|
)->fetch();
|
||||||
|
$hasStatusVerified = $row && stripos($row['COLUMN_TYPE'], "'verificato'") !== false;
|
||||||
|
} catch (PDOException $e) {}
|
||||||
|
|
||||||
|
logLine('Source schema: feedback_audit_log=' . ($hasAuditLog ? 'yes' : 'no')
|
||||||
|
. ', risk_score=' . ($hasRiskScore ? 'yes' : 'no')
|
||||||
|
. ', nota_admin=' . ($hasNotaAdmin ? 'yes' : 'no')
|
||||||
|
. ', status verificato=' . ($hasStatusVerified ? 'yes' : 'no'));
|
||||||
|
|
||||||
|
// ----------------------------- MAPPING TABLES ------------------------------
|
||||||
|
$KIND_MAP = [
|
||||||
|
'bug' => 'COMPLAINT',
|
||||||
|
'ux' => 'SUPPORT_REQUEST',
|
||||||
|
'funzionalita' => 'INFO_REQUEST',
|
||||||
|
'domanda' => 'INFO_REQUEST',
|
||||||
|
'altro' => 'OTHER',
|
||||||
|
];
|
||||||
|
$PRIORITY_MAP = [
|
||||||
|
'alta' => 'HIGH',
|
||||||
|
'media' => 'MEDIUM',
|
||||||
|
'bassa' => 'LOW',
|
||||||
|
];
|
||||||
|
$STATUS_MAP = [
|
||||||
|
'aperto' => 'OPEN',
|
||||||
|
'in_lavorazione' => 'IN_PROGRESS',
|
||||||
|
'risolto' => 'RESOLVED',
|
||||||
|
'chiuso' => 'CLOSED',
|
||||||
|
'verificato' => 'CLOSED',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ----------------------------- READ SOURCE --------------------------------
|
||||||
|
$sql = "SELECT * FROM feedback_reports ORDER BY id ASC";
|
||||||
|
if ($limit) { $sql .= " LIMIT " . (int)$limit; }
|
||||||
|
$rows = $src->query($sql)->fetchAll();
|
||||||
|
logLine('Source rows fetched: ' . count($rows));
|
||||||
|
|
||||||
|
if (empty($rows)) {
|
||||||
|
logLine('Niente da migrare. Esco.');
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-load existing external_refs in target (single roundtrip)
|
||||||
|
$existing = [];
|
||||||
|
foreach ($dst->query(
|
||||||
|
"SELECT external_ref FROM tickets WHERE external_ref LIKE 'nis2:%'"
|
||||||
|
)->fetchAll(PDO::FETCH_COLUMN) as $ref) {
|
||||||
|
$existing[$ref] = true;
|
||||||
|
}
|
||||||
|
logLine('Already imported in target: ' . count($existing));
|
||||||
|
|
||||||
|
// ----------------------------- INSERT PREP --------------------------------
|
||||||
|
$insTicket = $dst->prepare("
|
||||||
|
INSERT INTO tickets
|
||||||
|
(tenant_id, caller_phone, caller_name, caller_email,
|
||||||
|
requested_product, requested_product_label,
|
||||||
|
subject, operative_summary,
|
||||||
|
status, queue, priority, channel, kind,
|
||||||
|
external_ref, page_url, reporter_role,
|
||||||
|
ai_categoria, ai_priorita, ai_suggerimento, ai_response, ai_processed,
|
||||||
|
lang,
|
||||||
|
created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:tenant_id, :caller_phone, :caller_name, :caller_email,
|
||||||
|
:requested_product, :requested_product_label,
|
||||||
|
:subject, :operative_summary,
|
||||||
|
:status, :queue, :priority, :channel, :kind,
|
||||||
|
:external_ref, :page_url, :reporter_role,
|
||||||
|
:ai_categoria, :ai_priorita, :ai_suggerimento, :ai_response, :ai_processed,
|
||||||
|
:lang,
|
||||||
|
:created_at, :updated_at)
|
||||||
|
");
|
||||||
|
$insAttachment = $dst->prepare("
|
||||||
|
INSERT INTO ticket_attachments
|
||||||
|
(ticket_id, tenant_id, filename, mime_type, size, data, created_at)
|
||||||
|
VALUES
|
||||||
|
(:ticket_id, :tenant_id, :filename, :mime_type, :size, :data, :created_at)
|
||||||
|
");
|
||||||
|
$insMessage = $dst->prepare("
|
||||||
|
INSERT INTO ticket_messages
|
||||||
|
(ticket_id, tenant_id, role, content, metadata, created_at)
|
||||||
|
VALUES
|
||||||
|
(:ticket_id, :tenant_id, 'SYSTEM', :content, :metadata, :created_at)
|
||||||
|
");
|
||||||
|
|
||||||
|
// ----------------------------- LOOP ---------------------------------------
|
||||||
|
$counters = ['ok' => 0, 'skip' => 0, 'err' => 0, 'attach' => 0, 'audit' => 0];
|
||||||
|
|
||||||
|
if (!$dryRun) { $dst->beginTransaction(); }
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$extRef = 'nis2:' . (int)$r['id'];
|
||||||
|
if (isset($existing[$extRef])) {
|
||||||
|
$counters['skip']++;
|
||||||
|
logLine("[SKIP duplicate] $extRef");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tipo = $r['tipo'] ?? 'altro';
|
||||||
|
$priorita = $r['priorita'] ?? 'media';
|
||||||
|
$status = $r['status'] ?? 'aperto';
|
||||||
|
|
||||||
|
$kind = $KIND_MAP[$tipo] ?? 'OTHER';
|
||||||
|
$priority = $PRIORITY_MAP[$priorita] ?? 'MEDIUM';
|
||||||
|
$statusN = $STATUS_MAP[$status] ?? 'OPEN';
|
||||||
|
|
||||||
|
$descr = (string) ($r['descrizione'] ?? '');
|
||||||
|
$subject = mb_substr(trim(preg_replace('/\s+/', ' ', $descr)), 0, 200);
|
||||||
|
if ($subject === '') { $subject = "[NIS2 legacy #{$r['id']}]"; }
|
||||||
|
|
||||||
|
// Append nota_admin alla operative_summary se presente
|
||||||
|
$opSummary = $descr;
|
||||||
|
if ($hasNotaAdmin && !empty($r['nota_admin'])) {
|
||||||
|
$opSummary .= "\n\n--- Nota admin originale ---\n" . $r['nota_admin'];
|
||||||
|
}
|
||||||
|
if ($hasRiskScore && isset($r['risk_score']) && $r['risk_score'] !== null) {
|
||||||
|
$opSummary .= "\n\n[risk_score originale: " . $r['risk_score'] . "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map ai_priorita italiano → ENUM Nexus
|
||||||
|
$aiPrio = null;
|
||||||
|
if (!empty($r['ai_priorita'])) {
|
||||||
|
$aiPrio = $PRIORITY_MAP[$r['ai_priorita']] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
':tenant_id' => $NEXUS_TENANT_ID,
|
||||||
|
':caller_phone' => '', // non disponibile in feedback_reports
|
||||||
|
':caller_name' => $r['user_email'] ?? null, // fallback: nessun caller_name in source
|
||||||
|
':caller_email' => $r['user_email'] ?? null,
|
||||||
|
':requested_product' => 'NIS2',
|
||||||
|
':requested_product_label' => 'NIS2 Agile',
|
||||||
|
':subject' => $subject,
|
||||||
|
':operative_summary' => mb_substr($opSummary, 0, 1990),
|
||||||
|
':status' => $statusN,
|
||||||
|
':queue' => 'SUPPORT',
|
||||||
|
':priority' => $priority,
|
||||||
|
':channel' => 'FORM',
|
||||||
|
':kind' => $kind,
|
||||||
|
':external_ref' => $extRef,
|
||||||
|
':page_url' => $r['page_url'] ?? null,
|
||||||
|
':reporter_role' => $r['user_role'] ?? null,
|
||||||
|
':ai_categoria' => $r['ai_categoria'] ?? null,
|
||||||
|
':ai_priorita' => $aiPrio,
|
||||||
|
':ai_suggerimento' => $r['ai_suggerimento'] ?? null,
|
||||||
|
':ai_response' => $r['ai_risposta'] ?? null,
|
||||||
|
':ai_processed' => isset($r['ai_processed']) ? (int)$r['ai_processed'] : 0,
|
||||||
|
':lang' => 'it',
|
||||||
|
':created_at' => $r['created_at'] ?? date('Y-m-d H:i:s'),
|
||||||
|
':updated_at' => $r['updated_at'] ?? ($r['created_at'] ?? date('Y-m-d H:i:s')),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$counters['ok']++;
|
||||||
|
logLine("[DRY-RUN OK] $extRef kind=$kind status=$statusN created={$params[':created_at']}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$insTicket->execute($params);
|
||||||
|
$newId = (int) $dst->lastInsertId();
|
||||||
|
$counters['ok']++;
|
||||||
|
logLine("[OK] $extRef → ticket #$newId");
|
||||||
|
|
||||||
|
// Attachment inline base64
|
||||||
|
if (!empty($r['attachment'])) {
|
||||||
|
$att = (string) $r['attachment'];
|
||||||
|
$mime = 'image/png';
|
||||||
|
$rawB64 = $att;
|
||||||
|
if (preg_match('/^data:([^;]+);base64,(.*)$/', $att, $m)) {
|
||||||
|
$mime = $m[1];
|
||||||
|
$rawB64 = $m[2];
|
||||||
|
}
|
||||||
|
$size = (int) (strlen($rawB64) * 3 / 4);
|
||||||
|
$insAttachment->execute([
|
||||||
|
':ticket_id' => $newId,
|
||||||
|
':tenant_id' => $NEXUS_TENANT_ID,
|
||||||
|
':filename' => 'legacy_' . (int)$r['id'] . '.png',
|
||||||
|
':mime_type' => $mime,
|
||||||
|
':size' => $size,
|
||||||
|
':data' => $att,
|
||||||
|
':created_at' => $params[':created_at'],
|
||||||
|
]);
|
||||||
|
$counters['attach']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// feedback_audit_log → ticket_messages role=SYSTEM
|
||||||
|
if ($hasAuditLog) {
|
||||||
|
$stmt = $src->prepare(
|
||||||
|
"SELECT * FROM feedback_audit_log WHERE report_id = ? ORDER BY id ASC"
|
||||||
|
);
|
||||||
|
$stmt->execute([(int)$r['id']]);
|
||||||
|
foreach ($stmt->fetchAll() as $ev) {
|
||||||
|
$content = sprintf(
|
||||||
|
"[legacy audit] action=%s to_status=%s actor=%s",
|
||||||
|
$ev['action'] ?? '',
|
||||||
|
$ev['to_status'] ?? '',
|
||||||
|
$ev['actor_type']?? ''
|
||||||
|
);
|
||||||
|
$insMessage->execute([
|
||||||
|
':ticket_id' => $newId,
|
||||||
|
':tenant_id' => $NEXUS_TENANT_ID,
|
||||||
|
':content' => $content,
|
||||||
|
':metadata' => $ev['details'] ?? '{}',
|
||||||
|
':created_at' => $ev['created_at'] ?? $params[':created_at'],
|
||||||
|
]);
|
||||||
|
$counters['audit']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$counters['err']++;
|
||||||
|
logLine("[ERR] $extRef → " . $e->getMessage());
|
||||||
|
// continua: gli altri possono passare. Fail-safe: log e via.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
if ($counters['err'] > 0) {
|
||||||
|
$dst->rollBack();
|
||||||
|
logLine('Errori rilevati: ROLLBACK transazione, nessuna riga scritta.');
|
||||||
|
logLine("Counters: " . json_encode($counters));
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
$dst->commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
logLine('=== Riepilogo ===');
|
||||||
|
logLine("OK: {$counters['ok']}");
|
||||||
|
logLine("SKIP: {$counters['skip']}");
|
||||||
|
logLine("ERR: {$counters['err']}");
|
||||||
|
logLine("Attachments: {$counters['attach']}");
|
||||||
|
logLine("Audit msgs: {$counters['audit']}");
|
||||||
|
logLine("Log file: $logPath");
|
||||||
|
exit($counters['err'] > 0 ? 1 : 0);
|
||||||
Loading…
Reference in New Issue
Block a user