feat(industrial): update TypeScript types for Float→Decimal USD field migration (GOO-27)
Migration SQL (20260422120000_industrial_usd_to_decimal) and Prisma schema already reflected Decimal(18,4). This commit completes the TypeScript / frontend layer. API changes: - Domain repo interfaces (IndustrialListingListItem, IndustrialListingDetailData, IndustrialParkListItem, IndustrialParkDetailData, IndustrialMarketData): USD money fields changed from number|null → string|null (PostgreSQL numeric serialises as string in raw query results) - Raw DB interface types in Prisma repositories updated to string|null for Decimal columns - toDomain() mappers: parseFloat() added where entity props require number|null for business-logic arithmetic - estimate-industrial-rent handler: Number() cast on Prisma ORM Decimal objects before arithmetic and comparisons Web changes: - khu-cong-nghiep-api.ts: IndustrialParkListItem, IndustrialParkDetail, IndustrialListingItem, IndustrialMarketData USD fields → string|null with JSDoc - listing-card.tsx: parseFloat() wrapping for priceUsdM2/totalLeasePrice display - park-compare-client.tsx: parseFloat() for landRentUsdM2Year in radar score Note: pre-existing test failures in filter-bar/login/search specs are unrelated to this migration (confirmed present on branch before this change). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -48,7 +48,10 @@ export class EstimateIndustrialRentHandler
|
||||
// Calculate base rent based on property type
|
||||
const rentField = this.getRentField(propertyType);
|
||||
const rents = provinceParks
|
||||
.map((p) => p[rentField] as number | null)
|
||||
.map((p) => {
|
||||
const val = p[rentField];
|
||||
return val != null ? Number(val) : null;
|
||||
})
|
||||
.filter((r): r is number => r != null);
|
||||
|
||||
const provinceLow = rents.length > 0 ? Math.min(...rents) : null;
|
||||
@@ -58,7 +61,7 @@ export class EstimateIndustrialRentHandler
|
||||
// Determine base rent
|
||||
let baseRentUsdM2: number;
|
||||
if (specificPark && specificPark[rentField] != null) {
|
||||
baseRentUsdM2 = specificPark[rentField] as number;
|
||||
baseRentUsdM2 = Number(specificPark[rentField]);
|
||||
} else if (provinceAvg != null) {
|
||||
baseRentUsdM2 = provinceAvg;
|
||||
} else {
|
||||
@@ -126,9 +129,11 @@ export class EstimateIndustrialRentHandler
|
||||
const totalLeaseUsd = Math.round(totalMonthlyUsd * 12 * leaseDurationYears * 100) / 100;
|
||||
|
||||
// Management fee
|
||||
const managementFeeUsdM2 = specificPark?.managementFeeUsd ?? (provinceParks.length > 0
|
||||
? provinceParks.reduce((sum, p) => sum + (p.managementFeeUsd ?? 0), 0) / provinceParks.length || null
|
||||
: null);
|
||||
const managementFeeUsdM2 = specificPark?.managementFeeUsd != null
|
||||
? Number(specificPark.managementFeeUsd)
|
||||
: (provinceParks.length > 0
|
||||
? provinceParks.reduce((sum, p) => sum + (p.managementFeeUsd != null ? Number(p.managementFeeUsd) : 0), 0) / provinceParks.length || null
|
||||
: null);
|
||||
|
||||
return {
|
||||
estimated_rent_usd_m2: adjustedRent,
|
||||
|
||||
@@ -34,7 +34,8 @@ export interface IndustrialListingListItem {
|
||||
status: IndustrialListingStatus;
|
||||
title: string;
|
||||
areaM2: number;
|
||||
priceUsdM2: number | null;
|
||||
/** Decimal(18,4) serialised as string by PostgreSQL numeric — use parseFloat() for arithmetic. */
|
||||
priceUsdM2: string | null;
|
||||
pricingUnit: string | null;
|
||||
ceilingHeightM: number | null;
|
||||
hasMezzanine: boolean;
|
||||
@@ -64,10 +65,13 @@ export interface IndustrialListingDetailData {
|
||||
hasMezzanine: boolean;
|
||||
hasOfficeArea: boolean;
|
||||
officeAreaM2: number | null;
|
||||
priceUsdM2: number | null;
|
||||
/** Decimal(18,4) serialised as string — use parseFloat() for arithmetic. */
|
||||
priceUsdM2: string | null;
|
||||
pricingUnit: string | null;
|
||||
totalLeasePrice: number | null;
|
||||
managementFee: number | null;
|
||||
/** Decimal(18,4) serialised as string — use parseFloat() for arithmetic. */
|
||||
totalLeasePrice: string | null;
|
||||
/** Decimal(18,4) serialised as string — use parseFloat() for arithmetic. */
|
||||
managementFee: string | null;
|
||||
depositMonths: number | null;
|
||||
minLeaseYears: number | null;
|
||||
maxLeaseYears: number | null;
|
||||
|
||||
@@ -37,9 +37,12 @@ export interface IndustrialParkListItem {
|
||||
occupancyRate: number;
|
||||
remainingAreaHa: number;
|
||||
tenantCount: number;
|
||||
landRentUsdM2Year: number | null;
|
||||
rbfRentUsdM2Month: number | null;
|
||||
rbwRentUsdM2Month: number | null;
|
||||
/** Decimal(18,4) serialised as string — use parseFloat() for arithmetic. */
|
||||
landRentUsdM2Year: string | null;
|
||||
/** Decimal(18,4) serialised as string — use parseFloat() for arithmetic. */
|
||||
rbfRentUsdM2Month: string | null;
|
||||
/** Decimal(18,4) serialised as string — use parseFloat() for arithmetic. */
|
||||
rbwRentUsdM2Month: string | null;
|
||||
targetIndustries: string[];
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
@@ -66,10 +69,14 @@ export interface IndustrialParkDetailData {
|
||||
remainingAreaHa: number;
|
||||
tenantCount: number;
|
||||
establishedYear: number | null;
|
||||
landRentUsdM2Year: number | null;
|
||||
rbfRentUsdM2Month: number | null;
|
||||
rbwRentUsdM2Month: number | null;
|
||||
managementFeeUsd: number | null;
|
||||
/** Decimal(18,4) serialised as string — use parseFloat() for arithmetic. */
|
||||
landRentUsdM2Year: string | null;
|
||||
/** Decimal(18,4) serialised as string — use parseFloat() for arithmetic. */
|
||||
rbfRentUsdM2Month: string | null;
|
||||
/** Decimal(18,4) serialised as string — use parseFloat() for arithmetic. */
|
||||
rbwRentUsdM2Month: string | null;
|
||||
/** Decimal(18,4) serialised as string — use parseFloat() for arithmetic. */
|
||||
managementFeeUsd: string | null;
|
||||
infrastructure: Record<string, unknown> | null;
|
||||
connectivity: Record<string, unknown> | null;
|
||||
incentives: Record<string, unknown> | null;
|
||||
@@ -100,10 +107,12 @@ export interface IndustrialParkStatsData {
|
||||
export interface IndustrialMarketData {
|
||||
totalParks: number;
|
||||
avgOccupancyRate: number;
|
||||
avgLandRentUsdM2: number | null;
|
||||
avgRbfRentUsdM2: number | null;
|
||||
rentByRegion: { region: string; avgLandRent: number | null; avgRbfRent: number | null; parkCount: number }[];
|
||||
rentByProvince: { province: string; avgLandRent: number | null; avgRbfRent: number | null; parkCount: number }[];
|
||||
/** AVG(numeric) serialised as string by PostgreSQL. */
|
||||
avgLandRentUsdM2: string | null;
|
||||
/** AVG(numeric) serialised as string by PostgreSQL. */
|
||||
avgRbfRentUsdM2: string | null;
|
||||
rentByRegion: { region: string; avgLandRent: string | null; avgRbfRent: string | null; parkCount: number }[];
|
||||
rentByProvince: { province: string; avgLandRent: string | null; avgRbfRent: string | null; parkCount: number }[];
|
||||
}
|
||||
|
||||
export interface IIndustrialParkRepository {
|
||||
|
||||
@@ -196,10 +196,10 @@ export class PrismaIndustrialListingRepository implements IIndustrialListingRepo
|
||||
hasMezzanine: row.hasMezzanine,
|
||||
hasOfficeArea: row.hasOfficeArea,
|
||||
officeAreaM2: row.officeAreaM2,
|
||||
priceUsdM2: row.priceUsdM2,
|
||||
priceUsdM2: row.priceUsdM2 != null ? parseFloat(row.priceUsdM2 as unknown as string) : null,
|
||||
pricingUnit: row.pricingUnit,
|
||||
totalLeasePrice: row.totalLeasePrice,
|
||||
managementFee: row.managementFee,
|
||||
totalLeasePrice: row.totalLeasePrice != null ? parseFloat(row.totalLeasePrice as unknown as string) : null,
|
||||
managementFee: row.managementFee != null ? parseFloat(row.managementFee as unknown as string) : null,
|
||||
depositMonths: row.depositMonths,
|
||||
minLeaseYears: row.minLeaseYears,
|
||||
maxLeaseYears: row.maxLeaseYears,
|
||||
@@ -299,10 +299,10 @@ interface RawListing {
|
||||
hasMezzanine: boolean;
|
||||
hasOfficeArea: boolean;
|
||||
officeAreaM2: number | null;
|
||||
priceUsdM2: number | null;
|
||||
priceUsdM2: string | null;
|
||||
pricingUnit: string | null;
|
||||
totalLeasePrice: number | null;
|
||||
managementFee: number | null;
|
||||
totalLeasePrice: string | null;
|
||||
managementFee: string | null;
|
||||
depositMonths: number | null;
|
||||
minLeaseYears: number | null;
|
||||
maxLeaseYears: number | null;
|
||||
@@ -327,7 +327,7 @@ interface RawListingListItem {
|
||||
status: string;
|
||||
title: string;
|
||||
areaM2: number;
|
||||
priceUsdM2: number | null;
|
||||
priceUsdM2: string | null;
|
||||
pricingUnit: string | null;
|
||||
ceilingHeightM: number | null;
|
||||
hasMezzanine: boolean;
|
||||
|
||||
@@ -242,7 +242,7 @@ export class PrismaIndustrialParkRepository implements IIndustrialParkRepository
|
||||
}
|
||||
|
||||
async getMarketData(): Promise<IndustrialMarketData> {
|
||||
const [overall] = await this.prisma.$queryRaw<[{ totalParks: bigint; avgOccupancy: number; avgLandRent: number | null; avgRbfRent: number | null }]>`
|
||||
const [overall] = await this.prisma.$queryRaw<[{ totalParks: bigint; avgOccupancy: number; avgLandRent: string | null; avgRbfRent: string | null }]>`
|
||||
SELECT COUNT(*)::bigint as "totalParks",
|
||||
AVG("occupancyRate") as "avgOccupancy",
|
||||
AVG("landRentUsdM2Year") as "avgLandRent",
|
||||
@@ -250,14 +250,14 @@ export class PrismaIndustrialParkRepository implements IIndustrialParkRepository
|
||||
FROM "IndustrialPark" WHERE status = 'OPERATIONAL' OR status = 'FULL'
|
||||
`;
|
||||
|
||||
const rentByRegion = await this.prisma.$queryRaw<{ region: string; avgLandRent: number | null; avgRbfRent: number | null; parkCount: bigint }[]>`
|
||||
const rentByRegion = await this.prisma.$queryRaw<{ region: string; avgLandRent: string | null; avgRbfRent: string | null; parkCount: bigint }[]>`
|
||||
SELECT region::text, AVG("landRentUsdM2Year") as "avgLandRent",
|
||||
AVG("rbfRentUsdM2Month") as "avgRbfRent", COUNT(*)::bigint as "parkCount"
|
||||
FROM "IndustrialPark" WHERE status IN ('OPERATIONAL', 'FULL')
|
||||
GROUP BY region ORDER BY "avgLandRent" DESC NULLS LAST
|
||||
`;
|
||||
|
||||
const rentByProvince = await this.prisma.$queryRaw<{ province: string; avgLandRent: number | null; avgRbfRent: number | null; parkCount: bigint }[]>`
|
||||
const rentByProvince = await this.prisma.$queryRaw<{ province: string; avgLandRent: string | null; avgRbfRent: string | null; parkCount: bigint }[]>`
|
||||
SELECT province, AVG("landRentUsdM2Year") as "avgLandRent",
|
||||
AVG("rbfRentUsdM2Month") as "avgRbfRent", COUNT(*)::bigint as "parkCount"
|
||||
FROM "IndustrialPark" WHERE status IN ('OPERATIONAL', 'FULL')
|
||||
@@ -296,10 +296,10 @@ export class PrismaIndustrialParkRepository implements IIndustrialParkRepository
|
||||
remainingAreaHa: row.remainingAreaHa,
|
||||
tenantCount: row.tenantCount,
|
||||
establishedYear: row.establishedYear,
|
||||
landRentUsdM2Year: row.landRentUsdM2Year,
|
||||
rbfRentUsdM2Month: row.rbfRentUsdM2Month,
|
||||
rbwRentUsdM2Month: row.rbwRentUsdM2Month,
|
||||
managementFeeUsd: row.managementFeeUsd,
|
||||
landRentUsdM2Year: row.landRentUsdM2Year != null ? parseFloat(row.landRentUsdM2Year) : null,
|
||||
rbfRentUsdM2Month: row.rbfRentUsdM2Month != null ? parseFloat(row.rbfRentUsdM2Month) : null,
|
||||
rbwRentUsdM2Month: row.rbwRentUsdM2Month != null ? parseFloat(row.rbwRentUsdM2Month) : null,
|
||||
managementFeeUsd: row.managementFeeUsd != null ? parseFloat(row.managementFeeUsd) : null,
|
||||
infrastructure: row.infrastructure as Record<string, unknown> | null,
|
||||
connectivity: row.connectivity as Record<string, unknown> | null,
|
||||
incentives: row.incentives as Record<string, unknown> | null,
|
||||
@@ -407,10 +407,10 @@ interface RawPark {
|
||||
remainingAreaHa: number;
|
||||
tenantCount: number;
|
||||
establishedYear: number | null;
|
||||
landRentUsdM2Year: number | null;
|
||||
rbfRentUsdM2Month: number | null;
|
||||
rbwRentUsdM2Month: number | null;
|
||||
managementFeeUsd: number | null;
|
||||
landRentUsdM2Year: string | null;
|
||||
rbfRentUsdM2Month: string | null;
|
||||
rbwRentUsdM2Month: string | null;
|
||||
managementFeeUsd: string | null;
|
||||
infrastructure: Prisma.JsonValue;
|
||||
connectivity: Prisma.JsonValue;
|
||||
incentives: Prisma.JsonValue;
|
||||
|
||||
Reference in New Issue
Block a user