diff --git a/app/api/route.ts b/app/api/route.ts index c9b6e1f..48affcd 100644 --- a/app/api/route.ts +++ b/app/api/route.ts @@ -22,263 +22,192 @@ export async function POST(req: Request) { 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 postcodeSearchSector = postcodeSector + "%"; // add the % for SQL query - - // define a type for PricePaid so that it is treated as an array - type PricePaid = { - id: number; - postcode: string; - price: number; - }; - - const pricesPaidSector = await prisma.$queryRaw` - SELECT id, postcode, price - FROM "public"."pricespaid" - WHERE propertyType = ${input.houseType} AND postcode LIKE ${postcodeSearchSector} - `; // execute query and extract results - console.log( - "pricesPaidSector: ", - pricesPaidSector, - "postcodeSearchSector: ", - postcodeSearchSector - ); - const numberOfpricesPaidSector = Array.isArray(pricesPaidSector) - ? pricesPaidSector.length - : 0; // extract the number of entries - - if ( - pricesPaidSector == null || - numberOfpricesPaidSector <= minimumNumberPostcodes - ) { - const postcodeSearchDistrict = postcodeDistrict + "%"; // add the % for SQL query - const pricesPaidDistrict = await prisma.$queryRaw` - SELECT id,postcode,price - FROM "public"."pricespaid" - WHERE propertyType = ${input.houseType} AND postcode LIKE ${postcodeSearchDistrict} - `; // create the sql query and count how many items meet the criteria; execute the query and retrieve the results - console.log( - "pricesPaidDistrict: ", - pricesPaidDistrict, - "postcodeSearchDistrict: ", - postcodeSearchDistrict - ); - const numberOfpricesPaidDistrict = Array.isArray(pricesPaidDistrict) - ? pricesPaidDistrict.length - : 0; // extract the number of entries - if ( - pricesPaidDistrict == null || - numberOfpricesPaidDistrict <= minimumNumberPostcodes - ) { - const postcodeSearchArea = postcodeArea + "%"; // add the % for SQL query - const pricesPaidArea = await prisma.$queryRaw` - SELECT id,postcode,price - FROM "public"."pricespaid" - WHERE propertytype = ${input.houseType} AND postcode LIKE ${postcodeSearchArea} - `; // create the sql query and count how many items meet the criteria; execute the query and retrieve the results - console.log( - "pricesPaidArea: ", - pricesPaidArea, - "postcodeSearchArea: ", - postcodeSearchArea - ); - - const numberOfpricesPaidArea = Array.isArray(pricesPaidArea) - ? pricesPaidArea.length - : 0; // extract the number of entries - pricesPaid = pricesPaidArea; // if condtion is met, the granularity is appropriate - numberOfTransactions = numberOfpricesPaidArea; // check the granularity + 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 condtion is met, the granularity is appropriate - numberOfTransactions = numberOfpricesPaidDistrict; // check the granularity - granularityPostcode = postcodeDistrict; // granularity of the postcode} + 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 condtion is met, the granularity is appropriate - numberOfTransactions = numberOfpricesPaidSector; // check the granularity - granularityPostcode = postcodeSector; // granularity of the postcode} + 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; } - console.log("pricesPaid: ", pricesPaid); - - // Calculate the total price if (!pricesPaid) throw Error("Prices fetching failed"); - const totalPrice = pricesPaid.reduce( - (total, item) => total + item.price, - 0 - ); - console.log("totalPrice:", totalPrice); - - // Calculate the average price - const averagePrice = totalPrice / pricesPaid.length; - - // create type for buildPrice query - type BuildPrice = { - id: number; - housetype: string; - pricemid: number; - }; - - const buildPriceRes = await prisma.$queryRaw` - SELECT * FROM "public"."buildprices" - WHERE "housetype" = ${input.houseType} - `; - console.log("buildPriceRes:", buildPriceRes); - const buildPrice = buildPriceRes[0]["pricemid"]; - console.log("buildPrice:", buildPrice); - - // get the ITL3 value - // create type for itl3Res query - type ItlLookup = { - itl_lookup: string; - }; - - const itl3Res = await prisma.$queryRaw` - SELECT "itl_lookup"::text AS "itl_lookup" - FROM "public"."itl_lookup" - WHERE "postcode" = ${postcodeDistrict} - `; - const itlLookupValue = itl3Res[0].itl_lookup; - const itlLookupParts = itlLookupValue.split(","); - const itl3 = itlLookupParts[3]; // Extract the 3rd value (index 3) - console.log("itl3: ", itl3); - console.log("itl3Res: ", itl3Res); - - // create type for itl3Res query - type gdhiRes = { - gdhi_2020: number; - }; - - // get the gdhi value --> Note: this need to change to accommodate future data - const gdhiRes = await prisma.$queryRaw` - SELECT gdhi_2020 - FROM "public"."gdhi" - WHERE "itl3" = ${itl3} - `; - const gdhi = gdhiRes[0]["gdhi_2020"]; - console.log("gdhiRes: ", gdhiRes); - console.log("gdhi: ", gdhi); - - // create type for itl3Res query - type rentRes = { - monthlymeanrent: number; - }; - - // get the rent value --> Note: this need to change to accommodate future data - const rentRes = await prisma.$queryRaw` - SELECT monthlymeanrent - FROM "public"."rent" - WHERE itl3 = ${itl3} - `; - console.log("rentRes: ", rentRes); - let averageRentMonthly; - if (rentRes.length === 1) { - averageRentMonthly = rentRes[0].monthlymeanrent; - } else if (rentRes.length > 1) { - const totalRent = rentRes.reduce( - (sum, item) => sum + item.monthlymeanrent, - 0 - ); - averageRentMonthly = totalRent / rentRes.length; - console.log(averageRentMonthly); - // create type for rentAdjustment query - type rentAdjustment = { - year: string; - inflation: string; - additional: string; - total: string; - }; - - // get the rent adjustements --> Note: this need to change to accommodate future data - - const socialRentAdjustments = await prisma.$queryRaw` - SELECT * - FROM "public"."soc_rent_adjustments" - `; // execute the query and retrieve the results - console.log("socialRentAdjustments[0]: ", socialRentAdjustments[0]); - - // create type for socialRentEarningRes query - type socialRentEarningRes = { - earningsperweek: number; - }; - - // get the rent value --> Note: this need to change to accommodate future data - const socialRentEarningRes = await prisma.$queryRaw< - socialRentEarningRes[] - >` - SELECT earningsperweek - FROM "public"."socialrent" - WHERE SUBSTRING(itl3 FROM 1 FOR 4) = ${itl3.substring(0, 4)} - `; - - console.log("socialRentEarningRes: ", socialRentEarningRes); - let socialRentAverageEarning; - if (socialRentEarningRes.length === 1) { - socialRentAverageEarning = socialRentEarningRes[0].earningsperweek; - } else if (socialRentEarningRes.length > 1) { - const socialRentTotalEarning = socialRentEarningRes.reduce( - (sum, item) => sum + item.earningsperweek, - 0 - ); - socialRentAverageEarning = - socialRentTotalEarning / socialRentEarningRes.length; - } - console.log("socialRentAverageEarning: ", socialRentAverageEarning); - - // create type for hpiRes query - type hpiRes = { - hpi_2020: number; - }; - - // get the hpi value --> Note: this need to change to accommodate future data - const hpiRes = await prisma.$queryRaw` - SELECT hpi_2020 FROM "public"."hpi" - WHERE itl3 = ${itl3} - `; - console.log("hpiRes: ", hpiRes); - let hpi; - if (hpiRes.length === 1) { - hpi = hpiRes[0].hpi_2020; - } else { - const hpiTotal = hpiRes.reduce((sum, item) => sum + item.hpi_2020, 0); - hpi = hpiTotal / hpiRes.length; - } - console.log("averageHpi: ", hpi); - - // create type for gas bill query - type gasBillYearlyRes = { - bill: number; - }; - - // get the gas bill value --> Note: this need to change to accommodate future data - const gasBillYearlyRes = await prisma.$queryRaw` - SELECT bill FROM "public"."gas_bills" - WHERE SUBSTRING(itl FROM 1 FOR 3) = ${itl3.substring(0, 3)} - `; - console.log("gasBillYearlyRes: ", gasBillYearlyRes); - const gasBillYearly = gasBillYearlyRes[0].bill; // get the gas bill - console.log("gasBillYearly: ", 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, - buildPrice, - averageRentMonthly, - socialRentAdjustments, - socialRentAverageEarning, - numberOfTransactions, - granularityPostcode, - pricesPaid, - gasBillYearly, - }); + 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, + }); } catch (err) { console.log("ERROR: API - ", (err as Error).message); const response = { error: (err as Error).message }; diff --git a/app/schemas/calculationSchema.ts b/app/schemas/calculationSchema.ts index bca17c3..c5232e7 100644 --- a/app/schemas/calculationSchema.ts +++ b/app/schemas/calculationSchema.ts @@ -2,6 +2,9 @@ import { z } from "zod"; import { parse as parsePostcode, fix as fixPostcode } from "postcode"; import { HOUSE_TYPES } from "../models/Property"; +// Type not exported by postcode lib directly +type ValidPostcode = Extract, { valid: true }>; + const HouseTypeEnum = z.enum(HOUSE_TYPES); /** @@ -13,12 +16,12 @@ export const calculationSchema = z.object({ .min(1, "housePostcode is required") .refine(fixPostcode, "Invalid postcode") .transform(parsePostcode) - .refine((postcode) => postcode.valid), + .refine((postcode): postcode is ValidPostcode => postcode.valid), houseSize: z.coerce.number().positive("houseSize must be a positive integer"), houseAge: z.coerce.number().positive("houseAge must be a positive integer"), houseBedrooms: z.coerce .number() - .positive("houseBedroomsmust be a positive integer"), + .positive("houseBedrooms must be a positive integer"), houseType: HouseTypeEnum.refine( (value) => HouseTypeEnum.options.includes(value), {