Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/lib/__tests__/pdfExport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, it, expect } from "vitest";
import { exportVerifyResultToPDF } from "../../utils/pdfExport";
import type { VerifyBatchResponse } from "../api";

describe("pdfExport utility", () => {
const mockFullData: VerifyBatchResponse = {
success: true,
cached: false,
tokenId: "0.0.123456",
serialNumber: "5",
status: "verified",
verifiedAt: "2026-06-22T10:00:00.000Z",
nftMetadata: { name: "Batch #5 NFT" },
hcsTransactionIds: [
"0.0.123@1234567890.000000001",
"0.0.123@1234567890.000000002"
],
hcsMessages: [],
ai_summary: {
summary_en: "This batch contains organic premium coffee from Rwanda, fully verified on ledger.",
summary_fr: "Ce lot contient du café biologique premium du Rwanda, entièrement vérifié sur le registre.",
trustScore: 92,
trustExplanation: "All metadata matches the HCS ledger transactions and supply chain checkpoints perfectly.",
timeline: [
{
timestamp: "2026-06-20T08:00:00.000Z",
event: "Harvested arabica beans in Rwanda",
txId: "0.0.123@1234567890.000000001"
},
{
timestamp: "2026-06-21T10:00:00.000Z",
event: "Processed and packaged at cooperative",
txId: "0.0.123@1234567890.000000002"
}
],
generatedAt: "2026-06-22T10:00:00.000Z",
ms: 120
},
batch: {
id: "batch-uuid-111",
batch_name: "Kigali Select Arabica",
product_type: "Coffee Beans",
quantity: "250 kg",
location: "Kigali, Rwanda",
harvest_date: "2026-06-20",
photo_url: "https://example.com/photo.jpg",
hcs_tx_id: "0.0.123@1234567890.000000001",
created_at: "2026-06-22T10:00:00.000Z"
}
};

const mockMinimalData: VerifyBatchResponse = {
success: true,
cached: true,
tokenId: "0.0.789",
serialNumber: "1",
status: "registered",
verifiedAt: "2026-06-22T10:30:00.000Z",
nftMetadata: null,
hcsTransactionIds: [],
hcsMessages: []
};

it("should successfully generate a PDF document from full verified batch data", () => {
const doc = exportVerifyResultToPDF(mockFullData, {
qrCodeDataUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
language: "en"
});

expect(typeof doc.save).toBe("function");
expect(typeof doc.output).toBe("function");
expect(doc.internal.getNumberOfPages()).toBeGreaterThanOrEqual(1);
});

it("should successfully generate a PDF document with minimal verified batch data", () => {
const doc = exportVerifyResultToPDF(mockMinimalData);

expect(typeof doc.save).toBe("function");
expect(typeof doc.output).toBe("function");
expect(doc.internal.getNumberOfPages()).toBe(1);
});

it("should support French language summary if selected", () => {
const doc = exportVerifyResultToPDF(mockFullData, { language: "fr" });
expect(typeof doc.save).toBe("function");
});
});
41 changes: 40 additions & 1 deletion src/pages/BatchVerify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ import {
Calendar,
Download,
Camera,
FileText,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import axios from "axios";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { Helmet } from "react-helmet-async";
import { CopyButton } from "@/components/CopyButton";
import { exportVerifyResultToPDF } from "@/utils/pdfExport";

type NotFoundResult = Extract<VerifyBatchResult, { reason: "not_found" }>;

Expand Down Expand Up @@ -235,6 +237,34 @@ export default function BatchVerify() {
}
};

const handleExportPDF = () => {
if (!verifiedResult) return;
try {
const canvas = document.getElementById("verify-qr-canvas") as HTMLCanvasElement;
const qrCodeDataUrl = canvas?.toDataURL("image/png");

const doc = exportVerifyResultToPDF(verifiedResult, {
qrCodeDataUrl,
language,
});

const filenameId = verifiedResult.batch?.id || params.batchId || `${verifiedResult.tokenId}_${verifiedResult.serialNumber}`;
doc.save(`agrodex-certificate-${filenameId}.pdf`);

toast({
title: "PDF Exported",
description: "Your verification certificate has been downloaded.",
});
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to generate PDF.";
toast({
title: "Export Error",
description: message,
variant: "destructive",
});
}
};

return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50 dark:from-blue-950/20 dark:via-background dark:to-indigo-950/20 dark:bg-background text-foreground">
<Helmet>
Expand Down Expand Up @@ -450,10 +480,19 @@ export default function BatchVerify() {
{/* Only show details if batch was found */}
{verifiedResult && (
<Card className="border-blue-200 dark:border-blue-950/30 bg-card text-card-foreground shadow-lg">
<CardHeader className="pb-4">
<CardHeader className="pb-4 flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-xl font-bold text-gray-900 dark:text-white">
Verification Details
</CardTitle>
<Button
onClick={handleExportPDF}
variant="outline"
size="sm"
className="font-semibold border-rose-200 dark:border-rose-950/30 text-rose-600 hover:text-rose-700 hover:bg-rose-50 dark:hover:bg-rose-950/20 flex items-center gap-1.5"
>
<FileText className="h-4 w-4" />
Export PDF
</Button>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
Expand Down
Loading
Loading