-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Setup structure for simple three-tier architecture (#42)
* 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
1 parent
b45b76d
commit 526b938
Showing
7 changed files
with
288 additions
and
203 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
Oops, something went wrong.