"/gare/guue/integration.php", "pcp" => "/pcp/guue/integration.php", "concorsi" => "/concorsi/guue/integration.php", "albo_fornitori" => "/albo_fornitori/guue/integration.php", ]; /** * Versione SDK di default * * @var string */ const LATEST_SDK_VERSION = "v1.0.2"; /** * Ultima versione interna * * @var int */ const LATEST_INTERNAL_VERSION = 4; /** * Costante per indicare al costruttore di caricare * automaticamente la versione SDK del fascicolo */ const AUTOMATICALLY_GET_SDK_VERSION = null; /** * Definisce lo stato minimo perchè una scheda sia considerata pubblicata dai vari check */ const TRASMITTED_STATO = 50; /** * Definisce lo stato minimo perchè una scheda sia considerata pubblicata dai vari check */ const PUBLISHED_STATO = 110; /** * Connessione al database * * @var myPDO */ private $pdo; /** * Riferimento a eFormsManager se presente * * @var eFormsManager */ private $ted; /** * Sdk version path * * @var string */ private $path; /** * Form Details * * @var array */ private $form; /** * lot * * @var mixed */ public $lot; /** * XML Document check * * @var array */ public $check; /** * Available cards * * @var array */ public $schede; /** * Available cards settings * * @var array */ public $metadataSchede; /** * Fascicolo NPA * * @var array */ public $fascicolo; /** * I contenuti della scheda attuale. * Usa getCardData. */ private $card_data; /** * Info * * @var array */ public $info; /** * Current Card info * * @var array */ public $scheda; /** * version * * @var string */ public $version; /** * Object reference * * @var mixed */ public $oggetto; /** * Definisce se la scheda corrente necessita di aggiudicatari * * @var boolean */ public $has_aggiudicatari = false; /** * Definisce se la scheda corrente necessita di partecipanti * * @var boolean */ public $has_partecipanti = false; /** * Contiene la configurazione corrente delle API * * @var array */ private $api_config = []; /** * Se popolata sovrascrive la stazione appaltante usata per generare i JWT di autenticazione * * @var ?array */ public $overrideStazioneAppaltante; /** * Class initialization * * @param ?int $codice_fascicolo * @param ?int $codice_scheda * @param ?string $identificativo_scheda * @param ?string $version * @param ?eFormsManager $ted * @return ?NuovaPiattaformaAppalti */ public static function init(?int $codice_fascicolo = null, ?int $codice_scheda = null, ?string $identificativo_scheda = null, ?string $modulo_riferimento = null, ?int $id_riferimento = null, ?string $version = self::AUTOMATICALLY_GET_SDK_VERSION, ?eFormsManager $ted = null): ?NuovaPiattaformaAppalti { if($version == self::AUTOMATICALLY_GET_SDK_VERSION) { $version = self::fetchVersion($codice_fascicolo, $codice_scheda); if($version === null) { throw new Exception("Impossibile determinare la versione SDK NPA", 999); } } $npa = new self($version); $npa->fascicolo = get_campi("b_npa"); $npa->scheda = get_campi("b_npa_schede"); $npa->scheda["stato"] = 0; $success = $npa->initCard($codice_scheda, $identificativo_scheda); if ($success !== true && empty($codice_fascicolo)) { // Scheda non caricabile return null; } $identificativo_scheda = $npa->scheda["id_scheda"]; $metadata = $npa->metadataSchede[$identificativo_scheda] ?? []; if(!empty($metadata)) { $npa->has_aggiudicatari = $metadata["aggiudicatari"]; $npa->has_partecipanti = $metadata["partecipanti"]; } if (empty($codice_fascicolo) && !empty($npa->scheda)) { $codice_fascicolo = $npa->scheda["codice_npa"]; } $result = $npa->loadNpaDossier(!empty($codice_fascicolo) ? $codice_fascicolo : null, $modulo_riferimento, $id_riferimento); if(!$result) { throw new Exception("Impossibile inizializzare il fascicolo. Potrebbe essere stato cancellato o resettato.", 999); } if($ted !== null) { $npa->ted = $ted; } if($npa->scheda["codice"] > 0) { $npa->checkIfCardNeedsXMLDocument(); if (!(defined("__CMDS_JOB_EXEC") && !empty(__CMDS_JOB_EXEC))) { $npa->getSuggestions(); } // Workaround per ottenere il noticeID inviato quando mancante if( $npa->check["eform"]["required"] && // Se la scheda ha eForms !empty($npa->scheda["uuid"]) && // Se la scheda ha un idScheda (è già stata inviata) empty($npa->scheda["guue_sent_notice_id"]) // Se la scheda non ha il record di un notice_id inviato ) { $sent_notice_id = $npa->retrieveGUUESentNoticeID(); if(!empty($sent_notice_id)) { // Verifichiamo che il notice_id sia corretto, altrimenti lo rigeneriamo while(preg_match("/[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}/", $sent_notice_id) !== 1) { $sent_notice_id = uuidgen(); } // Aggiorniamo la scheda if($npa->updateScheda(["guue_sent_notice_id" => $sent_notice_id])) { $npa->checkIfCardNeedsXMLDocument(); } } } // Primo collegamento automatico della scheda GUUE if( $npa->scheda["codice"] > 0 && // Se la scheda esiste $npa->check["eform"]["required"] ) { $compatibleCard = []; if(empty($npa->scheda["codice_scheda_guue"])) { $compatibleCard = $npa->fetchCompatibleGUUECards(false); } else { $compatibleCard["codice"] = $npa->scheda["codice_scheda_guue"]; } if(!empty($compatibleCard)) { // Aggiorniamo la scheda if($npa->selezionaSchedaGUUE($compatibleCard["codice"])) { $npa->checkIfCardNeedsXMLDocument(); } } } } return $npa; } /** * Seleziona la nuova scheda GUUE * * @param integer $codiceScheda * @return boolean */ public function selezionaSchedaGUUE(int $codiceScheda) : bool { if($this->checkLock()) { return false; } // Se è già settato ci assicuriamo solo che if($codiceScheda !== $this->scheda["codice_scheda_guue"]) { // Prima resettiamo il lotto della scheda a noi associata if($this->scheda["codice_scheda_guue"] > 0) { $success = $this->pdo->go( "UPDATE b_guue SET lotto_riferimento = NULL WHERE codice = :codice", [":codice" => $this->scheda["codice_scheda_guue"]] ); if(!$success) return false; } // Settiamo il nuovo codice scheda $success = $this->updateScheda(["codice_scheda_guue" => $codiceScheda]); if(!$success) return false; } // Dopodichè associamo la nuova al lotto, se presente e se necessario $codiceLottoNpa = $this->scheda["codice_lotto"]; if(!empty($codiceLottoNpa)) { $lot = $this->getLots($codiceLottoNpa) ?? null; if(!empty($lot)) { $lot = reset($lot); $result = $this->pdo->go( "UPDATE b_guue SET lotto_riferimento = :lotto WHERE codice = :codice", [ ":lotto" => $lot["codice"], ":codice" => $codiceScheda ] ); return($result->errorInfo()[0] == "00000"); } return false; } return true; } /** * Class initialization for free requests * @param ?string $version * @return ?NuovaPiattaformaAppalti */ public static function initStatic( ?string $version = self::LATEST_SDK_VERSION): ?NuovaPiattaformaAppalti { $npa = new self($version); $npa->fascicolo = get_campi("b_npa"); $npa->scheda = get_campi("b_npa_schede"); $npa->scheda["stato"] = 0; $npa->fascicolo["codice"] = 0; return $npa; } public static function initForImport(String $modulo_riferimento, int $id_riferimento, ?string $version = self::LATEST_SDK_VERSION) : ?NuovaPiattaformaAppalti { $npa = new self($version); $npa->fascicolo = get_campi("b_npa"); $npa->fascicolo["modulo_riferimento"] = $modulo_riferimento; $npa->fascicolo["id_riferimento"] = $id_riferimento; $npa->scheda = get_campi("b_npa_schede"); $npa->scheda["stato"] = 0; return $npa; } /** * Check if the version exists. * * @param mixed $version * @return void */ public static function check_version(String $version): void { $path = dirname(__FILE__, 2); // If the folder for the specified version does not exist, we raise an exception. if (!file_exists("{$path}/versions/{$version}")) { throw new Exception("SDK '{$version}' not found!"); } } /** * Retrieves a list of cards organized and classified based on their module types * * @param mixed $version * @return array */ public static function getCategorizedCardsList(): ?array { if(valid_json(file_get_contents(__DIR__ . "/match-moduli.json"), $matchModuli, true)) { return $matchModuli; } return null; } private static $cardMetadataCache = null; /** * Ottiene i metadata delle schede * * @return array|null */ public static function getCardsMetadata(): ?array { if(self::$cardMetadataCache === null) { if(valid_json(file_get_contents(__DIR__ . "/schede.json"), $schede, true)) { self::$cardMetadataCache = $schede; } } return self::$cardMetadataCache; } public static function getInitialCards(?string $modulo = null, ?string $procedura = null, ?string $fase = null, ?string $settore = null, ?string $soglia = null, ?bool $concessione = null) { if (!in_array($settore, ["ordinario", "speciale", "difesa", null])) { throw new Exception("The '{$settore}' sector specified is invalid. Please choose from valid options: 'ordinario', 'speciale', 'difesa'."); } if (!in_array($soglia, ["soprasoglia", "sottosoglia", null])) { throw new Exception("The specified '{$soglia}' threshold is invalid. Valid options: 'soprasoglia', 'sottosoglia'."); } $schede = self::getCardsMetadata(); $matches = self::getCategorizedCardsList(); $cards = []; // If $modulo is specified, we retrieve all available cards for those modules. if (!is_null($modulo) && !empty($matches[$modulo])) { //TODO: Ensure array_get function returns desired results $cards = DotPath::get($matches[$modulo], "procedure.*.*.*", null, false); // If $procedura is specified, we retrieve all available cards for that procedure. if (!is_null($procedura)) { $cards = DotPath::get($matches[$modulo], "procedure.{$procedura}.*.*", null, false); // If $fase is specified, we retrieve all available cards for that phase. if (!is_null($fase)) { $cards = $matches[$modulo]["procedure"][$procedura]["fasi"][$fase] ?? $matches[$modulo]["procedure"][$procedura]["fasi"]["*"]; } } } $initialCards = []; foreach($cards as $card) { $metadata = $schede[$card] ?? []; if(empty($metadata)) continue; if($settore !== null) { if(!in_array($settore, $metadata["settori"])) continue; } if($settore !== null) { if(!$metadata[$soglia]) continue; } if(!is_null($concessione) && !is_null($metadata["concessione"])) { if($metadata["concessione"] != $concessione) { continue; } } $initialCards[$card] = $metadata; } return $initialCards; } /** * Retrieve the list of all saved cards from the database. * * @param ?array $filters * @param ?array $limits * @param ?array $order * @return Generator */ public static function listCards(?array $filters = [], ?array $limits = [], ?array $order = []): ?Generator { global $pdo; $count = "SELECT COUNT(codice)"; $select = "SELECT *"; $from = "FROM b_npa_schede"; $bind = []; $conditions = ["SOFT_DELETE != 'S'"]; if ($_SESSION["utente"]->gerarchia > 10) { $ids = Utente::getPermissionsInModulo($_SESSION["ente"]->codice, $_SESSION["utente"]->codice, "npa"); $ids = implode(",", $ids); $conditions[] = "codice IN ({$ids})"; } if (!$_SESSION["utente"]->fromHere()) { $bind[":codice_ente"] = $_SESSION["utente"]->codice_ente; $conditions[] = "codice_ente = :codice_ente"; } $where = ""; if (!empty($conditions)) { $where = "WHERE (" . implode(" ) AND (", $conditions) . ")"; } $results = $pdo->go("{$count} {$from} {$where}", $bind)->fetch(PDO::FETCH_COLUMN, 0); if (!empty($results)) { if (!empty($filters)) { $clauses = []; if (!empty($filters["codice"])) { $clauses[] = "codice = :codice"; $bind[":codice"] = $filters["codice"]; } if (!empty($filters["codice_riferimento"])) { $clauses[] = "id_riferimento = :id_riferimento"; $bind[":id_riferimento"] = $filters["codice_riferimento"]; } if (!empty($filters["codice_scheda"])) { $clauses[] = "id_scheda = :id_scheda"; $bind[":id_scheda"] = $filters["codice_scheda"]; } if (!empty($filters["stato"])) { $clauses[] = "stato = :stato"; $bind[":stato"] = $filters["stato"]; } if (!empty($clauses)) { $conditions[] = "(" . implode(") OR (", $clauses) . ")"; } } $where = ""; if (!empty($conditions)) { $where = "WHERE (" . implode(" ) AND (", $conditions) . ")"; } $sorting = ""; if (!empty($order)) { $sorting = "ORDER BY {$order["column"]} {$order["dir"]}"; } $restrictions = ""; if (!empty($limits) && $limits["lenght"] > 0) { $restrictions = "LIMIT {$limits["start"]}, {$limits["lenght"]}"; } $filtered = $pdo->go("{$count} {$from} {$where} {$sorting}", $bind)->fetch(PDO::FETCH_COLUMN, 0); yield [ "recordsTotal" => $results, "recordsFiltered" => $filtered ]; $results = $pdo->go("{$select} {$from} {$where} {$sorting} {$restrictions}", $bind); while ($result = $results->fetch(PDO::FETCH_ASSOC)) { yield $result; } } return null; } /** * Get the list of available statuses. * * @return array */ public static function listAvailableStatusList(): ?array { if(valid_json(file_get_contents(__DIR__ . "/stati.json"), $stati, true)) { return $stati; } return null; } /** * Retrieves a dictionary of errors within the npa * @param mixed $version * * @return array */ public static function getErrorsDictionary(): ?array { $erroriData = file_get_contents(__DIR__ . "/errori.json"); $erroriOverrideData = file_get_contents(__DIR__ . "/errori.override.json"); if(valid_json($erroriData, $errori, true)) { $errori = array_column($errori, null, "codice"); if(valid_json($erroriOverrideData, $erroriOverride, true)) { $erroriOverride = array_column($erroriOverride, null, "codice"); $errori = array_merge_recursive_distinct($errori, $erroriOverride); } return $errori; } return null; } /** * Get connected * * @return array */ public const MODULI_COLLEGATI = [ "gare", "concorsi", "albo_fornitori" ]; /** * Evaluates and returns a boolean value indicating whether the user is currently logged into the application * * @return bool */ public function isUserLoggedIn(string $role = DelegheRoles::RP): Bool { if (!empty($this->fascicolo["data"])) { $cf = $this->getUserFromRole($role, RoleSyncBehaviour::NEVER_SYNC); $timestamp = $this->pdo->go("SELECT `b_check_sessions`.`timestamp` FROM b_check_sessions JOIN b_utenti ON b_utenti.codice = b_check_sessions.codice_utente WHERE b_utenti.cf = :cf", [":cf" => $cf])->fetch(PDO::FETCH_COLUMN, 0); if (!empty($timestamp) && strtotime($timestamp) > strtotime("-3 hours")) { return true; } } return false; } /** * Retrieves and loads the data associated with a specific card * * @param mixed $codice_scheda * @param mixed $identificativo_scheda * @return bool */ public function initCard(?int $codice_scheda = 0, ?string $identificativo_scheda = ""): Bool { global $pdo; $bind = []; $scheda = null; if (!empty($codice_scheda)) { $clauses[] = "codice = :codice_scheda"; $bind[":codice_scheda"] = $codice_scheda; } if (!empty($this->fascicolo["codice"])) { $clauses[] = "codice_npa = :codice_fascicolo"; $bind[":codice_fascicolo"] = $this->fascicolo["codice"]; if (!empty($identificativo_scheda)) { $clauses[] = "id_scheda = :identificativo_scheda"; $bind[":identificativo_scheda"] = $identificativo_scheda; } } if (!empty($clauses)) { $where = implode(" AND ", $clauses); $sql = "SELECT * FROM b_npa_schede WHERE {$where} LIMIT 0, 1"; $scheda = $pdo->go($sql, $bind)->fetch(PDO::FETCH_ASSOC); if (!empty($scheda)) { // Verifica soft delete if (($scheda["soft_delete"] ?? "N") == "S") { return false; } $this->scheda = $scheda; $this->form = jsonToArray("{$this->path}/schede/{$this->scheda["id_scheda"]}.json"); if(!$this->loadNpaDossier($scheda["codice_npa"])) return false; if(empty($this->scheda["versione_scheda"])) { $this->scheda["versione_scheda"] = $this->version; } return true; } } if (empty($scheda)) { $metadata = $this->metadataSchede[$identificativo_scheda] ?? []; if (!empty($metadata)) { $this->scheda["codice"] = null; $this->scheda["id_scheda"] = $identificativo_scheda; $this->scheda["type"] = $metadata["tipologia"]; $this->scheda["stato"] = 0; if(empty($this->scheda["versione_scheda"])) { $this->scheda["versione_scheda"] = $this->version; } $this->form = jsonToArray("{$this->path}/schede/{$identificativo_scheda}.json"); return true; } } return false; } /** * Retrieves and loads information related to an NPA Dossier * * @param mixed $codice_fascicolo * @return void */ public function loadNpaDossier(?int $codice_fascicolo = null, ?string $modulo_riferimento = null, ?int $id_riferimento = null): bool { global $pdo; if (!empty($codice_fascicolo)) { $bind = [":codice_fascicolo" => $codice_fascicolo]; $sql = "SELECT * FROM b_npa WHERE codice = :codice_fascicolo ORDER BY revisione_fascicolo DESC LIMIT 0,1"; $fascicolo = $pdo->go($sql, $bind)->fetch(PDO::FETCH_ASSOC); } elseif (!is_null($modulo_riferimento) && !is_null($id_riferimento)) { $bind = []; $bind[":modulo_riferimento"] = $modulo_riferimento; $bind[":id_riferimento"] = $id_riferimento; $sql = "SELECT * FROM b_npa WHERE modulo_riferimento = :modulo_riferimento AND id_riferimento = :id_riferimento ORDER BY revisione_fascicolo DESC LIMIT 0,1"; $fascicolo = $pdo->go($sql, $bind)->fetch(PDO::FETCH_ASSOC); } if (! empty($fascicolo)) { $this->fascicolo = $fascicolo; if (empty($modulo_riferimento)) { $modulo_riferimento = $this->fascicolo["modulo_riferimento"]; } if (empty($id_riferimento)) { $id_riferimento = $this->fascicolo["id_riferimento"]; } } $schedeAppalto = $this->getLastCompletedCards(); if(!empty($schedeAppalto)) { foreach($schedeAppalto as $scheda) { if(valid_json($scheda["dati"], $dati, true)) { $stazioniAppaltanti = recursive_array_key_search_all("stazioniAppaltanti", $dati)[0] ?? []; if(!empty($stazioniAppaltanti)) { $stazioniAppaltanti = array_values($stazioniAppaltanti); $this->updateFascicolo(["stazioni_appaltanti" => json_encode($stazioniAppaltanti)]); break; } } } } // Detect fascicolo resettato if($codice_fascicolo < 0 || $id_riferimento < 0) { return false; } $this->initOggetto($modulo_riferimento, $id_riferimento); if (! empty($this->oggetto) && !is_a($this->oggetto, "AlboFornitori")) { $this->syncLots(); } $this->loadCards(); return true; } /** * Questo metodo inizializza un oggetto falso per le importazioni * * @return void */ public function initFakeOggetto(string $modulo_riferimento) { if(empty($_SESSION["ente"]) || empty($_SESSION["utente"])) throw new Exception("Nessun ente/utente accessibile"); $codice_ente = $_SESSION["ente"]->codice; $ente = $_SESSION["ente"]->getInfo(); if(empty($ente)) throw new Exception("Impossibile trovare l'ente {$codice_ente}"); $utente = $_SESSION["utente"]; $userInfo = method_exists($utente, "getInfo") ? $utente->getInfo() : []; // Prendiamo il CF dell'utente $cf = $userInfo["cf"] ?? null; $this->updateFascicolo([ "data" => serialize([ "beneficiario" => ["codice_fiscale" => $ente["cf"], "codice_ausa" => $ente["codice_ausa"]], "rup" => ["codice_fiscale" => $cf], ]), "modulo_riferimento" => $modulo_riferimento ]); } public function initOggetto(string $modulo_riferimento, int $id_riferimento) { switch ($modulo_riferimento) { case 'albo_fornitori': $this->oggetto = AlboFornitori::init($id_riferimento); $this->oggetto->npa = $this; break; case 'concorsi': $this->oggetto = concorso::init($id_riferimento); $this->oggetto->npa = $this; break; case 'gare': $this->oggetto = gara::init($id_riferimento); // Per la scheda S1 è necessario riferirsi al primo round if(($this->scheda["id_scheda"] ?? null) === "S1") { $codice_primoRound = $this->oggetto->getRounds()[0]["codice"] ?? -1; if($codice_primoRound > 0) { $newOggetto = gara::init($this->oggetto->info["codice"], $codice_primoRound); if(!empty($newOggetto)) { $this->oggetto = $newOggetto; } } } $this->oggetto->npa = $this; break; default: $this->oggetto = null; break; } if(empty($this->fascicolo["modulo_riferimento"])) $this->fascicolo["modulo_riferimento"] = $modulo_riferimento; // Tiene i data aggiornati if(empty($this->fascicolo["data"]) || (empty($this->fascicolo["id_appalto"]) && empty($this->fascicolo["id_pianificazione"]))) { $current_data = serialize([ "benficiario" => $this->oggetto->getEnteBeneficiario(), "rup" => $this->oggetto->getRup() ]); $this->updateFascicolo(["data" => $current_data]); } if (! empty($this->oggetto) && empty($this->fascicolo["codice"])) { $this->fascicolo["id_riferimento"] = $id_riferimento; $this->fascicolo["data"] = serialize([ "benficiario" => $this->oggetto->getEnteBeneficiario(), "rup" => $this->oggetto->getRup() ]); } } /** * Retrieves and loads information related to lots * * @return void */ public function syncLots(): void { $lots = $this->oggetto->getLotti(); $missing = array_column($lots, "codice"); $existing = $this->pdo->go("SELECT * FROM b_npa_lotti WHERE codice_npa = :codice_npa", [":codice_npa" => $this->fascicolo["codice"]])->fetchAll(PDO::FETCH_ASSOC); if (!empty($existing)) { $missing = array_diff(array_column($lots, "codice"), array_column($existing, "lotto_riferimento")); } if (!empty($missing)) { $salva = new Salva(); $salva->debug = false; $salva->nome_tabella = "b_npa_lotti"; $salva->operazione = "INSERT"; $salva->ignore = ["timestamp", "timestamp_trasmissione"]; $lots = array_column($lots, null, "codice"); foreach ($missing as $codice) { if (!empty($lots[$codice])) { $salva->oggetto = [ "codice_npa" => $this->fascicolo["codice"], "lotto_riferimento" => $codice, "timestamp_creazione" => date('d/m/Y H:i:s') ]; $salva->save(); } } } } /** * Sets information for a specific lot * * @return void */ public function setLotto(Int $id): void { $lot = $this->getLots($id); if (count($lot) == 1) { $this->lot = reset($lot); if(!empty($this->oggetto)) { $this->oggetto->setLotto($this->lot["codice"]); } $this->loadCards(); } } /** * Sets information for a specific lot * * @return void */ public function unsetLotto(): void { if (!empty($this->lot)) { $this->lot = null; $this->schede = null; if(!empty($this->oggetto->lotto)) { $this->oggetto->lotto = null; } } } /** * Retrieves information about lots * * @return array */ public function getLots(Int $id = null): ?array { $data = $this->oggetto->getLotti(); if (!empty($this->fascicolo["codice"])) { $sql = "SELECT lotto_riferimento, codice FROM b_npa_lotti WHERE codice_npa = :codice_npa"; $bind = [":codice_npa" => $this->fascicolo["codice"]]; if (!is_null($id)) { $sql = "{$sql} AND codice = :codice"; $bind[":codice"] = $id; } $lots = $this->pdo->go($sql, $bind)->fetchAll(PDO::FETCH_ASSOC); if (!empty($lots)) { $lots = array_column($lots, null, "lotto_riferimento"); $ids = array_keys($lots); foreach ($data as $index => $lot) { if (!in_array($lot["codice"], $ids)) { unset($data[$index]); } else { $data[$index]["codice_lotto_npa"] = $lots[$lot["codice"]]["codice"]; } } } } return $data; } /** * Ottiene la versione da utilizzare * * @param integer|null $codice_fascicolo * @param integer|null $codice_scheda * @return string|null Versione da uasre */ public static function fetchVersion(?int $codice_fascicolo = null, ?int $codice_scheda = null) : string { if($codice_scheda !== null) { $scheda = self::fetchSchede(["versione_scheda", "stato"], [ "s.codice" => $codice_scheda ], false, false); if(!empty($scheda)) { if($scheda["stato"] < 20) { return self::LATEST_SDK_VERSION; } return empty($scheda["versione_scheda"]) ? self::LATEST_SDK_VERSION : $scheda["versione_scheda"]; } } return self::LATEST_SDK_VERSION; } /** * Fetch and load the set of card for the given dossier. * * @return array */ public static function fetchLotti(array $fields = ["*"], array $filters = [], bool $join_npa = false, bool $multiple = true): array { $fields = array_filter($fields, function($field) { if($field === "*") return true; if(!myPDO::validFieldName($field)) { throw new Exception("Invalid field name {$field}"); } return true; }, ARRAY_FILTER_USE_BOTH); global $pdo; $fields_str = implode(",", $fields); $bind = []; $filter_str = []; // Per ogni filtro, lo aggiungiamo nei bind e nella query foreach ($filters as $field => $value) { if(!myPDO::validFieldName($field)) { throw new Exception("Invalid field name {$field}"); } $operand = "="; if (is_array($value)) { $operand = $value[0]; $value = $value[1]; } $bind_name = uniqid(); $bind[":{$bind_name}"] = $value; $filter_str[] = "{$field} $operand :{$bind_name}"; } // Ricostruiamo la query where if (!empty($filter_str)) { $filter_str = "WHERE " . implode(" AND ", $filter_str); } else { $filter_str = ""; } $join_str = ""; if ($join_npa) { if($fields_str == "*") { $fields_str .= ", l.codice AS codice, n.codice AS codice_npa"; } if (empty($filter_str)) { $filter_str .= " WHERE n.id_riferimento > 0"; } else { $filter_str .= " AND n.id_riferimento > 0"; } $join_str = "LEFT JOIN b_npa n ON n.codice = l.codice_npa"; } $limit_str = ""; if (!$multiple) { $limit_str = "LIMIT 1"; } $sql = "SELECT {$fields_str} FROM b_npa_lotti l {$join_str} {$filter_str} ORDER BY l.timestamp_creazione, l.codice ASC {$limit_str}"; $result = $pdo->go($sql, $bind); $lotti = $multiple ? $result->fetchAll(PDO::FETCH_ASSOC) : $result->fetch(PDO::FETCH_ASSOC); if (!is_array($lotti)) { return []; } return $lotti; } /** * Carica una lista di fascicoli con i filtri indicati * * @return array */ public static function fetchDossier(array $fields = ["*"], array $filters = [], bool $multiple = true): array { $fields = array_filter($fields, function($field) { if($field === "*") return true; if(!myPDO::validFieldName($field)) { throw new Exception("Invalid field name {$field}"); } return true; }, ARRAY_FILTER_USE_BOTH); global $pdo; // Aggiungo soft delete $filters["id_riferimento"] = [">", "0"]; $fields_str = implode(",", $fields); $bind = []; $filter_str = []; if(empty($filters["id_riferimento"])) $filters["id_riferimento"] = [">", "0"]; // Per ogni filtro, lo aggiungiamo nei bind e nella query foreach ($filters as $field => $value) { if(!myPDO::validFieldName($field)) { throw new Exception("Invalid field name {$field}"); } $operand = "="; if (is_array($value)) { $operand = $value[0]; $value = $value[1]; } $bind[":{$field}"] = $value; $filter_str[] = "`{$field}` $operand :{$field}"; } // Ricostruiamo la query where if (!empty($filter_str)) { $filter_str = "WHERE " . implode(" AND ", $filter_str); } else { $filter_str = ""; } $limit_str = ""; if (!$multiple) { $limit_str = "LIMIT 1"; } $sql = "SELECT {$fields_str} FROM b_npa {$filter_str} ORDER BY `codice`, `timestamp` ASC {$limit_str}"; $result = $pdo->go($sql, $bind); $fascicoli = $multiple ? $result->fetchAll(PDO::FETCH_ASSOC) : $result->fetch(PDO::FETCH_ASSOC); if (!is_array($fascicoli)) { return []; } return $fascicoli; } private static function buildFilter(array $filters, array &$bind): array { $filter_str = []; foreach ($filters as $field => $value) { if (strpos($field, "or_") === 0) { // Supporto ai gruppi OR $or_filter_str = self::buildFilter($value, $bind); if (!empty($or_filter_str)) { $or_filter_str = implode(" OR ", $or_filter_str); $filter_str[] = "({$or_filter_str})"; } } else { if (!myPDO::validFieldName($field)) { throw new Exception("Invalid field name {$field}"); } $operand = "="; if (is_array($value)) { $operand = $value[0]; $value = $value[1]; } $bind_name = uniqid(); if ($operand === "IN" && is_array($value)) { if (!empty($value)) { $in_str = "{$field} IN ("; foreach ($value as $idx => $val) { $in_str .= ":{$bind_name}_{$idx},"; $bind[":{$bind_name}_{$idx}"] = $val; } $in_str = rtrim($in_str, ",") . ")"; $filter_str[] = $in_str; } } elseif (in_array($operand, ["OR", "AND"], true) && is_array($value)) { $in_str = "("; foreach ($value as $idx => $val) { $subOperand = "="; if (is_array($val)) { $subOperand = $val[0]; $val = $val[1]; } elseif ($val === null) { $subOperand = "IS"; } $in_str .= " {$field} {$subOperand} :{$bind_name}_{$idx} $operand"; $bind[":{$bind_name}_{$idx}"] = $val; } $in_str = rtrim($in_str, " $operand") . ")"; $filter_str[] = $in_str; } else { $bind[":{$bind_name}"] = $value; $filter_str[] = "{$field} $operand :{$bind_name}"; } } } return $filter_str; } /** * Fetch and load the set of card for the given dossier. * * @return array */ public static function fetchSchede(array $fields = ["*"], array $filters = [], bool $join_npa = false, bool $multiple = true, $order = "ASC"): array { $fields = array_filter($fields, function($field) { if($field === "*") return true; if(!myPDO::validFieldName($field)) { throw new Exception("Invalid field name {$field}"); } return true; }, ARRAY_FILTER_USE_BOTH); global $pdo; // Disambiguazione codice $codiceKey = array_search("codice", $fields); if($codiceKey !== false && $join_npa) { $fields[$codiceKey] = "s.codice"; } $fields_str = implode(",", $fields); $bind = []; $filter_str = []; // Aggiungo soft delete $filters["soft_delete"] = ["!=", "S"]; $join_str = ""; if ($join_npa) { if($fields_str == "*") { $fields_str .= ", s.codice AS codice, n.codice AS codice_npa"; } $join_str = "LEFT JOIN b_npa n ON n.codice = s.codice_npa"; if(empty($filters["id_riferimento"])) $filters["id_riferimento"] = [">", "0"]; } // Per ogni filtro, lo aggiungiamo nei bind e nella query $filter_str = self::buildFilter($filters, $bind); // Ricostruiamo la query where if (!empty($filter_str)) { $filter_str = "WHERE " . implode(" AND ", $filter_str); } else { $filter_str = ""; } $limit_str = ""; if (!$multiple) { $limit_str = "LIMIT 1"; } $orderBy = "s.codice"; $orderDir = $order; if(is_array($order)) { $orderBy = "s.{$order[0]}"; $orderDir = $order[1]; } $orderDir = $orderDir == "ASC" ? "ASC" : "DESC"; $sql = "SELECT {$fields_str} FROM b_npa_schede s {$join_str} {$filter_str} ORDER BY {$orderBy} {$orderDir} {$limit_str}"; $result = $pdo->go($sql, $bind); $schede = $multiple ? $result->fetchAll(PDO::FETCH_ASSOC) : $result->fetch(PDO::FETCH_ASSOC); if (!is_array($schede)) { return []; } return $schede; } /** * Ottiene le schede categorizzate per id_scheda * @param bool $multiple Se false viene ritornata solo l'ultima scheda per id_scheda * @param bool $only_current_lot . Se true, vengono caricate le schede relative al lotto corrente (o alla gara se non c'è un lotto). Se false vengono incluse quelle del lotto corrente E della gara * * @return array */ public function getCardsByType($multiple = true, $only_current_lot = true): array { $schede = $this->getCards($only_current_lot); $retval = []; foreach ($schede as $scheda) { $scheda = array_merge($scheda, $this->metadataSchede[$scheda["id_scheda"]]); if ($multiple) { if (empty($retval[$scheda["id_scheda"]])) { $retval[$scheda["id_scheda"]] = []; } $retval[$scheda["id_scheda"]][] = $scheda; } else { if (isset($retval[$scheda["id_scheda"]])) { if ($retval[$scheda["id_scheda"]]["stato"] < $scheda["stato"]) { $retval[$scheda["id_scheda"]] = $scheda; } } else { $retval[$scheda["id_scheda"]] = $scheda; } } } return $retval; } /** * Fetch and load the set of card for the given dossier. * * @return void */ public function loadCards(): void { if (!empty($this->fascicolo)) { $this->schede = $this->getCards(true); } } /** * Ritorna, se presente, l'ultima scheda completata, al fine di stabilire il flusso * * @param boolean $only_current_lot Se true ottiene solo dalla scheda generale e dal lotto attuale * @return array|null */ public function lastCompletedCardForFlusso(bool $only_current_lot = true): ?array { $cards = $this->getCards($only_current_lot, ["timestamp_trasmissione", "DESC"]); foreach($cards as $scheda) { $scheda = array_merge($scheda, $this->metadataSchede[$scheda["id_scheda"]]); // Evitiamo di far contare rettifiche o modifiche if(!empty($scheda["codice_modifica"]) || !empty($scheda["codice_rettifica"])) { continue; } // Contano invece come complete schede che sono state modificate o rettificate if(self::checkCardComplete($scheda) || in_array($scheda["stato"], [-10, -20]) ) { return $scheda; } } return null; } /** * Ritorna, se disponibili, tutte le schede confermate * * @param mixed $only_current_lot * @return array */ public function getConfirmedCards(bool $only_current_lot = true) : ?array { $cards = $this->getCards($only_current_lot, true); if(! empty($cards)) { foreach($cards as $index => &$scheda) { $scheda = array_merge($scheda, $this->metadataSchede[$scheda["id_scheda"]]); if($scheda["stato"] < self::TRASMITTED_STATO) { unset($cards[$index]); } } return $cards; } return null; } /** * Ritorna, se disponibili, tutte le schede completate * * @param mixed $only_current_lot * @return array */ public function getCompletedCards(bool $only_current_lot = true) : ?array { $cards = $this->getCards($only_current_lot, true); if(! empty($cards)) { foreach($cards as $index => &$scheda) { $scheda = array_merge($scheda, $this->metadataSchede[$scheda["id_scheda"]]); if(! self::checkCardComplete($scheda) && ! in_array($scheda["stato"], [-10, -20]) ) { unset($cards[$index]); } } return $cards; } return null; } /** * Ritorna le ultime schede completate * * @param boolean $only_current_lot Se true ottiene solo dalla scheda generale e dal lotto attuale * @return array */ public function getLastCompletedCards(bool $only_current_lot = true): array { $retval = []; $cards = $this->getCards($only_current_lot, ["timestamp_trasmissione", "DESC"]); foreach($cards as $scheda) { $scheda = array_merge($scheda, $this->metadataSchede[$scheda["id_scheda"]]); // Contano invece come complete schede che sono state modificate o rettificate if(self::checkCardComplete($scheda) || in_array($scheda["stato"], [-10, -20]) ) { $retval[] = $scheda; } } return $retval; } /** * Ottiene le carte presenti per questo fascicolo * * @param boolean $only_current_lot Se true ottiene solo dalla scheda generale e dal lotto attuale * @param string|array $desc Se stringa, ordina per codice $order - altrimenti ordina per $order[0] $order[¹] * @return array */ public function getCards(bool $only_current_lot = true, $order = "ASC"): array { if (!empty($this->fascicolo)) { $select = ["codice", "dati", "codice_rettifica", "codice_modifica", "codice_lotto", "id_scheda", "stato", "errore", "timestamp_trasmissione", "versione_scheda", "timestamp_creazione"]; $filters = ["codice_npa" => $this->fascicolo["codice"]]; if (!empty($this->scheda["codice_lotto"]) || !empty($this->lot["codice_lotto_npa"])) { $codice_lotto = empty($this->scheda["codice_lotto"]) ? $this->lot["codice_lotto_npa"] : $this->scheda["codice_lotto"]; if ($only_current_lot) { $filters["codice_lotto"] = $codice_lotto; } else { $filters["codice_lotto"] = ["OR", [$codice_lotto, null, '', 0]]; } } else { $filters["codice_lotto"] = ["OR", [null, '', 0]]; } return self::fetchSchede($select, $filters, false, true, $order); } return []; } /** * Checking if the card is in an unmodifiable state * * @return bool */ public function checkLock(): Bool { if (!empty($this->scheda) && $this->scheda["stato"] >= 40 && ($this->scheda["errore"] ?? "NO") === "NO") { return true; } if (!empty($this->scheda) && $this->scheda["stato"] >= 90) { return true; } if (!empty($this->scheda) && $this->scheda["stato"] < 0) { return true; } return false; } /** * Ottiene le schede successive * * @param String|null $settore Settore corrente, se null viene ignorato * @param String|null $soglia Soglia corrente, se null viene ignorata * @param boolean|null $concessione Si tratta di una concessione? Se null viene ignorato * @param boolean|null $concorso Si tratta di un concorso? Se null viene ignorato * @return array Ritorna una lista di schede */ public function getCardsAndItsDescendantsList(string $settore = null, ?string $soglia = null, ?bool $concessione = null, ?bool $concorso = null): array { $schede = $this->getCards(false); if (!empty($schede)) { $list = array_filter($schede, fn ($card) => !empty($card["timestamp_trasmissione"])); if (empty($list)) { $list = $schede; } $card = array_values($list)[0]; $cards = $this->getNextCards(); $cards = array_merge(array_column($list, "id_scheda"), $cards); $cards = array_unique($cards); $cards = array_filter($this->metadataSchede, function (&$card, $key) use ($cards, $settore, $soglia, $concessione, $concorso) { if (in_array($key, $cards)) { if (!is_null($settore)) { if (!in_array($settore, $card["settori"])) { return false; } } if (!is_null($soglia)) { if ($soglia === "soprasoglia" && !$card[$soglia]) { return false; } } if(!is_null($concessione) && !is_null($card["concessione"])) { if($card["concessione"] != $concessione) { return false; } } if(!is_null($concorso) && !is_null($card["concorso"])) { if($card["concorso"] != $concorso) { return false; } } return true; } return false; }, ARRAY_FILTER_USE_BOTH); foreach ($cards as $key => &$card) { if (!empty($list[$key])) { $card["codice"] = $list[$key]["codice"]; } } return $cards; } return []; } /** * Organizes or sorts the cards based on their category * * @param mixed $cards * @return array */ public function arrangeCardsByCategory(array $cards): array { $arranged = []; foreach ($cards as $id => $card) { $arranged[$card["categoria"]][$id] = $card; } $arranged = array_replace(["pianificazione" => [], "pubblicazione" => [], "affidamento" => [], "aggiudicazione" => [], "esecuzione" => []], $arranged); $arranged = array_filter($arranged); return $arranged; } /** * Riordina una lista di schede in base alla gerarchia. * * @param array $cards Lista di schede in formato ["id" => scheda] (scheda non è usato, quindi può essere vuoto) * @return array */ public static function sortCardsByHierarchy(array $cards): array { // Sort in base alla profondità foreach ($cards as $_id => $_scheda) { // Salviamo ID all'interno della scheda perchè sarà usato come riferimento $cards[$_id]["id"] = $_id; } // Ordiniamo in base alla profondità (questo lo rende una lista ordinata) usort($cards, function ($a, $b) { return $a["order"] > $b["order"]; }); // Settiamo nuovamente id come chiave return array_column($cards, null, "id"); } /** * Selectively retrieves cards based on their type * * @param mixed $cards * @return array */ public function filterCardsByType(array $cards, Bool $lot_related): array { return array_filter($cards, function ($info) use ($lot_related) { return $info["lotto"] === $lot_related; }); } /** * Elimina la scheda corrente * * @return boolean */ public function deleteScheda(?string &$message=null): bool { if ($this->isCardDeletable()) { // Cancellazione via API if(!empty($this->scheda["uuid"])) { return $this->scheda["stato"] > self::TRASMITTED_STATO ? $this->deleteNotice($message) : $this->delete($message); } $bind = array(":codice" => $this->scheda["codice"]); $sql = "UPDATE b_npa_schede SET soft_delete = 'S' WHERE codice = :codice"; $ris = $this->pdo->go($sql, $bind); if ($ris->rowCount() === 1) { scriviLog("b_npa_schede", "DELETE", $this->pdo->getSQL(), $_POST["codice"]); return true; } } $message = __("Impossibile cancellare la scheda"); return false; } /** * Metodo veloce per cambiare una voce nel fascicolo * * @param array $data Dati da cambiare, associativo * @return boolean True in caso di successo */ public function updateFascicolo(array $data=[]): bool { $result = false; if($this->fascicolo["codice"] > 0) { $salva = new Salva(); $salva->nome_tabella = "b_npa"; $salva->operazione = "UPDATE"; $salva->debug = false; $data["codice"] = intval($this->fascicolo["codice"]); if(!empty($data["codice"])) { $salva->expect = array_keys($data); $salva->oggetto = $data; $result = $salva->save(); } } foreach($data as $key => $value) { $this->fascicolo[$key] = $value; } return $result == true; } public function updateScheda(array $data=[]): bool { $result = false; if($this->scheda["codice"] > 0) { $salva = new Salva(); $salva->nome_tabella = "b_npa_schede"; $salva->operazione = "UPDATE"; $data["codice"] = intval($this->scheda["codice"]); if(!empty($data["codice"])) { $salva->expect = array_keys($data); $salva->oggetto = $data; $result = $salva->save(); } foreach($data as $key => $value) { $this->scheda[$key] = $value; } } return $result == true; } /** * Store form data. * * @param array $data * @return bool */ public function save(array $data = [], bool $scheda = true): ?array { $salva = new Salva(); $salva->debug = false; if (empty($this->fascicolo["codice"])) { $info = []; if (!empty($this->oggetto)) { $info["benficiario"] = $this->oggetto->getEnteBeneficiario(); $info["rup"] = $this->oggetto->getRup(); } else { return null; } $this->fascicolo = [ "codice_ente" => $data["codice_ente"], "modulo_riferimento" => in_array($data["modulo_riferimento"], ["gare", "concorsi", "albo_fornitori", "contratti"]) ? $data["modulo_riferimento"] : null, "id_riferimento" => $data["id_riferimento"], "data" => serialize($info), "versione_interna" => self::LATEST_INTERNAL_VERSION ]; $salva->ignore = ["timestamp"]; $salva->nome_tabella = "b_npa"; $salva->operazione = "INSERT"; $salva->oggetto = $this->fascicolo; $this->fascicolo["codice"] = $salva->save(); if (empty($this->fascicolo["codice"])) { throw new Exception("Errore nel salvataggio del fascicolo."); } } if (!$scheda) { return null; } $record = $this->scheda; $record["dati"] = $data["npa"]; $record["dati"] = json_encode($record["dati"], JSON_PRETTY_PRINT); $record["codice_npa"] = $this->fascicolo["codice"]; $record["codice_lotto"] = $data["codice_lotto"] ?? null; $record["round_riferimento"] = $data["round_riferimento"]; $record["stato"] = $data["bozza"] == 0 ? 10 : 0; $record["soft_delete"] = "N"; $record["versione_scheda"] = $this->version; $salva->nome_tabella = "b_npa_schede"; $operazioneScheda = $salva->operazione = "UPDATE"; $salva->oggetto = $record; // Ignoriamo il codice rettifica dopo la prima iterazione $salva->ignore = ["codice_npa", "timestamp", "timestamp_creazione", "timestamp_trasmissione", "pending_cancellazione", "codice_rettifica", "codice_modifica"]; if (empty($record["codice"])) { $operazioneScheda = $salva->operazione = "INSERT"; $salva->ignore = ["timestamp", "timestamp_trasmissione", "pending_cancellazione"]; $salva->oggetto["errore"] = "NO"; $salva->oggetto["timestamp_creazione"] = date("d/m/Y H:i:s"); $salva->oggetto["errore"] = "NO"; $salva->oggetto["versione_scheda"] = self::LATEST_SDK_VERSION; if(isset($data["codice_rettifica"]) && is_numeric($data["codice_rettifica"])) { $salva->oggetto["codice_rettifica"] = $data["codice_rettifica"]; } else { $salva->ignore[] = "codice_rettifica"; } if(isset($data["codice_modifica"]) && is_numeric($data["codice_modifica"])) { $salva->oggetto["codice_modifica"] = $data["codice_modifica"]; } else { $salva->ignore[] = "codice_modifica"; } } if(isset($data["codice_scheda_guue"]) && is_numeric($data["codice_scheda_guue"])) { $salva->oggetto["codice_scheda_guue"] = $data["codice_scheda_guue"]; } else { $salva->ignore[] = "codice_scheda_guue"; } $record["codice"] = $salva->save(); if (!empty($record["codice"])) { $this->scheda = $record; if ($_SESSION["utente"]->gerarchia > 15 && $salva->operazione == "INSERT") { $permesso = [ "sezione" => "npa", "codice_elemento" => $this->info["codice"], "codice_gestore" => $_SESSION["ente"]->codice, "funzione" => "0", "codice_utente" => $_SESSION["utente"]->codice, ]; $salva_p = new salva(); $salva_p->debug = false; $salva_p->nome_tabella = "r_permessi"; $salva_p->operazione = "INSERT"; $salva_p->oggetto = $permesso; $salva_p->save(); } if (isset($this->oggetto) && method_exists($this->oggetto,"addToLog")) { $this->oggetto->addToLog($operazioneScheda,"Scheda ANAC: " . $this->scheda["id_scheda"]); } return $record; } return null; } /** * Rretrieves the codes associated with cost centers * * @param string $identifier * @return array */ public function getCodiciCentriDiCosto(String $identifier): ?array { $retval = []; if(DEVELOP_ENV) { $source_anac = $this->getCodiciCentriDiCostoFromAnac($identifier); $source_db = self::getCodiciCentriDiCostoFromDb($identifier); foreach($source_anac as $uuid => $centro) { $retval[$uuid] = ">>> $centro ({$uuid}) [COLL]"; } foreach($source_db as $uuid => $centro) { if(isset($retval[$uuid])) { $retval[$uuid] = "$centro ({$uuid}) [COLL+PROD]"; } else { $retval[$uuid] = "$centro ({$uuid}) [PROD]"; } } } else { $retval = $this->getCodiciCentriDiCostoFromAnac($identifier); if(empty($retval)) $retval = $this->getCodiciCentriDiCostoFromDB($identifier); foreach ($retval as $uuid => $centro) { $retval[$uuid] = "$centro ({$uuid})"; } } return $retval; } /** * Rretrieves the codes associated with cost centers * * @param string $identifier * * @return array */ public static function getCodiciCentriDiCostoFromDB(String $identifier): ?array { global $pdo; // Nota: su necessità questo metodo può diventare statico $identifier = trim(strtoupper($identifier)); $centri = $pdo->go( "SELECT * FROM b_centri_costo WHERE cf = :cf", ["cf" => $identifier] )->fetchAll(PDO::FETCH_ASSOC); if (!is_array($centri) || empty($centri)) { return []; } $retval = []; foreach ($centri as $centro) { $retval[$centro["uuid"]] = $centro["denominazione"]; if($centro["attivo"] !== "S") { $retval[$centro["uuid"]] .= " (Non Attivo)"; } } return $retval; } /** * Retrieves keys and UUIDs (Universally Unique Identifiers) associated with the current request * * @return array */ private function getKeysAndUUIDsForCurrentRequest(?string $type = null): ?array { $idAppalto = $this->getUUID("appalto"); $idPianificazione = $this->getUUID("pianificazione"); $idScheda = $this->getUUID("scheda"); if (empty($type)) { $type = $this->scheda["type"]; } if ($type == "pianificazione") { return [ "idPianificazione" => $idPianificazione, "idScheda" => $idScheda ]; } return [ "idAppalto" => $idAppalto, "idScheda" => $idScheda ]; } /** * Chiude tutte le operazioni in corso per questa scheda, * risolvendo eventuali problemi prima che nascano. * * @return void */ public function closeAllPendingOperations() { $this->pdo->go( "UPDATE b_npa_operazioni_schede SET esito = 'KO' WHERE (esito IS NULL OR esito = '') AND codice_scheda = :codice_scheda", [":codice_scheda" => $this->scheda["codice"]] ); } public static function fetchLastOperation(?int $card_id = null, ?string $operation = null, $esito = false, bool $multiple = false, ?int $lot = null, ?string $codice_partecipante = null) : ?array { global $pdo; $bind = [":codice_scheda" => $card_id]; $sql = "SELECT * FROM b_npa_operazioni_schede WHERE ("; $sql = "{$sql} codice_scheda = :codice_scheda"; if(empty($bind[":codice_scheda"])) { $sql = "{$sql} OR codice_scheda IS NULL"; } $sql = "{$sql} )"; if (!empty($lot)) { $sql = "{$sql} AND codice_lotto = :codice_lotto"; $bind[":codice_lotto"] = $lot; } if (!empty($operation)) { $sql = "{$sql} AND operazione = :operazione"; $bind[":operazione"] = $operation; } if(!empty($codice_partecipante)) { $sql = "{$sql} AND codice_partecipante = :codice_partecipante"; $bind[":codice_partecipante"] = $codice_partecipante; } if($esito === null) { $sql .= " AND (esito IS NULL OR esito = '' OR esito = 0)"; } elseif($esito !== false) { $sql .= " AND esito = :esito"; $bind[":esito"] = $esito; } $sql .= " ORDER BY timestamp DESC"; if(!$multiple) $sql = "{$sql} LIMIT 0,1"; $result = $pdo->go($sql, $bind); if ($result->rowCount() > 0) { $operations = $result->fetchAll(PDO::FETCH_ASSOC); return $multiple ? $operations : $operations[0]; } return null; } /** * Ottiene il link alla scheda corrente * * @return string|null */ public function getCardLink() : ?string { if(empty($this->scheda["codice"])) return null; $query = [ "scheda" => $this->scheda["id_scheda"], "codice" => $this->scheda["codice"], "modulo_riferimento" => $this->fascicolo["modulo_riferimento"] ?? null, "id_riferimento" => $this->fascicolo["id_riferimento"] ?? null, ]; if ($this->metadataSchede[$this->scheda["id_scheda"]]["lotto"]) { $query["codice_lotto"] = $this->scheda["codice_lotto"]; } return "/backend/npa/edit.php?" . http_build_query($query); } /** Retrieves the latest operation associated with a given card * * @param ?int $card_id * @param ?string $operation * @param ?bool $esito * @param ?bool $multiple * @param ?int $lot * @return array */ public function getLastOperation(?int $card_id = null, ?string $operation = null, $esito = false, bool $multiple = false, ?int $lot = null): ?array { return self::fetchLastOperation($card_id ?? $this->scheda["codice"], $operation, $esito, $multiple, $lot); } /** * Recupera il notice_id inviato ad ANAC nei precedenti invii * * @return string|null */ private function retrieveGUUESentNoticeID() : ?string { if(!$this->canDoRequest()) return null; $readData = $this->read(); if(!empty($readData->_scheda)) { $scheda = $readData->_scheda; if (isset($scheda->body->eform)) { $eform = base64_decode($scheda->body->eform); if($eform) { libxml_use_internal_errors(true); $eform_xml = new DOMDocument(); $eform_xml->loadXML($eform); if(!empty(libxml_get_errors())) return null; $xpath10 = new DOMXPath($eform_xml); libxml_clear_errors(); return $xpath10->evaluate("string(/*/cbc:ID[@schemeName='notice-id'])"); } } } return null; } /** * Examines whether cards associated with the given tender require either GUUE or ESPD CML documents * * @return void */ public function checkIfCardNeedsXMLDocument(): Bool { if(empty($this->check)) { $this->check = [ "eform" => ["required" => false, "ready" => false], "espd" => ["required" => false, "ready" => false] ]; if (!empty($this->scheda["id_scheda"]) && !empty($this->form["content"])) { if (!empty($this->oggetto)) { if (!empty(array_column($this->form["content"], null, "id")["espd"])) { $this->check["espd"]["required"] = true; $this->check["espd"]["xml"] = $this->oggetto->getManagerDGUE()->getRequestXML(); if (!empty($this->check["espd"]["xml"])) { $this->check["espd"]["ready"] = true; } else { $this->check["espd"]["ready"] = false; $check = false; } } } if (!empty($this->metadataSchede[$this->scheda["id_scheda"]]["guue"])) { $this->check["eform"]["required"] = true; if(!empty($this->scheda["codice_scheda_guue"])) { $idScheda = $this->scheda["codice_scheda_guue"]; if($this->ted === null) { try { $this->ted = new eFormsManager(eFormsManager::ANY_NOTICE_TYPE, $idScheda, $this); } catch(\Throwable $t) { $this->ted = null; } } if($this->ted) { // Verifica che la notice_id corrisponda if(!empty($this->scheda["guue_sent_notice_id"])) { $eform_notice_id = $this->ted->getNoticeID(); $sent_notice_id = $this->scheda["guue_sent_notice_id"]; // Se non corrisponde, forziamo $this->ted->overrideNoticeID($sent_notice_id); } $this->check["eform"]["codice"] = $this->scheda["codice_scheda_guue"]; $this->check["eform"]["ready"] = true; $this->check["eform"]["notice_id"] = $this->ted->getNoticeID(); $this->check["eform"]["ted"] = $this->ted; $this->check["eform"]["ready_for_transmission"] = $this->ted->isReadyForAnacTransmission(); } else { $this->check["eform"]["ready"] = false; $this->updateScheda(["codice_scheda_guue" => 0]); } } else { $this->check["eform"]["ready"] = false; } } } } return !$this->check["eform"]["required"] || ($this->check["eform"]["required"] && $this->check["eform"]["ready"]) && !$this->check["espd"]["required"] || ($this->check["espd"]["required"] && $this->check["espd"]["ready"]); } /** * Updates the CIG (Contract Identification Code) within the application * * @param String $identifier * @param String $cig * @return bool */ private function updateCig(String $identifier, String $cig): Bool { if (!empty($this->fascicolo)) { $select = "SELECT b_npa_lotti.codice"; $select = "{$select}, b_npa_lotti.lotto_riferimento AS id_riferimento"; $from = "FROM b_npa_lotti"; $where = "WHERE codice_npa = :codice_npa"; $bind = [":codice_npa" => $this->fascicolo["codice"]]; switch ($this->fascicolo["modulo_riferimento"]) { case 'concorsi': $select = "{$select}, b_concorsi_lotti.numero"; $join = "JOIN b_concorsi_lotti ON b_concorsi_lotti.codice = b_npa_lotti.lotto_riferimento"; $where = "{$where} AND b_concorsi_lotti.numero = :numero"; $bind[":numero"] = intval(str_replace("LOT-", "", $identifier)); break; case 'gare': $select = "{$select}, b_gare_lotti.numero"; $join = "JOIN b_gare_lotti ON b_gare_lotti.codice = b_npa_lotti.lotto_riferimento"; $where = "{$where} AND b_gare_lotti.numero = :numero"; $bind[":numero"] = intval(str_replace("LOT-", "", $identifier)); break; default: break; } $lot = $this->pdo->go("{$select} {$from} {$join} {$where}", $bind)->fetch(PDO::FETCH_ASSOC); if (!empty($lot["codice"])) { $this->pdo->go("UPDATE b_npa_lotti SET cig = :cig WHERE codice = :codice", [":codice" => $lot["codice"], ":cig" => $cig]); if (!empty($this->oggetto)) { $this->oggetto->updateCig($lot["id_riferimento"], $cig); return true; } } } return false; } /** * Verifica che l'istanza di npa corrente abbia un dossier che si può cancellare * * @return boolean */ public function canClearDossier() { // Otteniamo il codice $codice = $this->fascicolo["codice"] ?? null; if($codice === null) return false; // Verifichiamo che non ci sia alcuna scheda trasmessa $filterSchede = [ "stato" => [">=", 20], // Solo schede trasmesse in su "codice_npa" => $codice, // Solo schede di questo fascicolo ]; $schede_trasmesse = self::fetchSchede(["id_scheda"], $filterSchede); return empty($schede_trasmesse); } /** * Elimina il dossier dell'istanza NPA attuale * * @return void */ public function clearDossier($full = false) { // Otteniamo il codice corrente $codice = $this->fascicolo["codice"] ?? null; $data = $this->fascicolo["data"] ?? null; // Controlliamo se possiamo if($codice === null || $data === null || !$this->canClearDossier()) return; // Verifichiamo i permessi $canFullyClear = true; // Se è un cron skippiamo la verifica utente $isCron = defined("__CMDS_JOB_EXEC") && !empty(__CMDS_JOB_EXEC); if (!$isCron) { $utente = $_SESSION["utente"] ?? null; if(!$utente) { return false; } // Altrimenti verifichiamo chi può fare un clear completo $canFullyClear = $utente->isSupportoOrRoot(); } if(!$canFullyClear && $full) return; if($full) { // Supporto e root può pulire il fascicolo per intero $this->pdo->go("UPDATE b_npa SET data = NULL WHERE codice = :codice", [":codice" => $codice]); } else if(isset($this->oggetto) && method_exists($this->oggetto, "getRup")) { // Altrimenti viene solo refreshato il RP $data = unserialize($this->fascicolo["data"]); $rup_cf = $this->oggetto->getRup()["codice_fiscale"] ?? null; self::setUserRoleToDB($rup_cf, DelegheRoles::RP, $this->fascicolo["codice"]); } } /** * Verifica che la scheda sia nel suo ultimo stadio * * @var array $card La scheda da vrificare * * @return boolean */ public static function checkCardComplete(Array $card) : bool { return self::hasToBePublished($card) ? $card["stato"] >= self::PUBLISHED_STATO : $card["stato"] >= self::TRASMITTED_STATO; } /** * Verifica che una scheda abbia modifiche * * @param array $card * @return bool|int */ public static function cardHasModifyCard(Array $card) { $modifica = self::fetchSchede( ["*"], [ "codice_modifica" => $card["codice"], ], false, false ); if(empty($modifica)) return false; return $modifica["codice"]; } /** * Verifica che una scheda abbia rettifiche * * @param array $card * @return bool|int */ public static function cardHasRectifyCard(Array $card) { $rettifica = self::fetchSchede( ["*"], [ "codice_rettifica" => $card["codice"], ], false, false ); if(empty($rettifica)) return false; return $rettifica["codice"]; } /** * Verifica che una scheda sia rettificabile * * @param array $card Dati della scheda * @return boolean Se true la scheda è rettificabile */ public static function checkCardRectifiable(Array $card) : bool { if($card["stato"] < self::PUBLISHED_STATO) return false; // Andiamo a cercare eventuali schede di rettifica non pubblicate $rettifiche = self::fetchSchede( ["*"], [ "codice_rettifica" => $card["codice"], "stato" => ["<", "110"], ] ); if(!empty($rettifiche)) return false; return true; } /** * Verifica che una scheda sia modificabile * * @param array $card Dati della scheda * @return boolean Se true la scheda è rettificabile */ public static function checkCardModifiable(Array $card) : bool { if($card["stato"] < 90 || $card["stato"] >= self::PUBLISHED_STATO) return false; if(!empty($card["codice_modifica"])) return false; if(!empty($card["codice_rettifica"])) return false; // Rilevazione pubblicazione EU completa if(!empty($card["pubblicazione"])) { $pubblicazione = unserialize($card["pubblicazione"]); if(!empty($pubblicazione) && !empty($pubblicazione->datiPubblicazioneEU) && !empty($pubblicazione->datiPubblicazioneEU->dataPubblicazione)) { return false; } } // Andiamo a cercare eventuali schede di modifica non pubblicate $modifiche = self::fetchSchede( ["*"], [ "codice_modifica" => $card["codice"], "stato" => ["<", "110"], ] ); if(!empty($modifiche)) return false; return true; } /** * Fetches and provides a list of subordinate cards * * @return array */ public function getNextCards(): array { // Otteniamo le schede attuali categorizzate per ID, ci sarà ritornata per ognuna la più aggiornata $schedeByType = $this->getCardsByType(true, false); // Togliamo le pubblicazioni, loro non settano un flusso pare $schedeByTypeFiltrate = []; foreach($schedeByType as $id => $schede) { foreach($schede as $scheda) { // Evitiamo di far contare rettifiche o modifiche if(!empty($scheda["codice_modifica"]) || !empty($scheda["codice_rettifica"])) { continue; } // Contano invece come complete schede che sono state modificate o rettificate if(!(self::checkCardComplete($scheda) || in_array($scheda["stato"], ["-10", "-20"])) ){ continue; } // Se la scheda non definisce un flusso, la ignoriamo if(empty($scheda["flusso"]) || in_array("*", $scheda["flusso"])) { continue; } $schedeByTypeFiltrate[$id] = $scheda; break; } } $retval = []; // Per il momento usiamo il flusso della prima scheda if(empty($schedeByTypeFiltrate)) { $flussi = []; } else { $flussi = reset($schedeByTypeFiltrate)["flusso"]; } // Per l'ultima scheda completata $ultimaScheda = $this->lastCompletedCardForFlusso(false); if(empty($ultimaScheda)) { return []; } $retval = []; foreach ($ultimaScheda["schede_successive"] as $next_id_scheda) { // Se l'abbiamo già processata continuiamo if (in_array($next_id_scheda, $retval)) continue; // Otteniamo i metadata della successiva $next_metadata = $this->metadataSchede[$next_id_scheda] ?? null; if (!$next_metadata) continue; // Verifichiamo che sia compatibile con i flussi attuali ( se ci sono ) $next_flussi = $next_metadata["flusso"]; if(!empty($flussi) && !empty($next_flussi) && $next_flussi[0] != ["*"]) { $compatible = false; if(in_array("*", $next_flussi)) $compatible = true; foreach($flussi as $flusso) { if(in_array("{$flusso}", $next_flussi)) $compatible = true; if(in_array("!{$flusso}", $next_flussi)) $compatible = false; break; } if(!$compatible) continue; } // Aggiungiamo la scheda a quelle trovate $retval[] = $next_id_scheda; } return $retval; } /** * Settings default stuff for initialization * * @return void */ private function __construct(String $version = self::LATEST_SDK_VERSION) { self::check_version($version); global $pdo, $config; if ((DEMO_ENV || DEVELOP_ENV) && !empty($_SESSION["utente"]) && $_SESSION["ente"]->getInfo()["spid"] == "N") { $_SESSION["utente"]->authenticationLevel = 2; $_SESSION["utente"]->publicSSO = "SPID"; } $this->pdo = $pdo; $this->version = $version; $this->path = dirname(__FILE__, 2) . "/versions/{$version}"; $this->metadataSchede = self::getCardsMetadata(); $this->info = get_campi("b_npa"); $this->info["codice"] = 0; $this->info["stato"] = 0; $this->info["codice_gestore"] = $_SESSION["ente"]->codice; $this->info["codice_ente"] = $_SESSION["utente"]->codice_ente ?? $_SESSION["ente"]->codice; $this->api_config = require __DIR__ . "/configurazione.php"; } private static function __processCardDataForRendering($values) { if(is_array($values)) { /*if(array_is_list($values)) { // Conversione da formato ANAC $newvalues = []; foreach($values as $value) { $newvalues[uniqid("rep_")] = $value; } $values = $newvalues; }*/ // Conversione da vecchio formato $keys = array_keys($values); if (isset($keys[0]) && strpos($keys[0], "rep_") !== false) { $values = array_values($values); } foreach($values as $index => $value) { if(in_array($index, ["espd", "eform"], true)) { $value = "TO-BE-FILLED"; } $values[$index] = self::__processCardDataForRendering($value); } } elseif(is_bool($values)) { return $values ? "true" : "false"; } elseif(is_numeric($values)) { return "{$values}"; } elseif(is_string($values)) { $datetime = DateTime::createFromFormat(DateTime::ATOM, $values); if (!is_bool($datetime)) { return $datetime->format("d/m/Y H:i"); } } return $values; } /** * Ottiene i dati della scheda per il rendering. * Questo metodo è compatibile con il formato interno del json * e anche con quello usato da ANAC. Utile nei casi in cui la scheda venga * importata * * @return array */ public function getCardData(): Array { if(!empty($this->card_data)) return $this->card_data; if(empty($this->scheda["dati"]) || !valid_json($this->scheda["dati"], $data, TRUE)) { return []; } $this->card_data = self::__processCardDataForRendering($data); return $this->card_data; } /** * Organize repeteble group from dictionary to array * * @param array $data * @return void */ public function organizeRepeteableGroups(array &$data, ?array $types = [], bool $root = true): void { date_default_timezone_set('Europe/Rome'); $index = ""; if (empty($types)) { $types = $this->getFormInputsTypes($this->form["content"]); } foreach ($data as $key => &$content) { if (is_array($content)) { $content = array_filter($content, function ($value) { if (is_array($value) && count($value) == 0) { return false; } return ($value !== NULL && $value !== FALSE && $value !== ''); }); if (!array_is_list($content) && count($content) == 1 && isset($content["idTipologica"])) { $content = []; } else { $keys = array_keys($content); if (isset($keys[0]) && strpos($keys[0], "rep_") !== false) { $content = array_values($content); } } $info = $types; if (!is_numeric($key) && !empty($types[$key])) { $info = $types[$key]; } $this->organizeRepeteableGroups($content, $info, false); $content = array_filter($content, function (&$value, $key) use($info) { $required = $info[$key]["_required"] ?? false; if($required) { // Workaround per assicurarci che un oggetto vuoto sia effettivamente un oggetto if(empty($value)) { $dataType = $info[$key]["_dataType"] ?? null; $repeatable = $info[$key]["_repeatable"] ?? false; $contentType = $info[$key]["contentType"] ?? null; if($this->version === "v1.0") { if ($dataType === "object" && !$repeatable) $value["forceJSONObject"] = true; } else { if (($contentType === "group" || $dataType === "object") && !$repeatable) $value["forceJSONObject"] = true; } // Probabilmente questo funzionerebbe anche, ma non ci giurerei // $value = new stdClass(); } return true; } //if($key === "codiceAusa") return TRUE; if (is_array($value) && count($value) == 0) { return false; } return ($value !== NULL || $value !== ''); }, ARRAY_FILTER_USE_BOTH); } else { if (!empty($types[$key]["type"])) { switch ($types[$key]["type"]) { case 'cpv': $content = str_pad($content, 8, "0", STR_PAD_RIGHT); break; case 'codice_istat': $option = $this->pdo->go("SELECT codice_istat FROM b_istat_anac WHERE codice_istat = :codice_istat", [":codice_istat" => $content])->fetch(PDO::FETCH_ASSOC); if($option) { if(self::LATEST_SDK_VERSION === "v1.0") $content = ltrim($option["codice_istat"], "0"); else $content = $option["codice_istat"]; } break; } } if (!empty($types[$key]["format"])) { switch ($types[$key]["format"]) { case 'double': $content = filter_var($content, FILTER_VALIDATE_FLOAT); break; case 'datetime': case 'date-time': $datetime = DateTime::createFromFormat("Y-m-d H:i", datetime2mysql($content)); if (!is_bool($datetime)) { $content = $datetime->format(DateTime::ATOM); } break; case 'integer': case 'number': $content = filter_var($content, FILTER_VALIDATE_INT); break; case 'bool': $content = filter_var($content, FILTER_VALIDATE_BOOLEAN); break; default: break; } } } } if($root) { if(($this->scheda["id_scheda"] ?? null) === "P7_3") { if(empty($data["anacForm"]["lotti"])) { $data["anacForm"]["lotti"] = []; } } } } /** * Get the card uuid * * @return string */ private function getUUID(String $type = "appalto"): String { if ($type == "appalto" && !empty($this->fascicolo["id_appalto"])) { return $this->fascicolo["id_appalto"]; } if ($type == "pianificazione" && !empty($this->fascicolo["id_pianificazione"])) { return $this->fascicolo["id_pianificazione"]; } if ($type == "scheda" && !empty($this->scheda["uuid"])) { return $this->scheda["uuid"]; } return "00000000-0000-0000-0000-000000000000"; } /** * Verifica che una scheda possa essere risincronizzata con ANAC * * @return boolean */ public function canSyncScheda() : bool { if($this->scheda["stato"] < 20) return false; $dataFascicolo = unserialize($this->fascicolo["data"]); if(!($dataFascicolo["abilita_resync"] ?? false)) { return false; } return true; } /** * Verifica che una scheda sia modificabile * * @return boolean Se true la scheda è modificabile */ public function isCardModifiable() : bool { $metadata = $this->metadataSchede[$this->scheda["id_scheda"]]; $scheda = array_merge($metadata, $this->scheda); $dataFascicolo = unserialize($this->fascicolo["data"]); if(!($dataFascicolo["abilita_modifica_appalto"] ?? false)) { // Workaround per disattivare modifiche su schede d'appalto (provvisorio) if($scheda["lotto"] === false && $scheda["categoria"] != "pianificazione") { return false; } } return self::checkCardModifiable($scheda); } /** * Verifica che una scheda sia rettificabile * * @return boolean Se true la scheda è rettificabile */ public function isCardRectifiable() : bool { $metadata = $this->metadataSchede[$this->scheda["id_scheda"]]; $scheda = array_merge($metadata, $this->scheda); return self::checkCardRectifiable($scheda); } /** * Determina se la scheda i nquestione ha bisogno di essere pubblicata * * @param array $scheda * @return boolean */ public static function hasToBePublished(array $scheda) : bool { // Retrocompatibilità if(($scheda["id_scheda"] ?? "") == "AD3" && ($scheda["versione_scheda"] ?? self::LATEST_SDK_VERSION) == "v1.0") { return false; } $metadata = self::getCardsMetadata()[$scheda["id_scheda"]]; return $metadata["pubblicazione"]; } /** * Determina che una scheda sia cancellabile * * @param string|null $text Se passato viene popolato con il testo giusto da mostrare all'utente * @param boolean|null $server_side Se passato viene popolato con una booleana che determina che la richiesta sia server_side o meno * @return boolean */ public function isCardDeletable(?string &$text = null, ?bool &$server_side = null) : bool { $server_side = true; // temporaneamente non consentiamo di cancellare rettifiche if(!empty($card["codice_rettifica"])) { return false; } // Non trasmessa if(empty($this->scheda["uuid"]) && $this->scheda["stato"] < 40) { $server_side = false; $text = "Elimina"; return true; } // Trasmessa ma non confermata if($this->scheda["stato"] < 40) { $text = "Elimina scheda trasmessa"; return true; } // In attesa di conferma if($this->scheda["stato"] == 40) { $text = "Elimina scheda in conferma"; return true; } // In attesa di pubblicazione if($this->scheda["stato"] >= 90 && $this->scheda["stato"] < 110) { $text = "Richiedi cancellazione della pubblicazione"; return true; } return false; } public function createDossierRevision() : bool { if(empty($this->fascicolo)) throw new Exception("Impossibile effettuare l'operazione senza un fascicolo"); // Crea una copia del dossier attuale $salva = new salva(); $new_fascicolo = $this->fascicolo; unset($new_fascicolo["id_pianificazione"]); unset($new_fascicolo["id_appalto"]); if(empty($new_fascicolo["revisione_fascicolo"])) $new_fascicolo["revisione_fascicolo"] = 1; $new_fascicolo["revisione_fascicolo"] += 1; $new_fascicolo["versione_interna"] = self::LATEST_INTERNAL_VERSION; $salva->nome_tabella = "b_npa"; $salva->oggetto = $new_fascicolo; $salva->operazione = "INSERT"; $vecchio_codice = $this->fascicolo["codice"]; $nuovo_codice = $salva->save(); if($nuovo_codice) { // Evitiamo di associare quello attuale $this->fascicolo["id_riferimento"] *= -1; $deleteOld = new salva(); $deleteOld->nome_tabella = "b_npa"; $deleteOld->oggetto = $this->fascicolo; $deleteOld->operazione = "UPDATE"; if(!$deleteOld->save()) return false; // Resettiamo i notice_id della GUUE $this->pdo->go(<< 0 ) SQL,[ ":vecchio_codice" => $vecchio_codice, //":guue_sent_notice_id" => uuidgen_guue(), ]); // Riportiamo le schede generali, anche se resettate a bozza $this->pdo->go(<< $vecchio_codice, ":nuovo_codice" => $nuovo_codice, ":versione_scheda" => self::LATEST_SDK_VERSION, //":guue_sent_notice_id" => uuidgen_guue(), ]); $this->fascicolo = $new_fascicolo; // Rimuovo eventuali riferimenti ai fascicoli degli OE $this->pdo->go("UPDATE b_fvoe SET codice_operatore = -ABS(codice_operatore), stato = -1, uuid_partecipante = NULL WHERE codice_npa = :codice_npa", [":codice_npa" => $vecchio_codice]); return true; } return false; } public function fetchCompatibleGUUECards(bool $multiple=true, bool $draft = false) : Array { $filters = [ "codice_elemento" => $this->fascicolo["id_riferimento"], "modulo" => $this->fascicolo["modulo_riferimento"], "stato" => $draft ? ["IN", ["BOZZA", "DA TRASMETTERE"]] : ["IN", ["TRASMESSO", "PUBBLICATO"]], "form" => ["IN", $this->metadataSchede[$this->scheda["id_scheda"]]["guue"]], ]; // Verifichiamo se c'è un lotto collegato nelle multilotto if(count($this->getLots()) > 1) { $codice_lotto_npa = $this->scheda["codice_lotto"]; if(!empty($codice_lotto_npa)) { $lot = $this->getLots($codice_lotto_npa) ?? null; if(!empty($lot)) { $lot = reset($lot); $filters["lotto_riferimento"] = ["OR", [$lot["codice"], null, 0]]; } } } $allSchede = eFormsManager::fetchSchede( ["codice", "form"], $filters, true ); $codice = $this->scheda["codice"]; $schede = array_filter($allSchede, function($scheda) use($codice) { // Check che la scheda non sia già usata $assignedANAC = NuovaPiattaformaAppalti::fetchSchede( [ "s.codice", "id_scheda" ], [ "s.codice" => ["!=", $codice], "codice_scheda_guue" => $scheda["codice"], "id_riferimento" => $this->fascicolo["id_riferimento"], "modulo_riferimento" => $this->fascicolo["modulo_riferimento"] ], true, false ); return empty($assignedANAC); }); if($multiple) return $schede; return empty($schede) ? [] : reset($schede); } // Override utente per Cron private $__cronOverrideRole = null; private $__cronOverrideCF = null; public function setCronOverrideUser(string $role, string $cf) { $this->__cronOverrideRole = $role; $this->__cronOverrideCF = $cf; } private function publishGUUEOnSuccessfulPublication() { if(isset($this->scheda["codice_scheda_guue"])) { $codice_guue = $this->scheda["codice_scheda_guue"]; } else { $this->checkIfCardNeedsXMLDocument(); if ($this->check["eform"]["required"]) { $codice_guue = $this->check["eform"]["codice"] ?? 0; } } if(!empty($codice_guue)) { $this->pdo->go("UPDATE b_guue SET stato = :stato WHERE codice = :codice", [":stato" => "PUBBLICATO", ":codice" => $codice_guue]); } } public function resetGUUEonSoftDelete() { if(isset($this->scheda["codice_scheda_guue"])) { $codice_guue = $this->scheda["codice_scheda_guue"]; } else { $this->checkIfCardNeedsXMLDocument(); if ($this->check["eform"]["required"]) { $codice_guue = $this->check["eform"]["codice"] ?? 0; } } if(!empty($codice_guue)) { $this->pdo->go("UPDATE b_guue SET stato = :stato, notice_id = NULL WHERE codice = :codice", [":stato" => "BOZZA", ":codice" => $codice_guue]); } } private $__givenInfo = []; /** * Ottiene la descrizione dei metadati di una scheda. Usato per GPT * * @param string $id_scheda * @return string */ public function getInfoScheda(string $id_scheda) : string { if(!empty($this->__givenInfo[$id_scheda])) return ""; $this->__givenInfo[$id_scheda] = True; $metadata = $this->metadataSchede[$id_scheda] ?? null; unset($metadata["order"]); unset($metadata["multipla"]); unset($metadata["settori"]); unset($metadata["obbligatoria"]); $retval = ""; foreach ($metadata as $id => $val) { if(is_array($val)) { $val = implode(",", $val); $retval .= "$id => $val, "; } else if(is_bool($val)) { if($val) $retval .= "$id => sì, "; } elseif(is_string($val)) { $retval .= "$id => \"{$val}\", "; } else { $retval .= "$id => $val, "; } } return "\n- Info sulla scheda {$id_scheda}: $retval" . PHP_EOL; } /** * Verifica che l'utente corrente possa accedere * alla presente istanza NPA. * * @return bool */ public function verifyUtente() { if(empty($_SESSION["utente"])) return false; if(empty($this->fascicolo)) return false; if(!$_SESSION["utente"]->permission("npa")) { return false; } if (!empty($this->fascicolo["modulo_riferimento"]) && !empty($this->fascicolo["id_riferimento"])) { if(!$_SESSION["utente"]->permission($this->fascicolo["modulo_riferimento"], $this->fascicolo["id_riferimento"])) { return false; } } return true; } /** * Ritorna l'idAvviso dinamicamente * * @return string|null */ public function getANACNoticeID() : ?string { if(empty($this->scheda["info"])) return NULL; $info = unserialize($this->scheda["info"]); return empty($info->idNuovoAvviso) ? $info->idAvviso : $info->idNuovoAvviso; } /** * Crea una revisione (modifica/rettifica) di una scheda * * @param boolean $conserva_dati * @param string $tipo * @param string|null $message Viene popolato in caso di errore con il messaggio d'errore * @param array|null $dati Viene popolato in caso di successo con la query per arrivare alla scheda * @return bool */ public function createRevision(bool $conserva_dati, string $tipo, ?string &$message=null, ?array &$dati = null) : bool{ $message = "Operazione non valida"; if(empty($this->scheda)) return false; if(!in_array($tipo, ["modifica", "rettifica"])) return false; if($tipo === "modifica") { if(!$this->isCardModifiable()) { $message = "Questa scheda non può essere modificata."; return false; } $tipo_codice = "codice_modifica"; } else { if(!$this->isCardRectifiable()) { $message = "Questa scheda non può essere rettificata."; return false; } $tipo_codice = "codice_rettifica"; } $nuovo_codice_guue = null; if(!empty($this->scheda["codice_scheda_guue"])) { $nuovo_codice_guue = eFormsManager::duplicaScheda($this->scheda["codice_scheda_guue"], true, $conserva_dati); } $salva = new salva(); $salva->nome_tabella = "b_npa_schede"; $salva->oggetto = $this->scheda; unset($salva->oggetto["codice"]); $salva->operazione = "INSERT"; unset($salva->oggetto["dati"]); unset($salva->oggetto["codice_rettifica"]); unset($salva->oggetto["codice_modifica"]); if(isset($_SESSION["utente"])) { $salva->oggetto["utente_modifica"] = $_SESSION["utente"]->codice; } $salva->ignore = ["timestamp_trasmissione", "timestamp_modifica", "uuid", "stato", "info", "pubblicazione"]; $salva->oggetto[$tipo_codice] = $this->scheda["codice"]; $salva->oggetto["timestamp_creazione"] = date("d/m/Y H:i:s"); if($nuovo_codice_guue !== null) { $salva->oggetto["codice_scheda_guue"] = $nuovo_codice_guue; } else { $salva->ignore[] = "codice_scheda_guue"; } if($conserva_dati) { $salva->oggetto["dati"] = $this->scheda["dati"]; } else { if(valid_json($this->scheda["dati"], $dati, true) && isset($dati["anacForm"]["stazioniAppaltanti"])) { $salva->oggetto["dati"] = json_encode([ "anacForm" => [ "stazioniAppaltanti" => $dati["anacForm"]["stazioniAppaltanti"] ] ]); } } $codice = $salva->save(); if(!empty($codice)) { $dati = [ "codice" => $codice, "scheda" => $salva->oggetto["id_scheda"], "id_riferimento" => $this->fascicolo["id_riferimento"], "modulo_riferimento" => $this->fascicolo["modulo_riferimento"], ]; return true; } $message = "Impossibile creare la {$tipo}"; return false; } /** * Verifica che la scheda attuale rispetti ancora i requisiti di soglia dell'oggetto * * @return bool */ public function checkSoglia() { if(empty($this->oggetto) || !method_exists($this->oggetto, "getSogliaGara")) return true; $sogliaGara = $this->oggetto->getSogliaGara(); $metadata = $this->metadataSchede[$this->scheda["id_scheda"]]; if($sogliaGara == "soprasoglia" && !$metadata["soprasoglia"]) return false; return true; } /** * Verifica che non ci siano rettifiche o modifiche in corso di compilazione nel fascicolo * * @param array|null $filters Se indicato, indica filtri extra per cercare * @param array $additionalCards Se specificato, indica le schede extra che vanno trattate come rettifiche * @return boolean */ public function hasPendingRettificheOrModifiche(?array $filters = null, array $additionalCards = []) { return self::hasPendingRettificheOrModificheStatic($this->fascicolo["codice"], $filters, $additionalCards ); } /** * Verifica che non ci siano rettifiche o modifiche in corso di compilazione nel fascicolo * * @param int $codice_npa * @param array|null $filters Se indicato, indica filtri extra per cercare * @param array $additionalCards Se specificato, indica le schede extra che vanno trattate come rettifiche * @return boolean */ public static function hasPendingRettificheOrModificheStatic(int $codice_npa, ?array $filters = null, array $additionalCards = []) { $filters = empty($filters) ? [] : $filters; $filters["stato"] = ["<", 20]; // Solo pending $filters["codice_npa"] = $codice_npa; // Solo di questo fascicolo $orFilters = [ "codice_rettifica" => [">", 0], "codice_modifica" => [">", 0], ]; if(!empty($additionalCards)) { $orFilters["id_scheda"] = ["IN", $additionalCards]; } $filters[uniqid("or_")] = $orFilters; $rettifiche = self::fetchSchede( ["id_scheda"], $filters ); return !empty($rettifiche); } /** * Verifica tra le schede trasmesse se sono in uso partecipanti o aggiudicatari * * @param boolean $aggiudicatari Se true ricerca schede con aggiudicatari, altrimenti schede con partecipanti * @param boolean $unlockOnRettifica Se true e c'è una rettifica in corso non vengono bloccati i partecipanti * @return boolean */ public function hasLockOnPartecipanti(bool $aggiudicatari = false, bool $unlockOnRettifica = true, ?int $codice_lotto_npa = null) { return self::hasLockOnPartecipantiStatic($this->fascicolo["codice"], $aggiudicatari, $unlockOnRettifica, $codice_lotto_npa); } /** * Ottiene l'ultima versione di un dato all'interno delle schede inviate * @param string $dotPath * @param bool $onlyCurrentLot * @return array|null */ public function getAnacLastSent(string $dotPath, bool $onlyCurrentLot = true) : ?array { $completedCards = $this->getLastCompletedCards($onlyCurrentLot); foreach($completedCards as $scheda) { $dati = json_decode($scheda["dati"], true); $match = DotPath::get($dati, $dotPath, null); if($match !== null) { return $match; } } return null; } /** * Verifica tra le schede trasmesse se sono in uso partecipanti o aggiudicatari * * @param int $codice_npa Codice fascicolo NPA di riferimento * @param boolean $aggiudicatari Se true ricerca schede con aggiudicatari, altrimenti schede con partecipanti * @param boolean $unlockOnRettifica Se true e c'è una rettifica in corso non vengono bloccati i partecipanti * @return boolean */ public static function hasLockOnPartecipantiStatic(int $codice_npa, bool $aggiudicatari = false, bool $unlockOnRettifica = true, $codice_lotto_npa = null) { $filterSchede = [ "stato" => [">=", 20], "codice_npa" => $codice_npa, ]; if(! empty($codice_lotto_npa)) { $filters["codice_lotto"] = ["OR", [$codice_lotto_npa, null, '', 0]]; } $schedeTrasmesse = self::fetchSchede(["id_scheda"], $filterSchede); if(empty($schedeTrasmesse)) { return false; } // Ci servono solo gli idScheda $schedeTrasmesse = array_unique(array_column($schedeTrasmesse, "id_scheda", null)); $metadataSchede = self::getCardsMetadata(); foreach($schedeTrasmesse as $scheda) { if($scheda === "S1") { continue; } // Le S1 vivono nel loro mondo, isolate da tutti, beate loro. $metadata = $metadataSchede[$scheda]; $shouldLock = $metadata["aggiudicatari"]; if(!$shouldLock && !$aggiudicatari) { $shouldLock = $metadata["partecipanti"]; } if($shouldLock) { // Verifichiamo che non sia in corso una rettifica, in quel caso possiamo sbloccare if($unlockOnRettifica) { $hasPendingRettificheOrModifiche = self::hasPendingRettificheOrModificheStatic($codice_npa, ["id_scheda" => $scheda]); if($hasPendingRettificheOrModifiche) return false; } return true; } } return false; } private $formRequirements = []; /** * Verifica che la scheda corrente richieda partecipanti o aggiudicatari e setta continue su false se non dovessero essere presenti * * @return array */ public function verifyFormRequirements() : array{ if(!empty($this->formRequirements)) return $this->formRequirements; $suggestions = $this->getSuggestions(); $idScheda = $this->scheda["id_scheda"]; $metadata = $this->metadataSchede[$idScheda]; $lottoCorrente = $this->scheda["codice_lotto"]; $retval = [ "continue" => true, "aggiudicazioni" => $metadata["aggiudicatari"], "partecipanti" => $metadata["partecipanti"] ]; $toCheck = []; if($idScheda === "S4") { $ultimiAggiudicatari = $this->getAnacLastSent("anacForm.aggiudicazioni", false); if(!empty($ultimiAggiudicatari)) { foreach($ultimiAggiudicatari as $aggiudicazione) { $lotIdentifier = "LOT-" . str_pad($this->lot["numero"], 4, "0", STR_PAD_LEFT); if(!isset($aggiudicazione["lotIdentifier"]) || $aggiudicazione["lotIdentifier"] === $lotIdentifier) { $ultimiAggiudicatariInviati = array_unique(recursive_array_key_search_all("idPartecipante", $aggiudicazione)); } } } $aggiudicatariCorrenti = array_unique(array_column($this->getPartecipanti(true, false), "uuid")); $retval["S4"] = true; $retval["continue"] = !empty(arrayDiff($aggiudicatariCorrenti, $ultimiAggiudicatariInviati)); } elseif($metadata["aggiudicatari"]) { if($lottoCorrente == 0) { foreach($this->getLots() as $lot) { $this->setLotto($lot["codice_lotto_npa"]); $agg = $this->getPartecipanti(true); if(empty($agg)) { $retval["non_aggiudicati"][] = $lot; } $this->unsetLotto(); } $retval["continue"] = count($this->getLots()) > count($retval["non_aggiudicati"] ?? []); } else { $agg = $this->getPartecipanti(true); if(empty($agg)) { // Le soprasoglia possono partire senza aggiudicazioni if($metadata['soprasoglia'] ?? false) { if(!in_array($this->lot['stato'], [99, 95])) { $retval["non_aggiudicati"][] = $this->lot; } // Evitiamo che l'utente possa compilare manualmente questa sezione $this->locked_sections[] = "anacForm.aggiudicazioni.*.offertePresentate.*."; $this->locked_repetitions["offertePresentate"] = true; } else { $retval["continue"] = false; } } } } elseif($metadata["partecipanti"]) { $toCheck = DotPath::get($suggestions, "anacForm.elencoSoggetti", []); } foreach($toCheck as $lotto) { if(empty($lotto["partecipanti"])) { $retval["continue"] = false; break; } } $this->formRequirements = $retval; return $retval; } }