From a603807e8e58b04e7a3580a85f2c753e669e6b88 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Wed, 12 Aug 2026 06:26:46 +0300 Subject: [PATCH] feat(freight-api): seal EDR side of contracts with the one global stamp --- .../contracts/contract-provider-stamp.spec.ts | 99 ++++++++++++ .../contracts/contract-view-model.builder.ts | 32 +++- apps/edr-freight-api/src/main.ts | 11 +- .../documents/invoice-document.service.ts | 12 +- .../documents/seal-markup.util.spec.ts | 78 +++++++++ .../billing/documents/seal-markup.util.ts | 64 ++++++++ .../bookings/booking-contract.service.ts | 1 + .../modules/bookings/dto/contract-view.dto.ts | 6 + .../dto/complete-identity-verification.dto.ts | 10 ++ .../contracts/contract-staff-stamp.spec.ts | 152 ++++++++++++++++++ .../contracts/contract-transition.service.ts | 26 ++- .../stamp-settings.service.spec.ts | 110 +++++++++++++ .../stamp-settings/stamp-settings.service.ts | 24 ++- .../warehouses/warehouse-inventory.service.ts | 15 +- .../src/seed/freight-permissions.registry.ts | 10 +- .../components/profile/MySignatureCard.tsx | 146 ++++------------- .../pages/bookings/BookingContractPage.tsx | 23 +-- .../src/pages/contracts/ContractViewPage.tsx | 19 +-- .../src/services/bookings.service.ts | 7 +- .../src/services/contracts.service.ts | 7 +- .../src/services/signatures.service.ts | 10 +- 21 files changed, 679 insertions(+), 183 deletions(-) create mode 100644 apps/edr-freight-api/src/contracts/contract-provider-stamp.spec.ts create mode 100644 apps/edr-freight-api/src/modules/billing/documents/seal-markup.util.spec.ts create mode 100644 apps/edr-freight-api/src/modules/billing/documents/seal-markup.util.ts create mode 100644 apps/edr-freight-api/src/modules/contracts/contract-staff-stamp.spec.ts create mode 100644 apps/edr-freight-api/src/modules/stamp-settings/stamp-settings.service.spec.ts diff --git a/apps/edr-freight-api/src/contracts/contract-provider-stamp.spec.ts b/apps/edr-freight-api/src/contracts/contract-provider-stamp.spec.ts new file mode 100644 index 000000000..023e4ac8c --- /dev/null +++ b/apps/edr-freight-api/src/contracts/contract-provider-stamp.spec.ts @@ -0,0 +1,99 @@ +import { ContractViewModelBuilder, ContractSignatureView } from "./contract-view-model.builder"; + +/** + * The EDR side of a contract is sealed with the ONE global company stamp, read + * live at render time; the client side keeps whatever stamp the customer + * uploaded. These specs pin that asymmetry — the standing rule is that + * centralizing the EDR seal must not touch customer stamps. + */ +describe("ContractViewModelBuilder.attachProviderStamp", () => { + const STAMP = "data:image/png;base64,RURS"; + + const build = (stampImageUrl: string | null = STAMP) => { + const getStampImageUrl = jest.fn().mockResolvedValue(stampImageUrl); + const builder = Object.create( + ContractViewModelBuilder.prototype, + ) as ContractViewModelBuilder; + Object.assign(builder, { stampSettings: { getStampImageUrl } }); + return { builder, getStampImageUrl }; + }; + + const sig = (role: "STAFF" | "CUSTOMER", extra: Partial = {}) => + ({ + role, + signerDisplayName: `${role} signer`, + signedAt: "1 January 2026", + signatureImageUrl: "https://minio.local/sig.png", + ...extra, + }) as ContractSignatureView; + + it("stamps the EDR side with the global stamp", async () => { + const { builder } = build(); + const signatures = [sig("STAFF")]; + + await builder.attachProviderStamp(signatures); + + expect(signatures[0]!.stampImageUrl).toBe(STAMP); + }); + + it("leaves the customer side untouched", async () => { + const { builder } = build(); + const customerStamp = "data:image/png;base64,Q1VTVA=="; + const signatures = [ + sig("CUSTOMER", { stampImageUrl: customerStamp }), + sig("STAFF"), + ]; + + await builder.attachProviderStamp(signatures); + + expect(signatures[0]!.stampImageUrl).toBe(customerStamp); + expect(signatures[1]!.stampImageUrl).toBe(STAMP); + }); + + it("does not read the stamp at all when EDR has not signed yet", async () => { + const { builder, getStampImageUrl } = build(); + const signatures = [sig("CUSTOMER")]; + + await builder.attachProviderStamp(signatures); + + expect(getStampImageUrl).not.toHaveBeenCalled(); + expect(signatures[0]!.stampImageUrl).toBeUndefined(); + }); + + it("renders unstamped rather than failing when no stamp is configured", async () => { + const { builder } = build(null); + const signatures = [sig("STAFF")]; + + await expect(builder.attachProviderStamp(signatures)).resolves.toBeUndefined(); + expect(signatures[0]!.stampImageUrl).toBeNull(); + }); + + it("reads the stamp once for every EDR signature row", async () => { + const { builder, getStampImageUrl } = build(); + const signatures = [sig("STAFF"), sig("STAFF")]; + + await builder.attachProviderStamp(signatures); + + expect(getStampImageUrl).toHaveBeenCalledTimes(1); + expect(signatures.map((s) => s.stampImageUrl)).toEqual([STAMP, STAMP]); + }); + + it("is applied by loadSignatures, so the HTML view and the PDF agree", async () => { + const { builder } = build(); + Object.assign(builder, { + bookingsRepository: { + findContractSignatures: jest.fn().mockResolvedValue([ + { signerRole: "STAFF", signerDisplayName: "EDR", signedAt: new Date() }, + ]), + }, + }); + + const views = await ( + builder as unknown as { + loadSignatures(id: string): Promise; + } + ).loadSignatures("b-1"); + + expect(views[0]!.stampImageUrl).toBe(STAMP); + }); +}); diff --git a/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts b/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts index d1158d035..94a6809e6 100644 --- a/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts +++ b/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts @@ -9,6 +9,7 @@ import { import { ContractPricingScheduleBuilder, PricingSchedule } from './contract-pricing-schedule.builder'; import { ContractRateScheduleBuilder, RateSchedule } from './contract-rate-schedule.builder'; import { ContractTemplateResolver } from './contract-template.resolver'; +import { StampSettingsService } from '../modules/stamp-settings/stamp-settings.service'; import { ContractTemplateMeta, getTemplateMeta } from './contract-template.registry'; export interface ContractSignatureView { @@ -16,6 +17,11 @@ export interface ContractSignatureView { signerDisplayName: string; signedAt: string; signatureImageUrl?: string | null; + /** + * Round company seal shown beside the signature. Populated for the EDR + * (STAFF) side only, from the single global stamp — see attachProviderStamp. + */ + stampImageUrl?: string | null; } /** @@ -114,6 +120,7 @@ export class ContractViewModelBuilder { private readonly templateResolver: ContractTemplateResolver, private readonly pricingBuilder: ContractPricingScheduleBuilder, private readonly rateScheduleBuilder: ContractRateScheduleBuilder, + private readonly stampSettings: StampSettingsService, ) {} async build(bookingId: string): Promise<{ booking: Booking; view: ContractViewModel }> { @@ -194,7 +201,30 @@ export class ContractViewModelBuilder { private async loadSignatures(bookingId: string): Promise { const rows = await this.bookingsRepository.findContractSignatures(bookingId); - return rows.map((s) => this.toSignatureView(s)); + const views = rows.map((s) => this.toSignatureView(s)); + await this.attachProviderStamp(views); + return views; + } + + /** + * Stamp the EDR side of the contract with the ONE global company stamp + * (StampSettingsService) — staff never upload or pick a stamp, so nothing is + * stored per signature and the seal is read live at render time. The client + * side is left alone: a customer's own stamp is their business. + * + * Read live and deliberately not snapshotted, so replacing the company stamp + * re-seals contracts on their next render. `getStampImageUrl()` never throws + * and returns a data URL, which `signatures_block.hbs` renders as-is and the + * signature inliner skips. + */ + async attachProviderStamp(signatures: ContractSignatureView[]): Promise { + const staff = signatures.filter((s) => s.role === 'STAFF'); + if (staff.length === 0) return; + + const stampImageUrl = await this.stampSettings.getStampImageUrl(); + for (const sig of staff) { + sig.stampImageUrl = stampImageUrl; + } } toSignatureView(row: BookingContractSignature): ContractSignatureView { diff --git a/apps/edr-freight-api/src/main.ts b/apps/edr-freight-api/src/main.ts index f26cd19e7..5d2168baa 100644 --- a/apps/edr-freight-api/src/main.ts +++ b/apps/edr-freight-api/src/main.ts @@ -15,10 +15,13 @@ import { getAuditLoggerConfig } from "@tria-plc/auditlog"; import { AppModule } from "./app.module"; /** - * JSON body ceiling. Signing posts the signature AND the company stamp as - * base64 in one JSON body, and base64 inflates bytes by ~4/3 — a 50MB asset is - * ~67MB on the wire. Express defaults to 100kb, which rejected any real stamp - * image with a 413 "request entity too large". + * JSON body ceiling. Customer signing posts the signature AND the customer's + * own company stamp as base64 in one JSON body, and base64 inflates bytes by + * ~4/3 — a 50MB asset is ~67MB on the wire. Express defaults to 100kb, which + * rejected any real stamp image with a 413 "request entity too large". + * (Staff signing posts only a signature: EDR's seal is the one global stamp, + * read server-side. Uploading that stamp under Settings goes through this same + * ceiling, so the headroom is still needed on both counts.) * * Sized to clear the 50MB per-document ceiling * (`DOCUMENT_UPLOAD_MAX_BYTES`) after base64 inflation, with room for the diff --git a/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts b/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts index 72146491d..268e94ad9 100644 --- a/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts +++ b/apps/edr-freight-api/src/modules/billing/documents/invoice-document.service.ts @@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common"; import { StampSettingsService } from "../../stamp-settings/stamp-settings.service"; import { PdfRenderService } from "./pdf-render.service"; +import { sealClass, sealImageCss, sealMarkup } from "./seal-markup.util"; import { PdfColor, assembleSinglePagePdf, @@ -239,10 +240,8 @@ export class InvoiceDocumentService { const showCategory = Boolean(model.categoryHeader); const sealText = model.sealText ?? (model.kind === "RECEIPT" || model.status === "PAID" ? "EDR PAID" : "EDR"); - const sealMarkup = model.stampImageUrl - ? `Company stamp` - : esc(sealText); - const sealClass = model.stampImageUrl ? "seal seal-image" : "seal"; + const sealInner = sealMarkup(model.stampImageUrl, sealText); + const sealCssClass = sealClass(model.stampImageUrl); const summaryRows = model.summary .map((row) => `
${esc(row.label)}${esc(row.value)}
`) @@ -281,8 +280,7 @@ export class InvoiceDocumentService { .meta { text-align: right; font-size: 12px; color: #475569; } .meta strong { display: block; color: #0f172a; font-size: 17px; margin-top: 5px; } .seal { position: absolute; right: 28px; top: 118px; width: 116px; height: 116px; border: 4px double #0f766e; border-radius: 999px; color: #0f766e; display: flex; align-items: center; justify-content: center; text-align: center; font-weight: 800; font-size: 18px; transform: rotate(-14deg); opacity: .82; } - .seal.seal-image { border: none; border-radius: 0; opacity: 1; transform: none; } - .seal img { max-width: 100%; max-height: 100%; object-fit: contain; } + ${sealImageCss()} .summary { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 28px; margin: 24px 150px 16px 0; font-size: 13px; } .summary div { border-bottom: 1px solid #e2e8f0; padding: 7px 0; } .summary span { color: #64748b; display: block; font-size: 11px; margin-bottom: 3px; } @@ -310,7 +308,7 @@ export class InvoiceDocumentService { Issued: ${esc(date(model.issuedAt))} -
${sealMarkup}
+
${sealInner}
${summaryRows}
diff --git a/apps/edr-freight-api/src/modules/billing/documents/seal-markup.util.spec.ts b/apps/edr-freight-api/src/modules/billing/documents/seal-markup.util.spec.ts new file mode 100644 index 000000000..d07f8ebf6 --- /dev/null +++ b/apps/edr-freight-api/src/modules/billing/documents/seal-markup.util.spec.ts @@ -0,0 +1,78 @@ +import { sealClass, sealImageCss, sealMarkup } from "./seal-markup.util"; + +/** + * These three helpers are the single image-vs-text branch shared by every + * EDR document's round seal, so a regression here silently unstamps invoices, + * warehouse release papers and handover papers at once. + */ +describe("seal markup helpers", () => { + const STAMP = "data:image/png;base64,QUJD"; + + describe("sealMarkup", () => { + it("renders the stamp image when one is configured", () => { + expect(sealMarkup(STAMP, ["EDR", "Warehouse"])).toBe( + `Company stamp`, + ); + }); + + it("falls back to text rings when no stamp is configured", () => { + expect(sealMarkup(null, ["EDR", "Warehouse", "Cleared"])).toBe( + "EDR
Warehouse
Cleared
", + ); + }); + + it("treats undefined as unset", () => { + expect(sealMarkup(undefined, "EDR")).toBe("EDR"); + }); + + it("accepts a bare string as a single line", () => { + expect(sealMarkup(null, "EDR")).toBe("EDR"); + }); + + it("escapes text lines so document data cannot inject markup", () => { + expect(sealMarkup(null, [''])).toBe( + "<script>alert("x")</script>", + ); + }); + + it("escapes the image src so it cannot break out of the attribute", () => { + expect(sealMarkup('data:image/png;base64,A" onerror="x', "EDR")).toBe( + 'Company stamp', + ); + }); + }); + + describe("sealClass", () => { + it("adds the image modifier only when stamped", () => { + expect(sealClass(STAMP)).toBe("seal seal-image"); + expect(sealClass(null)).toBe("seal"); + }); + + it("honours a document's own seal selector", () => { + expect(sealClass(STAMP, "sig-stamp-box")).toBe( + "sig-stamp-box sig-stamp-box-image", + ); + expect(sealClass(null, "sig-stamp-box")).toBe("sig-stamp-box"); + }); + }); + + describe("sealImageCss", () => { + it("neutralizes the drawn ring and rotation for a real stamp image", () => { + const css = sealImageCss(); + + expect(css).toContain(".seal.seal-image { border: none;"); + expect(css).toContain("transform: none;"); + // The ::before pseudo-element draws the inner ring of the text seal. + expect(css).toContain(".seal.seal-image::before { content: none; }"); + expect(css).toContain(".seal img { max-width: 100%;"); + }); + + it("scopes every rule to the given selector", () => { + const css = sealImageCss("sig-stamp-box"); + + expect(css).not.toContain(".seal"); + expect(css).toContain(".sig-stamp-box.sig-stamp-box-image"); + expect(css).toContain(".sig-stamp-box img"); + }); + }); +}); diff --git a/apps/edr-freight-api/src/modules/billing/documents/seal-markup.util.ts b/apps/edr-freight-api/src/modules/billing/documents/seal-markup.util.ts new file mode 100644 index 000000000..2dd9c4715 --- /dev/null +++ b/apps/edr-freight-api/src/modules/billing/documents/seal-markup.util.ts @@ -0,0 +1,64 @@ +/** + * The single decision every EDR document makes about its round seal: draw the + * one uploaded company stamp when one is configured (StampSettingsService), or + * fall back to the plain text rings the document styles itself. + * + * Only the image-vs-text branch and the image overrides live here — each + * document keeps its own `.seal` geometry (the invoice's seal is absolutely + * positioned top-right, the warehouse papers' sit inline above the signature + * lines), so centralizing the source of the stamp does not relayout anything. + * + * These helpers are for the HTML/Chromium render path. The hand-built vector + * fallbacks in styled-pdf.util.ts cannot embed a raster image and continue to + * draw their vector seal — see InvoiceDocumentService for that caveat. + */ + +/** Escape a value for interpolation into HTML text or a quoted attribute. */ +function escapeHtml(value: unknown): string { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** + * CSS overrides that neutralize a document's own ring/rotation styling when the + * seal is a real stamp image. Append inside a document's @@ -5722,7 +5730,7 @@ export class WarehouseInventoryService {
Officer in charge name / signature / date
-
EDR
Warehouse
Cleared
+
${sealMarkup(data.stampImageUrl, ['EDR', 'Warehouse', 'Cleared'])}
Customer or driver name / signature / date
@@ -5762,6 +5770,8 @@ export class WarehouseInventoryService { signerDisplayName: string; signatureImageUrl: string | null; } | null; + /** The one global company stamp; null falls back to the drawn text seal. */ + stampImageUrl?: string | null; }): string { const esc = (value: unknown) => String(value ?? '-') @@ -5839,6 +5849,7 @@ export class WarehouseInventoryService { .seal { position: relative; width: 96px; height: 96px; margin: -28px auto 0; border: 3px double #17633a; border-radius: 999px; color: #17633a; display: flex; align-items: center; justify-content: center; text-align: center; font-weight: 800; font-size: 14px; line-height: 1.05; transform: rotate(-17deg); text-transform: uppercase; } .seal::before { content: ""; position: absolute; width: 78px; height: 78px; border: 1px solid #17633a; border-radius: 999px; } .seal span { position: relative; } + ${sealImageCss()} @@ -5882,7 +5893,7 @@ export class WarehouseInventoryService {
Officer in charge name / signature / date
-
EDR
Warehouse
Handover
+
${sealMarkup(data.stampImageUrl, ['EDR', 'Warehouse', 'Handover'])}
${approval?.signatureImageUrl ? `` : ''}
${approval ? esc(approval.signerDisplayName) : 'Customer or driver name / signature / date'}
diff --git a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts index fb9afb5e1..56664de57 100644 --- a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts +++ b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts @@ -1301,7 +1301,10 @@ export const GRANULAR_SPLIT_PERMISSIONS: FreightPermissionSeed[] = [ "Edit contract templates & articles", ), // Granular split of contract-template access. `view` opens the sidebar page; - // `read` is API-read-only for other pages that display template data. + // `read` is API-read-only for other pages that display template data — and is + // NOT written out here: deriveReadPermissions mints the `:read` twin of every + // `:view` key, so a hand-written one duplicates the key (Postgres 21000 on the + // seeder's ON CONFLICT (key) insert) and carries a v4 id where twins are v5. perm( "b4e00001-0001-4000-8000-000000000003", "edr_freight_app:settings:contract_templates:create", @@ -1317,11 +1320,6 @@ export const GRANULAR_SPLIT_PERMISSIONS: FreightPermissionSeed[] = [ "edr_freight_app:settings:contract_templates:delete", "Delete bulk contract templates", ), - perm( - "b4e00001-0001-4000-8000-000000000006", - "edr_freight_app:settings:contract_templates:read", - "Read contract template data (API only)", - ), perm( "b4f00001-0001-4000-8000-000000000001", "edr_freight_app:settings:support_content:view", diff --git a/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx b/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx index 0a70b3aec..b13ba4291 100644 --- a/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx +++ b/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx @@ -1,11 +1,10 @@ import { useState } from "react"; -import { FileSignature, Loader2, Stamp } from "lucide-react"; +import { FileSignature, Loader2 } from "lucide-react"; import { useMutation, useQuery } from "@tanstack/react-query"; import toast from "react-hot-toast"; import { api } from "@/services/api"; import { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad"; -import { StampUpload } from "@/components/contracts/StampUpload"; import { Card, CardContent, @@ -27,9 +26,13 @@ import { } from "@edr/ui-common"; /** - * Lets the signed-in user view and update the reusable signature and company - * stamp stored on their profile — managed independently of each other. Both - * are offered when signing a booking contract. + * Lets the signed-in user view and update the reusable signature stored on + * their profile, offered for approval when signing a contract. + * + * Signature only — there is no per-employee stamp. EDR seals with ONE global + * company stamp, managed under Settings and applied server-side, so a staff + * member never uploads or picks a stamp. (Customers do upload their own, in + * the portal — that is a different card.) */ export function MySignatureCard() { const { user } = useAuth(); @@ -39,10 +42,8 @@ export function MySignatureCard() { const saveMutation = useMutation(api.signatures.save.mutationOptions()); const [signatureOpen, setSignatureOpen] = useState(false); - const [stampOpen, setStampOpen] = useState(false); const [signerName, setSignerName] = useState(""); const [signatureData, setSignatureData] = useState(null); - const [stampData, setStampData] = useState(null); const defaultName = user?.name?.en || user?.username || user?.email || ""; @@ -60,7 +61,6 @@ export function MySignatureCard() { { signerDisplayName: signerName.trim(), signatureImageBase64: signatureData, - // Stamp untouched — it is managed by its own dialog. }, { onSuccess: () => { @@ -72,38 +72,16 @@ export function MySignatureCard() { ); }; - const openStampDialog = () => { - setStampData(saved?.stampImageUrl ?? null); - setStampOpen(true); - }; - - const saveStamp = () => { - if (!stampData) return; - saveMutation.mutate( - { - signerDisplayName: savedName || defaultName, - // Signature untouched — stamp-only update. - stampImageBase64: stampData, - }, - { - onSuccess: () => { - toast.success("Stamp saved"); - setStampOpen(false); - }, - onError: () => toast.error("Failed to save stamp"), - }, - ); - }; - return ( - Signature & Stamp + Signature - This signature can be reused to sign booking contracts. + This signature can be reused to sign booking contracts. The EDR + company stamp is applied automatically — you do not upload one. @@ -112,54 +90,29 @@ export function MySignatureCard() {
) : ( - <> -
- {saved?.signatureImageUrl ? ( - <> -
- My saved signature -
-

- Saved as {saved.signerDisplayName} -

- - ) : ( -

- You have not saved a signature yet. +

+ {saved?.signatureImageUrl ? ( + <> +
+ My saved signature +
+

+ Saved as {saved.signerDisplayName}

- )} - -
- -
- {saved?.stampImageUrl ? ( - <> -
- My saved company stamp -
-

Company stamp

- - ) : ( -

- You have not uploaded a company stamp yet. -

- )} - -
- + + ) : ( +

+ You have not saved a signature yet. +

+ )} + +
)} @@ -203,39 +156,6 @@ export function MySignatureCard() { - - - - - Company stamp - - Upload your official company stamp or seal as an image. It is - stored on your profile and applied next to your signature on - contracts. - - - - - - - - - ); } diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingContractPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingContractPage.tsx index f176fc05d..e4d570da6 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingContractPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingContractPage.tsx @@ -12,7 +12,6 @@ import toast from "react-hot-toast"; import Breadcrumbs from "@/components/ui/Breadcrumbs"; import { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad"; -import { StampUpload } from "@/components/contracts/StampUpload"; import { bookingSurface } from "@/components/bookings/booking-ui.styles"; import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; import { invalidateBookingDetail } from "@/utils/queryInvalidation"; @@ -41,9 +40,6 @@ export default function BookingContractPage() { const [signOpen, setSignOpen] = useState(false); const [signerName, setSignerName] = useState(""); const [signatureData, setSignatureData] = useState(null); - // Company stamp: prefilled from the profile, or uploaded here when none is - // saved yet. - const [stampData, setStampData] = useState(null); // When the user has a saved signature we offer it for approval first; they // can switch to drawing a fresh one. const [drawNew, setDrawNew] = useState(false); @@ -59,7 +55,6 @@ export default function BookingContractPage() { const savedSignature = data?.savedSignature ?? null; const savedSignatureImage = savedSignature?.signatureImageUrl ?? null; - const savedStampImage = savedSignature?.stampImageUrl ?? null; // Show the approval view only while a saved signature exists and the user // hasn't opted to draw a new one. const usingSaved = Boolean(savedSignatureImage) && !drawNew; @@ -103,8 +98,6 @@ export default function BookingContractPage() { // approve it; otherwise start with an empty pad. setSignerName(savedSignature?.signerDisplayName ?? ""); setSignatureData(null); - // Prefill with the reusable stamp saved on the profile; still replaceable. - setStampData(savedStampImage); setDrawNew(false); setSignOpen(true); }; @@ -113,12 +106,12 @@ export default function BookingContractPage() { if (!canSign || !signerName.trim()) return; // Approve the saved signature, or submit the freshly drawn one. const image = usingSaved ? savedSignatureImage : signatureData; - // The API rejects a STAFF signature without a stamp. - if (!image || !stampData) return; + if (!image) return; + // No stamp is sent: EDR's seal is the ONE global company stamp, applied + // server-side at render time (see StampSettingsService). signMutation.mutate({ role: "STAFF", signatureImageBase64: image, - stampImageBase64: stampData, signerDisplayName: signerName.trim(), consentText: "I agree to the terms of this contract.", }); @@ -243,15 +236,6 @@ export default function BookingContractPage() { ) : ( )} -