This commit is contained in:
@@ -144,7 +144,7 @@ describe('DocumentNumberingService', () => {
|
|||||||
it('voidAndReplace should verify audit log exists', async () => {
|
it('voidAndReplace should verify audit log exists', async () => {
|
||||||
const auditRepo = module.get(getRepositoryToken(DocumentNumberAudit));
|
const auditRepo = module.get(getRepositoryToken(DocumentNumberAudit));
|
||||||
(auditRepo.findOne as jest.Mock).mockResolvedValue({
|
(auditRepo.findOne as jest.Mock).mockResolvedValue({
|
||||||
generatedNumber: 'DOC-001',
|
documentNumber: 'DOC-001',
|
||||||
counterKey: JSON.stringify({ projectId: 1, correspondenceTypeId: 1 }),
|
counterKey: JSON.stringify({ projectId: 1, correspondenceTypeId: 1 }),
|
||||||
templateUsed: 'test',
|
templateUsed: 'test',
|
||||||
});
|
});
|
||||||
@@ -162,7 +162,7 @@ describe('DocumentNumberingService', () => {
|
|||||||
it('cancelNumber should log cancellation', async () => {
|
it('cancelNumber should log cancellation', async () => {
|
||||||
const auditRepo = module.get(getRepositoryToken(DocumentNumberAudit));
|
const auditRepo = module.get(getRepositoryToken(DocumentNumberAudit));
|
||||||
(auditRepo.findOne as jest.Mock).mockResolvedValue({
|
(auditRepo.findOne as jest.Mock).mockResolvedValue({
|
||||||
generatedNumber: 'DOC-002',
|
documentNumber: 'DOC-002',
|
||||||
counterKey: {},
|
counterKey: {},
|
||||||
});
|
});
|
||||||
(auditRepo.save as jest.Mock).mockResolvedValue({ id: 3 });
|
(auditRepo.save as jest.Mock).mockResolvedValue({ id: 3 });
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
@Index(['documentId'])
|
@Index(['documentId'])
|
||||||
@Index(['status'])
|
@Index(['status'])
|
||||||
@Index(['operation'])
|
@Index(['operation'])
|
||||||
@Index(['generatedNumber'])
|
@Index(['documentNumber'])
|
||||||
@Index(['reservationToken'])
|
@Index(['reservationToken'])
|
||||||
export class DocumentNumberAudit {
|
export class DocumentNumberAudit {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
@@ -21,8 +21,8 @@ export class DocumentNumberAudit {
|
|||||||
@Column({ name: 'document_id', nullable: true })
|
@Column({ name: 'document_id', nullable: true })
|
||||||
documentId?: number;
|
documentId?: number;
|
||||||
|
|
||||||
@Column({ name: 'generated_number', length: 100 })
|
@Column({ name: 'document_number', length: 100 })
|
||||||
generatedNumber!: string;
|
documentNumber!: string;
|
||||||
|
|
||||||
@Column({ name: 'counter_key', type: 'json' })
|
@Column({ name: 'counter_key', type: 'json' })
|
||||||
counterKey!: any;
|
counterKey!: any;
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export class DocumentNumberingService {
|
|||||||
const sequence = await this.counterService.incrementCounter(key);
|
const sequence = await this.counterService.incrementCounter(key);
|
||||||
|
|
||||||
// 4. Format Number
|
// 4. Format Number
|
||||||
const generatedNumber = await this.formatService.format({
|
const documentNumber = await this.formatService.format({
|
||||||
projectId: ctx.projectId,
|
projectId: ctx.projectId,
|
||||||
correspondenceTypeId: ctx.typeId,
|
correspondenceTypeId: ctx.typeId,
|
||||||
subTypeId: ctx.subTypeId,
|
subTypeId: ctx.subTypeId,
|
||||||
@@ -161,7 +161,7 @@ export class DocumentNumberingService {
|
|||||||
|
|
||||||
// 5. Audit Log
|
// 5. Audit Log
|
||||||
const audit = await this.logAudit({
|
const audit = await this.logAudit({
|
||||||
generatedNumber,
|
documentNumber,
|
||||||
counterKey: JSON.stringify(key),
|
counterKey: JSON.stringify(key),
|
||||||
templateUsed: 'DELEGATED_TO_FORMAT_SERVICE',
|
templateUsed: 'DELEGATED_TO_FORMAT_SERVICE',
|
||||||
context: ctx,
|
context: ctx,
|
||||||
@@ -175,7 +175,7 @@ export class DocumentNumberingService {
|
|||||||
type_id: String(ctx.typeId),
|
type_id: String(ctx.typeId),
|
||||||
});
|
});
|
||||||
|
|
||||||
return { number: generatedNumber, auditId: audit.id };
|
return { number: documentNumber, auditId: audit.id };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
await this.logError(error, ctx, 'GENERATE');
|
await this.logError(error, ctx, 'GENERATE');
|
||||||
throw error;
|
throw error;
|
||||||
@@ -322,7 +322,7 @@ export class DocumentNumberingService {
|
|||||||
}) {
|
}) {
|
||||||
// 1. Find the audit log for this number to get context
|
// 1. Find the audit log for this number to get context
|
||||||
const lastAudit = await this.auditRepo.findOne({
|
const lastAudit = await this.auditRepo.findOne({
|
||||||
where: { generatedNumber: dto.documentNumber },
|
where: { documentNumber: dto.documentNumber },
|
||||||
order: { createdAt: 'DESC' },
|
order: { createdAt: 'DESC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -334,7 +334,7 @@ export class DocumentNumberingService {
|
|||||||
);
|
);
|
||||||
// Create a void audit anyway if possible?
|
// Create a void audit anyway if possible?
|
||||||
await this.logAudit({
|
await this.logAudit({
|
||||||
generatedNumber: dto.documentNumber,
|
documentNumber: dto.documentNumber,
|
||||||
counterKey: {}, // Unknown
|
counterKey: {}, // Unknown
|
||||||
templateUsed: 'VOID_UNKNOWN',
|
templateUsed: 'VOID_UNKNOWN',
|
||||||
context: { userId: 0, ipAddress: '0.0.0.0' }, // System
|
context: { userId: 0, ipAddress: '0.0.0.0' }, // System
|
||||||
@@ -349,7 +349,7 @@ export class DocumentNumberingService {
|
|||||||
|
|
||||||
// 2. Log VOID
|
// 2. Log VOID
|
||||||
await this.logAudit({
|
await this.logAudit({
|
||||||
generatedNumber: dto.documentNumber,
|
documentNumber: dto.documentNumber,
|
||||||
counterKey: lastAudit.counterKey,
|
counterKey: lastAudit.counterKey,
|
||||||
templateUsed: lastAudit.templateUsed,
|
templateUsed: lastAudit.templateUsed,
|
||||||
context: { userId: 0, ipAddress: '0.0.0.0' }, // TODO: Pass userId from controller
|
context: { userId: 0, ipAddress: '0.0.0.0' }, // TODO: Pass userId from controller
|
||||||
@@ -409,14 +409,14 @@ export class DocumentNumberingService {
|
|||||||
}) {
|
}) {
|
||||||
// Similar to VOID but status CANCELLED
|
// Similar to VOID but status CANCELLED
|
||||||
const lastAudit = await this.auditRepo.findOne({
|
const lastAudit = await this.auditRepo.findOne({
|
||||||
where: { generatedNumber: dto.documentNumber },
|
where: { documentNumber: dto.documentNumber },
|
||||||
order: { createdAt: 'DESC' },
|
order: { createdAt: 'DESC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const contextKey = lastAudit?.counterKey;
|
const contextKey = lastAudit?.counterKey;
|
||||||
|
|
||||||
await this.logAudit({
|
await this.logAudit({
|
||||||
generatedNumber: dto.documentNumber,
|
documentNumber: dto.documentNumber,
|
||||||
counterKey: contextKey || {},
|
counterKey: contextKey || {},
|
||||||
templateUsed: lastAudit?.templateUsed || 'CANCEL',
|
templateUsed: lastAudit?.templateUsed || 'CANCEL',
|
||||||
context: {
|
context: {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ describe('ManualOverrideService', () => {
|
|||||||
expect(counterService.forceUpdateCounter).toHaveBeenCalledWith(dto, 999);
|
expect(counterService.forceUpdateCounter).toHaveBeenCalledWith(dto, 999);
|
||||||
expect(auditService.log).toHaveBeenCalledWith(
|
expect(auditService.log).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
generatedNumber: 'OVERRIDE-TO-999',
|
documentNumber: 'OVERRIDE-TO-999',
|
||||||
operation: 'MANUAL_OVERRIDE',
|
operation: 'MANUAL_OVERRIDE',
|
||||||
status: 'MANUAL',
|
status: 'MANUAL',
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class ManualOverrideService {
|
|||||||
// 2. Log Audit
|
// 2. Log Audit
|
||||||
await this.auditService.log({
|
await this.auditService.log({
|
||||||
documentId: undefined, // No specific document
|
documentId: undefined, // No specific document
|
||||||
generatedNumber: `OVERRIDE-TO-${dto.newLastNumber}`,
|
documentNumber: `OVERRIDE-TO-${dto.newLastNumber}`,
|
||||||
operation: 'MANUAL_OVERRIDE',
|
operation: 'MANUAL_OVERRIDE',
|
||||||
status: 'MANUAL',
|
status: 'MANUAL',
|
||||||
counterKey: dto, // CounterKeyDto part of ManualOverrideDto
|
counterKey: dto, // CounterKeyDto part of ManualOverrideDto
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ import { UserModule } from '../user/user.module'; // ✅ 1. Import UserModule
|
|||||||
|
|
||||||
ElasticsearchModule.registerAsync({
|
ElasticsearchModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
node:
|
node: `http://${configService.get<string>('ELASTICSEARCH_HOST', 'localhost')}:${configService.get<string>('ELASTICSEARCH_PORT', '9200')}`,
|
||||||
configService.get<string>('ELASTICSEARCH_NODE') ||
|
|
||||||
'http://localhost:9200',
|
|
||||||
auth: {
|
auth: {
|
||||||
username: configService.get<string>('ELASTICSEARCH_USERNAME') || '',
|
username: configService.get<string>('ELASTICSEARCH_USERNAME') || '',
|
||||||
password: configService.get<string>('ELASTICSEARCH_PASSWORD') || '',
|
password: configService.get<string>('ELASTICSEARCH_PASSWORD') || '',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { SearchQueryDto } from './dto/search-query.dto';
|
|||||||
export class SearchService implements OnModuleInit {
|
export class SearchService implements OnModuleInit {
|
||||||
private readonly logger = new Logger(SearchService.name);
|
private readonly logger = new Logger(SearchService.name);
|
||||||
private readonly indexName = 'dms_documents';
|
private readonly indexName = 'dms_documents';
|
||||||
|
private isElasticsearchAvailable = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly esService: ElasticsearchService,
|
private readonly esService: ElasticsearchService,
|
||||||
@@ -14,6 +15,20 @@ export class SearchService implements OnModuleInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
|
// Test Elasticsearch connection first
|
||||||
|
try {
|
||||||
|
await this.esService.ping();
|
||||||
|
this.logger.log('Elasticsearch connection successful');
|
||||||
|
this.isElasticsearchAvailable = true;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Elasticsearch connection failed: ${(error as Error).message}`,
|
||||||
|
(error as Error).stack
|
||||||
|
);
|
||||||
|
this.isElasticsearchAvailable = false;
|
||||||
|
return; // Don't try to create index if connection fails
|
||||||
|
}
|
||||||
|
|
||||||
await this.createIndexIfNotExists();
|
await this.createIndexIfNotExists();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +109,12 @@ export class SearchService implements OnModuleInit {
|
|||||||
const { q, type, projectId, page = 1, limit = 20 } = queryDto;
|
const { q, type, projectId, page = 1, limit = 20 } = queryDto;
|
||||||
const from = (page - 1) * limit;
|
const from = (page - 1) * limit;
|
||||||
|
|
||||||
|
// Early fallback if Elasticsearch is not available
|
||||||
|
if (!this.isElasticsearchAvailable) {
|
||||||
|
this.logger.warn('Search unavailable - Elasticsearch not connected');
|
||||||
|
return { data: [], meta: { total: 0, page, limit, took: 0 } };
|
||||||
|
}
|
||||||
|
|
||||||
const mustQueries: any[] = [];
|
const mustQueries: any[] = [];
|
||||||
|
|
||||||
// 1. Full-text search logic
|
// 1. Full-text search logic
|
||||||
@@ -146,7 +167,14 @@ export class SearchService implements OnModuleInit {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Search failed: ${(error as Error).message}`);
|
const err = error as Error;
|
||||||
|
this.logger.error(`Search failed: ${err.message}`, err.stack);
|
||||||
|
this.logger.debug(
|
||||||
|
`Search query context: ${JSON.stringify({
|
||||||
|
query: queryDto,
|
||||||
|
esNode: this.configService.get('ELASTICSEARCH_NODE'),
|
||||||
|
})}`
|
||||||
|
);
|
||||||
return { data: [], meta: { total: 0, page, limit, took: 0 } };
|
return { data: [], meta: { total: 0, page, limit, took: 0 } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user