Skip to content

Commit

Permalink
feat: Setup structure for simple three-tier architecture (#42)
Browse files Browse the repository at this point in the history
* fix: Rebase issues

* chore: Tidy up

* feat: Setup DB singleton

* feat: ITLLookup

* feat: GDHI

* feat: Define handler layer by creating calculationService

* chore: Tidy up import, export and names
  • Loading branch information
DafyddLlyr authored Aug 16, 2024
1 parent b45b76d commit 526b938
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 203 deletions.
206 changes: 3 additions & 203 deletions app/api/route.ts
Original file line number Diff line number Diff line change
@@ -1,218 +1,18 @@
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
import { Calculation, calculationSchema } from "../schemas/calculationSchema";
import * as calculationService from "../services/calculationService";

const prisma = new PrismaClient();

// define and export the GET handler function
export async function POST(req: Request) {
try {
// Parse and validate user input
const data = await req.json();
const input: Calculation = calculationSchema.parse(data);

// data are going to be queried at different levels of granularity based on the postcode
const postcode = input.housePostcode;
const postcodeArea = postcode.area; // extract only the characters for the area, e.g SE
const postcodeDistrict = postcode.district; // extract only characters for the district, SE17
const postcodeSector = postcode.sector; // extract only the characters for the sector, SE17 1

// create the progressive queries
const minimumNumberPostcodes = 30; // minimum number of entries to create the average
let pricesPaid; // declare the variable for prices paid
let numberOfTransactions; // declare the variable for numbers of transactions retrieved
let granularityPostcode; // declare the granularity of the postcode
let averagePrice;

const pricesPaidSector = await prisma.pricesPaid.aggregate({
where: {
propertyType: {
equals: input.houseType,
},
postcode: {
startsWith: postcodeSector,
},
},
_count: {
id: true,
},
_avg: {
price: true,
},
});

const numberPerSector = pricesPaidSector._count.id;
const isMinMetBySector = numberPerSector >= minimumNumberPostcodes;

if (!isMinMetBySector) {
const pricesPaidDistrict = await prisma.pricesPaid.aggregate({
where: {
propertyType: {
equals: input.houseType,
},
postcode: {
startsWith: postcodeDistrict,
},
},
_count: {
id: true,
},
_avg: {
price: true,
},
});

const numberPerDistrict = pricesPaidDistrict._count.id;
const isMinMetByDistrict = numberPerDistrict >= minimumNumberPostcodes;

if (!isMinMetByDistrict) {
const pricesPaidArea = await prisma.pricesPaid.aggregate({
where: {
propertyType: {
equals: input.houseType,
},
postcode: {
startsWith: postcodeArea,
},
},
_count: {
id: true,
},
_avg: {
price: true,
},
});
const numberPerArea = pricesPaidArea._count.id;

pricesPaid = pricesPaidArea; // if condition is met, the granularity is appropriate
numberOfTransactions = numberPerArea; // check the granularity
granularityPostcode = postcodeArea; // granularity of the postcode when performing the average price search
averagePrice = pricesPaidArea._avg.price;
} else {
pricesPaid = pricesPaidDistrict; // if condition is met, the granularity is appropriate
numberOfTransactions = numberPerDistrict; // check the granularity
granularityPostcode = postcodeDistrict; // granularity of the postcode
averagePrice = pricesPaidDistrict._avg.price;
}
} else {
pricesPaid = pricesPaidSector; // if condition is met, the granularity is appropriate
numberOfTransactions = numberPerSector; // check the granularity
granularityPostcode = postcodeSector; // granularity of the postcode
averagePrice = pricesPaidSector._avg.price;
}

if (averagePrice === null) {
throw new Error("Unable to calculate average price");
}

const { priceMid: buildPrice } = await prisma.buildPrices.findFirstOrThrow({
where: {
houseType: { equals: input.houseType },
},
select: { priceMid: true },
});
// TODO: Make columns non-nullable
if (!buildPrice) throw Error("Missing buildPrice");

const { itl3 } = await prisma.itlLookup.findFirstOrThrow({
where: {
postcode: postcodeDistrict,
itl3: {
not: null,
},
},
select: {
itl3: true,
},
});
if (!itl3) throw Error("Missing itl3");

const { gdhi2020: gdhi } = await prisma.gDHI.findFirstOrThrow({
where: {
itl3: { equals: itl3 },
},
select: { gdhi2020: true },
});
if (!gdhi) throw Error("Missing gdhi");

const {
_avg: { monthlyMeanRent: averageRentMonthly },
} = await prisma.rent.aggregate({
where: { itl3 },
_avg: {
monthlyMeanRent: true,
},
});
if (!averageRentMonthly) throw Error("Missing averageRentMonthly");

const socialRentAdjustments = await prisma.socialRentAdjustments.findMany();
const itl3Prefix = itl3.substring(0, 4);

const {
_avg: { earningsPerWeek: socialRentAveEarning },
} = await prisma.socialRent.aggregate({
where: {
itl3: {
startsWith: itl3Prefix,
},
},
_avg: {
earningsPerWeek: true,
},
});

if (!socialRentAveEarning) throw Error("Missing socialRentAveEarning");

const {
_avg: { hpi2020: averageHpi },
} = await prisma.hPI.aggregate({
where: {
itl3: {
endsWith: itl3,
},
},
_avg: {
hpi2020: true,
},
});
if (!averageHpi) throw Error("Missing averageHpi");

const { bill: gasBillYearly } = await prisma.gasBills.findFirstOrThrow({
where: {
itl: {
startsWith: itl3.substring(0, 3),
},
},
select: {
bill: true,
},
});
if (!gasBillYearly) throw Error("Missing gasBillYearly");

return NextResponse.json({
postcode: input.housePostcode,
houseType: input.houseType,
houseAge: input.houseAge,
houseBedrooms: input.houseBedrooms,
houseSize: input.houseSize,
averagePrice: parseFloat(averagePrice.toFixed(2)),
itl3,
gdhi,
hpi: averageHpi,
buildPrice,
averageRentMonthly,
socialRentAdjustments,
socialRentAveEarning,
numberOfTransactions,
granularityPostcode,
pricesPaid,
gasBillYearly,
});
const householdData = await calculationService.getHouseholdData(input);
return NextResponse.json(householdData);
} catch (err) {
console.log("ERROR: API - ", (err as Error).message);
const response = { error: (err as Error).message };
return NextResponse.json(response, { status: 500 });
} finally {
await prisma.$disconnect();
}
}
15 changes: 15 additions & 0 deletions app/data/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PrismaClient } from "@prisma/client";

