Initial commit - Route Commerce platform

This commit is contained in:
2026-06-01 19:40:55 +00:00
commit 53a9671461
617 changed files with 106132 additions and 0 deletions
@@ -0,0 +1,175 @@
import { svcHeaders } from "@/lib/svc-headers";
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_PAT = process.env.SUPABASE_PAT!;
type LotRow = {
lot_id: string;
lot_number: string;
crop_type: string;
variety: string | null;
harvest_date: string;
field_location: string | null;
field_block: string | null;
worker_name: string | null;
packer_name: string | null;
quantity_lbs: number | null;
quantity_used_lbs: number | null;
yield_estimate_lbs: number | null;
yield_unit: string | null;
bin_id: string | null;
status: string;
};
type EventRow = {
lot_id: string;
event_type: string;
event_time: string;
location: string | null;
notes: string | null;
bin_id: string | null;
created_by_name: string | null;
};
async function adminFetchJson(endpoint: string, body: unknown) {
const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/${endpoint}`, {
method: "POST",
headers: {
...svcHeaders(SUPABASE_PAT),
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) return null;
return res.json();
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const brandId = searchParams.get("brandId");
const startDate = searchParams.get("startDate");
const endDate = searchParams.get("endDate");
if (!brandId || !startDate || !endDate) {
return new Response("Missing brandId, startDate, or endDate", { status: 400 });
}
const lotsRaw = await adminFetchJson("get_lots_for_period", {
p_brand_id: brandId,
p_start_date: startDate,
p_end_date: endDate,
});
const lots: LotRow[] = (lotsRaw ?? []) as LotRow[];
if (!lots || lots.length === 0) {
return new Response("No lots found for this period", { status: 404 });
}
const lotIds = lots.map((l) => l.lot_id);
const eventsRaw = await adminFetchJson("get_events_for_lots", { p_lot_ids: lotIds });
const allEvents: EventRow[] = (eventsRaw ?? []) as EventRow[];
const eventsByLot: Record<string, EventRow[]> = {};
for (const evt of allEvents) {
if (!eventsByLot[evt.lot_id]) eventsByLot[evt.lot_id] = [];
eventsByLot[evt.lot_id].push(evt);
}
const lines: string[] = [];
lines.push("FSMA FOOD SAFETY MODERNIZATION ACT — Produce Traceability Report");
lines.push(`Brand ID,${brandId}`);
lines.push(`Report Period,${startDate} to ${endDate}`);
lines.push(`Generated,${new Date().toLocaleString("en-US")}`);
lines.push(`Total Lots,${lots.length}`);
lines.push("");
const totalQty = lots.reduce((s, l) => s + (l.quantity_lbs ?? 0), 0);
const totalUsed = lots.reduce((s, l) => s + (l.quantity_used_lbs ?? 0), 0);
const crops = [...new Set(lots.map((l) => l.crop_type))];
const units = [...new Set(lots.map((l) => l.yield_unit ?? "lbs"))];
const primaryUnit = units.length === 1 ? units[0] : "lbs";
lines.push("SUMMARY");
lines.push(`Total Lots Harvested,${lots.length}`);
lines.push(`Total Weight (${primaryUnit}),${totalQty.toLocaleString()}`);
lines.push(`Total Used (${primaryUnit}),${totalUsed.toLocaleString()}`);
lines.push(`Remaining (${primaryUnit}),${(totalQty - totalUsed).toLocaleString()}`);
lines.push(`Crop Types,"${crops.join(", ")}"`);
lines.push(`Yield Unit,"${units.join(", ")}"`);
lines.push(`Date Range,${startDate} to ${endDate}`);
lines.push("");
lines.push("ONE-UP / ONE-DOWN TRACEABILITY");
lines.push("Lot Number,Crop,Variety,Harvest Date,Field,Block,Worker,Packer,Qty (lbs),Used (lbs),Remaining (lbs),Yield Est. (lbs),Yield Unit,Yield Variance %,Bin ID,Status,Event,Event Time,Location,Bin ID,Notes,Recorded By");
for (const lot of lots) {
const evts = eventsByLot[lot.lot_id] ?? [];
const remaining = (lot.quantity_lbs ?? 0) - (lot.quantity_used_lbs ?? 0);
if (evts.length === 0) {
const yieldVariance = (lot.yield_estimate_lbs && lot.yield_estimate_lbs > 0)
? (((lot.quantity_lbs ?? 0) - lot.yield_estimate_lbs) / lot.yield_estimate_lbs * 100).toFixed(1)
: "";
lines.push(esc([
lot.lot_number, lot.crop_type, lot.variety ?? "", lot.harvest_date ?? "",
lot.field_location ?? "", lot.field_block ?? "", lot.worker_name ?? "",
lot.packer_name ?? "", String(lot.quantity_lbs ?? ""),
String(lot.quantity_used_lbs ?? ""), String(Math.max(0, remaining)),
String(lot.yield_estimate_lbs ?? ""), lot.yield_unit ?? "",
yieldVariance,
lot.bin_id ?? "", lot.status ?? "",
"—", "—", "—", "—", "—", "—",
]));
} else {
for (let i = 0; i < evts.length; i++) {
const evt = evts[i];
const evtTime = new Date(evt.event_time).toLocaleString("en-US", {
year: "numeric", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit",
});
const yieldVariance = (lot.yield_estimate_lbs && lot.yield_estimate_lbs > 0)
? (((lot.quantity_lbs ?? 0) - lot.yield_estimate_lbs) / lot.yield_estimate_lbs * 100).toFixed(1)
: "";
lines.push(esc([
i === 0 ? lot.lot_number : "",
i === 0 ? lot.crop_type : "",
i === 0 ? (lot.variety ?? "") : "",
i === 0 ? (lot.harvest_date ?? "") : "",
i === 0 ? (lot.field_location ?? "") : "",
i === 0 ? (lot.field_block ?? "") : "",
i === 0 ? (lot.worker_name ?? "") : "",
i === 0 ? (lot.packer_name ?? "") : "",
i === 0 ? String(lot.quantity_lbs ?? "") : "",
i === 0 ? String(lot.quantity_used_lbs ?? "") : "",
i === 0 ? String(Math.max(0, remaining)) : "",
i === 0 ? String(lot.yield_estimate_lbs ?? "") : "",
i === 0 ? (lot.yield_unit ?? "") : "",
i === 0 ? yieldVariance : "",
i === 0 ? (lot.bin_id ?? "") : "",
i === 0 ? (lot.status ?? "") : "",
evt.event_type ?? "",
evtTime,
evt.location ?? "",
evt.bin_id ?? "",
(evt.notes ?? "").replace(/"/g, '""'),
evt.created_by_name ?? "",
]));
}
}
}
lines.push("");
lines.push("END OF REPORT");
lines.push("Powered by Route Commerce — routecommerce.com");
const csv = lines.join("\n");
return new Response(csv, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": `attachment; filename="fsma-report-${startDate}-to-${endDate}.csv"`,
},
});
}
function esc(values: string[]): string {
return values.map((v) => `"${v.replace(/"/g, '""')}"`).join(",");
}
@@ -0,0 +1,162 @@
import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
import QRCode from "qrcode";
import { svcHeaders } from "@/lib/svc-headers";
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_PAT = process.env.SUPABASE_PAT!;
async function getLotDetail(lotId: string) {
const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/get_harvest_lot_detail`, {
method: "POST",
headers: {
...svcHeaders(SUPABASE_PAT),
"Content-Type": "application/json",
},
body: JSON.stringify({ p_lot_id: lotId }),
});
if (!res.ok) return null;
const data = await res.json();
return data?.[0] ?? null;
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const lotId = searchParams.get("lotId");
const type = searchParams.get("type") ?? "field";
const size = searchParams.get("size") ?? "4x2";
const copies = Math.min(Math.max(parseInt(searchParams.get("copies") ?? "1"), 1), 10);
if (!lotId) return new Response("Missing lotId", { status: 400 });
const lot = await getLotDetail(lotId);
if (!lot) return new Response("Lot not found", { status: 404 });
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
const traceUrl = `${baseUrl}/trace/${lot.lot_number}`;
const pdfDoc = await PDFDocument.create();
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const BLACK = rgb(0, 0, 0);
const WHITE = rgb(1, 1, 1);
// Thermal label sizes: 4x2" = 288×144 pts, 4x3" = 288×216 pts
const LABEL_W = 288;
const LABEL_H = size === "4x3" ? 216 : 144;
// QR at maximum: fills right column top-to-bottom
const QR_SIZE = size === "4x3" ? 136 : 116;
const GAP = 18;
// QR generation
let qrDataUrl: string | null = null;
try {
qrDataUrl = await QRCode.toDataURL(traceUrl, {
width: QR_SIZE * 3,
margin: 2,
color: { dark: "#000000", light: "#FFFFFF" },
});
} catch (e) {
// QR generation failed silently
}
for (let i = 0; i < copies; i++) {
const page = pdfDoc.addPage([LABEL_W, LABEL_H * 2 + GAP]);
const y = LABEL_H * 2 + GAP - (i + 1) * LABEL_H;
// White background for pure black-on-white thermal printing
page.drawRectangle({ x: 0, y, width: LABEL_W, height: LABEL_H, color: WHITE });
const LEFT = 10;
const RIGHT_X = LABEL_W - QR_SIZE - 6;
const TOP = y + LABEL_H - 8;
const QR_Y = y + 6;
function drawLine(text: string, x: number, yPos: number, sz: number, isBold = false) {
const f = isBold ? fontBold : font;
page.drawText(text, { x, y: yPos, size: sz, font: f, color: BLACK });
}
function yPos(row: number, baseSize: number) {
return TOP - row * (baseSize + 2);
}
const dataSize = size === "4x3" ? 8.5 : 7;
const rowH = dataSize + 3;
// ─── Brand header ───────────────────────────────────────────
drawLine("ROUTE TRACE", LEFT, yPos(0, 6), 6, true);
// ─── Lot number — dominant ───────────────────────────────────
const lotNumSize = size === "4x3" ? 28 : 22;
drawLine(lot.lot_number, LEFT, yPos(1, lotNumSize), lotNumSize, true);
// ─── Crop + variety ─────────────────────────────────────────
const cropSize = size === "4x3" ? 12 : 10;
drawLine(lot.crop_type, LEFT, yPos(2, cropSize), cropSize, true);
if (lot.variety) {
drawLine(lot.variety, LEFT, yPos(2.7, cropSize - 1), cropSize - 1);
}
// ─── Left column data ───────────────────────────────────────
let row = 3;
if (type === "field") {
if (lot.quantity_lbs) {
drawLine(`${Number(lot.quantity_lbs).toLocaleString()} ${lot.yield_unit ?? "lbs"}`, LEFT, yPos(row, 11), 11, true); row += 1;
}
if (lot.harvest_date) { drawLine(`Harvested: ${lot.harvest_date}`, LEFT, yPos(row, dataSize), dataSize); row += 1; }
if (lot.field_location) { drawLine(`Field: ${lot.field_location}`, LEFT, yPos(row, dataSize), dataSize); row += 1; }
if (lot.field_block) { drawLine(`Block: ${lot.field_block}`, LEFT, yPos(row, dataSize), dataSize); row += 1; }
if (lot.worker_name) { drawLine(`Worker: ${lot.worker_name}`, LEFT, yPos(row, dataSize), dataSize); row += 1; }
if (lot.yield_estimate_lbs) {
drawLine(`Est: ${Number(lot.yield_estimate_lbs).toLocaleString()} ${lot.yield_unit ?? "lbs"}`, LEFT, yPos(row, dataSize), dataSize); row += 1;
}
} else {
if (lot.quantity_lbs) {
drawLine(`${Number(lot.quantity_lbs).toLocaleString()} ${lot.yield_unit ?? "lbs"}`, LEFT, yPos(row, 13), 13, true); row += 1;
}
if (lot.harvest_date) { drawLine(`Packed: ${lot.harvest_date}`, LEFT, yPos(row, dataSize), dataSize); row += 1; }
if (lot.field_location) { drawLine(`From: ${lot.field_location}`, LEFT, yPos(row, dataSize), dataSize); row += 1; }
if (lot.worker_name) { drawLine(`Worker: ${lot.worker_name}`, LEFT, yPos(row, dataSize), dataSize); row += 1; }
if (lot.destination_stop_id) {
drawLine(`Dest: #${lot.destination_stop_id.slice(0, 8)}`, LEFT, yPos(row, dataSize), dataSize); row += 1;
}
}
// ─── Right column — bin / container / pallets ──────────────
let ryR = TOP;
if (lot.bin_id) { drawLine(`BIN ${lot.bin_id}`, RIGHT_X, ryR, dataSize, true); ryR -= rowH; }
if (lot.container_id) { drawLine(`CONT ${lot.container_id}`, RIGHT_X, ryR, dataSize, true); ryR -= rowH; }
if (lot.pallets) { drawLine(`${lot.pallets} PLT`, RIGHT_X, ryR, dataSize, true); ryR -= rowH; }
if (lot.field_block && type === "shed") {
drawLine(`Block: ${lot.field_block}`, RIGHT_X, ryR, dataSize); ryR -= rowH;
}
// ─── QR code — right column, full height ────────────────────
if (qrDataUrl) {
try {
const qrBase64 = qrDataUrl.replace(/^data:image\/png;base64,/, "");
const qrBytes = Uint8Array.from(atob(qrBase64), (c) => c.charCodeAt(0));
const qrImg = await pdfDoc.embedPng(qrBytes);
page.drawImage(qrImg, {
x: RIGHT_X,
y: QR_Y,
width: QR_SIZE,
height: QR_SIZE,
});
} catch (_) {}
}
// Small URL label under QR
page.drawText("/trace", { x: RIGHT_X, y: y + 2, size: 5, font, color: BLACK });
}
const pdfBytes = await pdfDoc.save();
return new Response(new Uint8Array(pdfBytes), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `inline; filename="${lot.lot_number}-${type}-${size}.pdf"`,
},
});
}
@@ -0,0 +1,206 @@
import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
import { svcHeaders } from "@/lib/svc-headers";
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_PAT = process.env.SUPABASE_PAT!;
async function getLotWithChain(lotId: string) {
const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/get_harvest_lot_detail`, {
method: "POST",
headers: {
...svcHeaders(SUPABASE_PAT),
"Content-Type": "application/json",
},
body: JSON.stringify({ p_lot_id: lotId }),
});
if (!res.ok) return null;
const data = await res.json();
return data?.[0] ?? null;
}
async function getLotOrders(lotId: string) {
const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/get_lot_orders`, {
method: "POST",
headers: {
...svcHeaders(SUPABASE_PAT),
"Content-Type": "application/json",
},
body: JSON.stringify({ p_lot_id: lotId }),
});
if (!res.ok) return [];
return await res.json();
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const lotId = searchParams.get("lotId");
const format = searchParams.get("format") ?? "csv";
if (!lotId) return new Response("Missing lotId", { status: 400 });
const lot = await getLotWithChain(lotId);
if (!lot) return new Response("Lot not found", { status: 404 });
const orders = await getLotOrders(lotId);
if (format === "csv") {
const lines: string[] = [];
lines.push("ROUTE TRACE — Lot Traceability Report");
lines.push("");
lines.push("1. LOT INFORMATION");
lines.push(`Lot Number,${lot.lot_number}`);
lines.push(`Crop Type,${lot.crop_type}`);
if (lot.variety) lines.push(`Variety,${lot.variety}`);
lines.push(`Harvest Date,${lot.harvest_date}`);
if (lot.field_location) lines.push(`Field / Location,${lot.field_location}`);
if (lot.field_block) lines.push(`Field Block,${lot.field_block}`);
if (lot.worker_name) lines.push(`Worker,${lot.worker_name}`);
if (lot.packer_name) lines.push(`Packer,${lot.packer_name}`);
if (lot.quantity_lbs != null) lines.push(`Quantity (${lot.yield_unit ?? "lbs"}),${lot.quantity_lbs}`);
if (lot.yield_estimate_lbs != null) lines.push(`Yield Estimate (${lot.yield_unit ?? "lbs"}),${lot.yield_estimate_lbs}`);
if (lot.bin_id) lines.push(`Bin ID,${lot.bin_id}`);
if (lot.container_id) lines.push(`Container ID,${lot.container_id}`);
if (lot.pallets != null) lines.push(`Pallets,${lot.pallets}`);
lines.push(`Current Status,${lot.status}`);
if (lot.yield_unit && lot.yield_unit !== "lbs") lines.push(`Yield Unit,${lot.yield_unit}`);
if (lot.notes) lines.push(`Notes,"${lot.notes.replace(/"/g, '""')}"`);
lines.push("");
lines.push("2. TRACE TIMELINE (FSMA One-Up/One-Down)");
lines.push("Event,Timestamp,Location,Bin ID,Notes,Recorded By");
for (const evt of (lot.events ?? [])) {
const ts = new Date(evt.event_time).toLocaleString("en-US", {
year: "numeric", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit",
});
lines.push(`${evt.event_type},${ts},${evt.location ?? ""},${evt.bin_id ?? ""},${(evt.notes ?? "").replace(/"/g, '""')},${evt.created_by_name ?? ""}`);
}
lines.push("");
lines.push("3. ORDER FULFILLMENT");
if (orders.length > 0) {
lines.push("Order ID,Customer,Stop,Date,Qty (lbs),Fulfillment");
for (const o of orders) {
lines.push(`${o.id},${(o.customer_name ?? "").replace(/"/g, '""')},${(o.stop_name ?? "").replace(/"/g, '""')},${o.order_date},${o.item_quantity ?? ""},${o.fulfillment ?? ""}`);
}
} else {
lines.push("No orders assigned.");
}
lines.push("");
lines.push("4. COMPLIANCE SUMMARY");
lines.push(`Total Events,${lot.events?.length ?? 0}`);
lines.push(`Harvested By,${lot.worker_name ?? "Unknown"}`);
lines.push(`Packed By,${lot.packer_name ?? "N/A"}`);
lines.push(`Destination Stop,${lot.destination_stop_id ? `#${lot.destination_stop_id.slice(0, 8)}` : "N/A"}`);
lines.push(`Report Generated,${new Date().toLocaleString("en-US")}`);
lines.push("");
lines.push("Powered by Route Commerce — routecommerce.com");
const csv = lines.join("\n");
return new Response(csv, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": `attachment; filename="${lot.lot_number}-trace-report.csv"`,
},
});
}
// PDF report
const pdfDoc = await PDFDocument.create();
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const BLACK = rgb(0, 0, 0);
const WHITE = rgb(1, 1, 1);
const GREEN = rgb(0.133, 0.553, 0.133);
const page = pdfDoc.addPage([612, 792]); // letter
const L = 40;
let y = 752;
function heading(text: string, sz = 14) {
page.drawText(text, { x: L, y, size: sz, font: fontBold, color: BLACK });
y -= sz + 6;
}
function row(label: string, value: string, sz = 10) {
page.drawText(label + ":", { x: L, y, size: sz, font, color: BLACK });
page.drawText(value, { x: L + 140, y, size: sz, font: fontBold, color: BLACK });
y -= sz + 4;
}
function sectionHeader(text: string) {
page.drawRectangle({ x: L, y: y - 2, width: 532, height: 12, color: rgb(0.133, 0.133, 0.133) });
page.drawText(text, { x: L + 4, y: y - 9, size: 9, font: fontBold, color: WHITE });
y -= 20;
}
// Header
page.drawRectangle({ x: 0, y: 754, width: 612, height: 38, color: rgb(0.133, 0.4, 0.2) });
page.drawText("ROUTE TRACE", { x: L, y: 760, size: 20, font: fontBold, color: WHITE });
page.drawText("Lot Traceability Report", { x: L + 160, y: 764, size: 10, font, color: WHITE });
page.drawText(lot.lot_number, { x: L + 320, y: 758, size: 16, font: fontBold, color: WHITE });
y = 738;
// Section 1: Lot Info
sectionHeader("1. LOT INFORMATION");
row("Crop Type", lot.crop_type, 12);
if (lot.variety) row("Variety", lot.variety);
row("Harvest Date", lot.harvest_date);
if (lot.field_location) row("Field / Location", lot.field_location);
if (lot.field_block) row("Field Block", lot.field_block);
if (lot.worker_name) row("Worker", lot.worker_name);
if (lot.packer_name) row("Packer", lot.packer_name);
if (lot.quantity_lbs != null) row(`Quantity (${lot.yield_unit ?? "lbs"})`, lot.quantity_lbs.toLocaleString());
if (lot.yield_estimate_lbs != null) row(`Yield Estimate (${lot.yield_unit ?? "lbs"})`, lot.yield_estimate_lbs.toLocaleString());
if (lot.bin_id) row("Bin ID", lot.bin_id);
if (lot.container_id) row("Container ID", lot.container_id);
if (lot.pallets != null) row("Pallets", String(lot.pallets));
row("Status", lot.status?.replace("_", " ").toUpperCase() ?? "");
y -= 10;
// Section 2: Trace Timeline
sectionHeader("2. TRACE TIMELINE — FSMA One-Up / One-Down");
for (const evt of (lot.events ?? [])) {
const ts = new Date(evt.event_time).toLocaleString("en-US", {
month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit",
});
const label = `${evt.event_type?.replace("_", " ").toUpperCase() ?? ""}${ts}`;
page.drawText(label, { x: L, y, size: 9, font: fontBold, color: BLACK });
y -= 10;
if (evt.location) { page.drawText(` Location: ${evt.location}`, { x: L, y, size: 8, font, color: BLACK }); y -= 9; }
if (evt.bin_id) { page.drawText(` Bin: ${evt.bin_id}`, { x: L, y, size: 8, font, color: BLACK }); y -= 9; }
if (evt.created_by_name) { page.drawText(` By: ${evt.created_by_name}`, { x: L, y, size: 8, font, color: BLACK }); y -= 9; }
if (evt.notes) { page.drawText(` Note: ${evt.notes}`, { x: L, y, size: 8, font, color: BLACK }); y -= 9; }
y -= 4;
if (y < 80) {
const np = pdfDoc.addPage([612, 792]);
y = 752;
}
}
y -= 6;
// Section 3: Orders
if (orders.length > 0) {
sectionHeader("3. ORDER FULFILLMENT");
for (const o of orders) {
page.drawText(`${o.customer_name} | ${o.stop_name} | ${o.order_date} | ${o.item_quantity?.toLocaleString() ?? "—"} lbs | ${o.fulfillment ?? ""}`, {
x: L, y, size: 9, font, color: BLACK,
});
y -= 11;
}
y -= 6;
}
// Compliance footer
page.drawRectangle({ x: L, y: Math.max(y - 24, 40), width: 532, height: 1, color: rgb(0.8, 0.8, 0.8) });
y -= 14;
page.drawText("Report Generated by Route Commerce — routecommerce.com", { x: L, y, size: 7, font, color: rgb(0.5, 0.5, 0.5) });
const pdfBytes = await pdfDoc.save();
return new Response(new Uint8Array(pdfBytes), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="${lot.lot_number}-trace-report.pdf"`,
},
});
}