Skip to content

Commit

Permalink
feat: Query new PricesPaidSummary model (#241)
Browse files Browse the repository at this point in the history
* feat: Query new `PricePaidSummary` model

* test: Update test cases

* feat: Gracefully handle non-English postcodes (#244)

* feat: Error handling for non-English postcodes

* fix: Typos

* test: Fix tests for new API errors
  • Loading branch information
DafyddLlyr authored Jan 9, 2025
1 parent 414e75f commit e88e485
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 205 deletions.
13 changes: 10 additions & 3 deletions app/api/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ describe("POST API Route", () => {

// Assertions for the expected error response
expect(NextResponse.json).toHaveBeenCalledWith(
expect.objectContaining({ error: expect.any(String) }),
expect.objectContaining({
error: expect.objectContaining({ code: "UNHANDLED_EXCEPTION" }),
}),
{ status: 500 }
);
});
Expand Down Expand Up @@ -112,7 +114,7 @@ describe("POST API Route", () => {
expect(calculationService.getHouseholdData).toHaveBeenCalledWith({
...validApiInput,
// Parsed postcode object
housePostcode: {
housePostcode: {
area: "SE",
district: "SE17",
incode: "1PE",
Expand All @@ -125,7 +127,12 @@ describe("POST API Route", () => {
},
});
expect(NextResponse.json).toHaveBeenCalledWith(
{ error: errorMessage },
{
error: {
code: "UNHANDLED_EXCEPTION",
message: "Service error",
},
},
{ status: 500 }
);
});
Expand Down
20 changes: 17 additions & 3 deletions app/api/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { calculationSchema } from "../schemas/calculationSchema";
import * as calculationService from "../services/calculationService";
import calculateFairhold from "../models/testClasses";
import { APIError } from "../lib/exceptions";

export async function POST(req: Request) {
try {
Expand All @@ -11,9 +12,22 @@ export async function POST(req: Request) {
const householdData = await calculationService.getHouseholdData(input);
const processedData = calculateFairhold(householdData);
return NextResponse.json(processedData);
} catch (err) {
console.log("ERROR: API - ", (err as Error).message);
const response = { error: (err as Error).message };
} catch (error) {
console.log("ERROR: API - ", (error as Error).message);

if (error instanceof APIError) {
return NextResponse.json(
{ error },
{ status: error.status },
);
}

const response = {
error: {
message: (error as Error).message,
code: "UNHANDLED_EXCEPTION"
},
};
return NextResponse.json(response, { status: 500 });
}
}
22 changes: 22 additions & 0 deletions app/components/ui/CalculatorInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { APIError } from "@/app/lib/exceptions";

type View = "form" | "loading" | "dashboard";

Expand Down Expand Up @@ -68,10 +69,31 @@ const CalculatorInput = () => {
body: JSON.stringify(data),
});
const processedData = await response.json();
if (!response.ok) return handleErrors(processedData.error);

setData(processedData);
setView("dashboard");
};

const handleErrors = (error: APIError) => {
switch (error.code) {
case "ITL3_NOT_FOUND":
case "INSUFFICIENT_PRICES_PAID_DATA":
methods.setError(
"housePostcode",
{ message:
"Insufficient data for this postcode. Please try again with a different postcode"
}
);
break;
case "UNHANDLED_EXCEPTION":
default:
console.error(error)
};

setView("form");
}

if (view === "form") {
return (
<div className="flex flex-col justify-center max-w-xl mx-auto px-10">
Expand Down
2 changes: 1 addition & 1 deletion app/data/itlRepo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe("itlRepo", () => {
await expect(
itlRepo.getItl3ByPostcodeDistrict(postcodeDistrict)
).rejects.toThrow(
`Data error: Unable get get itl3 for postcode district ${postcodeDistrict}`
`Data error: Unable get itl3 for postcode district ${postcodeDistrict}`
);
});
});
10 changes: 6 additions & 4 deletions app/data/itlRepo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import prisma from "./db";
import { APIError } from "../lib/exceptions";

const getItl3ByPostcodeDistrict = async (
postcodeDistrict: string
Expand All @@ -15,10 +16,11 @@ const getItl3ByPostcodeDistrict = async (

return itl3;
} catch (error) {
throw new Error(
`Data error: Unable get get itl3 for postcode district ${postcodeDistrict}`
);

throw new APIError({
status: 500,
message: `Data error: Unable get itl3 for postcode district ${postcodeDistrict}`,
code: "ITL3_NOT_FOUND"
});
}
};

Expand Down
119 changes: 19 additions & 100 deletions app/data/pricesPaidRepo.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
// __tests__/pricesPaidRepo.test.ts
import { pricesPaidRepo } from "./pricesPaidRepo"; // Adjust the import according to your file structure
import prisma from "./db"; // Your Prisma setup file
import { pricesPaidRepo } from "./pricesPaidRepo";
import prisma from "./db";

jest.mock("./db", () => ({
pricesPaid: {
aggregate: jest.fn(), // Mock the aggregate method
pricesPaidSummary: {
findFirst: jest.fn(),
},
}));

const postcodeDistrict = "SW1A";
const postcodeArea = "SW1";
const postcodeSector = "SW1A1";
const houseType = "Detached";

describe("pricesPaidRepo", () => {
afterEach(() => {
jest.clearAllMocks(); // Clear mocks after each test
jest.clearAllMocks();
jest.resetAllMocks();
});

it("should return prices paid data for valid sector", async () => {
const postcodeDistrict = "SW1A"; // Example postcode district
const postcodeArea = "SW1"; // Example postcode area
const postcodeSector = "SW1A1"; // Example postcode sector
const houseType = "Detached"; // Example house type

// Mock the Prisma client response for sector
(prisma.pricesPaid.aggregate as jest.Mock).mockResolvedValueOnce({
_count: { id: 35 }, // Enough postcodes
_avg: { price: 500000 }, // Average price
it("should return prices paid data", async () => {
(prisma.pricesPaidSummary.findFirst as jest.Mock).mockResolvedValueOnce({
averagePrice: 500000,
transactionCount: 35,
postcode: postcodeSector,
});

const result = await pricesPaidRepo.getPricesPaidByPostcodeAndHouseType(
Expand All @@ -39,83 +39,8 @@ describe("pricesPaidRepo", () => {
});
});

it("should return prices paid data for valid district when sector count is below minimum", async () => {
const postcodeDistrict = "SW1A"; // Example postcode district
const postcodeArea = "SW1"; // Example postcode area
const postcodeSector = "SW1A1"; // Example postcode sector
const houseType = "Detached"; // Example house type

// Mock the Prisma client response for sector (below minimum)
(prisma.pricesPaid.aggregate as jest.Mock)
.mockResolvedValueOnce({
_count: { id: 25 }, // Below minimum
_avg: { price: 500000 },
})
.mockResolvedValueOnce({
_count: { id: 35 }, // Enough postcodes for district
_avg: { price: 600000 },
});

const result = await pricesPaidRepo.getPricesPaidByPostcodeAndHouseType(
postcodeDistrict,
postcodeArea,
postcodeSector,
houseType
);

expect(result).toEqual({
averagePrice: 600000,
numberOfTransactions: 35,
granularityPostcode: postcodeDistrict,
});
});

it("should return prices paid data for valid area when district count is below minimum", async () => {
const postcodeDistrict = "SW1A"; // Example postcode district
const postcodeArea = "SW1"; // Example postcode area
const postcodeSector = "SW1A1"; // Example postcode sector
const houseType = "Detached"; // Example house type

// Mock the Prisma client response for sector (below minimum)
(prisma.pricesPaid.aggregate as jest.Mock)
.mockResolvedValueOnce({
_count: { id: 25 }, // Below minimum
_avg: { price: 500000 },
})
.mockResolvedValueOnce({
_count: { id: 20 }, // Below minimum for district
_avg: { price: 600000 },
})
.mockResolvedValueOnce({
_count: { id: 40 }, // Enough postcodes for area
_avg: { price: 700000 },
});

const result = await pricesPaidRepo.getPricesPaidByPostcodeAndHouseType(
postcodeDistrict,
postcodeArea,
postcodeSector,
houseType
);

expect(result).toEqual({
averagePrice: 700000,
numberOfTransactions: 40,
granularityPostcode: postcodeArea,
});
});

it("should throw an error when average price is null", async () => {
const postcodeDistrict = "SW1A"; // Example postcode district
const postcodeArea = "SW1"; // Example postcode area
const postcodeSector = "SW1A1"; // Example postcode sector
const houseType = "Detached"; // Example house type

// Mock the Prisma client response for sector (below minimum)
(prisma.pricesPaid.aggregate as jest.Mock).mockResolvedValueOnce({
_count: { id: 35 }, // Enough postcodes
_avg: { price: null }, // Null average price
});
it("should throw an error if no transaction data is found", async () => {
(prisma.pricesPaidSummary.findFirst as jest.Mock).mockResolvedValueOnce(null);

await expect(
pricesPaidRepo.getPricesPaidByPostcodeAndHouseType(
Expand All @@ -130,13 +55,7 @@ describe("pricesPaidRepo", () => {
});

it("should throw an error for any other error", async () => {
const postcodeDistrict = "SW1A"; // Example postcode district
const postcodeArea = "SW1"; // Example postcode area
const postcodeSector = "SW1A1"; // Example postcode sector
const houseType = "Detached"; // Example house type

// Mock the Prisma client to throw an error
(prisma.pricesPaid.aggregate as jest.Mock).mockRejectedValueOnce(
(prisma.pricesPaidSummary.findFirst as jest.Mock).mockRejectedValueOnce(
new Error("Database error")
);

Expand Down
Loading

0 comments on commit e88e485

Please sign in to comment.