erp-core-backend-v2/src/modules/webhooks/services/webhooks.service.ts
rckrdmrd d616370440 fix: Resolve entity/service field mismatches and build errors (84→19)
- Add class-validator and class-transformer dependencies
- Fix inventory entities index.ts exports
- Add ConflictError to shared types
- Fix ai.service.ts quota field names
- Fix audit.service.ts field names and remove missing methods
- Fix storage.service.ts bucket and file field names
- Rewrite partners.service.ts/controller.ts to match entity
- Fix product.entity.ts computed column syntax
- Fix inventory-adjustment-line.entity.ts computed column
- Fix webhooks.service.ts field names
- Fix whatsapp.service.ts order field names
- Fix swagger.config.ts import.meta.url issue

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 02:27:03 -06:00

265 lines
7.8 KiB
TypeScript

import { Repository, FindOptionsWhere, In, LessThan } from 'typeorm';
import { WebhookEventType, WebhookEndpoint, WebhookDelivery, WebhookEvent } from '../entities';
export class WebhooksService {
constructor(
private readonly eventTypeRepository: Repository<WebhookEventType>,
private readonly endpointRepository: Repository<WebhookEndpoint>,
private readonly deliveryRepository: Repository<WebhookDelivery>,
private readonly eventRepository: Repository<WebhookEvent>
) {}
// ============================================
// EVENT TYPES
// ============================================
async findAllEventTypes(): Promise<WebhookEventType[]> {
return this.eventTypeRepository.find({
where: { isActive: true },
order: { category: 'ASC', code: 'ASC' },
});
}
async findEventTypeByCode(code: string): Promise<WebhookEventType | null> {
return this.eventTypeRepository.findOne({ where: { code } });
}
async findEventTypesByCategory(category: string): Promise<WebhookEventType[]> {
return this.eventTypeRepository.find({
where: { category: category as any, isActive: true },
order: { code: 'ASC' },
});
}
// ============================================
// ENDPOINTS
// ============================================
async findAllEndpoints(tenantId: string): Promise<WebhookEndpoint[]> {
return this.endpointRepository.find({
where: { tenantId },
order: { name: 'ASC' },
});
}
async findActiveEndpoints(tenantId: string): Promise<WebhookEndpoint[]> {
return this.endpointRepository.find({
where: { tenantId, isActive: true },
order: { name: 'ASC' },
});
}
async findEndpoint(id: string): Promise<WebhookEndpoint | null> {
return this.endpointRepository.findOne({ where: { id } });
}
async findEndpointByUrl(tenantId: string, url: string): Promise<WebhookEndpoint | null> {
return this.endpointRepository.findOne({ where: { tenantId, url } });
}
async createEndpoint(
tenantId: string,
data: Partial<WebhookEndpoint>,
createdBy?: string
): Promise<WebhookEndpoint> {
const endpoint = this.endpointRepository.create({
...data,
tenantId,
createdBy,
});
return this.endpointRepository.save(endpoint);
}
async updateEndpoint(
id: string,
data: Partial<WebhookEndpoint>,
updatedBy?: string
): Promise<WebhookEndpoint | null> {
const endpoint = await this.findEndpoint(id);
if (!endpoint) return null;
Object.assign(endpoint, data, { updatedBy });
return this.endpointRepository.save(endpoint);
}
async deleteEndpoint(id: string): Promise<boolean> {
const result = await this.endpointRepository.softDelete(id);
return (result.affected ?? 0) > 0;
}
async toggleEndpoint(id: string, isActive: boolean): Promise<WebhookEndpoint | null> {
const endpoint = await this.findEndpoint(id);
if (!endpoint) return null;
endpoint.isActive = isActive;
return this.endpointRepository.save(endpoint);
}
async findEndpointsForEvent(tenantId: string, eventTypeCode: string): Promise<WebhookEndpoint[]> {
return this.endpointRepository
.createQueryBuilder('endpoint')
.where('endpoint.tenant_id = :tenantId', { tenantId })
.andWhere('endpoint.is_active = true')
.andWhere(':eventTypeCode = ANY(endpoint.subscribed_events)', { eventTypeCode })
.getMany();
}
async updateEndpointHealth(
id: string,
isHealthy: boolean,
consecutiveFailures: number
): Promise<void> {
// Update using available fields on WebhookEndpoint entity
// isHealthy maps to isActive, tracking failures via delivery stats
await this.endpointRepository.update(id, {
isActive: isHealthy,
failedDeliveries: consecutiveFailures > 0 ? consecutiveFailures : undefined,
});
}
// ============================================
// EVENTS
// ============================================
async createEvent(tenantId: string, data: Partial<WebhookEvent>): Promise<WebhookEvent> {
const event = this.eventRepository.create({
...data,
tenantId,
status: 'pending',
});
return this.eventRepository.save(event);
}
async findEvent(id: string): Promise<WebhookEvent | null> {
return this.eventRepository.findOne({
where: { id },
relations: ['deliveries'],
});
}
async findPendingEvents(limit: number = 100): Promise<WebhookEvent[]> {
return this.eventRepository.find({
where: { status: 'pending' },
order: { createdAt: 'ASC' },
take: limit,
});
}
async updateEventStatus(id: string, status: string): Promise<void> {
const updates: Partial<WebhookEvent> = { status: status as any };
if (status === 'processed') {
updates.processedAt = new Date();
}
await this.eventRepository.update(id, updates);
}
// ============================================
// DELIVERIES
// ============================================
async createDelivery(data: Partial<WebhookDelivery>): Promise<WebhookDelivery> {
const delivery = this.deliveryRepository.create({
...data,
status: 'pending',
});
return this.deliveryRepository.save(delivery);
}
async findDelivery(id: string): Promise<WebhookDelivery | null> {
return this.deliveryRepository.findOne({ where: { id } });
}
async findDeliveriesForEvent(eventId: string): Promise<WebhookDelivery[]> {
return this.deliveryRepository.find({
where: { eventId },
order: { attemptNumber: 'ASC' },
});
}
async findDeliveriesForEndpoint(
endpointId: string,
limit: number = 50
): Promise<WebhookDelivery[]> {
return this.deliveryRepository.find({
where: { endpointId },
order: { createdAt: 'DESC' },
take: limit,
relations: ['event'],
});
}
async updateDeliveryResult(
id: string,
status: string,
responseStatus?: number,
responseBody?: string,
duration?: number,
errorMessage?: string
): Promise<void> {
const updates: Partial<WebhookDelivery> = {
status: status as any,
responseStatus,
responseBody,
responseTimeMs: duration,
errorMessage,
completedAt: new Date(),
};
await this.deliveryRepository.update(id, updates);
}
async findPendingRetries(limit: number = 100): Promise<WebhookDelivery[]> {
const now = new Date();
return this.deliveryRepository.find({
where: {
status: 'pending',
nextRetryAt: LessThan(now),
},
order: { nextRetryAt: 'ASC' },
take: limit,
});
}
async scheduleRetry(id: string, nextRetryAt: Date, attemptNumber: number): Promise<void> {
await this.deliveryRepository.update(id, {
nextRetryAt,
attemptNumber,
status: 'pending',
});
}
// ============================================
// STATISTICS
// ============================================
async getEndpointStats(
endpointId: string,
startDate: Date,
endDate: Date
): Promise<{
total: number;
success: number;
failed: number;
avgDuration: number;
}> {
const result = await this.deliveryRepository
.createQueryBuilder('d')
.select('COUNT(*)', 'total')
.addSelect('SUM(CASE WHEN d.status = :success THEN 1 ELSE 0 END)', 'success')
.addSelect('SUM(CASE WHEN d.status = :failed THEN 1 ELSE 0 END)', 'failed')
.addSelect('AVG(d.duration_ms)', 'avgDuration')
.where('d.endpoint_id = :endpointId', { endpointId })
.andWhere('d.created_at BETWEEN :startDate AND :endDate', { startDate, endDate })
.setParameter('success', 'success')
.setParameter('failed', 'failed')
.getRawOne();
return {
total: parseInt(result.total) || 0,
success: parseInt(result.success) || 0,
failed: parseInt(result.failed) || 0,
avgDuration: parseFloat(result.avgDuration) || 0,
};
}
}