Skip to content

Commit a1ff9fb

Browse files
Merge pull request #2246 from AletheiaFact/feature/verification-request-tracking-improvements
UX: Add tracking card, improve latestStatus, and show feedback for missing dates
2 parents 4057a4a + 413874c commit a1ff9fb

15 files changed

+115
-67
lines changed

public/locales/en/tracking.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"description_POSTED": "Verification request complete and reviewed. Results posted.",
66
"description_DECLINED": "Verification request declined following detailed analysis. The veracity of the information provided could not be confirmed.",
77
"errorInvalidId": "Invalid verification request ID format.",
8-
"errorFetchData": "Failed to load tracking information. Please try again later."
8+
"errorFetchData": "Failed to load tracking information. Please try again later.",
9+
"noDateFound": "No date history found"
910
}

public/locales/pt/tracking.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"description_POSTED": "Denúncia completa e revisada. Resultados publicados.",
66
"description_DECLINED": "Denúncia negada após análise detalhada. Não foi possível confirmar a veracidade dos dados fornecidos.",
77
"errorInvalidId": "O identificador da denúncia é inválido.",
8-
"errorFetchData": "Não foi possível carregar as informações de rastreamento. Tente novamente."
8+
"errorFetchData": "Não foi possível carregar as informações de rastreamento. Tente novamente.",
9+
"noDateFound": "Nenhum histórico de data encontrado"
910
}

server/mocks/VerificationRequestMock.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ export const mockQuery = {
2222
export const mockVerificationRequestModel = {
2323
find: jest.fn().mockReturnValue(mockQuery),
2424
aggregate: jest.fn().mockReturnThis(),
25-
};
25+
getById: jest.fn(),
26+
};

server/tracking/tracking.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { TrackingController } from "./tracking.controller";
33
import { TrackingService } from "./tracking.service";
44
import { HistoryModule } from "../history/history.module";
55
import { AbilityModule } from "../auth/ability/ability.module";
6+
import { VerificationRequestModule } from "../verification-request/verification-request.module";
67

