Files
goodgo-platform/apps/web/lib/currency.ts
Ho Ngoc Hai 55a01c5738 feat(web): centralise Vietnamese price formatting across all pages
Create a single `currency.ts` utility with `formatPrice`, `formatVND`,
`formatPricePerM2`, and `parseVND` to replace 9+ duplicated inline
formatters. This fixes inconsistent decimal handling (1.5M was truncated
to "1 triệu") and standardises price/m² display. Integrated across
property cards, listing detail, dashboard, analytics, payments, pricing,
and admin moderation pages with 19 new unit tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-10 23:33:31 +07:00

122 lines
3.9 KiB
TypeScript

/**
* Vietnamese currency formatting utilities.
*
* Centralised formatter for all price displays across the platform.
* Converts raw VND numbers into human-readable Vietnamese format:
* 3,500,000,000 -> "3.5 ty"
* 150,000,000 -> "150 trieu"
* 800,000 -> "800.000"
*/
// ---------------------------------------------------------------------------
// Core formatter
// ---------------------------------------------------------------------------
/**
* Format a VND amount into compact Vietnamese notation.
*
* @example
* formatPrice(3_500_000_000) // "3.5 ty"
* formatPrice(150_000_000) // "150 trieu"
* formatPrice(1_500_000) // "1.5 trieu"
* formatPrice(800_000) // "800.000"
* formatPrice("3500000000") // "3.5 ty" (string input accepted)
*/
export function formatPrice(amount: string | number): string {
const num = typeof amount === 'string' ? Number(amount) : amount;
if (!Number.isFinite(num) || num < 0) return '0';
if (num >= 1_000_000_000) {
const billions = num / 1_000_000_000;
return `${stripTrailingZero(billions.toFixed(1))} t\u1ef7`;
}
if (num >= 1_000_000) {
const millions = num / 1_000_000;
return `${stripTrailingZero(millions.toFixed(1))} tri\u1ec7u`;
}
return num.toLocaleString('vi-VN');
}
// ---------------------------------------------------------------------------
// Variant: with currency suffix
// ---------------------------------------------------------------------------
/**
* Format a VND amount with a " \u0111" currency suffix.
* Returns "Mi\u1ec5n ph\u00ed" for zero amounts.
*
* @example
* formatVND(4_990_000) // "4.99 trieu d"
* formatVND(0) // "Mien phi"
*/
export function formatVND(amount: string | number): string {
const num = typeof amount === 'string' ? Number(amount) : amount;
if (!Number.isFinite(num) || num < 0) return '0 \u0111';
if (num === 0) return 'Mi\u1ec5n ph\u00ed';
if (num >= 1_000_000_000) {
const billions = num / 1_000_000_000;
return `${stripTrailingZero(billions.toFixed(1))} t\u1ef7 \u0111`;
}
if (num >= 1_000_000) {
const millions = num / 1_000_000;
return `${stripTrailingZero(millions.toFixed(1))} tri\u1ec7u \u0111`;
}
return num.toLocaleString('vi-VN') + ' \u0111';
}
// ---------------------------------------------------------------------------
// Variant: price per square metre
// ---------------------------------------------------------------------------
/**
* Format a VND/m\u00b2 value.
*
* @example
* formatPricePerM2(50_500_000) // "50.5 tr/m\u00b2"
* formatPricePerM2(500_000) // "500.000 \u0111/m\u00b2"
*/
export function formatPricePerM2(price: string | number): string {
const num = typeof price === 'string' ? Number(price) : price;
if (!Number.isFinite(num) || num < 0) return '0 \u0111/m\u00b2';
if (num >= 1_000_000_000) {
const billions = num / 1_000_000_000;
return `${stripTrailingZero(billions.toFixed(1))} t\u1ef7/m\u00b2`;
}
if (num >= 1_000_000) {
const millions = num / 1_000_000;
return `${stripTrailingZero(millions.toFixed(1))} tr/m\u00b2`;
}
return `${num.toLocaleString('vi-VN')} \u0111/m\u00b2`;
}
// ---------------------------------------------------------------------------
// Parser (reverse direction)
// ---------------------------------------------------------------------------
/**
* Parse a formatted Vietnamese price string back into a number.
* Returns `null` if the input cannot be parsed.
*/
export function parseVND(formatted: string): number | null {
const cleaned = formatted.replace(/[^\d]/g, '');
if (cleaned === '') return null;
return Number(cleaned);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Remove a trailing ".0" so "3.0 ty" becomes "3 ty". */
function stripTrailingZero(str: string): string {
return str.replace(/\.0$/, '');
}