- Portfolio Risk
+ Merchant Monitoring Risk
- Risk levels of approved merchants from completed onboarding flows.
+ Risk levels of all merchant monitoring reports.
@@ -58,13 +57,13 @@ export const PortfolioRiskStatistics: FunctionComponent<
dominantBaseline="middle"
className={'text-lg font-bold'}
>
- {Object.values(cases.approved).reduce((acc, curr) => acc + curr, 0)}
+ {Object.values(riskLevelCounts).reduce((acc, curr) => acc + curr, 0)}
- Merchants
+ Reports
({
+ data={Object.entries(riskLevelCounts).map(([riskLevel, value]) => ({
name: `${titleCase(riskLevel)} Risk`,
value,
}))}
@@ -89,123 +88,32 @@ export const PortfolioRiskStatistics: FunctionComponent<
-
- {filters?.map(filter => {
- const totalRisk = Object.values(filter.riskLevels).reduce((acc, curr) => acc + curr, 0);
-
- return (
-
-
- {filter.name} Risk
-
- {filter.description}
-
-
- = 5,
- })}
- >
- {totalRisk}
-
-
- {filter.entityPlural}
-
- ({
- name: `${titleCase(riskLevel)} Risk`,
- value,
- }),
- )}
- cx={47}
- cy={47}
- innerRadius={43}
- outerRadius={52}
- fill="#8884d8"
- paddingAngle={5}
- dataKey="value"
- cornerRadius={9999}
- >
- {Object.keys(riskLevelToFillColor).map(riskLevel => (
- |
- ))}
-
-
-
-
-
-
-
- );
- })}
-
Risk Indicators
diff --git a/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/hooks/usePortfolioRiskStatisticsLogic/usePortfolioRiskStatisticsLogic.tsx b/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/hooks/usePortfolioRiskStatisticsLogic/usePortfolioRiskStatisticsLogic.tsx
index 5b67de93d0..596541cb86 100644
--- a/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/hooks/usePortfolioRiskStatisticsLogic/usePortfolioRiskStatisticsLogic.tsx
+++ b/apps/backoffice-v2/src/pages/Statistics/components/PortfolioRiskStatistics/hooks/usePortfolioRiskStatisticsLogic/usePortfolioRiskStatisticsLogic.tsx
@@ -6,13 +6,12 @@ import {
riskLevelToFillColor,
} from '@/pages/Statistics/components/PortfolioRiskStatistics/constants';
import { z } from 'zod';
-import { HomeMetricsOutputSchema } from '@/domains/metrics/hooks/queries/useHomeMetricsQuery/useHomeMetricsQuery';
+import { MetricsResponseSchema } from '@/domains/business-reports/hooks/queries/useBusinessReportMetricsQuery/useBusinessReportMetricsQuery';
export const usePortfolioRiskStatisticsLogic = ({
- riskIndicators,
- reports,
- cases,
-}: z.infer) => {
+ riskLevelCounts,
+ violationCounts,
+}: z.infer) => {
const [parent] = useAutoAnimate();
const [riskIndicatorsSorting, setRiskIndicatorsSorting] = useState('desc');
const onSortRiskIndicators = useCallback(
@@ -21,9 +20,13 @@ export const usePortfolioRiskStatisticsLogic = ({
},
[],
);
- const totalRiskIndicators = riskIndicators.reduce((acc, curr) => acc + curr.count, 0);
+ const totalRiskIndicators = Object.values(violationCounts).reduce((acc, curr) => acc + curr, 0);
const filteredRiskIndicators = useMemo(() => {
- return structuredClone(riskIndicators)
+ return Object.entries(violationCounts)
+ .map(([name, count]) => ({
+ name,
+ count,
+ }))
.sort((a, b) => {
if (riskIndicatorsSorting === 'asc') {
return a.count - b.count;
@@ -32,7 +35,7 @@ export const usePortfolioRiskStatisticsLogic = ({
return b.count - a.count;
})
.slice(0, 5);
- }, [riskIndicators, riskIndicatorsSorting]);
+ }, [violationCounts, riskIndicatorsSorting]);
const widths = useMemo(() => {
const maxValue = Math.max(...filteredRiskIndicators.map(item => item.count), 0);
@@ -40,37 +43,12 @@ export const usePortfolioRiskStatisticsLogic = ({
item.count === 0 ? 0 : Math.max((item.count / maxValue) * 100, 2),
);
}, [filteredRiskIndicators]);
- const filters = [
- {
- name: 'Merchant Monitoring',
- description: 'Risk Risk levels of all merchant monitoring reports.',
- entityPlural: 'Reports',
- riskLevels: {
- low: reports.all.low,
- medium: reports.all.medium,
- high: reports.all.high,
- critical: reports.all.critical,
- },
- },
- {
- name: 'Merchant Onboarding',
- description: 'Risk levels of all active onboarding cases.',
- entityPlural: 'Cases',
- riskLevels: {
- low: cases.inProgress.low,
- medium: cases.inProgress.medium,
- high: cases.inProgress.high,
- critical: cases.inProgress.critical,
- },
- },
- ];
return {
riskLevelToFillColor,
parent,
widths,
riskLevelToBackgroundColor,
- filters,
riskIndicatorsSorting,
onSortRiskIndicators,
filteredRiskIndicators,
diff --git a/services/workflows-service/prisma/data-migrations b/services/workflows-service/prisma/data-migrations
index 186cc70140..bfc772b0ad 160000
--- a/services/workflows-service/prisma/data-migrations
+++ b/services/workflows-service/prisma/data-migrations
@@ -1 +1 @@
-Subproject commit 186cc70140c064c184f116ba388e31464670b0a8
+Subproject commit bfc772b0ade3ae49465629d6c85ac26aac3796ab
diff --git a/services/workflows-service/src/business-report/business-report.controller.external.ts b/services/workflows-service/src/business-report/business-report.controller.external.ts
index 74ae494278..90f47afc20 100644
--- a/services/workflows-service/src/business-report/business-report.controller.external.ts
+++ b/services/workflows-service/src/business-report/business-report.controller.external.ts
@@ -38,6 +38,7 @@ import { PrismaService } from '@/prisma/prisma.service';
import { AdminAuthGuard } from '@/common/guards/admin-auth.guard';
import { BusinessReportFindingsListResponseDto } from '@/business-report/dtos/business-report-findings.dto';
import { MerchantMonitoringClient } from '@/business-report/merchant-monitoring-client';
+import { BusinessReportMetricsDto } from './dtos/business-report-metrics-dto';
@ApiBearerAuth()
@swagger.ApiTags('Business Reports')
@@ -115,6 +116,15 @@ export class BusinessReportControllerExternal {
return await this.merchantMonitoringClient.listFindings();
}
+ @common.Get('/metrics')
+ @swagger.ApiOkResponse({ type: BusinessReportMetricsDto })
+ @swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
+ async getMetrics(@CurrentProject() currentProjectId: TProjectId) {
+ const { id: customerId } = await this.customerService.getByProjectId(currentProjectId);
+
+ return await this.merchantMonitoringClient.getMetrics({ customerId });
+ }
+
@common.Post()
@swagger.ApiOkResponse({})
@swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
diff --git a/services/workflows-service/src/business-report/dtos/business-report-metrics-dto.ts b/services/workflows-service/src/business-report/dtos/business-report-metrics-dto.ts
new file mode 100644
index 0000000000..ab75feabd6
--- /dev/null
+++ b/services/workflows-service/src/business-report/dtos/business-report-metrics-dto.ts
@@ -0,0 +1,58 @@
+import { IsNumber, IsObject, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+
+import { ApiProperty } from '@nestjs/swagger';
+
+export class RiskLevelCountsDto {
+ @ApiProperty({
+ description: 'Number of low risk reports',
+ example: 5,
+ type: Number,
+ })
+ @IsNumber()
+ low!: number;
+
+ @ApiProperty({
+ description: 'Number of medium risk reports',
+ example: 3,
+ type: Number,
+ })
+ @IsNumber()
+ medium!: number;
+
+ @ApiProperty({
+ description: 'Number of high risk reports',
+ example: 2,
+ type: Number,
+ })
+ @IsNumber()
+ high!: number;
+
+ @ApiProperty({
+ description: 'Number of critical risk reports',
+ example: 1,
+ type: Number,
+ })
+ @IsNumber()
+ critical!: number;
+}
+
+export class BusinessReportMetricsDto {
+ @ApiProperty({
+ description: 'Counts of reports by risk level',
+ type: RiskLevelCountsDto,
+ })
+ @ValidateNested()
+ @Type(() => RiskLevelCountsDto)
+ riskLevelCounts!: RiskLevelCountsDto;
+
+ @ApiProperty({
+ description: 'Counts of violations by type',
+ example: { PROHIBITED_CONTENT: 2, MISSING_INFORMATION: 1 },
+ type: 'object',
+ additionalProperties: { type: 'number' },
+ })
+ @IsObject()
+ @Type(() => Object)
+ violationCounts!: Record;
+}
diff --git a/services/workflows-service/src/business-report/merchant-monitoring-client.ts b/services/workflows-service/src/business-report/merchant-monitoring-client.ts
index c14e611322..9d9ae81e0a 100644
--- a/services/workflows-service/src/business-report/merchant-monitoring-client.ts
+++ b/services/workflows-service/src/business-report/merchant-monitoring-client.ts
@@ -64,6 +64,16 @@ const FindManyReportsResponseSchema = z.object({
data: z.array(ReportSchema),
});
+const MetricsResponseSchema = z.object({
+ riskLevelCounts: z.object({
+ low: z.number(),
+ medium: z.number(),
+ high: z.number(),
+ critical: z.number(),
+ }),
+ violationCounts: z.record(z.string(), z.number()),
+});
+
@Injectable()
export class MerchantMonitoringClient {
private axios: AxiosInstance;
@@ -255,4 +265,17 @@ export class MerchantMonitoringClient {
return response.data ?? [];
}
+
+ public async getMetrics({ customerId }: { customerId: string }) {
+ const response = await this.axios.get('merchants/analysis/metrics', {
+ params: {
+ customerId,
+ },
+ headers: {
+ Authorization: `Bearer ${env.UNIFIED_API_TOKEN}`,
+ },
+ });
+
+ return MetricsResponseSchema.parse(response.data);
+ }
}