feat(freight-api): seal EDR side of contracts with the one global stamp

This commit is contained in:
ghost2023
2026-08-12 06:26:46 +03:00
parent e52d666647
commit a603807e8e
21 changed files with 679 additions and 183 deletions

View File

@@ -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<ContractSignatureView[]> {
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<void> {
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 {