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:
Ho Ngoc Hai
2026-04-23 00:34:40 +07:00
parent 0329455e9a
commit 36a9b00cf1
9 changed files with 99 additions and 54 deletions

View File

@@ -15,9 +15,9 @@ interface ListingCardProps {
export function IndustrialListingCard({ listing }: ListingCardProps) {
const priceText = listing.priceUsdM2
? `$${listing.priceUsdM2}/${listing.pricingUnit ?? 'm²/tháng'}`
? `$${parseFloat(listing.priceUsdM2)}/${listing.pricingUnit ?? 'm²/tháng'}`
: listing.totalLeasePrice
? `$${listing.totalLeasePrice.toLocaleString()}`
? `$${parseFloat(listing.totalLeasePrice).toLocaleString()}`
: 'Liên hệ';
const leaseTermText =

View File

@@ -45,7 +45,7 @@ function normalizeScore(park: IndustrialParkDetail, metric: string): number {
case 'area':
return Math.min((park.totalAreaHa / 1000) * 100, 100);
case 'rent': {
const rent = park.landRentUsdM2Year ?? 0;
const rent = park.landRentUsdM2Year != null ? parseFloat(park.landRentUsdM2Year) : 0;
return rent > 0 ? Math.min((rent / 150) * 100, 100) : 0;
}
case 'infrastructure': {

View File

@@ -23,9 +23,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;
@@ -51,10 +54,14 @@ export interface IndustrialParkDetail {
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, string> | null;
connectivity: Record<string, { name: string; distanceKm: number }> | null;
incentives: Record<string, unknown> | null;
@@ -84,10 +91,12 @@ export interface IndustrialParkStats {
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 }[];
}
// ─── Industrial Listing Types ───────────────────────────
@@ -125,9 +134,11 @@ export interface IndustrialListingItem {
description: string | null;
areaM2: number;
ceilingHeightM: number | null;
priceUsdM2: number | null;
/** Decimal(18,4) serialised as string. Use parseFloat() for arithmetic. */
priceUsdM2: string | null;
pricingUnit: string | null;
totalLeasePrice: number | null;
/** Decimal(18,4) serialised as string. Use parseFloat() for arithmetic. */
totalLeasePrice: string | null;
minLeaseYears: number | null;
maxLeaseYears: number | null;
availableFrom: string | null;