Skip to content

Commit 91febca

Browse files
chore: code cleanup; added and updated some jest tests
1 parent 21d53d2 commit 91febca

7 files changed

Lines changed: 683 additions & 38 deletions

File tree

bc_obps/compliance/service/penalty_calculation_service.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,6 @@ def calculate_penalty(
355355
Returns:
356356
CalculatedPenaltyData Dataclass
357357
"""
358-
# TODO: incorporate penalty_types AUTOMATIC_OVERDUE and LATE_SUBMISSION (ggeapar)
359358
refresh_result = ElicensingDataRefreshService.refresh_data_wrapper_by_compliance_report_version_id(
360359
compliance_report_version_id=obligation.compliance_report_version_id
361360
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"use client";
2+
3+
import ComplianceStepButtons from "@/compliance/src/app/components/ComplianceStepButtons";
4+
import { FormBase } from "@bciers/components/form";
5+
import { IChangeEvent } from "@rjsf/core";
6+
import { useMemo, useRef, useState } from "react";
7+
import {
8+
penaltyCalculatorSchema,
9+
penaltyCalculatorUiSchema,
10+
} from "@/compliance/src/app/data/jsonSchema/manageObligation/internal/PenaltyCalculatorSchema";
11+
import { getPenaltyAccrualCalculationData } from "@/compliance/src/app/utils/getPenaltyAccrualCalculationData";
12+
13+
type PenaltyAccrualRow = {
14+
date?: string;
15+
daily_penalty?: string | number | null;
16+
daily_compounded?: string | number | null;
17+
accumulated_penalty?: string | number | null;
18+
accumulated_compounded?: string | number | null;
19+
interest_rate?: string | number | null;
20+
};
21+
22+
type CalculatedPenaltyResponse = {
23+
penalty_type?: string;
24+
days_late?: number;
25+
total_penalty?: string | number | null;
26+
daily_accumulated_list?: PenaltyAccrualRow[];
27+
};
28+
29+
type PenaltyCalculatorFormData = {
30+
penalty_type: string;
31+
final_day_of_penalty_accrual?: string;
32+
penalty_summary: {
33+
total_penalty_amount?: string | number | null;
34+
days_late?: number | string | null;
35+
};
36+
accrual_data: {
37+
tableData: Array<Array<string | number | null | undefined>>;
38+
};
39+
};
40+
41+
const normalizePenaltyTypeForForm = (value?: string): string => {
42+
const normalized = value?.trim().toLowerCase();
43+
44+
if (normalized === "ggeapar" || normalized === "late submission") {
45+
return "ggeapar";
46+
}
47+
48+
return "automatic_overdue";
49+
};
50+
51+
const mapPenaltyTypeToFrontend = (penaltyType?: string): string => {
52+
return normalizePenaltyTypeForForm(penaltyType);
53+
};
54+
55+
interface Props {
56+
complianceReportVersionId: number;
57+
penaltyData?: CalculatedPenaltyResponse;
58+
initialPenaltyType: string;
59+
initialFinalDayOfPenaltyAccrual: string;
60+
}
61+
62+
const normalizeDateString = (value?: string): string | null => {
63+
if (!value) {
64+
return null;
65+
}
66+
67+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
68+
return value;
69+
}
70+
71+
const isoDateMatch = value.match(/^(\d{4}-\d{2}-\d{2})T/);
72+
if (isoDateMatch?.[1]) {
73+
return isoDateMatch[1];
74+
}
75+
76+
return null;
77+
};
78+
79+
const mapApiDataToFormData = (
80+
data: CalculatedPenaltyResponse | undefined,
81+
penaltyType: string,
82+
finalDay: string,
83+
): PenaltyCalculatorFormData => {
84+
const accrualRows = (data?.daily_accumulated_list ?? []).map((row) => [
85+
row.date,
86+
row.daily_penalty,
87+
row.daily_compounded,
88+
row.accumulated_penalty,
89+
row.accumulated_compounded,
90+
row.interest_rate,
91+
]);
92+
93+
return {
94+
penalty_type: penaltyType,
95+
final_day_of_penalty_accrual: finalDay,
96+
penalty_summary: {
97+
total_penalty_amount: data?.total_penalty,
98+
days_late: data?.days_late,
99+
},
100+
accrual_data: {
101+
tableData: accrualRows,
102+
},
103+
};
104+
};
105+
106+
export default function PenaltyCalculatorComponent({
107+
complianceReportVersionId,
108+
penaltyData,
109+
initialPenaltyType,
110+
initialFinalDayOfPenaltyAccrual,
111+
}: Readonly<Props>) {
112+
const backUrl = `/compliance-administration/compliance-summaries/${complianceReportVersionId}/review-compliance-obligation-report`;
113+
const derivedPenaltyType =
114+
initialPenaltyType ?? mapPenaltyTypeToFrontend(penaltyData?.penalty_type);
115+
116+
console.log(
117+
derivedPenaltyType,
118+
penaltyData?.penalty_type,
119+
initialPenaltyType,
120+
);
121+
122+
const initialFormData = useMemo(
123+
() =>
124+
mapApiDataToFormData(
125+
penaltyData,
126+
derivedPenaltyType,
127+
initialFinalDayOfPenaltyAccrual,
128+
),
129+
[penaltyData, derivedPenaltyType, initialFinalDayOfPenaltyAccrual],
130+
);
131+
132+
const [formData, setFormData] =
133+
useState<PenaltyCalculatorFormData>(initialFormData);
134+
const lastRequestIdRef = useRef(0);
135+
136+
const handleChange = async (e: IChangeEvent<PenaltyCalculatorFormData>) => {
137+
const nextFormData = e.formData;
138+
if (!nextFormData) {
139+
return;
140+
}
141+
142+
const selectedPenaltyType = normalizePenaltyTypeForForm(
143+
nextFormData?.penalty_type ??
144+
formData?.penalty_type ??
145+
initialPenaltyType,
146+
);
147+
const selectedFinalDay =
148+
nextFormData?.final_day_of_penalty_accrual ??
149+
formData?.final_day_of_penalty_accrual ??
150+
initialFinalDayOfPenaltyAccrual;
151+
const normalizedFinalDay = normalizeDateString(selectedFinalDay);
152+
153+
setFormData({
154+
...nextFormData,
155+
penalty_type: selectedPenaltyType,
156+
final_day_of_penalty_accrual: selectedFinalDay,
157+
});
158+
159+
if (!selectedPenaltyType || !normalizedFinalDay) {
160+
return;
161+
}
162+
163+
const requestId = Date.now();
164+
lastRequestIdRef.current = requestId;
165+
166+
const refreshedPenaltyData = await getPenaltyAccrualCalculationData(
167+
complianceReportVersionId,
168+
{
169+
penalty_type: selectedPenaltyType,
170+
final_day_of_penalty_accrual: normalizedFinalDay,
171+
},
172+
);
173+
174+
if (lastRequestIdRef.current !== requestId) {
175+
return;
176+
}
177+
178+
setFormData(
179+
mapApiDataToFormData(
180+
refreshedPenaltyData,
181+
selectedPenaltyType,
182+
normalizedFinalDay,
183+
),
184+
);
185+
};
186+
187+
return (
188+
<FormBase
189+
schema={penaltyCalculatorSchema}
190+
uiSchema={penaltyCalculatorUiSchema}
191+
formData={formData}
192+
onChange={handleChange}
193+
className="w-full"
194+
>
195+
<ComplianceStepButtons backUrl={backUrl} />
196+
</FormBase>
197+
);
198+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"use client";
2+
3+
type PenaltySummaryValue = {
4+
total_penalty_amount?: string | number | null;
5+
days_late?: string | number | null;
6+
};
7+
8+
const getDisplayValue = (value: string | number | null | undefined): string => {
9+
if (value === null || value === undefined || value === "") {
10+
return "-";
11+
}
12+
return String(value);
13+
};
14+
15+
const getFormattedPenaltyAmount = (
16+
value: string | number | null | undefined,
17+
): string => {
18+
if (value === null || value === undefined || value === "") {
19+
return "-";
20+
}
21+
22+
const numericValue = Number(String(value).replace(/,/g, ""));
23+
if (Number.isNaN(numericValue)) {
24+
return String(value);
25+
}
26+
27+
return numericValue.toLocaleString("en-CA", {
28+
minimumFractionDigits: 2,
29+
maximumFractionDigits: 2,
30+
});
31+
};
32+
33+
type PenaltySummaryFieldProps = {
34+
formData?: PenaltySummaryValue;
35+
label?: string;
36+
};
37+
38+
export const PenaltySummaryField = ({
39+
formData,
40+
label,
41+
}: PenaltySummaryFieldProps) => {
42+
const summary = (formData ?? {}) as PenaltySummaryValue;
43+
const totalPenaltyAmount = getFormattedPenaltyAmount(
44+
summary.total_penalty_amount,
45+
);
46+
const daysLate = getDisplayValue(summary.days_late);
47+
48+
return (
49+
<div className="w-full">
50+
<p className="mb-2 text-bc-bg-blue">{label ?? "Penalty summary"}</p>
51+
<div className="flex w-full flex-nowrap gap-4">
52+
<div
53+
style={{
54+
width: "350px",
55+
minWidth: "350px",
56+
maxWidth: "350px",
57+
borderWidth: "1px",
58+
borderStyle: "solid",
59+
borderColor: "currentColor",
60+
}}
61+
className="rounded-md bg-red-50 p-4 text-bc-error-red"
62+
>
63+
<p className="text-sm font-medium">Total penalty amount</p>
64+
<p className="mt-1 text-2xl font-bold">${totalPenaltyAmount}</p>
65+
</div>
66+
<div
67+
style={{
68+
width: "350px",
69+
minWidth: "350px",
70+
maxWidth: "350px",
71+
borderWidth: "1px",
72+
borderStyle: "solid",
73+
borderColor: "currentColor",
74+
}}
75+
className="rounded-md bg-white p-4 text-bc-bg-blue"
76+
>
77+
<p className="text-sm font-medium">Days late</p>
78+
<p className="mt-1 text-2xl font-bold">{daysLate}</p>
79+
</div>
80+
</div>
81+
</div>
82+
);
83+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"use client";
2+
3+
import { WidgetProps } from "@rjsf/utils";
4+
5+
const DEFAULT_VALUE = "automatic_overdue";
6+
7+
const options = [
8+
{
9+
value: "automatic_overdue",
10+
label: "Automatic overdue",
11+
},
12+
{
13+
value: "ggeapar",
14+
label: "GGEAPAR",
15+
},
16+
] as const;
17+
18+
export const PenaltyTypeButtonGroupWidget = ({
19+
label,
20+
value,
21+
onChange,
22+
disabled,
23+
readonly,
24+
}: WidgetProps) => {
25+
const selectedValue = (value as string) || DEFAULT_VALUE;
26+
27+
return (
28+
<div className="w-full">
29+
<p className="mb-2">{label}</p>
30+
<div
31+
className="flex flex-nowrap gap-0"
32+
role="radiogroup"
33+
aria-label={label}
34+
>
35+
{options.map((option) => {
36+
const isSelected = selectedValue === option.value;
37+
38+
return (
39+
<button
40+
key={option.value}
41+
type="button"
42+
role="radio"
43+
aria-checked={isSelected}
44+
disabled={disabled || readonly}
45+
onClick={() => onChange(option.value)}
46+
style={{
47+
width: "525px",
48+
minWidth: "525px",
49+
maxWidth: "525px",
50+
height: "37.5px",
51+
minHeight: "37.5px",
52+
maxHeight: "37.5px",
53+
}}
54+
className={[
55+
"shrink-0 basis-[525px] border font transition-colors first:rounded-l last:rounded-r",
56+
isSelected
57+
? "border-bc-blue bg-bc-bg-blue text-white"
58+
: "border-bc-blue bg-white text-bc-links",
59+
disabled || readonly ? "cursor-not-allowed opacity-50" : "",
60+
].join(" ")}
61+
>
62+
{option.label}
63+
</button>
64+
);
65+
})}
66+
</div>
67+
</div>
68+
);
69+
};

bciers/apps/compliance/src/app/utils/getPenaltyAccrualCalculationData.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,6 @@ export const getPenaltyAccrualCalculationData = async (
4646
): Promise<any> => {
4747
const { final_day_of_penalty_accrual, penalty_type, ...restParams } = params;
4848

49-
console.log(
50-
`penalty type: ${penalty_type}, final_day_of_penalty_accrual: ${final_day_of_penalty_accrual}`,
51-
);
52-
5349
const mappedPenaltyType = mapPenaltyTypeForApi(penalty_type);
5450

5551
const mappedEndDate = normalizeEndDate(final_day_of_penalty_accrual);

0 commit comments

Comments
 (0)