const prismaClientSingleton = () => {
return new PrismaClient();
};

declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;

const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();

export default prisma;

if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;
26 changes: 26 additions & 0 deletions app/data/gdhiRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import prisma from "./db";

const getGDHI2020ByITL3 = async (
itl3: string
): Promise<number> => {
try {
const { gdhi2020 } = await prisma.gDHI.findFirstOrThrow({
where: {
AND: {
itl3: { equals: itl3 },
// TODO: Add `NOT NULL` constraint to column
gdhi2020: { not: null }
},
},
select: { gdhi2020: true },
});

return gdhi2020 as number;
} catch (error) {
throw Error(`Data error: Unable to find gdhi2020 for itl3 ${itl3}`);
}
};

export const gdhiRepo = {
getGDHI2020ByITL3,
}
28 changes: 28 additions & 0 deletions app/data/itlRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import prisma from "./db";

const getItl3ByPostcodeDistrict = async (
postcodeDistrict: string
): Promise<string> => {
try {
const { itl3 } = await prisma.itlLookup.findFirstOrThrow({
where: {
postcode: postcodeDistrict,
itl3: {
not: null,
},
},
select: {
itl3: true,
},
});

// Cast to string as 'not: null' clause in Prisma query does not type narrow
return itl3 as string;
} catch (error) {
throw new Error(`Data error: Unable get get itl3 for postcode district ${postcodeDistrict}`);
}
};

export const itlRepo = {
getItl3ByPostcodeDistrict,
}
Loading

0 comments on commit 526b938

Please sign in to comment.