Skip to content

Commit

Permalink
Merge pull request #83 from sgcarstrends/82-coe-pqp-prices
Browse files Browse the repository at this point in the history
Add COE PQP Rates
  • Loading branch information
ruchernchong authored Jan 29, 2025
2 parents dc82033 + 241795c commit 2b969bf
Show file tree
Hide file tree
Showing 4 changed files with 434 additions and 7 deletions.
329 changes: 329 additions & 0 deletions src/lib/__tests__/getPqpRates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
import getPqpRates from "@/lib/getPqpRates";
import { type COE, VehicleClass } from "@/types";
import { describe, expect, it } from "vitest";

describe("getPqpRates", () => {
const mockCoeData: COE[] = [
{
month: "2025-01",
bidding_no: 2,
vehicle_class: "Category A",
quota: 1069,
bids_success: 1058,
bids_received: 1484,
premium: 93601,
},
{
month: "2025-01",
bidding_no: 2,
vehicle_class: "Category B",
quota: 686,
bids_success: 686,
bids_received: 1071,
premium: 116625,
},
{
month: "2025-01",
bidding_no: 2,
vehicle_class: "Category C",
quota: 234,
bids_success: 233,
bids_received: 373,
premium: 65476,
},
{
month: "2025-01",
bidding_no: 2,
vehicle_class: "Category D",
quota: 536,
bids_success: 530,
bids_received: 651,
premium: 7721,
},
{
month: "2025-01",
bidding_no: 1,
vehicle_class: "Category A",
quota: 1034,
bids_success: 1034,
bids_received: 1381,
premium: 93699,
},
{
month: "2025-01",
bidding_no: 1,
vehicle_class: "Category B",
quota: 677,
bids_success: 674,
bids_received: 1266,
premium: 121501,
},
{
month: "2025-01",
bidding_no: 1,
vehicle_class: "Category C",
quota: 248,
bids_success: 246,
bids_received: 364,
premium: 67891,
},
{
month: "2025-01",
bidding_no: 1,
vehicle_class: "Category D",
quota: 520,
bids_success: 513,
bids_received: 595,
premium: 9001,
},
{
month: "2024-12",
bidding_no: 2,
vehicle_class: "Category A",
quota: 1035,
bids_success: 998,
bids_received: 1506,
premium: 96000,
},
{
month: "2024-12",
bidding_no: 2,
vehicle_class: "Category B",
quota: 676,
bids_success: 667,
bids_received: 987,
premium: 109000,
},
{
month: "2024-12",
bidding_no: 2,
vehicle_class: "Category C",
quota: 255,
bids_success: 255,
bids_received: 400,
premium: 69890,
},
{
month: "2024-12",
bidding_no: 2,
vehicle_class: "Category D",
quota: 519,
bids_success: 504,
bids_received: 641,
premium: 8381,
},
{
month: "2024-12",
bidding_no: 1,
vehicle_class: "Category A",
quota: 1037,
bids_success: 1034,
bids_received: 1475,
premium: 94000,
},
{
month: "2024-12",
bidding_no: 1,
vehicle_class: "Category B",
quota: 689,
bids_success: 689,
bids_received: 866,
premium: 103010,
},
{
month: "2024-12",
bidding_no: 1,
vehicle_class: "Category C",
quota: 262,
bids_success: 249,
bids_received: 418,
premium: 70289,
},
{
month: "2024-12",
bidding_no: 1,
vehicle_class: "Category D",
quota: 517,
bids_success: 516,
bids_received: 570,
premium: 7878,
},
{
month: "2024-11",
bidding_no: 2,
vehicle_class: "Category A",
quota: 1041,
bids_success: 1038,
bids_received: 1320,
premium: 89889,
},
{
month: "2024-11",
bidding_no: 2,
vehicle_class: "Category B",
quota: 678,
bids_success: 678,
bids_received: 874,
premium: 105081,
},
{
month: "2024-11",
bidding_no: 2,
vehicle_class: "Category C",
quota: 235,
bids_success: 214,
bids_received: 378,
premium: 69000,
},
{
month: "2024-11",
bidding_no: 2,
vehicle_class: "Category D",
quota: 524,
bids_success: 524,
bids_received: 637,
premium: 8669,
},
{
month: "2024-11",
bidding_no: 1,
vehicle_class: "Category A",
quota: 1040,
bids_success: 1035,
bids_received: 1338,
premium: 99889,
},
{
month: "2024-11",
bidding_no: 1,
vehicle_class: "Category B",
quota: 696,
bids_success: 683,
bids_received: 1039,
premium: 108001,
},
{
month: "2024-11",
bidding_no: 1,
vehicle_class: "Category C",
quota: 236,
bids_success: 209,
bids_received: 365,
premium: 68340,
},
{
month: "2024-11",
bidding_no: 1,
vehicle_class: "Category D",
quota: 520,
bids_success: 520,
bids_received: 598,
premium: 9089,
},
{
month: "2024-10",
bidding_no: 2,
vehicle_class: "Category A",
quota: 1058,
bids_success: 1048,
bids_received: 1499,
premium: 102900,
},
{
month: "2024-10",
bidding_no: 2,
vehicle_class: "Category B",
quota: 670,
bids_success: 669,
bids_received: 975,
premium: 113890,
},
{
month: "2024-10",
bidding_no: 2,
vehicle_class: "Category C",
quota: 222,
bids_success: 222,
bids_received: 337,
premium: 72939,
},
{
month: "2024-10",
bidding_no: 2,
vehicle_class: "Category D",
quota: 522,
bids_success: 515,
bids_received: 626,
premium: 9589,
},
{
month: "2024-10",
bidding_no: 1,
vehicle_class: "Category A",
quota: 994,
bids_success: 986,
bids_received: 1604,
premium: 103799,
},
{
month: "2024-10",
bidding_no: 1,
vehicle_class: "Category B",
quota: 671,
bids_success: 652,
bids_received: 1084,
premium: 116002,
},
{
month: "2024-10",
bidding_no: 1,
vehicle_class: "Category C",
quota: 218,
bids_success: 216,
bids_received: 321,
premium: 75009,
},
{
month: "2024-10",
bidding_no: 1,
vehicle_class: "Category D",
quota: 520,
bids_success: 519,
bids_received: 604,
premium: 10001,
},
];

it("should correctly calculate PQP rates for all vehicle categories", () => {
expect(getPqpRates(mockCoeData)).toEqual({
"2025-02": {
"Category A": 94513,
"Category B": 110537,
"Category C": 68481,
"Category D": 8457,
},
"2025-01": {
"Category A": 97747,
"Category B": 109164,
"Category C": 70912,
"Category D": 8935,
},
});
});

it("should handle empty input array", () => {
const result = getPqpRates([]);
expect(result).toEqual({});
});

it("should handle single category data", () => {
const singleCategoryData = mockCoeData.filter(
({ vehicle_class }) => vehicle_class === VehicleClass.CategoryA,
);
const result = getPqpRates(singleCategoryData);
expect(result).toEqual({
"2025-02": { "Category A": 94513 },
"2025-01": { "Category A": 97747 },
});
});
});
67 changes: 67 additions & 0 deletions src/lib/getPqpRates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { COE, VehicleClass } from "@/types";

