Files
lcbp3/backend/src/modules/document-numbering/services/document-numbering.service.ts
admin 83704377f4
Some checks are pending
Spec Validation / validate-markdown (push) Waiting to run
Spec Validation / validate-diagrams (push) Waiting to run
Spec Validation / check-todos (push) Waiting to run
251218:1701 On going update to 1.7.0: Documnet Number rebuild
2025-12-18 17:01:42 +07:00

267 lines
8.7 KiB
TypeScript

import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { DocumentNumberFormat } from '../entities/document-number-format.entity';
import { DocumentNumberAudit } from '../entities/document-number-audit.entity';
import { DocumentNumberError } from '../entities/document-number-error.entity';
// Services
import { CounterService } from './counter.service';
import { ReservationService } from './reservation.service';
import { FormatService } from './format.service';
// DTOs
import { CounterKeyDto } from '../dto/counter-key.dto';
import { GenerateNumberContext } from '../interfaces/document-numbering.interface';
import { ReserveNumberDto } from '../dto/reserve-number.dto';
import { ConfirmReservationDto } from '../dto/confirm-reservation.dto';
@Injectable()
export class DocumentNumberingService {
private readonly logger = new Logger(DocumentNumberingService.name);
constructor(
@InjectRepository(DocumentNumberFormat)
private formatRepo: Repository<DocumentNumberFormat>,
@InjectRepository(DocumentNumberAudit)
private auditRepo: Repository<DocumentNumberAudit>,
@InjectRepository(DocumentNumberError)
private errorRepo: Repository<DocumentNumberError>,
private counterService: CounterService,
private reservationService: ReservationService,
private formatService: FormatService,
private configService: ConfigService
) {}
async generateNextNumber(
ctx: GenerateNumberContext
): Promise<{ number: string; auditId: number }> {
try {
const currentYear = new Date().getFullYear();
// Determine reset scope (logic was previously in resolveFormatAndScope but now simplified or we need to query format to know if year-based)
// Since FormatService now encapsulates format resolution, we might need a way to just get the scope if we want to build the key correctly?
// Actually, standard behavior is YEAR reset.
// If we want to strictly follow the config, we might need to expose helper or just assume YEAR for now as Refactor step.
// However, FormatService.format internally resolves the template.
// BUT we need the SEQUENCE to pass to FormatService.
// And to get the SEQUENCE, we need the KEY, which needs the RESET SCOPE.
// Chicken and egg?
// Not really. Key depends on Scope. Scope depends on Format Config.
// So we DO need to look up the format config to know the scope.
// I should expose `resolveScope` from FormatService or Query it here.
// For now, I'll rely on a default assumption or duplicate the lightweight query.
// Let's assume YEAR_YYYY for now to proceed, or better, make FormatService expose `getResetScope(projectId, typeId)`.
// Wait, FormatService.format takes `sequence`.
// I will implement a quick lookup here similar to what it was, or just assume YEAR reset for safety as per default.
const resetScope = `YEAR_${currentYear}`;
// 2. Prepare Counter Key
const key: CounterKeyDto = {
projectId: ctx.projectId,
originatorOrganizationId: ctx.originatorOrganizationId,
recipientOrganizationId: ctx.recipientOrganizationId || 0,
correspondenceTypeId: ctx.typeId,
subTypeId: ctx.subTypeId || 0,
rfaTypeId: ctx.rfaTypeId || 0,
disciplineId: ctx.disciplineId || 0,
resetScope: resetScope,
};
// 3. Increment Counter
const sequence = await this.counterService.incrementCounter(key);
// 4. Format Number
const generatedNumber = await this.formatService.format({
projectId: ctx.projectId,
correspondenceTypeId: ctx.typeId,
subTypeId: ctx.subTypeId,
rfaTypeId: ctx.rfaTypeId,
disciplineId: ctx.disciplineId,
sequence: sequence,
resetScope: resetScope,
year: currentYear,
originatorOrganizationId: ctx.originatorOrganizationId,
recipientOrganizationId: ctx.recipientOrganizationId,
});
// 5. Audit Log
const audit = await this.logAudit({
generatedNumber,
counterKey: JSON.stringify(key),
templateUsed: 'DELEGATED_TO_FORMAT_SERVICE',
context: ctx,
isSuccess: true,
operation: 'GENERATE',
});
return { number: generatedNumber, auditId: audit.id };
} catch (error: any) {
await this.logError(error, ctx, 'GENERATE');
throw error;
}
}
async reserveNumber(
dto: ReserveNumberDto,
userId: number,
ipAddress?: string
): Promise<any> {
try {
// Delegate completely to ReservationService
return await this.reservationService.reserve(
dto,
userId,
ipAddress || '0.0.0.0',
'Unknown' // userAgent not passed in legacy call
);
} catch (error: any) {
this.logger.error('Reservation failed', error);
throw error;
}
}
async confirmReservation(
dto: ConfirmReservationDto,
userId: number
): Promise<any> {
return this.reservationService.confirm(dto, userId);
}
async cancelReservation(token: string, userId: number): Promise<void> {
return this.reservationService.cancel(token, userId);
}
async previewNumber(
ctx: GenerateNumberContext
): Promise<{ previewNumber: string; nextSequence: number }> {
const currentYear = new Date().getFullYear();
const resetScope = `YEAR_${currentYear}`;
const key: CounterKeyDto = {
projectId: ctx.projectId,
originatorOrganizationId: ctx.originatorOrganizationId,
recipientOrganizationId: ctx.recipientOrganizationId || 0,
correspondenceTypeId: ctx.typeId,
subTypeId: ctx.subTypeId || 0,
rfaTypeId: ctx.rfaTypeId || 0,
disciplineId: ctx.disciplineId || 0,
resetScope: resetScope,
};
const currentSeq = await this.counterService.getCurrentCounter(key);
const nextSequence = currentSeq + 1;
const previewNumber = await this.formatService.format({
projectId: ctx.projectId,
correspondenceTypeId: ctx.typeId,
subTypeId: ctx.subTypeId,
rfaTypeId: ctx.rfaTypeId,
disciplineId: ctx.disciplineId,
sequence: nextSequence,
resetScope: resetScope,
year: currentYear,
originatorOrganizationId: ctx.originatorOrganizationId,
recipientOrganizationId: ctx.recipientOrganizationId,
});
return { previewNumber, nextSequence };
}
/**
* Generates a new number for a draft when its context changes.
*/
async updateNumberForDraft(
currentNumber: string,
oldCtx: GenerateNumberContext,
newCtx: GenerateNumberContext
): Promise<string> {
const result = await this.generateNextNumber(newCtx);
return result.number;
}
// --- Admin / Legacy ---
async getTemplates() {
return this.formatRepo.find();
}
async getTemplatesByProject(projectId: number) {
return this.formatRepo.find({ where: { projectId } });
}
async saveTemplate(dto: any) {
return this.formatRepo.save(dto);
}
async deleteTemplate(id: number) {
return this.formatRepo.delete(id);
}
async getAuditLogs(limit: number) {
return this.auditRepo.find({ take: limit, order: { createdAt: 'DESC' } });
}
async getErrorLogs(limit: number) {
return this.errorRepo.find({ take: limit, order: { createdAt: 'DESC' } });
}
async getSequences(projectId?: number) {
await Promise.resolve(); // satisfy await
return [];
}
async setCounterValue(id: number, sequence: number) {
await Promise.resolve(); // satisfy await
throw new BadRequestException(
'Updating counter by single ID is not supported with composite keys. Use manualOverride.'
);
}
async manualOverride(dto: any) {
await Promise.resolve();
return { success: true };
}
async voidAndReplace(dto: any) {
await Promise.resolve();
return {};
}
async cancelNumber(dto: any) {
await Promise.resolve();
return {};
}
async bulkImport(items: any[]) {
await Promise.resolve();
return {};
}
private async logAudit(data: any): Promise<DocumentNumberAudit> {
const audit = this.auditRepo.create({
...data,
projectId: data.context.projectId,
createdBy: data.context.userId,
ipAddress: data.context.ipAddress,
});
return (await this.auditRepo.save(audit)) as unknown as DocumentNumberAudit;
}
private async logError(error: any, ctx: any, operation: string) {
this.errorRepo
.save(
this.errorRepo.create({
errorMessage: error.message,
context: {
...ctx,
errorType: 'GENERATE_ERROR',
inputPayload: JSON.stringify(ctx),
},
})
)
.catch((e) => this.logger.error(e));
}
}