Files
atlhousingreport/classes/Process_ImportRequest.php
2026-05-21 20:43:16 -04:00

599 lines
20 KiB
PHP

<?php
declare(strict_types=1);
$basePath = realpath(__DIR__ . '/..');
require_once $basePath . '/vendor/autoload.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
// =====================
// Process_ImportRequest
// =====================
class Process_ImportRequest {
// Variables
private $log = "";
private $settings = "";
private $connection = "";
// ==================
// Class Constructor.
// ==================
public function __construct() {
$basePath = realpath(__DIR__ . '/..');
$logfile = $basePath . "/scripts/importrequests.log";
// ==========================
// Log Rotation (50 MB limit)
// ==========================
$maxLogSize = 50 * 1024 * 1024; // 50 MB
if (file_exists($logfile) && filesize($logfile) >= $maxLogSize) {
$rotated = $logfile . '-';
rename($logfile, $rotated);
}
$this->log = fopen($logfile, "a");
if ($this->log === false) {
error_log("Process_ImportRequest : Unable to open log file");
exit();
}
$this->log(str_repeat("*", 50));
// Get Settings
$this->settings = new SimpleXMLElement((file_exists($basePath . "/xml/settings.xml") ? file_get_contents($basePath . "/xml/settings.xml") : "<settings/>"));
if ($this->settings->count() === 0) {
error_log("Process_ImportRequest : Unable to initialize settings");
exit();
}
// Open Database Connections
$this->connection = $this->connect();
}
// ================================================
// Return a New DEC-DR Database Database Connection
// ================================================
public function connect() {
if ($this->settings->Environment == 'Production') {
$host = 'localhost';
} else {
$host = '192.168.1.190';
}
try {
$database = (string) $this->settings->DatabaseName;
$user = (string) $this->settings->DatabaseUser;
$password = (string) $this->settings->DatabasePassword;
$connection = new PDO("mysql:host={$host};dbname={$database};charset=utf8", $user, $password);
$connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException | Exception $e) {
trigger_error($e->getMessage());
}
return $connection;
}
// =============================
// Log a message to the Log File
// =============================
private function log($message = "") {
fwrite($this->log, date("Y-m-d H:i A") . " : " . "{$message} \n");
}
// ===================
// Process Parcel Data
// ===================
public function process() {
$this->log("Starting All Record Import Job");
$this->runImportJob();
$this->purgeImportJobs();
$this->log("Completed All Record Import Job");
}
// ==============
// Run Import Job
// ==============
private function runImportJob() {
$this->log("Processing Import Job");
$nextjob = $this->claimNextJob();
if (!$nextjob) {
$this->log("Nothing to do.");
return;
}
$importjob_serial = (int) $nextjob['importjob_serial'];
$importjob_file_path = (string) $nextjob['importjob_file_path'];
try {
$this->parseCSVFile($importjob_file_path);
$SQL = "UPDATE importjobs
SET importjob_status = 'complete',
importjob_completed = NOW()
WHERE importjob_serial = :importjob_serial ";
$update = $this->connect()->prepare($SQL);
$update->execute([':importjob_serial' => $importjob_serial]);
$this->sendWebHookAlert("Import Job completed successfully.");
$this->email_admins("Import Job Success.", "Import Job completed successfully.");
} catch (Throwable $e) {
$SQL = "UPDATE importjobs
SET importjob_status = 'failed',
importjob_completed = NOW(),
importjob_error_message = :importjob_error_message
WHERE importjob_serial = :importjob_serial";
$fail = $this->connect()->prepare($SQL);
$fail->execute([':importjob_serial' => $importjob_serial, ':importjob_error_message' => $e->getMessage()]);
// Notify Admins of error
$this->sendWebHookAlert("Import Job {$importjob_serial} has failed. Check Error Logs for details.");
$this->email_admins("Import Job Failure.", "Import Job {$importjob_serial} has failed. Check Error Logs for details.");
}
}
// =================================
// Purge Imports Older then 24 Hours
// =================================
private function purgeImportJobs() {
$this->log("Purging Import Jobs Database Files");
$this->purgeFiles();
}
// =======================
// Claim Next Job in Queue
// =======================
private function claimNextJob() {
$this->connection->beginTransaction();
$SQL = "SELECT view_importjobs.*
FROM view_importjobs
WHERE view_importjobs.importjob_status = 'queued'
ORDER BY view_importjobs.importjob_created ASC
LIMIT 1 FOR UPDATE";
$statement = $this->connection->prepare($SQL);
$statement->execute();
$job = $statement->fetch(PDO::FETCH_ASSOC);
if (!$job) {
$this->connection->commit();
return null;
}
$SQL = "UPDATE importjobs
SET importjob_status = 'running'
WHERE importjob_serial = :importjob_serial";
$statement = $this->connection->prepare($SQL);
$statement->execute([':importjob_serial' => (int) $job['importjob_serial']]);
$this->connection->commit();
return $job;
}
// ====================
// Purges Files on Disk
// ====================
private function purgeFiles() {
$SQL = "select *
from importjobs
where importjobs.importjob_created < (NOW() - INTERVAL 24 HOUR)";
$importfiles = $this->connect()->query($SQL)->fetchAll(PDO::FETCH_ASSOC);
if (count($importfiles) == 0) {
$this->log("Nothing To Purge");
return;
}
foreach ($importfiles as $files) {
$filename = $files['importjob_file_path'];
if (is_file($filename)) {
unlink($filename);
}
}
$baseDir = "./uploads/Production/";
if (!is_dir($baseDir)) {
$this->log("Imports directory does not exist: {$baseDir}");
} else {
foreach (new DirectoryIterator($baseDir) as $item) {
if ($item->isDot() || !$item->isDir()) {
continue;
}
$folderPath = $item->getPathname();
// Check if directory is empty
$files = scandir($folderPath);
if ($files !== false && count($files) === 2) {
rmdir($folderPath);
}
}
}
$this->log("Purging Import Jobs Database Records");
$SQL = "delete from importjobs where importjobs.importjob_created < (NOW() - INTERVAL 24 HOUR)";
$this->connection->query($SQL);
}
// =======================
// Format Raw Phone Number
// =======================
public function formatPhoneNumber(string $phonenumber): string {
// Remove everything except digits
$digits = preg_replace('/\D+/', '', $phonenumber);
// Handle leading country code (US)
if (strlen($digits) === 11 && str_starts_with($digits, '1')) {
$digits = substr($digits, 1);
}
return substr($digits, 0, 3) . '-' . substr($digits, 3, 3) . '-' . substr($digits, 6, 4);
}
// ==============
// Parse CSV File
// ==============
public function parseCSVFile($file) {
$csvfile = fopen($file, 'r');
if ($csvfile === false) {
throw new RuntimeException("Unable to open CSV file: {$file}");
}
$RecordCount = 0;
while (($data = fgetcsv($csvfile, 0, ",", '"', '\\')) !== FALSE) {
if (count($data) < 36) {
$this->log("Skipping malformed CSV row with " . count($data) . " columns.");
continue;
}
$PermitFactsId = $data[1];
$EntryDate = $this->fixDate($data[2]);
$PermitNumber = $data[4];
$PermitDate = $this->fixDate($data[5]);
$ProjectAddress = $data[7];
$SubdivisionName = $data[8];
$ProjectType = $data[9];
$Company = substr($data[10], 0, 30);
$Address = substr($data[11], 0, 30);
$City = $data[12];
$State = $data[13];
$Zip = $data[14];
$Phone = $data[15];
$ProjectCity = $data[16];
$ProjectState = $data[17];
$Size = $data[18] ?: 0;
$Units = $data[19];
$BuildingType = $data[20];
$Value = $data[21] ?: 0;
$WorkType = $data[22];
$County = $data[23] ?: 0;
$Lot = $data[24];
$DistrictLL = $data[25];
$Ctl = $data[26];
$OwnerName = substr($data[27], 0, 30);
$OwnerAddress = substr($data[28], 0, 30);
$OwnerCity = $data[29];
$OwnerState = substr($data[30], 0, 2);
$OwnerZip = $data[31];
$OwnerPhone = $data[32];
$YTDPermits = $data[33] ?: 0;
$YTDValue = $data[34] ?: 0;
$TwelveMonthPreviousPermits = $data[35] ?: 0;
$County = $this->getCountiesByName($County)['county_serial'] ?? 0;
$SQL = "INSERT INTO permits (
permit_permit_facts_id,
permit_entry_date,
permit_permit_number,
permit_permit_date,
permit_project_addr,
permit_sub_div_name,
permit_project_type,
permit_company,
permit_address,
permit_city,
permit_state,
permit_zip,
permit_phone,
permit_project_city,
permit_project_state,
permit_size,
permit_units,
permit_building_type,
permit_value,
permit_work_type,
permit_county,
permit_lot,
permit_dist_ll,
permit_ct1,
permit_owner_name,
permit_owner_address,
permit_owner_city,
permit_owner_state,
permit_owner_zip,
permit_owner_phone,
permit_ytd_permits,
permit_ytd_value,
permit_12mth_prev_permits )
VALUES (
:permit_permit_facts_id,
:permit_entry_date,
:permit_permit_number,
:permit_permit_date,
:permit_project_addr,
:permit_sub_div_name,
:permit_project_type,
:permit_company,
:permit_address,
:permit_city,
:permit_state,
:permit_zip,
:permit_phone,
:permit_project_city,
:permit_project_state,
:permit_size,
:permit_units,
:permit_building_type,
:permit_value,
:permit_work_type,
:permit_county,
:permit_lot,
:permit_dist_ll,
:permit_ct1,
:permit_owner_name,
:permit_owner_address,
:permit_owner_city,
:permit_owner_state,
:permit_owner_zip,
:permit_owner_phone,
:permit_ytd_permits,
:permit_ytd_value,
:permit_12mth_prev_permits )";
$statement = $this->connection->prepare($SQL);
$statement->bindValue(":permit_permit_facts_id", $PermitFactsId);
$statement->bindValue(":permit_permit_number", $PermitNumber);
$statement->bindValue(":permit_entry_date", $EntryDate);
$statement->bindValue(":permit_permit_date", $PermitDate);
$statement->bindValue(":permit_project_addr", $ProjectAddress);
$statement->bindValue(":permit_sub_div_name", $SubdivisionName);
$statement->bindValue(":permit_project_type", $ProjectType);
$statement->bindValue(":permit_company", $Company);
$statement->bindValue(":permit_address", $Address);
$statement->bindValue(":permit_city", $City);
$statement->bindValue(":permit_state", $State);
$statement->bindValue(":permit_zip", $Zip);
$statement->bindValue(":permit_phone", $Phone);
$statement->bindValue(":permit_project_city", $ProjectCity);
$statement->bindValue(":permit_project_state", $ProjectState);
$statement->bindValue(":permit_size", $Size);
$statement->bindValue(":permit_units", $Units);
$statement->bindValue(":permit_building_type", $BuildingType);
$statement->bindValue(":permit_value", $Value);
$statement->bindValue(":permit_work_type", $WorkType);
$statement->bindValue(":permit_county", $County);
$statement->bindValue(":permit_lot", $Lot);
$statement->bindValue(":permit_dist_ll", $DistrictLL);
$statement->bindValue(":permit_ct1", $Ctl);
$statement->bindValue(":permit_owner_name", $OwnerName);
$statement->bindValue(":permit_owner_address", $OwnerAddress);
$statement->bindValue(":permit_owner_city", $OwnerCity);
$statement->bindValue(":permit_owner_state", $OwnerState);
$statement->bindValue(":permit_owner_zip", $OwnerZip);
$statement->bindValue(":permit_owner_phone", $OwnerPhone);
$statement->bindValue(":permit_ytd_permits", $YTDPermits);
$statement->bindValue(":permit_ytd_value", $YTDValue);
$statement->bindValue(":permit_12mth_prev_permits", $TwelveMonthPreviousPermits);
$statement->execute();
$RecordCount++;
}
return true;
}
// ========================
// Return a Counties Object
// ========================
public function getCountiesByName($county_name) {
$SQL = "select view_counties.*
from view_counties
where view_counties.county_name = :county_name";
$statement = $this->connection->prepare($SQL);
$statement->execute([':county_name' => $county_name]);
$county_name = $statement->fetch(PDO::FETCH_ASSOC);
return $county_name;
}
// ==========================
// Send an Alert/Notification
// ==========================
public function sendWebHookAlert($message, $title = "DEC Application Alert", $alerttype = "1fff00") {
$webhookUrl = (string) $this->settings->DiscordWebHookURL;
$data = [
'embeds' => [[
'title' => $title,
'description' => $message,
'color' => hexdec($alerttype),
'footer' => [
'text' => 'Real Estate Data Inc. System Notificiation',
],
'timestamp' => date('c'), // ISO 8601 format
]]
];
$jsonData = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ch = curl_init($webhookUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
$response = curl_exec($ch);
if ($response === false) {
$this->log('Curl error: ' . curl_error($ch));
}
curl_close($ch);
}
// ============
// Email Admins
// ============
public function email_admins($subject = "", $message = "") {
$this->log(date("Y-m-d H:i:s") . " - Emailing Admins...");
$settings = new SimpleXMLElement((file_exists("./xml/settings.xml") ? file_get_contents("./xml/settings.xml") : "<settings/>"));
print_r($settings);
die();
// Server Variables
$host = (string) $settings->SmtpServer;
$username = (string) $settings->SmtpUserName;
$password = (string) $settings->SmtpPassword;
// Recipient Variables
$from_email = (string) $settings->FromEmail;
$from_name = (string) $settings->FromName;
$support_email = (string) $settings->SupportEmail;
$support_name = (string) $settings->SupportName;
if ((string) $settings->Environment == 'Production') {
$admin_email = (string) $settings->AdminEmail;
$admin_name = (string) $settings->AdminName;
}
// Content Variables
$mail = new PHPMailer(true);
try {
//Server settings
$mail->isSMTP();
$mail->Host = $host;
$mail->SMTPAuth = true;
$mail->Username = $username;
$mail->Password = $password;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mail->Port = 465;
//Recipients
$mail->setFrom($from_email, $from_name);
$mail->addAddress($support_email, $support_name);
if ($settings->Environment == 'Production') {
$mail->addAddress($admin_email, $admin_name);
}
//Content
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $message;
$mail->send();
} catch (Exception $e) {
$this->log("Message could not be sent. Mailer Error: {$e->ErrorInfo}");
}
}
// ========================
// Return an Formatted Date
// ========================
public function fixDate($givenDate) {
$formattedDate = date('Y-m-d', strtotime($givenDate));
return $formattedDate;
}
}
?>