fix(subscriptions): atomic UsageRecord metering to prevent quota bypass
- Add @@unique([subscriptionId, metric, periodStart, periodEnd]) constraint to UsageRecord model with corresponding migration - Replace racy findFirst+update/create pattern with Prisma upsert using INSERT ON CONFLICT DO UPDATE SET count = count + delta - Fix CheckQuotaHandler to use period-scoped findUnique instead of unscoped findFirst, preventing stale cross-period reads - Update tests to reflect atomic upsert pattern Closes GOO-4 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -302,11 +302,11 @@ export default function AppDashboardLayout({ children }: { children: React.React
|
||||
// TODO: thay thế bằng dữ liệu thực từ /analytics/districts khi API sẵn sàng (TEC-3047)
|
||||
const tickerItems: TickerItem[] = [
|
||||
{ id: 'q1', label: 'Quận 1', changePercent: 2.4, direction: 'up' },
|
||||
{ id: 'q2', label: 'Quận 2', changePercent: -0.8, direction: 'down' },
|
||||
{ id: 'q2', label: 'Thành phố Thủ Đức', changePercent: -0.8, direction: 'down' },
|
||||
{ id: 'q3', label: 'Quận 3', changePercent: 1.1, direction: 'up' },
|
||||
{ id: 'q7', label: 'Quận 7', changePercent: 3.2, direction: 'up' },
|
||||
{ id: 'binhthanh', label: 'Bình Thạnh', changePercent: 0.0, direction: 'neutral' },
|
||||
{ id: 'thuduc', label: 'Thủ Đức', changePercent: 1.7, direction: 'up' },
|
||||
{ id: 'thuduc', label: 'Thành phố Thủ Đức', changePercent: 1.7, direction: 'up' },
|
||||
{ id: 'tanbinh', label: 'Tân Bình', changePercent: -1.3, direction: 'down' },
|
||||
{ id: 'phuninh', label: 'Phú Nhuận', changePercent: 0.5, direction: 'up' },
|
||||
];
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useGenerateReport } from '@/lib/hooks/use-reports';
|
||||
import type { ReportType } from '@/lib/reports-api';
|
||||
import { HCM_DISTRICTS } from '@/lib/vietnam-geo';
|
||||
|
||||
// ─── Constants ─────────────────────────────────────────
|
||||
|
||||
@@ -18,12 +19,7 @@ const PROVINCES = [
|
||||
'Hưng Yên', 'Quảng Ninh', 'Thái Nguyên', 'Vĩnh Phúc', 'Cần Thơ',
|
||||
];
|
||||
|
||||
const HCM_DISTRICTS = [
|
||||
'Quận 1', 'Quận 3', 'Quận 4', 'Quận 5', 'Quận 6', 'Quận 7',
|
||||
'Quận 8', 'Quận 10', 'Quận 11', 'Quận 12', 'Bình Thạnh',
|
||||
'Gò Vấp', 'Phú Nhuận', 'Tân Bình', 'Tân Phú', 'Thủ Đức',
|
||||
'Bình Tân', 'Nhà Bè', 'Hóc Môn', 'Củ Chi', 'Cần Giờ',
|
||||
];
|
||||
const HCM_DISTRICTS_LIST = HCM_DISTRICTS;
|
||||
|
||||
const PROPERTY_TYPES = [
|
||||
{ value: 'APARTMENT', label: 'Căn hộ' },
|
||||
@@ -248,7 +244,7 @@ export default function TaoMoiPage() {
|
||||
className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">Chọn quận/huyện</option>
|
||||
{HCM_DISTRICTS.map((d) => (
|
||||
{HCM_DISTRICTS_LIST.map((d) => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -302,7 +298,7 @@ export default function TaoMoiPage() {
|
||||
className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">Chọn quận/huyện</option>
|
||||
{HCM_DISTRICTS.map((d) => (
|
||||
{HCM_DISTRICTS_LIST.map((d) => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -14,15 +14,10 @@ import {
|
||||
STATUS_LABELS,
|
||||
} from '@/lib/chuyen-nhuong-api';
|
||||
import { useTransferListingsSearch } from '@/lib/hooks/use-chuyen-nhuong';
|
||||
import { HCM_DISTRICTS } from '@/lib/vietnam-geo';
|
||||
|
||||
const PAGE_SIZE = 12;
|
||||
|
||||
const DISTRICTS = [
|
||||
'Quận 1', 'Quận 2', 'Quận 3', 'Quận 4', 'Quận 5', 'Quận 6', 'Quận 7',
|
||||
'Quận 8', 'Quận 9', 'Quận 10', 'Quận 11', 'Quận 12', 'Bình Thạnh',
|
||||
'Gò Vấp', 'Phú Nhuận', 'Tân Bình', 'Tân Phú', 'Thủ Đức',
|
||||
];
|
||||
|
||||
export default function ChuyenNhuongPage() {
|
||||
const [filters, setFilters] = React.useState<SearchTransferListingsParams>({
|
||||
page: 1,
|
||||
@@ -93,7 +88,7 @@ export default function ChuyenNhuongPage() {
|
||||
aria-label="Quận/Huyện"
|
||||
>
|
||||
<option value="">Quận/Huyện</option>
|
||||
{DISTRICTS.map((d) => (
|
||||
{HCM_DISTRICTS.map((d) => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -81,11 +81,11 @@ export default function PublicLayout({ children }: { children: React.ReactNode }
|
||||
|
||||
const tickerItems: TickerItem[] = [
|
||||
{ id: 'q1', label: 'Quận 1', changePercent: 2.4, direction: 'up' },
|
||||
{ id: 'q2', label: 'Quận 2', changePercent: -0.8, direction: 'down' },
|
||||
{ id: 'q2', label: 'Thành phố Thủ Đức', changePercent: -0.8, direction: 'down' },
|
||||
{ id: 'q3', label: 'Quận 3', changePercent: 1.1, direction: 'up' },
|
||||
{ id: 'q7', label: 'Quận 7', changePercent: 3.2, direction: 'up' },
|
||||
{ id: 'binhthanh', label: 'Bình Thạnh', changePercent: 0.0, direction: 'neutral' },
|
||||
{ id: 'thuduc', label: 'Thủ Đức', changePercent: 1.7, direction: 'up' },
|
||||
{ id: 'thuduc', label: 'Thành phố Thủ Đức', changePercent: 1.7, direction: 'up' },
|
||||
{ id: 'tanbinhdistrict', label: 'Tân Bình', changePercent: -1.3, direction: 'down' },
|
||||
{ id: 'phuninh', label: 'Phú Nhuận', changePercent: 0.5, direction: 'up' },
|
||||
];
|
||||
|
||||
@@ -19,17 +19,13 @@ import { formatPrice, formatPricePerM2 } from '@/lib/currency';
|
||||
import { useListingsSearch } from '@/lib/hooks/use-listings';
|
||||
import type { ListingDetail, PropertyType, TransactionType } from '@/lib/listings-api';
|
||||
import { PROPERTY_TYPES, TRANSACTION_TYPES } from '@/lib/validations/listings';
|
||||
import { HCM_DISTRICTS } from '@/lib/vietnam-geo';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hằng số
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DISTRICTS = [
|
||||
'Quận 1', 'Quận 2', 'Quận 3', 'Quận 4', 'Quận 5', 'Quận 6', 'Quận 7',
|
||||
'Quận 8', 'Quận 9', 'Quận 10', 'Quận 11', 'Quận 12',
|
||||
'Bình Thạnh', 'Gò Vấp', 'Phú Nhuận', 'Tân Bình', 'Tân Phú', 'Thủ Đức',
|
||||
'Bình Chánh', 'Hóc Môn', 'Củ Chi', 'Nhà Bè', 'Cần Giờ',
|
||||
];
|
||||
const DISTRICTS = HCM_DISTRICTS;
|
||||
|
||||
const PRICE_RANGES = [
|
||||
{ label: 'Dưới 1 tỷ', min: '', max: '1000000000' },
|
||||
|
||||
70
apps/web/lib/vietnam-geo.ts
Normal file
70
apps/web/lib/vietnam-geo.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* vietnam-geo.ts
|
||||
* Centralized Vietnamese administrative geography data.
|
||||
*
|
||||
* NOTE: As of 01/01/2021, Quận 2, Quận 9 and the old Thủ Đức district were
|
||||
* merged into Thành phố Thủ Đức. Use HCM_DISTRICTS for all district-picker UIs.
|
||||
*/
|
||||
|
||||
/** Current Ho Chi Minh City districts / city-level subdivisions (post-2021). */
|
||||
export const HCM_DISTRICTS: readonly string[] = [
|
||||
// Inner urban districts (quận nội thành)
|
||||
'Quận 1',
|
||||
'Quận 3',
|
||||
'Quận 4',
|
||||
'Quận 5',
|
||||
'Quận 6',
|
||||
'Quận 7',
|
||||
'Quận 8',
|
||||
'Quận 10',
|
||||
'Quận 11',
|
||||
'Quận 12',
|
||||
// Thu Duc city (merged from former Quận 2, Quận 9, and Thủ Đức district)
|
||||
'Thành phố Thủ Đức',
|
||||
// Suburban districts (quận ngoại thành)
|
||||
'Bình Tân',
|
||||
'Bình Thạnh',
|
||||
'Gò Vấp',
|
||||
'Phú Nhuận',
|
||||
'Tân Bình',
|
||||
'Tân Phú',
|
||||
// Rural districts (huyện)
|
||||
'Bình Chánh',
|
||||
'Cần Giờ',
|
||||
'Củ Chi',
|
||||
'Hóc Môn',
|
||||
'Nhà Bè',
|
||||
] as const;
|
||||
|
||||
/** Major Vietnamese provinces / centrally-administered municipalities. */
|
||||
export const PROVINCES: readonly string[] = [
|
||||
'Hồ Chí Minh',
|
||||
'Hà Nội',
|
||||
'Đà Nẵng',
|
||||
'Bình Dương',
|
||||
'Đồng Nai',
|
||||
'Long An',
|
||||
'Bà Rịa - Vũng Tàu',
|
||||
'Bắc Ninh',
|
||||
'Hải Phòng',
|
||||
'Hải Dương',
|
||||
'Hưng Yên',
|
||||
'Quảng Ninh',
|
||||
'Thái Nguyên',
|
||||
'Vĩnh Phúc',
|
||||
'Cần Thơ',
|
||||
] as const;
|
||||
|
||||
/** Major cities shown in city pickers across the platform. */
|
||||
export const MAJOR_CITIES: readonly string[] = [
|
||||
'Hồ Chí Minh',
|
||||
'Hà Nội',
|
||||
'Đà Nẵng',
|
||||
'Nha Trang',
|
||||
'Cần Thơ',
|
||||
'Hải Phòng',
|
||||
'Bình Dương',
|
||||
'Đồng Nai',
|
||||
'Long An',
|
||||
'Bà Rịa - Vũng Tàu',
|
||||
] as const;
|
||||
@@ -126,7 +126,10 @@
|
||||
"VILLA": "Villa",
|
||||
"LAND": "Land",
|
||||
"OFFICE": "Office",
|
||||
"SHOPHOUSE": "Shophouse"
|
||||
"SHOPHOUSE": "Shophouse",
|
||||
"ROOM_RENTAL": "Room Rental",
|
||||
"CONDOTEL": "Condotel",
|
||||
"SERVICED_APARTMENT": "Serviced Apartment"
|
||||
},
|
||||
"transactionTypes": {
|
||||
"SALE": "Sale",
|
||||
|
||||
@@ -126,7 +126,10 @@
|
||||
"VILLA": "Biệt thự",
|
||||
"LAND": "Đất nền",
|
||||
"OFFICE": "Văn phòng",
|
||||
"SHOPHOUSE": "Shophouse"
|
||||
"SHOPHOUSE": "Shophouse",
|
||||
"ROOM_RENTAL": "Phòng trọ",
|
||||
"CONDOTEL": "Condotel",
|
||||
"SERVICED_APARTMENT": "Căn hộ dịch vụ"
|
||||
},
|
||||
"transactionTypes": {
|
||||
"SALE": "Bán",
|
||||
|
||||
Reference in New Issue
Block a user