• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Implementing automated compliance reporting for custom shipping tracking histories ledgers using mpdf engine

Implementing automated compliance reporting for custom shipping tracking histories ledgers using mpdf engine

Data Model for Shipping History Ledgers

To implement automated compliance reporting for custom shipping tracking histories, a robust and auditable data model is paramount. This model must capture every significant event in a shipment’s lifecycle, including timestamps, locations, responsible parties, and any associated metadata relevant to compliance. We’ll define a conceptual schema that can be implemented in a relational database (e.g., PostgreSQL, MySQL) or a NoSQL document store, with a focus on immutability and auditability.

Each ledger entry should represent an atomic, immutable event. Key fields include:

  • event_id (UUID): Unique identifier for the event.
  • shipment_id (VARCHAR/UUID): Identifier for the shipment this event belongs to.
  • timestamp (DATETIME/TIMESTAMP): Precise time the event occurred.
  • event_type (ENUM/VARCHAR): Categorization of the event (e.g., ‘PICKED_UP’, ‘IN_TRANSIT’, ‘DELIVERED’, ‘CUSTOMS_CLEARED’, ‘DELAYED’, ‘EXCEPTION’).
  • location_latitude (DECIMAL): Latitude of the event location.
  • location_longitude (DECIMAL): Longitude of the event location.
  • location_description (VARCHAR): Human-readable description of the location (e.g., ‘Warehouse A, New York’, ‘Port of Los Angeles’).
  • actor_id (VARCHAR/UUID): Identifier for the entity (person, system, carrier) performing the action.
  • actor_type (ENUM/VARCHAR): Type of actor (e.g., ‘CARRIER’, ‘WAREHOUSE_STAFF’, ‘SYSTEM’, ‘CUSTOMER’).
  • notes (TEXT): Free-form text for additional details, crucial for compliance exceptions.
  • metadata (JSON/TEXT): Structured data for specific event types (e.g., customs declaration numbers, temperature readings for cold chain).
  • signature_hash (VARCHAR): Cryptographic hash of the event data and potentially a digital signature, ensuring integrity.

For immutability, once an event is recorded, it should never be updated or deleted. If corrections are needed, a new, compensating event should be added with a reference to the original erroneous event. This creates a clear, auditable trail.

Database Schema Example (PostgreSQL)

Here’s a PostgreSQL schema that enforces some of these principles. We’ll use a `CHECK` constraint to prevent direct updates to immutable fields and a trigger to automatically generate a signature hash.