interface PQPResult {
month: string;
vehicle_class: string;
pqp: number;
}

type COEByCategory = Record<VehicleClass, COE[]>;

const getPqpRates = (data: COE[]): Record<string, Record<string, number>> => {
const groupedByCategory: COEByCategory = data.reduce((acc, coe) => {
if (!acc[coe.vehicle_class]) {
acc[coe.vehicle_class] = [];
}
acc[coe.vehicle_class].push(coe);
return acc;
}, {} as COEByCategory);

const calculateMonthDifference = (currentMonth: string, bidMonth: string) => {
const [currentYear, currentMonthNum] = currentMonth.split("-").map(Number);
const [bidYear, bidMonthNum] = bidMonth.split("-").map(Number);
return (currentYear - bidYear) * 12 + (currentMonthNum - bidMonthNum);
};

const calculatePqp = (coe: COE[]) =>
Math.ceil(coe.reduce((sum, { premium }) => sum + premium, 0) / coe.length);

const pqpResults: PQPResult[] = Object.entries(groupedByCategory).flatMap(
([vehicle_class, biddings]) => {
const uniqueMonths = [...new Set(biddings.map(({ month }) => month))];

return uniqueMonths
.map((month) => {
const relevantBids = biddings
.filter((bid) => {
const monthsDiff = calculateMonthDifference(month, bid.month);
return monthsDiff >= 0 && monthsDiff <= 2;
})
.slice(0, 6);

if (relevantBids.length !== 6) {
return null;
}

const pqp = calculatePqp(relevantBids);

return { month, vehicle_class, pqp };
})
.filter((result) => !!result);
},
);

return pqpResults.reduce((acc, { month, vehicle_class, pqp }) => {
const nextMonth = new Date(month);
nextMonth.setMonth(nextMonth.getMonth() + 1);
const nextMonthKey = nextMonth.toISOString().slice(0, 7);

if (!acc[nextMonthKey]) {
acc[nextMonthKey] = {};
}
acc[nextMonthKey][vehicle_class] = pqp;
return acc;
}, {});
};

export default getPqpRates;
Loading

0 comments on commit 2b969bf

Please sign in to comment.