add reusable per-user contract signatures with profile prefill and approval

This commit is contained in:
Marshal
2026-06-16 13:34:04 +00:00
parent 62ce2dc64f
commit 524d3b6418
12 changed files with 307 additions and 3 deletions

View File

@@ -23,6 +23,7 @@ import { ContractViewDto } from './dto/contract-view.dto';
import { SignContractDto } from './dto/sign-contract.dto';
import { ContractSignerRole } from './entities/booking-contract-signature.entity';
import { BookingBatchService } from '../train-scheduling/booking-batch.service';
import { SignaturesService } from '../signatures/signatures.service';
@Injectable()
export class BookingContractService {
@@ -38,6 +39,7 @@ export class BookingContractService {
private readonly pdfService: ContractPdfService,
@Inject(forwardRef(() => BookingBatchService))
private readonly bookingBatchService: BookingBatchService,
private readonly signaturesService: SignaturesService,
) {}
buildContractSummary(booking: Booking): string {
@@ -75,10 +77,16 @@ export class BookingContractService {
return { summary };
}
async getContractView(bookingId: string): Promise<ContractViewDto> {
async getContractView(
bookingId: string,
viewerUserId?: string,
): Promise<ContractViewDto> {
const { view } = await this.viewModelBuilder.build(bookingId);
await this.inlineSignatureImages(view.signatures);
const html = this.renderer.render(view);
const savedSignature = viewerUserId
? ((await this.signaturesService.getForUser(viewerUserId)) ?? undefined)
: undefined;
return {
bookingId: view.bookingId,
reference: view.reference,
@@ -90,6 +98,7 @@ export class BookingContractService {
canSignStaff: view.canSignStaff,
hasContractDocument: view.hasContractDocument,
signatures: view.signatures,
savedSignature,
pricingSchedule: view.pricing as unknown as Record<string, unknown>,
};
}
@@ -194,6 +203,23 @@ export class BookingContractService {
ipAddress: options.ipAddress ?? null,
});
// Persist the just-used signature to the signer's reusable profile so they
// don't have to redraw it on the next contract. Best-effort: a failure here
// must never block contract execution.
if (options.signerUserId) {
try {
await this.signaturesService.upsertForUser({
userId: options.signerUserId,
signerDisplayName: dto.signerDisplayName,
signatureImageBase64: dto.signatureImageBase64,
});
} catch (err) {
this.logger.warn(
`Could not save reusable signature for user ${options.signerUserId}: ${err}`,
);
}
}
const updates: Record<string, unknown> = {};
if (role === 'CUSTOMER') {

View File

@@ -342,8 +342,12 @@ export class BookingsController {
@Get(':id/contract/view')
@ApiOkResponse({ type: ContractViewDto })
@ApiOperation({ summary: 'Contract HTML view for portal and backoffice' })
getContractView(@Param('id', ParseUUIDPipe) id: string) {
return this.contractService.getContractView(id);
getContractView(
@Param('id', ParseUUIDPipe) id: string,
@Request() req: { user?: { id?: string; sub?: string } },
) {
const userId = req.user?.id ?? req.user?.sub;
return this.contractService.getContractView(id, userId);
}
@Get(':id/contract/document')

View File

@@ -6,6 +6,7 @@ import { CompaniesModule } from '../companies/companies.module';
import { FilesModule } from '../files/files.module';
import { MinioModule } from '../minio/minio.module';
import { RuleEngineModule } from '../rule-engine/rule-engine.module';
import { SignaturesModule } from '../signatures/signatures.module';
import { BookingContractService } from './booking-contract.service';
import { BookingPaymentService } from './booking-payment.service';
import { BookingPricingService } from './booking-pricing.service';
@@ -50,6 +51,7 @@ import { CbeExchangeService } from '../cbe-exchange/cbe-exchange.service';
CompaniesModule,
// CustomersModule,
RuleEngineModule,
SignaturesModule,
],
controllers: [BookingsController, PayController],
providers: [

View File

@@ -14,6 +14,14 @@ export class ContractSignatureDto {
signatureImageUrl?: string | null;
}
export class SavedSignatureViewDto {
@ApiProperty()
signerDisplayName!: string;
@ApiPropertyOptional()
signatureImageUrl?: string | null;
}
export class ContractViewDto {
@ApiProperty()
bookingId!: string;
@@ -45,6 +53,9 @@ export class ContractViewDto {
@ApiProperty({ type: [ContractSignatureDto] })
signatures!: ContractSignatureDto[];
@ApiPropertyOptional({ type: SavedSignatureViewDto })
savedSignature?: SavedSignatureViewDto;
@ApiPropertyOptional()
pricingSchedule?: Record<string, unknown>;
}