This commit is contained in:
+31
-6
@@ -15,6 +15,9 @@ import { JwtAuthGuard } from '../../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../../common/decorators/require-permission.decorator';
|
||||
import { CurrentUser } from '../../../common/decorators/current-user.decorator';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { DocumentNumberFormat } from '../entities/document-number-format.entity';
|
||||
import { ManualOverrideDto } from '../dto/manual-override.dto';
|
||||
|
||||
@ApiTags('Admin / Document Numbering')
|
||||
@ApiBearerAuth()
|
||||
@@ -40,7 +43,9 @@ export class DocumentNumberingAdminController {
|
||||
@Post('templates')
|
||||
@ApiOperation({ summary: 'Create or Update a numbering template' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async saveTemplate(@Body() dto: any) {
|
||||
async saveTemplate(
|
||||
@Body() dto: Partial<DocumentNumberFormat> & { projectId?: number | string }
|
||||
) {
|
||||
return this.service.saveTemplate(dto);
|
||||
}
|
||||
|
||||
@@ -74,28 +79,48 @@ export class DocumentNumberingAdminController {
|
||||
summary: 'Manually override or set a document number counter',
|
||||
})
|
||||
@RequirePermission('system.manage_settings')
|
||||
async manualOverride(@Body() dto: any, @CurrentUser() user: any) {
|
||||
return this.service.manualOverride(dto, user.userId);
|
||||
async manualOverride(
|
||||
@Body() dto: ManualOverrideDto,
|
||||
@CurrentUser() user: User
|
||||
) {
|
||||
return this.service.manualOverride(dto, user.user_id);
|
||||
}
|
||||
|
||||
@Post('void-and-replace')
|
||||
@ApiOperation({ summary: 'Void a number and replace with a new generation' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async voidAndReplace(@Body() dto: any) {
|
||||
async voidAndReplace(
|
||||
@Body()
|
||||
dto: {
|
||||
documentNumber: string;
|
||||
reason: string;
|
||||
replace: boolean;
|
||||
projectId?: number;
|
||||
typeId?: number;
|
||||
}
|
||||
) {
|
||||
return this.service.voidAndReplace(dto);
|
||||
}
|
||||
|
||||
@Post('cancel')
|
||||
@ApiOperation({ summary: 'Cancel/Skip a specific document number' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async cancelNumber(@Body() dto: any) {
|
||||
async cancelNumber(
|
||||
@Body()
|
||||
dto: {
|
||||
documentNumber: string;
|
||||
reason: string;
|
||||
projectId?: number;
|
||||
typeId?: number;
|
||||
}
|
||||
) {
|
||||
return this.service.cancelNumber(dto);
|
||||
}
|
||||
|
||||
@Post('bulk-import')
|
||||
@ApiOperation({ summary: 'Bulk import/set document number counters' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async bulkImport(@Body() items: any[]) {
|
||||
async bulkImport(@Body() items: ManualOverrideDto[]) {
|
||||
return this.service.bulkImport(items);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class ReserveNumberDto {
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: Record<string, any>;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class ReserveNumberResponseDto {
|
||||
|
||||
@@ -25,7 +25,7 @@ export class DocumentNumberAudit {
|
||||
documentNumber!: string;
|
||||
|
||||
@Column({ name: 'counter_key', type: 'json' })
|
||||
counterKey!: any;
|
||||
counterKey!: Record<string, unknown> | unknown;
|
||||
|
||||
@Column({ name: 'template_used', length: 200 })
|
||||
templateUsed!: string;
|
||||
@@ -73,7 +73,7 @@ export class DocumentNumberAudit {
|
||||
newValue?: string;
|
||||
|
||||
@Column({ name: 'metadata', type: 'json', nullable: true })
|
||||
metadata?: any;
|
||||
metadata?: Record<string, unknown>;
|
||||
|
||||
@Column({ name: 'user_id', nullable: true })
|
||||
userId?: number;
|
||||
|
||||
@@ -38,7 +38,7 @@ export class DocumentNumberError {
|
||||
stackTrace?: string;
|
||||
|
||||
@Column({ name: 'context_data', type: 'json', nullable: true })
|
||||
contextData?: any;
|
||||
contextData?: Record<string, unknown>;
|
||||
|
||||
@Column({ name: 'user_id', nullable: true })
|
||||
userId?: number;
|
||||
|
||||
+1
-1
@@ -93,5 +93,5 @@ export class DocumentNumberReservation {
|
||||
userAgent!: string | null;
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
metadata!: any | null;
|
||||
metadata!: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
@@ -23,10 +23,16 @@ import { MetricsService } from './metrics.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';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import {
|
||||
ReserveNumberDto,
|
||||
ReserveNumberResponseDto,
|
||||
} from '../dto/reserve-number.dto';
|
||||
import {
|
||||
ConfirmReservationDto,
|
||||
ConfirmReservationResponseDto,
|
||||
} from '../dto/confirm-reservation.dto';
|
||||
import { ManualOverrideDto } from '../dto/manual-override.dto';
|
||||
import { UuidResolverService } from '../../../common/services/uuid-resolver.service';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentNumberingService {
|
||||
@@ -48,25 +54,10 @@ export class DocumentNumberingService {
|
||||
private manualOverrideService: ManualOverrideService,
|
||||
private metricsService: MetricsService,
|
||||
@InjectEntityManager()
|
||||
private entityManager: EntityManager
|
||||
private entityManager: EntityManager,
|
||||
private uuidResolver: UuidResolverService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* ADR-019: Resolve projectId (INT or UUID string) to internal INT ID
|
||||
*/
|
||||
private async resolveProjectId(projectId: number | string): Promise<number> {
|
||||
if (typeof projectId === 'number') return projectId;
|
||||
const num = Number(projectId);
|
||||
if (!isNaN(num)) return num;
|
||||
const project = await this.entityManager.findOne(Project, {
|
||||
where: { uuid: projectId },
|
||||
select: ['id'],
|
||||
});
|
||||
if (!project)
|
||||
throw new NotFoundException(`Project with UUID ${projectId} not found`);
|
||||
return project.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* ADR-019: Public facade for controllers to resolve project/organization IDs
|
||||
*/
|
||||
@@ -74,24 +65,8 @@ export class DocumentNumberingService {
|
||||
type: 'project' | 'organization',
|
||||
id: number | string
|
||||
): Promise<number> {
|
||||
if (type === 'project') return this.resolveProjectId(id);
|
||||
return this.resolveOrganizationId(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* ADR-019: Resolve organizationId (INT or UUID string) to internal INT ID
|
||||
*/
|
||||
private async resolveOrganizationId(orgId: number | string): Promise<number> {
|
||||
if (typeof orgId === 'number') return orgId;
|
||||
const num = Number(orgId);
|
||||
if (!isNaN(num)) return num;
|
||||
const org = await this.entityManager.findOne(Organization, {
|
||||
where: { uuid: orgId },
|
||||
select: ['id'],
|
||||
});
|
||||
if (!org)
|
||||
throw new NotFoundException(`Organization with UUID ${orgId} not found`);
|
||||
return org.id;
|
||||
if (type === 'project') return this.uuidResolver.resolveProjectId(id);
|
||||
return this.uuidResolver.resolveOrganizationId(id);
|
||||
}
|
||||
|
||||
async generateNextNumber(
|
||||
@@ -176,7 +151,7 @@ export class DocumentNumberingService {
|
||||
});
|
||||
|
||||
return { number: documentNumber, auditId: audit.id };
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
await this.logError(error, ctx, 'GENERATE');
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -190,7 +165,7 @@ export class DocumentNumberingService {
|
||||
dto: ReserveNumberDto,
|
||||
userId: number,
|
||||
ipAddress?: string
|
||||
): Promise<any> {
|
||||
): Promise<ReserveNumberResponseDto> {
|
||||
try {
|
||||
// Delegate completely to ReservationService
|
||||
return await this.reservationService.reserve(
|
||||
@@ -199,7 +174,7 @@ export class DocumentNumberingService {
|
||||
ipAddress || '0.0.0.0',
|
||||
'Unknown' // userAgent not passed in legacy call
|
||||
);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
this.logger.error('Reservation failed', error);
|
||||
throw error;
|
||||
}
|
||||
@@ -208,7 +183,7 @@ export class DocumentNumberingService {
|
||||
async confirmReservation(
|
||||
dto: ConfirmReservationDto,
|
||||
userId: number
|
||||
): Promise<any> {
|
||||
): Promise<ConfirmReservationResponseDto> {
|
||||
return this.reservationService.confirm(dto, userId);
|
||||
}
|
||||
|
||||
@@ -273,16 +248,18 @@ export class DocumentNumberingService {
|
||||
}
|
||||
|
||||
async getTemplatesByProject(projectId: number | string) {
|
||||
const internalId = await this.resolveProjectId(projectId);
|
||||
const internalId = await this.uuidResolver.resolveProjectId(projectId);
|
||||
return this.formatRepo.find({
|
||||
where: { projectId: internalId },
|
||||
relations: ['project', 'correspondenceType'],
|
||||
});
|
||||
}
|
||||
|
||||
async saveTemplate(dto: any) {
|
||||
async saveTemplate(
|
||||
dto: Partial<DocumentNumberFormat> & { projectId?: number | string }
|
||||
) {
|
||||
if (dto.projectId) {
|
||||
dto.projectId = await this.resolveProjectId(dto.projectId);
|
||||
dto.projectId = await this.uuidResolver.resolveProjectId(dto.projectId);
|
||||
}
|
||||
return this.formatRepo.save(dto);
|
||||
}
|
||||
@@ -312,7 +289,7 @@ export class DocumentNumberingService {
|
||||
);
|
||||
}
|
||||
|
||||
async manualOverride(dto: any, userId: number) {
|
||||
async manualOverride(dto: ManualOverrideDto, userId: number) {
|
||||
return this.manualOverrideService.applyOverride(dto, userId);
|
||||
}
|
||||
async voidAndReplace(dto: {
|
||||
@@ -433,7 +410,7 @@ export class DocumentNumberingService {
|
||||
return { status: 'CANCELLED' };
|
||||
}
|
||||
|
||||
async bulkImport(items: any[]) {
|
||||
async bulkImport(items: ManualOverrideDto[]) {
|
||||
const results = { success: 0, failed: 0, errors: [] as string[] };
|
||||
|
||||
// items expected to be ManualOverrideDto[] or similar
|
||||
@@ -464,15 +441,32 @@ export class DocumentNumberingService {
|
||||
return results;
|
||||
}
|
||||
|
||||
private async logAudit(data: any): Promise<DocumentNumberAudit> {
|
||||
private async logAudit(data: {
|
||||
documentNumber: string;
|
||||
counterKey: unknown;
|
||||
templateUsed: string;
|
||||
context: { projectId?: number; userId?: number; ipAddress?: string };
|
||||
isSuccess: boolean;
|
||||
operation: string;
|
||||
status?: string;
|
||||
oldValue?: string;
|
||||
newValue?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}): Promise<DocumentNumberAudit> {
|
||||
const audit = this.auditRepo.create({
|
||||
...data,
|
||||
projectId: data.context.projectId,
|
||||
createdBy: data.context.userId,
|
||||
documentNumber: data.documentNumber,
|
||||
counterKey: data.counterKey,
|
||||
templateUsed: data.templateUsed,
|
||||
isSuccess: data.isSuccess,
|
||||
operation: data.operation,
|
||||
status: data.status,
|
||||
oldValue: data.oldValue,
|
||||
newValue: data.newValue,
|
||||
metadata: data.metadata,
|
||||
userId: data.context.userId,
|
||||
ipAddress: data.context.ipAddress,
|
||||
// map other fields
|
||||
});
|
||||
return (await this.auditRepo.save(audit)) as unknown as DocumentNumberAudit;
|
||||
return this.auditRepo.save(audit);
|
||||
}
|
||||
|
||||
private mapErrorType(error: Error): string {
|
||||
|
||||
Reference in New Issue
Block a user