feat: implement recovery incident service and controller with metric aggregation and CRUD operations

This commit is contained in:
Syed Waseem
2026-08-10 15:18:20 +05:30
parent 98df6f18b4
commit 06a1ee518e
2 changed files with 206 additions and 0 deletions
@@ -22,12 +22,27 @@ export class RecoveryIncidentController {
return this.recoveryIncidentService.findAll();
}
@Get('metrics')
@ApiOperation({ summary: 'Get recovery incidents metrics summary' })
getMetrics() {
return this.recoveryIncidentService.getMetrics();
}
@Get(':id')
@ApiOperation({ summary: 'Get a recovery incident by ID' })
findOne(@Param('id') id: string) {
return this.recoveryIncidentService.findOne(id);
}
@Patch(':id/status')
@ApiOperation({ summary: 'Update status of a recovery incident' })
updateStatus(
@Param('id') id: string,
@Body('status') status: string,
) {
return this.recoveryIncidentService.updateStatus(id, status);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a recovery incident' })
update(@Param('id') id: string, @Body() updateDto: UpdateRecoveryIncidentDto) {
@@ -6,6 +6,16 @@ import { CreateRecoveryIncidentDto } from './dto/create-recovery-incident.dto';
import { UpdateRecoveryIncidentDto } from './dto/update-recovery-incident.dto';
import { getTenantId } from '../../common/tenant/tenant.context';
export interface MetricCardData {
id?: string;
title: string;
value?: string;
trendText: string;
trendValue: string;
trendType: 'positive' | 'negative' | 'neutral';
sparklineColor: 'green' | 'red';
}
@Injectable()
export class RecoveryIncidentService {
constructor(
@@ -31,6 +41,181 @@ export class RecoveryIncidentService {
});
}
async getMetrics(): Promise<MetricCardData[]> {
const tenantId = getTenantId();
const incidents = await this.recoveryIncidentRepo.find({
where: { tenantId },
});
const now = new Date();
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const fourteenDaysAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000);
const getIncidentDate = (i: RecoveryIncident): Date => {
if (i.date) return new Date(i.date);
if (i.createdAt) return new Date(i.createdAt);
return now;
};
const currentWeekIncidents = incidents.filter((i) => getIncidentDate(i) >= sevenDaysAgo);
const previousWeekIncidents = incidents.filter((i) => {
const d = getIncidentDate(i);
return d >= fourteenDaysAgo && d < sevenDaysAgo;
});
// Helper for parsing currency string into number
const parseValue = (val?: string): number => {
if (!val) return 0;
const num = parseFloat(val.replace(/[^0-9.]/g, '')) || 0;
return num;
};
// Helper for formatting sum into string (e.g. $412k, $1.2M, $500)
const formatCurrency = (amount: number): string => {
if (amount >= 1000000) {
return `$${(amount / 1000000).toFixed(1).replace(/\.0$/, '')}M`;
}
if (amount >= 1000) {
return `$${Math.round(amount / 1000)}k`;
}
return `$${amount.toLocaleString()}`;
};
// Helper to check if an incident is satisfied
const isSatisfied = (i: RecoveryIncident): boolean => {
const s = (i.status || '').toLowerCase();
return s.includes('appr') || s.includes('active') || s.includes('success') || Boolean(i.isPerksClaimed);
};
// Helper to check if an incident is pending
const isPending = (i: RecoveryIncident): boolean => {
const s = (i.status || '').toLowerCase();
return s.includes('pending') || s.includes('review') || s.includes('new');
};
// 1. Total Recoveries
const totalCount = incidents.length;
const currentWeekTotal = currentWeekIncidents.length;
const previousWeekTotal = previousWeekIncidents.length;
let totalTrendVal = '0%';
let totalTrendType: 'positive' | 'negative' | 'neutral' = 'neutral';
let totalSparkColor: 'green' | 'red' = 'green';
if (previousWeekTotal > 0) {
const pct = Math.round(((currentWeekTotal - previousWeekTotal) / previousWeekTotal) * 100);
totalTrendVal = `${pct >= 0 ? '+' : ''}${pct}%`;
totalTrendType = pct >= 0 ? 'positive' : 'negative';
totalSparkColor = pct >= 0 ? 'green' : 'red';
} else if (currentWeekTotal > 0) {
totalTrendVal = '+100%';
totalTrendType = 'positive';
totalSparkColor = 'green';
}
// 2. Pending Approval
const pendingIncidents = incidents.filter(isPending);
const pendingCount = pendingIncidents.length;
const currentWeekPending = currentWeekIncidents.filter(isPending).length;
const previousWeekPending = previousWeekIncidents.filter(isPending).length;
let pendingTrendVal = '0%';
let pendingTrendType: 'positive' | 'negative' | 'neutral' = 'neutral';
let pendingSparkColor: 'green' | 'red' = 'green';
if (previousWeekPending > 0) {
const pct = Math.round(((currentWeekPending - previousWeekPending) / previousWeekPending) * 100);
pendingTrendVal = `${pct >= 0 ? '+' : ''}${pct}%`;
pendingTrendType = pct >= 0 ? 'positive' : 'negative';
pendingSparkColor = pct >= 0 ? 'green' : 'red';
} else if (pendingCount > 0) {
pendingTrendVal = 'High Priority';
pendingTrendType = 'positive';
pendingSparkColor = 'green';
}
// 3. Refund Value
const totalRefundSum = incidents.reduce((acc, curr) => acc + parseValue(curr.value), 0);
const currentWeekRefundSum = currentWeekIncidents.reduce((acc, curr) => acc + parseValue(curr.value), 0);
const previousWeekRefundSum = previousWeekIncidents.reduce((acc, curr) => acc + parseValue(curr.value), 0);
let refundTrendVal = '0%';
let refundTrendType: 'positive' | 'negative' | 'neutral' = 'neutral';
let refundSparkColor: 'green' | 'red' = 'green';
if (previousWeekRefundSum > 0) {
const pct = Math.round(((currentWeekRefundSum - previousWeekRefundSum) / previousWeekRefundSum) * 100);
refundTrendVal = `${pct >= 0 ? '+' : ''}${pct}%`;
refundTrendType = pct >= 0 ? 'positive' : 'negative';
refundSparkColor = pct >= 0 ? 'green' : 'red';
} else if (currentWeekRefundSum > 0) {
refundTrendVal = '+100%';
refundTrendType = 'positive';
refundSparkColor = 'green';
}
// 4. Customer Satisfaction
const satisfiedCount = incidents.filter(isSatisfied).length;
const satisfactionRate = totalCount > 0 ? Math.round((satisfiedCount / totalCount) * 100) : 0;
const currentSatisfied = currentWeekIncidents.filter(isSatisfied).length;
const currentSatRate = currentWeekIncidents.length > 0 ? Math.round((currentSatisfied / currentWeekIncidents.length) * 100) : 0;
const previousSatisfied = previousWeekIncidents.filter(isSatisfied).length;
const previousSatRate = previousWeekIncidents.length > 0 ? Math.round((previousSatisfied / previousWeekIncidents.length) * 100) : 0;
let satTrendVal = '0%';
let satTrendType: 'positive' | 'negative' | 'neutral' = 'neutral';
let satSparkColor: 'green' | 'red' = 'green';
if (previousWeekIncidents.length > 0 && currentWeekIncidents.length > 0) {
const diff = currentSatRate - previousSatRate;
satTrendVal = `${diff >= 0 ? '+' : ''}${diff}%`;
satTrendType = diff >= 0 ? 'positive' : 'negative';
satSparkColor = diff >= 0 ? 'green' : 'red';
} else if (satisfactionRate > 0) {
satTrendVal = `+${satisfactionRate}%`;
satTrendType = 'positive';
satSparkColor = 'green';
}
return [
{
id: 'total-recoveries',
title: 'Total Recoveries',
value: totalCount.toLocaleString(),
trendValue: totalTrendVal,
trendText: 'since last week',
trendType: totalTrendType,
sparklineColor: totalSparkColor,
},
{
id: 'pending-approval',
title: 'Pending Approval',
value: pendingCount.toLocaleString(),
trendValue: pendingTrendVal,
trendText: 'since last week',
trendType: pendingTrendType,
sparklineColor: pendingSparkColor,
},
{
id: 'refund-value',
title: 'Refund Value',
value: formatCurrency(totalRefundSum),
trendValue: refundTrendVal,
trendText: 'since last week',
trendType: refundTrendType,
sparklineColor: refundSparkColor,
},
{
id: 'customer-satisfaction',
title: 'Customer Satisfaction',
value: `${satisfactionRate}%`,
trendValue: satTrendVal,
trendText: 'since last week',
trendType: satTrendType,
sparklineColor: satSparkColor,
},
];
}
async findOne(id: string): Promise<RecoveryIncident> {
const tenantId = getTenantId();
const incident = await this.recoveryIncidentRepo.findOne({
@@ -56,6 +241,12 @@ export class RecoveryIncidentService {
return this.recoveryIncidentRepo.save(incident);
}
async updateStatus(id: string, status: string): Promise<RecoveryIncident> {
const incident = await this.findOne(id);
incident.status = status;
return this.recoveryIncidentRepo.save(incident);
}
async remove(id: string): Promise<void> {
const incident = await this.findOne(id);
await this.recoveryIncidentRepo.remove(incident);