mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
feat(bookings): two-level clearance charges (port + misc) billed to customer with invoices
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Readable } from "stream";
|
||||
import { DataSource } from "typeorm";
|
||||
|
||||
import { FilesService } from "../files/files.service";
|
||||
import { FileRecord } from "../files/entities/file.entity";
|
||||
import { MinioService } from "../minio/minio.service";
|
||||
import { StampSettingsRepository } from "./stamp-settings.repository";
|
||||
import { StampSetting } from "./entities/stamp-setting.entity";
|
||||
@@ -28,7 +26,6 @@ export class StampSettingsService {
|
||||
private readonly repository: StampSettingsRepository,
|
||||
private readonly filesService: FilesService,
|
||||
private readonly minioService: MinioService,
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
/** The settings row, created empty on first access. */
|
||||
@@ -53,14 +50,12 @@ export class StampSettingsService {
|
||||
* `data:` URL or null. Never throws — document generation must succeed even
|
||||
* if the stamp lookup fails; callers fall back to their own seal on null.
|
||||
*
|
||||
* The data-URL-or-null guarantee is load-bearing, not cosmetic. Callers do
|
||||
* two things with this value that a bare MinIO URL silently corrupts:
|
||||
* ContractTransitionService base64-decodes it to snapshot the seal onto a
|
||||
* signature row (a URL decodes to garbage bytes, not an error, permanently
|
||||
* sealing an executed contract with a broken image), and the HTML render
|
||||
* path inlines it into an <img> that headless Chromium cannot fetch. So
|
||||
* where getView() may hand a raw URL to a browser that can load it, this
|
||||
* degrades to null and lets the caller draw its text/vector seal instead.
|
||||
* The data-URL-or-null guarantee is load-bearing, not cosmetic: the HTML
|
||||
* render paths inline this value into an <img> that headless Chromium
|
||||
* cannot fetch over the network. So where getView() may hand a raw URL to
|
||||
* a browser that can load it, this degrades to null and lets the caller
|
||||
* draw its text/vector seal instead. (Contract signing no longer consumes
|
||||
* this — staff signatures reference the stampFileId directly.)
|
||||
*/
|
||||
async getStampImageUrl(): Promise<string | null> {
|
||||
try {
|
||||
@@ -81,13 +76,20 @@ export class StampSettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Replace the stamp image, storing it in MinIO via FilesService. */
|
||||
/**
|
||||
* Replace the stamp image, storing it in MinIO via FilesService.
|
||||
*
|
||||
* The replaced file is NEVER deleted: contract signatures reference stamp
|
||||
* files by id (ContractTransitionService points staff signatures at the
|
||||
* current stampFileId instead of copying the image), so each retired file
|
||||
* is the immutable record of which seal executed the contracts signed while
|
||||
* it was current. Deleting it would strip the seal off those contracts.
|
||||
*/
|
||||
async setStamp(
|
||||
stampImageBase64: string,
|
||||
updatedById?: string | null,
|
||||
): Promise<StampSettingView> {
|
||||
const current = await this.get();
|
||||
const previousFileId = current.stampFileId ?? null;
|
||||
|
||||
const fileRecord = await this.filesService.upload({
|
||||
resourceId: current.id,
|
||||
@@ -102,28 +104,22 @@ export class StampSettingsService {
|
||||
updatedById: updatedById ?? null,
|
||||
});
|
||||
|
||||
if (previousFileId && previousFileId !== fileRecord.id) {
|
||||
await this.dataSource.getRepository(FileRecord).delete(previousFileId);
|
||||
}
|
||||
|
||||
this.logger.log(`Company stamp updated by ${updatedById ?? "unknown user"}`);
|
||||
return this.getView();
|
||||
}
|
||||
|
||||
/** Clear the stamp (invoices fall back to the programmatic seal). */
|
||||
/**
|
||||
* Clear the stamp (invoices fall back to the programmatic seal). The file
|
||||
* is kept for the same reason as in {@link setStamp}.
|
||||
*/
|
||||
async clearStamp(updatedById?: string | null): Promise<StampSettingView> {
|
||||
const current = await this.get();
|
||||
const previousFileId = current.stampFileId ?? null;
|
||||
|
||||
await this.repository.update(current.id, {
|
||||
stampFileId: null,
|
||||
updatedById: updatedById ?? null,
|
||||
});
|
||||
|
||||
if (previousFileId) {
|
||||
await this.dataSource.getRepository(FileRecord).delete(previousFileId);
|
||||
}
|
||||
|
||||
return this.getView();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user