78
@Module({
89
imports: [
910
HistoryModule,
1011
AbilityModule,
12+
VerificationRequestModule
1113
],
1214
controllers: [TrackingController],
1315
providers: [TrackingService],

server/tracking/tracking.service.spec.ts

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { TrackingService } from "./tracking.service";
33
import { HistoryService } from "../history/history.service";
44
import { NotFoundException } from "@nestjs/common";
55
import { TargetModel } from "../history/schema/history.schema";
6-
import {
7-
historyServiceMock,
8-
mockHistoryItem,
9-
mockHistoryResponse
6+
import {
7+
historyServiceMock,
8+
mockHistoryItem,
9+
mockHistoryResponse
1010
} from "../mocks/HistoryMock";
1111
import { VerificationRequestStatus } from "../verification-request/dto/types";
12+
import { VerificationRequestService } from "../verification-request/verification-request.service";
13+
import { mockVerificationRequestModel } from "../mocks/VerificationRequestMock";
1214

1315
describe("TrackingService (Unit)", () => {
1416
let service: TrackingService;
@@ -22,6 +24,10 @@ describe("TrackingService (Unit)", () => {
2224
provide: HistoryService,
2325
useValue: historyServiceMock,
2426
},
27+
{
28+
provide: VerificationRequestService,
29+
useValue: mockVerificationRequestModel,
30+
},
2531
],
2632
}).compile();
2733

@@ -31,6 +37,11 @@ describe("TrackingService (Unit)", () => {
3137

3238
beforeEach(() => {
3339
jest.clearAllMocks();
40+
41+
mockVerificationRequestModel.getById.mockResolvedValue({
42+
id: String(mockHistoryItem.targetId),
43+
status: VerificationRequestStatus.PRE_TRIAGE,
44+
});
3445
});
3546

3647
describe("getTrackingStatus", () => {
@@ -49,6 +60,7 @@ describe("TrackingService (Unit)", () => {
4960
TargetModel.VerificationRequest,
5061
expect.objectContaining({ pageSize: 50 })
5162
);
63+
expect(mockVerificationRequestModel.getById).toHaveBeenCalledWith(targetIdStr);
5264
});
5365

5466
it("should filter out events where status did not change", async () => {
@@ -76,7 +88,7 @@ describe("TrackingService (Unit)", () => {
7688

7789
it("should throw NotFoundException and log warning when no history exists", async () => {
7890
historyService.getHistoryForTarget.mockResolvedValue({ history: [] });
79-
91+
8092
const loggerWarnSpy = jest.spyOn(service['logger'], 'warn');
8193

8294
await expect(service.getTrackingStatus(targetIdStr)).rejects.toThrow(
@@ -90,7 +102,7 @@ describe("TrackingService (Unit)", () => {
90102
it("should throw a generic error and log error when an unexpected failure occurs", async () => {
91103
const unexpectedError = new Error("Database connection lost");
92104
historyService.getHistoryForTarget.mockRejectedValue(unexpectedError);
93-
105+
94106
const loggerErrorSpy = jest.spyOn(service['logger'], 'error');
95107

96108
await expect(service.getTrackingStatus(targetIdStr)).rejects.toThrow(
@@ -102,30 +114,16 @@ describe("TrackingService (Unit)", () => {
102114
);
103115
});
104116

105-
it("should correctly identify the latestStatus as the last item in the array", async () => {
106-
const multipleHistory = {
107-
...mockHistoryResponse,
108-
history: [
109-
{
110-
...mockHistoryItem,
111-
details: { after: { status: VerificationRequestStatus.PRE_TRIAGE } }
112-
},
113-
{
114-
...mockHistoryItem,
115-
_id: "latest-id",
116-
details: {
117-
before: { status: VerificationRequestStatus.PRE_TRIAGE },
118-
after: { status: VerificationRequestStatus.IN_TRIAGE }
119-
}
120-
}
121-
],
122-
};
123-
historyService.getHistoryForTarget.mockResolvedValue(multipleHistory);
117+
it("should return currentStatus from the verification request regardless of history", async () => {
118+
historyService.getHistoryForTarget.mockResolvedValue(mockHistoryResponse);
119+
mockVerificationRequestModel.getById.mockResolvedValue({
120+
id: targetIdStr,
121+
status: VerificationRequestStatus.IN_TRIAGE,
122+
});
124123

125124
const result = await service.getTrackingStatus(targetIdStr);
126125

127126
expect(result.currentStatus).toBe(VerificationRequestStatus.IN_TRIAGE);
128-
expect(result.historyEvents.at(-1)?.status).toBe(VerificationRequestStatus.IN_TRIAGE);
129127
});
130128
});
131-
});
129+
});

server/tracking/tracking.service.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import { Injectable, InternalServerErrorException, Logger, NotFoundException } f
22
import { HistoryService } from "../history/history.service";
33
import { HistoryType, TargetModel } from "../history/schema/history.schema";
44
import { TrackingResponseDTO } from "./types/tracking.interfaces";
5+
import { VerificationRequestService } from "../verification-request/verification-request.service";
56

67
@Injectable()
78
export class TrackingService {
89
private readonly logger = new Logger(TrackingService.name);
9-
10-
constructor(private readonly historyService: HistoryService) {}
10+
11+
constructor(
12+
private readonly historyService: HistoryService,
13+
private verificationRequestService: VerificationRequestService,
14+
) { }
1115

1216
/**
1317
* Returns the history of status changes (tracking) for a specific verification request.
@@ -17,6 +21,9 @@ export class TrackingService {
1721
*/
1822
async getTrackingStatus(verificationRequestId: string): Promise<TrackingResponseDTO> {
1923
try {
24+
// We only need to call verification requests here because we don't have histories for all verification request steps yet, so it's safer to use the latestStatus from the verification request instead of the history.
25+
const verificationRequest = await this.verificationRequestService.getById(verificationRequestId);
26+
2027
const { history } = await this.historyService.getHistoryForTarget(verificationRequestId, TargetModel.VerificationRequest, {
2128
page: 0,
2229
pageSize: 50,
@@ -36,7 +43,7 @@ export class TrackingService {
3643
date: new Date(history.date),
3744
}));
3845

39-
const latestStatus = tracking.length > 0 ? tracking.at(-1)?.status : null;
46+
const latestStatus = verificationRequest.status
4047

4148
return {
4249
currentStatus: latestStatus,
@@ -49,7 +56,7 @@ export class TrackingService {
4956
}
5057
this.logger.error(
5158
`Failed to fetch tracking for ID: ${verificationRequestId}`,
52-
error.stack,
59+
error.stack,
5360
);
5461
throw new InternalServerErrorException("Internal server error while fetching tracking status.");
5562
}

server/tracking/types/tracking.interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { VerificationRequestStatus } from "../../verification-request/dto/types";
22

33
export interface TrackingResponseDTO {
4-
currentStatus: VerificationRequestStatus;
4+
currentStatus: string;
55
historyEvents: HistoryItem[];
66
}
77
interface HistoryItem {

src/components/Tracking/TrackingCard.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const initialTrackingState: TrackingResponseDTO = {
1313
historyEvents: [],
1414
};
1515

16-
const TrackingCard = ({ verificationRequestId }: TrackingCardProps) => {
16+
const TrackingCard = ({ verificationRequestId, isMinimal }: TrackingCardProps) => {
1717
const [trackingData, setTrackingData] = useState<TrackingResponseDTO>(initialTrackingState);
1818
const [isLoading, setIsLoading] = useState(true);
1919
const { t } = useTranslation();
@@ -49,16 +49,25 @@ const TrackingCard = ({ verificationRequestId }: TrackingCardProps) => {
4949
flexDirection: "column",
5050
maxWidth: "100%",
5151
padding: "24px",
52-
margin: "24px 0",
52+
height: isMinimal ? "100%" : "auto",
53+
margin: isMinimal ? 0 : "24px 0",
5354
gap: 12
5455
}}
5556
>
56-
<Typography variant="h5">
57+
<Typography
58+
style={{
59+
fontFamily: "initial",
60+
fontSize: 26,
61+
lineHeight: 1.35
62+
}}
63+
variant="h1"
64+
>
5765
{t("tracking:verificationProgress")}
5866
</Typography>
5967
<TrackingStep
6068
currentStatus={currentStatus}
6169
historyEvents={historyEvents}
70+
isMinimal={isMinimal}
6271
/>
6372
</CardBase>
6473
);

src/components/Tracking/TrackingStep.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const TrackingStep = ({
3232
stepDate,
3333
isCompleted,
3434
isDeclined,
35+
isMinimal
3536
}: TrackingStepProps) => {
3637
const { t } = useTranslation();
3738

@@ -42,6 +43,7 @@ const TrackingStep = ({
4243

4344
return (
4445
<StepLabelStyled
46+
sx={{ padding: isMinimal ? 0 : "8px 0" }}
4547
icon={<Icon style={{ color }} />}
4648
error={isDeclined}
4749
backgroundStatusColor={bg}
@@ -52,11 +54,15 @@ const TrackingStep = ({
5254
{t(`verificationRequest:${translationKey}`)}
5355
</Typography>
5456

55-
{stepDate && (
56-
<Typography variant="caption" className="dateLabel">
57-
<LocalizedDate date={stepDate} showTime />
58-
</Typography>
59-
)}
57+
{stepDate ? (
58+
<Typography variant="caption" className="dateLabel">
59+
{stepDate === "noData" ? (
60+
t("tracking:noDateFound")
61+
) : (
62+
<LocalizedDate date={stepDate} showTime />
63+
)}
64+
</Typography>
65+
) : null}
6066
</Grid>
6167

6268
<Typography variant="body2" className="description">
@@ -66,4 +72,4 @@ const TrackingStep = ({
6672
);
6773
};
6874

69-
export default TrackingStep;
75+
export default TrackingStep;

src/components/Tracking/TrackingStepper.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,24 @@ const getVisibleSteps = (currentStatus: VerificationRequestStatus): Verification
1717
const TrackingStepper = ({
1818
currentStatus,
1919
historyEvents,
20+
isMinimal,
2021
}: TrackingResponseDTO) => {
2122
const dynamicSteps = getVisibleSteps(currentStatus);
2223
const completedStepIndex = dynamicSteps.indexOf(currentStatus);
2324

2425
const getDateForStep = (stepStatus: VerificationRequestStatus) => {
25-
const history = historyEvents.find(h => h.status === stepStatus);
26-
return history ? new Date(history.date) : null;
26+
const history = historyEvents.find(history => history.status === stepStatus);
27+
28+
if (history) {
29+
return new Date(history.date);
30+
}
31+
32+
// This is only necessary because we are not creating histories for this step, so it's good UX to provide feedback. This will no longer be necessary once all steps have a history. Tracked in issue #2244
33+
if (stepStatus === VerificationRequestStatus.IN_TRIAGE) {
34+
return "noData"
35+
}
36+
37+
return null;
2738
};
2839

2940
return (
@@ -40,6 +51,7 @@ const TrackingStepper = ({
4051
stepDate={stepDate}
4152
isCompleted={isCompleted}
4253
isDeclined={isDeclined}
54+
isMinimal={isMinimal}
4355
/>
4456
</Step>
4557
);
@@ -48,4 +60,4 @@ const TrackingStepper = ({
4860
);
4961
};
5062

51-
export default TrackingStepper;
63+
export default TrackingStepper;

0 commit comments

Comments
 (0)