CREATE TABLE shipping_history_ledger (
    event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    shipment_id VARCHAR(255) NOT NULL,
    timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
    event_type VARCHAR(50) NOT NULL,
    location_latitude DECIMAL(9, 6),
    location_longitude DECIMAL(9, 6),
    location_description VARCHAR(255),
    actor_id VARCHAR(255),
    actor_type VARCHAR(50),
    notes TEXT,
    metadata JSONB,
    signature_hash VARCHAR(128) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Index for efficient querying by shipment
CREATE INDEX idx_shipping_history_shipment_id ON shipping_history_ledger (shipment_id);
CREATE INDEX idx_shipping_history_timestamp ON shipping_history_ledger (timestamp);

-- Function to generate signature hash
CREATE OR REPLACE FUNCTION generate_signature_hash()
RETURNS TRIGGER AS $$
DECLARE
    data_to_hash TEXT;
BEGIN
    -- Concatenate relevant fields for hashing. Exclude signature_hash itself and created_at.
    data_to_hash := NEW.event_id::TEXT || NEW.shipment_id || NEW.timestamp::TEXT || NEW.event_type ||
                    COALESCE(NEW.location_latitude::TEXT, '') || COALESCE(NEW.location_longitude::TEXT, '') ||
                    COALESCE(NEW.location_description, '') || COALESCE(NEW.actor_id, '') ||
                    COALESCE(NEW.actor_type, '') || COALESCE(NEW.notes, '') || COALESCE(NEW.metadata::TEXT, '');

    -- Use SHA256 for hashing. Ensure consistent casting and null handling.
    NEW.signature_hash := sha256(data_to_hash::bytea);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Trigger to call the function before insert
CREATE TRIGGER trg_before_insert_signature
BEFORE INSERT ON shipping_history_ledger
FOR EACH ROW
EXECUTE FUNCTION generate_signature_hash();

-- Optional: Prevent updates to critical fields
-- This is a basic example; more robust immutability might involve application-level logic
-- or more complex triggers that check OLD vs NEW values.
-- For simplicity, we'll rely on application logic for updates and focus on insert integrity.
-- If updates were strictly forbidden, you'd use a BEFORE UPDATE trigger to RAISE EXCEPTION.

PHP Implementation for Event Recording

The PHP application layer is responsible for creating these ledger entries. It should validate incoming data, ensure all required fields are present, and then insert the record into the database. The database trigger handles the `signature_hash` generation.

Consider a service class that abstracts the ledger interaction.

<?php

namespace App\Services;

use App\Database\Connection; // Assuming a PDO connection wrapper
use Ramsey\Uuid\Uuid;

class ShippingLedgerService
{
    private $db;

    public function __construct(Connection $db)
    {
        $this->db = $db->getConnection();
    }

    /**
     * Records a new event in the shipping history ledger.
     *
     * @param string $shipmentId
     * @param string $eventType
     * @param \DateTimeInterface $timestamp
     * @param array $location (['latitude' => float, 'longitude' => float, 'description' => string])
     * @param string|null $actorId
     * @param string|null $actorType
     * @param string|null $notes
     * @param array|null $metadata
     * @return string The UUID of the newly created event.
     * @throws \Exception If the record fails to insert.
     */
    public function recordEvent(
        string $shipmentId,
        string $eventType,
        \DateTimeInterface $timestamp,
        array $location = [],
        ?string $actorId = null,
        ?string $actorType = null,
        ?string $notes = null,
        ?array $metadata = null
    ): string {
        $eventId = Uuid::uuid4()->toString();

        $sql = "INSERT INTO shipping_history_ledger (
                    event_id, shipment_id, timestamp, event_type,
                    location_latitude, location_longitude, location_description,
                    actor_id, actor_type, notes, metadata
                ) VALUES (
                    :event_id, :shipment_id, :timestamp, :event_type,
                    :location_latitude, :location_longitude, :location_description,
                    :actor_id, :actor_type, :notes, :metadata
                )";

        $stmt = $this->db->prepare($sql);

        $params = [
            ':event_id' => $eventId,
            ':shipment_id' => $shipmentId,
            ':timestamp' => $timestamp->format('Y-m-d H:i:sP'), // ISO 8601 with timezone
            ':event_type' => $eventType,
            ':location_latitude' => $location['latitude'] ?? null,
            ':location_longitude' => $location['longitude'] ?? null,
            ':location_description' => $location['description'] ?? null,
            ':actor_id' => $actorId,
            ':actor_type' => $actorType,
            ':notes' => $notes,
            ':metadata' => $metadata ? json_encode($metadata) : null,
        ];

        try {
            $stmt->execute($params);
            // The signature_hash is automatically generated by the DB trigger.
            // We could fetch it back if needed, but for basic recording, it's not strictly necessary here.
            return $eventId;
        } catch (\PDOException $e) {
            // Log the error properly in a production environment
            error_log("Failed to record shipping event: " . $e->getMessage());
            throw new \Exception("Could not record shipping event.", 0, $e);
        }
    }

    /**
     * Retrieves the full history for a given shipment.
     *
     * @param string $shipmentId
     * @return array
     */
    public function getShipmentHistory(string $shipmentId): array
    {
        $sql = "SELECT * FROM shipping_history_ledger WHERE shipment_id = :shipment_id ORDER BY timestamp ASC";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([':shipment_id' => $shipmentId]);
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }

    /**
     * Verifies the integrity of a shipment's history.
     *
     * @param string $shipmentId
     * @return bool True if all records are intact, false otherwise.
     */
    public function verifyShipmentIntegrity(string $shipmentId): bool
    {
        $events = $this->getShipmentHistory($shipmentId);
        foreach ($events as $event) {
            $originalHash = $event['signature_hash'];
            // Re-calculate hash based on stored data, excluding the signature_hash itself.
            $dataToHash = $event['event_id'] . $event['shipment_id'] . $event['timestamp'] . $event['event_type'] .
                          ($event['location_latitude'] ?? '') . ($event['location_longitude'] ?? '') .
                          ($event['location_description'] ?? '') . ($event['actor_id'] ?? '') .
                          ($event['actor_type'] ?? '') . ($event['notes'] ?? '') .
                          ($event['metadata'] ?? ''); // Assuming metadata is stored as string or JSON string

            $calculatedHash = hash('sha256', $dataToHash);

            if ($originalHash !== $calculatedHash) {
                // Log the discrepancy for auditing
                error_log("Integrity check failed for shipment {$shipmentId}, event {$event['event_id']}. Expected hash: {$originalHash}, calculated: {$calculatedHash}");
                return false;
            }
        }
        return true;
    }
}
?>

Automated PDF Report Generation with mPDF

The mPDF library is a powerful PHP library for generating PDF documents from HTML and CSS. It’s suitable for creating compliance reports that need to be human-readable and formally presented.

First, ensure you have mPDF installed via Composer:

composer require mpdf/mpdf

Next, create a PHP script that fetches shipment history and renders it into a PDF. This script will act as the report generator.

<?php

