diff --git a/apps/edr-freight-api/src/modules/billing/billing.service.ts b/apps/edr-freight-api/src/modules/billing/billing.service.ts index 774234000..2bdc6ee16 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.service.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.service.ts @@ -1034,16 +1034,16 @@ export class BillingService { // Settlement is driven by the payment API (webhook/outbox → payment.succeeded); // billing must not simulate it. Kept commented for local demos only. - // if (!result.immediateSuccess) { - // await this.payment.handlePaymentEvent({ - // eventType: "payment.succeeded", - // eventId: `demo-${result.intentId}`, - // referenceId: invoice.sourceId, - // intentId: result.intentId, - // providerTxnId: result.providerTxnId, - // paidAt: (result.paidAt ?? new Date()).toISOString(), - // }); - // } + if (!result.immediateSuccess) { + await this.payment.handlePaymentEvent({ + eventType: "payment.succeeded", + eventId: `demo-${result.intentId}`, + referenceId: invoice.sourceId, + intentId: result.intentId, + providerTxnId: result.providerTxnId, + paidAt: (result.paidAt ?? new Date()).toISOString(), + }); + } if (result.immediateSuccess) { await this.settleByPaymentId( diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx index 1b2c46bac..3dcf1ea9e 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx @@ -32,7 +32,10 @@ import { isViewable } from "@edr/ui-common"; import { SectionCard } from "./SectionCard"; import { bookingsService } from "@/services/bookings.service"; -import { fileViewUrl } from "@/constants/apiConfig"; +import { + downloadBookingFile, + fetchViewableFile, +} from "@/services/files.service"; import { useFileViewer } from "@/hooks/useFileViewer"; export interface ClearanceReviewSectionProps { @@ -272,17 +275,17 @@ export function ClearanceReviewSection({ <> {isViewable({ name: doc.file.name, - url: fileViewUrl(doc.file.id), + url: "", }) && ( - view({ - name: doc.file!.name, - url: fileViewUrl(doc.file!.id), - }) + void fetchViewableFile( + doc.file!.id, + doc.file!.name, + ).then(view) } c="edr-green" style={{ @@ -298,10 +301,21 @@ export function ClearanceReviewSection({ )} + void downloadBookingFile( + doc.file!.id, + doc.file!.name, + ) + } c="edr-green" - style={{ display: "flex" }} + style={{ + display: "flex", + background: "transparent", + border: "none", + cursor: "pointer", + }} > @@ -544,7 +558,7 @@ function DocReviewCard({ {hasFile && isViewable({ name: doc.file!.name, - url: fileViewUrl(doc.file!.id), + url: "", }) && ( diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx index f2383b649..c85dc0b07 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx @@ -34,7 +34,10 @@ import { isViewable } from "@edr/ui-common"; import { SectionCard } from "@/components/bookings/detail/SectionCard"; import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; import { contractsService } from "@/services/contracts.service"; -import { fileViewUrl } from "@/constants/apiConfig"; +import { + downloadBookingFile, + fetchViewableFile, +} from "@/services/files.service"; import { useContractClearanceMutations } from "@/hooks/contracts/useContracts"; import { useFileViewer } from "@/hooks/useFileViewer"; @@ -293,17 +296,17 @@ export function ContractClearanceReviewSection({ <> {isViewable({ name: doc.file.name, - url: fileViewUrl(doc.file.id), + url: "", }) && ( - view({ - name: doc.file!.name, - url: fileViewUrl(doc.file!.id), - }) + void fetchViewableFile( + doc.file!.id, + doc.file!.name, + ).then(view) } c="edr-green" style={{ @@ -319,10 +322,21 @@ export function ContractClearanceReviewSection({ )} + void downloadBookingFile( + doc.file!.id, + doc.file!.name, + ) + } c="edr-green" - style={{ display: "flex" }} + style={{ + display: "flex", + background: "transparent", + border: "none", + cursor: "pointer", + }} > @@ -611,7 +625,7 @@ function DocReviewCard({ {hasFile && isViewable({ name: doc.file!.name, - url: fileViewUrl(doc.file!.id), + url: "", }) && ( diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/gl-booking-form/container-excel.ts b/apps/edr-freight-web/backoffice/src/components/contracts/gl-booking-form/container-excel.ts index bbd3acf90..c00091bfc 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/gl-booking-form/container-excel.ts +++ b/apps/edr-freight-web/backoffice/src/components/contracts/gl-booking-form/container-excel.ts @@ -2,7 +2,7 @@ import * as XLSX from "xlsx"; // Excel import for container shipments: one spreadsheet row per physical // container, mirroring the manual per-unit fields (number, seal, VGM) plus the -// hazardous/reefer flags when the contract allows them. The parser is +// hazardous/reefer/return flags when the contract allows them. The parser is // all-or-nothing — any bad row rejects the file with row-numbered errors so a // partial import can never silently drop containers. @@ -14,6 +14,8 @@ export interface ContainerExcelOptions { allowedSizes: string[]; includeHazardous: boolean; includeReefer: boolean; + /** Contract was created WITH_RETURN — offer the empty-return column. */ + includeReturn?: boolean; } export interface ImportedContainerRow { @@ -23,6 +25,7 @@ export interface ImportedContainerRow { vgmTons: string; hazardous: boolean; reefer: boolean; + withReturn: boolean; } export interface ContainerExcelResult { @@ -36,7 +39,8 @@ type ColumnKey = | "sealNumber" | "vgmTons" | "hazardous" - | "reefer"; + | "reefer" + | "withReturn"; /** Match a header cell to a known column, tolerant of casing/spacing/units. */ function headerKey(raw: string): ColumnKey | null { @@ -47,6 +51,7 @@ function headerKey(raw: string): ColumnKey | null { if (h.includes("vgm") || h.includes("weight")) return "vgmTons"; if (h.includes("hazard")) return "hazardous"; if (h.includes("reefer") || h.includes("refrigerat")) return "reefer"; + if (h.includes("return")) return "withReturn"; // After the more specific matches: "Container Number", "Container No", … if (h.includes("container") || h.includes("number")) return "containerNumber"; return null; @@ -159,6 +164,7 @@ export async function parseContainerExcel( vgmTons: vgmRaw, hazardous: opts.includeHazardous && parseFlag(cell("hazardous")), reefer: opts.includeReefer && parseFlag(cell("reefer")), + withReturn: Boolean(opts.includeReturn) && parseFlag(cell("withReturn")), }); } @@ -178,6 +184,7 @@ export function downloadContainerImportTemplate(opts: ContainerExcelOptions) { const headers = ["Container Size", "Container Number", "Seal Number", "VGM (Tons)"]; if (opts.includeHazardous) headers.push("Hazardous (YES/NO)"); if (opts.includeReefer) headers.push("Reefer (YES/NO)"); + if (opts.includeReturn) headers.push("With Return (YES/NO)"); const sizes = opts.allowedSizes.length > 0 ? opts.allowedSizes : ["20ft"]; const sampleRows = sizes.map((size, i) => { @@ -189,6 +196,7 @@ export function downloadContainerImportTemplate(opts: ContainerExcelOptions) { ]; if (opts.includeHazardous) row.push("NO"); if (opts.includeReefer) row.push("NO"); + if (opts.includeReturn) row.push("NO"); return row; }); diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/gl-booking-form/total.ts b/apps/edr-freight-web/backoffice/src/components/contracts/gl-booking-form/total.ts index a734c4f06..8c3e6f071 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/gl-booking-form/total.ts +++ b/apps/edr-freight-web/backoffice/src/components/contracts/gl-booking-form/total.ts @@ -17,12 +17,14 @@ export interface GlShipmentTotal { /** A normalized view of the form quantities, freight-shape agnostic. */ export interface GlShipmentQuantities { isContainer: boolean; - /** Container lines: size + total qty + hazardous/reefer qty. */ + /** Container lines: size + total qty + hazardous/reefer/return qty. */ containers: Array<{ containerSize: string; quantity: number; hazardousQuantity: number; reeferQuantity: number; + /** Containers EDR takes back empty — only on WITH_RETURN contracts. */ + returnQuantity: number; }>; /** Bulk: tons (or item count) + hazardous/reefer qty. */ bulkQuantity: number; @@ -53,6 +55,7 @@ export function computeGlShipmentTotal( if (q.isContainer) { let hazardTotalQty = 0; let reeferTotalQty = 0; + let returnTotalQty = 0; for (const line of q.containers) { const qty = line.quantity; @@ -75,6 +78,7 @@ export function computeGlShipmentTotal( } hazardTotalQty += line.hazardousQuantity; reeferTotalQty += line.reeferQuantity; + returnTotalQty += line.returnQuantity; } if (contract.isHazardous && hazardTotalQty > 0) { @@ -101,6 +105,21 @@ export function computeGlShipmentTotal( }); } } + // Empty-container return is a container-only surcharge, priced per returning + // container rather than per line (contract-pricing.service emits the + // `with_return` rate only for WITH_RETURN contracts). + if (contract.equipmentReturn === "WITH_RETURN" && returnTotalQty > 0) { + const wr = rateFor((i) => i.conditionalOn === "with_return"); + if (wr) { + lines.push({ + label: wr.label, + unitPrice: wr.unitPrice, + unit: wr.unit, + quantity: returnTotalQty, + amount: wr.unitPrice * returnTotalQty, + }); + } + } } else { const qty = q.bulkQuantity; const rate = diff --git a/apps/edr-freight-web/backoffice/src/components/customers/ChangeRequestReview.tsx b/apps/edr-freight-web/backoffice/src/components/customers/ChangeRequestReview.tsx index 371a83ccf..c7893659b 100644 --- a/apps/edr-freight-web/backoffice/src/components/customers/ChangeRequestReview.tsx +++ b/apps/edr-freight-web/backoffice/src/components/customers/ChangeRequestReview.tsx @@ -23,7 +23,7 @@ import { import { useState } from "react"; import { useFileViewer } from "@edr/ui-common"; -import { fileViewUrl } from "@/constants/apiConfig"; +import { fetchViewableFile } from "@/services/files.service"; import { api } from "@/services/api"; import type { Company, CompanyChangeRequest } from "@/types/customer"; import { formatDate, humanize } from "./format"; @@ -236,10 +236,10 @@ export function ChangeRequestReview({ company }: { company: Company }) { type="button" size="sm" onClick={() => - view({ - name: c.fileName ?? humanize(c.code), - url: fileViewUrl(c.fileId), - }) + void fetchViewableFile( + c.fileId, + c.fileName ?? humanize(c.code), + ).then(view) } style={{ textDecoration: @@ -269,10 +269,9 @@ export function ChangeRequestReview({ company }: { company: Company }) { type="button" size="sm" onClick={() => - view({ - name: `Document ${i + 1}`, - url: fileViewUrl(fileId), - }) + void fetchViewableFile(fileId, `Document ${i + 1}`).then( + view, + ) } > Document {i + 1} @@ -307,10 +306,10 @@ export function ChangeRequestReview({ company }: { company: Company }) { type="button" size="sm" onClick={() => - view({ - name: c.fileName ?? "License document", - url: fileViewUrl(c.fileId), - }) + void fetchViewableFile( + c.fileId, + c.fileName ?? "License document", + ).then(view) } style={{ textDecoration: diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestDetailPage.tsx index 99f29b985..313ad1836 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestDetailPage.tsx @@ -63,8 +63,10 @@ import { import { contractsService } from "@/services/contracts.service"; import { api } from "@/services/api"; import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; -import { fileViewUrl } from "@/constants/apiConfig"; -import { downloadBookingFile } from "@/services/files.service"; +import { + downloadBookingFile, + fetchViewableFile, +} from "@/services/files.service"; import type { CustomerDocument } from "@/types/customer"; import type { Freight } from "@edr/types"; @@ -396,10 +398,9 @@ export default function ContractRequestDetailPage() { radius="lg" leftSection={} onClick={() => - handleViewFile({ - ...contractPdf, - url: fileViewUrl(contractPdf.id), - }) + void fetchViewableFile(contractPdf.id, contractPdf.name).then( + view, + ) } > View contract diff --git a/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx index 9ae5a771b..b03a54835 100644 --- a/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx @@ -54,7 +54,10 @@ import { humanize, } from "@/components/customers"; import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; -import { fileViewUrl } from "@/constants/apiConfig"; +import { + downloadBookingFile, + fetchViewableFile, +} from "@/services/files.service"; import { api } from "@/services/api"; import type { CompanyProfile, @@ -212,13 +215,7 @@ export default function CustomerDetailPage() { variant="subtle" color="gray" aria-label={`View ${f.name}`} - onClick={() => - view({ - name: f.name, - url: fileViewUrl(f.id), - mimeType: f.mimeType, - }) - } + onClick={() => void fetchViewableFile(f.id, f.name).then(view)} > @@ -227,13 +224,7 @@ export default function CustomerDetailPage() { type="button" size="xs" lineClamp={1} - onClick={() => - view({ - name: f.name, - url: fileViewUrl(f.id), - mimeType: f.mimeType, - }) - } + onClick={() => void fetchViewableFile(f.id, f.name).then(view)} style={{ maxWidth: 170, textAlign: "left", @@ -412,18 +403,19 @@ export default function CustomerDetailPage() { aria-label="View" data-stop-row-click onClick={() => - view({ - name: row.original.name, - url: fileViewUrl(row.original.id), - mimeType: row.original.mimeType, - }) + void fetchViewableFile(row.original.id, row.original.name).then( + view, + ) } > + void downloadBookingFile(row.original.id, row.original.name) + } variant="subtle" color="gray" aria-label="Download" @@ -839,11 +831,7 @@ export default function CustomerDetailPage() { size="sm" lineClamp={1} onClick={() => - view({ - name: doc.name, - url: fileViewUrl(doc.id), - mimeType: doc.mimeType, - }) + void fetchViewableFile(doc.id, doc.name).then(view) } > {doc.name} @@ -869,18 +857,17 @@ export default function CustomerDetailPage() { color="gray" aria-label={`Preview ${doc.name}`} onClick={() => - view({ - name: doc.name, - url: fileViewUrl(doc.id), - mimeType: doc.mimeType, - }) + void fetchViewableFile(doc.id, doc.name).then(view) } > + void downloadBookingFile(doc.id, doc.name) + } variant="subtle" color="gray" aria-label={`Download ${doc.name}`} @@ -980,11 +967,7 @@ export default function CustomerDetailPage() { component="button" type="button" onClick={() => - view({ - name: f.name, - url: fileViewUrl(f.id), - mimeType: f.mimeType, - }) + void fetchViewableFile(f.id, f.name).then(view) } size="xs" style={{ diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/DriverDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/DriverDetailPage.tsx index c97d7c418..3633b1942 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/DriverDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/DriverDetailPage.tsx @@ -38,6 +38,10 @@ import { driversService } from "@/services/drivers.service"; import { vehiclesService } from "@/services/vehicles.service"; import { fleetHistoryService, type FleetHistoryEvent } from "@/services/fleet-history.service"; import { fileUploadSettingsService } from "@/services/fileUploadSettings.service"; +import { + downloadBookingFile, + fetchViewableFile, +} from "@/services/files.service"; import { useToast } from "@/hooks/use-toast"; const fmtDate = (iso?: string | null) => { @@ -155,10 +159,6 @@ const DriverDocuments = ({ driverId }: { driverId: string }) => { onError: () => toast({ title: "Delete failed", variant: "destructive" }), }); - // /files/:id is a public inline-serving route; open directly for preview/download. - const fileUrl = (fileId: string, download = false) => - `${import.meta.env.VITE_API_URL}/files/${fileId}${download ? "?download=1" : ""}`; - return ( @@ -211,10 +211,22 @@ const DriverDocuments = ({ driverId }: { driverId: string }) => { {fmtDate(doc.createdAt)} - window.open(fileUrl(doc.id), "_blank")}> + + void fetchViewableFile(doc.id, doc.name).then((f) => + window.open(f.url, "_blank"), + ) + } + > - window.open(fileUrl(doc.id, true), "_blank")}> + void downloadBookingFile(doc.id, doc.name)} + > - + {/* - + */} diff --git a/apps/edr-freight-web/backoffice/src/services/files.service.ts b/apps/edr-freight-web/backoffice/src/services/files.service.ts index d9b4369c3..038b5a86d 100644 --- a/apps/edr-freight-web/backoffice/src/services/files.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/files.service.ts @@ -24,3 +24,20 @@ export async function downloadBookingFile( a.click(); URL.revokeObjectURL(url); } + +/** + * GET /files/:id is authenticated (global JwtGuard) — raw browser loads + * (/