require_once __DIR__ . '/vendor/autoload.php'; // Adjust path as necessary

use App\Services\ShippingLedgerService;
use App\Database\Connection; // Assuming your DB connection setup

// --- Configuration ---
$shipmentIdToReport = 'SHIPMENT-XYZ-12345'; // This would typically come from a request parameter or cron job
$outputFilePath = __DIR__ . '/reports/compliance_report_' . $shipmentIdToReport . '_' . date('YmdHis') . '.pdf';
$logoPath = __DIR__ . '/public/images/company_logo.png'; // Path to your company logo

// --- Dependencies ---
// Initialize your database connection (replace with your actual connection logic)
$dbConnection = new Connection('mysql:host=localhost;dbname=your_db', 'user', 'password');
$ledgerService = new ShippingLedgerService($dbConnection);

// --- Report Generation Logic ---
try {
    // 1. Fetch shipment history
    $history = $ledgerService->getShipmentHistory($shipmentIdToReport);

    if (empty($history)) {
        throw new \Exception("No history found for shipment ID: {$shipmentIdToReport}");
    }

    // 2. Verify integrity (optional but recommended for compliance)
    $isIntegrityValid = $ledgerService->verifyShipmentIntegrity($shipmentIdToReport);

    // 3. Prepare HTML content for the PDF
    $html = '<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Compliance Report - Shipment ' . htmlspecialchars($shipmentIdToReport) . '</title>
    <style>
        body { font-family: DejaVu Sans, sans-serif; font-size: 10pt; }
        .header { text-align: center; margin-bottom: 20px; }
        .header img { max-width: 150px; margin-bottom: 10px; }
        .report-title { font-size: 16pt; font-weight: bold; margin-bottom: 15px; }
        .shipment-details { margin-bottom: 20px; border-collapse: collapse; width: 100%; }
        .shipment-details th, .shipment-details td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        .shipment-details th { background-color: #f2f2f2; }
        .history-table { border-collapse: collapse; width: 100%; margin-top: 20px; }
        .history-table th, .history-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        .history-table th { background-color: #f2f2f2; }
        .history-table tr:nth-child(even) { background-color: #f9f9f9; }
        .integrity-status { font-weight: bold; color: ' . ($isIntegrityValid ? 'green' : 'red') . '; }
        .metadata-block { background-color: #eee; padding: 5px; margin-top: 5px; font-size: 9pt; white-space: pre-wrap; word-wrap: break-word; }
    </style>
</head>
<body>
    <div class="header">';

if (file_exists($logoPath)) {
    // mPDF requires absolute paths or URLs for images.
    // Ensure $logoPath is an absolute path.
    $absLogoPath = realpath($logoPath);
    $html .= '<img src="file://' . $absLogoPath . '" alt="Company Logo"><br>';
}

$html .= '
        <div class="report-title">Shipping Compliance History Report</div>
        <p>Generated on: ' . date('Y-m-d H:i:s') . '</p>
    </div>

    <table class="shipment-details">
        <tr><th>Shipment ID</th><td>' . htmlspecialchars($shipmentIdToReport) . '</td></tr>
        <tr><th>Integrity Status</th><td class="integrity-status">' . ($isIntegrityValid ? 'VALID' : 'INVALID') . '</td></tr>
        <tr><th>First Event</th><td>' . htmlspecialchars($history[0]['timestamp']) . '</td></tr>
        <tr><th>Last Event</th><td>' . htmlspecialchars(end($history)['timestamp']) . '</td></tr>
    </table>

    <h3>Event Log</h3>
    <table class="history-table">
        <thead>
            <tr>
                <th>Timestamp</th>
                <th>Event Type</th>
                <th>Location</th>
                <th>Actor</th>
                <th>Notes</th>
                <th>Metadata</th>
                <th>Signature Hash</th>
            </tr>
        </thead>
        <tbody>';

    foreach ($history as $event) {
        $html .= '<tr>';
        $html .= '<td>' . htmlspecialchars($event['timestamp']) . '</td>';
        $html .= '<td>' . htmlspecialchars($event['event_type']) . '</td>';
        $location = trim($event['location_description'] ?? '');
        if (!empty($event['location_latitude']) && !empty($event['location_longitude'])) {
            $location .= ' (' . $event['location_latitude'] . ', ' . $event['location_longitude'] . ')';
        }
        $html .= '<td>' . htmlspecialchars($location) . '</td>';
        $actor = trim($event['actor_type'] ?? '' . ' ' . $event['actor_id'] ?? '');
        $html .= '<td>' . htmlspecialchars($actor) . '</td>';
        $html .= '<td>' . nl2br(htmlspecialchars($event['notes'] ?? '')) . '</td>';

        $metadataHtml = '';
        if (!empty($event['metadata'])) {
            $metadata = is_string($event['metadata']) ? json_decode($event['metadata'], true) : $event['metadata'];
            if (is_array($metadata)) {
                $metadataHtml = json_encode($metadata, JSON_PRETTY_PRINT);
            } else {
                $metadataHtml = htmlspecialchars($event['metadata']);
            }
        }
        $html .= '<td><div class="metadata-block">' . htmlspecialchars($metadataHtml) . '</div></td>';
        $html .= '<td>' . htmlspecialchars($event['signature_hash']) . '</td>';
        $html .= '</tr>';
    }

    $html .= '
        </tbody>
    </table>

</body>
</html>';

    // 4. Initialize mPDF
    // Use 'en-GB' for locale, 'UTF-8' for encoding.
    // 'default_font' is important if you don't specify fonts in CSS. DejaVu Sans is good for broad character support.
    $mpdf = new \Mpdf\Mpdf([
        'mode' => 'utf-8',
        'format' => 'A4-P', // Portrait A4
        'default_font' => 'dejavusans',
        'tempDir' => sys_get_temp_dir() . '/mpdf_temp' // Specify a writable temp directory
    ]);

    // Set some basic PDF metadata
    $mpdf->SetCreator("YourCompany Compliance Bot");
    $mpdf->SetAuthor("YourCompany Compliance Bot");
    $mpdf->SetTitle("Compliance Report - Shipment " . $shipmentIdToReport);
    $mpdf->SetSubject("Shipping Compliance History");
    $mpdf->SetKeywords("compliance, shipping, history, audit");

    // 5. Write HTML to PDF
    $mpdf->WriteHTML($html);

    // 6. Ensure output directory exists
    $outputDir = dirname($outputFilePath);
    if (!is_dir($outputDir)) {
        if (!mkdir($outputDir, 0755, true)) {
            throw new \Exception("Failed to create output directory: {$outputDir}");
        }
    }

    // 7. Save the PDF
    $mpdf->Output($outputFilePath, \Mpdf\Output\Destination::FILE);

    echo "Compliance report generated successfully: " . $outputFilePath . "\n";

} catch (\Exception $e) {
    // Log the error properly
    error_log("Error generating compliance report: " . $e->getMessage());
    echo "Error: " . $e->getMessage() . "\n";
    // In a web context, you'd return an error response.
}
?>

Automating Report Generation (Cron Jobs)

To make this process truly automated, you’ll want to schedule the report generation script to run periodically. This is typically done using cron jobs on Linux/macOS systems or Task Scheduler on Windows.

For example, to generate a report for a specific shipment every day at 3:00 AM, you would add the following line to your crontab (edit with crontab -e):

0 3 * * * /usr/bin/php /path/to/your/project/generate_report.php --shipment-id SHIPMENT-XYZ-12345 >> /path/to/your/project/logs/cron.log 2>&1

Explanation:

  • 0 3 * * *: Cron schedule (minute 0, hour 3, every day, every month, every day of the week).
  • /usr/bin/php: The absolute path to your PHP executable. Use which php to find it.
  • /path/to/your/project/generate_report.php: The absolute path to your report generation script.
  • --shipment-id SHIPMENT-XYZ-12345: Passing the shipment ID as a command-line argument. You’ll need to modify the PHP script to parse these arguments (e.g., using $argv or a library like symfony/console).
  • >> /path/to/your/project/logs/cron.log 2>&1: Redirects both standard output and standard error to a log file, which is crucial for debugging cron jobs.

You might also want a cron job that iterates through all active shipments needing reports, or a system that triggers report generation upon specific events (e.g., shipment completion, customs clearance).

Security and Compliance Considerations

Data Integrity: The `signature_hash` is the cornerstone of data integrity. Regularly verifying these hashes (as demonstrated in `verifyShipmentIntegrity`) is critical. Any mismatch indicates tampering or data corruption.

Access Control: Ensure that only authorized personnel or systems can write to the ledger and generate reports. Implement robust authentication and authorization mechanisms.

Data Retention: Define and enforce data retention policies according to regulatory requirements. The ledger, being immutable, naturally supports long-term archival.

Audit Trails: The ledger itself serves as an audit trail. The PDF reports provide a human-readable snapshot for external auditors or internal reviews. Ensure the PDF generation process is also logged.

Error Handling and Monitoring: Implement comprehensive logging for both the ledger recording process and the PDF generation. Set up monitoring to alert administrators of any failures.

GDPR/Privacy: If shipment data includes Personally Identifiable Information (PII), ensure compliance with relevant data privacy regulations. The ledger should be designed to minimize PII where possible or handle it securely.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (662)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (5)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (647)
  • SEO & Growth (492)
  • Server (118)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (189)
  • WordPress Plugin Development (197)
  • WordPress Plugin Development (340)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala