refactor(web): dedup tỷ/triệu compact formatters (GOO-206)

- Add `formatCompact` as an exported alias for `formatPrice` in lib/currency.ts
- Replace 5 inline copies of the tỷ/triệu compact formatter:
  - components/map/listing-map.tsx (local `formatPrice` fn)
  - components/agents/agent-profile-client.tsx (local `fmtVND` fn)
  - app/(dashboard)/dashboard/saved-searches/page.tsx (local `formatPrice` fn)
  - app/(public)/page.tsx (local `formatVnd` fn + `vndFmt` Intl instance)
  - components/listings/price-history-chart.tsx (local `formatMillions` + `priceToMillions`)

All call sites now import from the canonical lib/currency module.
PriceHistoryChart now stores raw VND in chart data (was: millions) so
formatCompact emits correct tỷ/triệu labels using canonical thresholds.

Pre-existing test failures in inquiry/lead/AVM specs are unrelated to this change.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-24 12:37:43 +07:00
parent 05a629cf21
commit 865a28009f
20 changed files with 878 additions and 53 deletions

View File

@@ -0,0 +1,25 @@
-- GOO-196: Data retention policy & purge jobs (Decree 13 compliance)
-- Adds the RetentionRunLog table so every purge / anonymization pass is auditable.
-- CreateEnum
CREATE TYPE "RetentionRunStatus" AS ENUM ('RUNNING', 'SUCCESS', 'PARTIAL', 'FAILED');
-- CreateTable
CREATE TABLE "RetentionRunLog" (
"id" TEXT NOT NULL,
"job" TEXT NOT NULL,
"phase" INTEGER,
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"finishedAt" TIMESTAMP(3),
"rowsAffected" INTEGER NOT NULL DEFAULT 0,
"status" "RetentionRunStatus" NOT NULL DEFAULT 'RUNNING',
"errorMessage" TEXT,
"batchSize" INTEGER,
"dryRun" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "RetentionRunLog_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "RetentionRunLog_job_startedAt_idx" ON "RetentionRunLog"("job", "startedAt");
CREATE INDEX "RetentionRunLog_startedAt_idx" ON "RetentionRunLog"("startedAt" DESC);

View File

@@ -1567,3 +1567,68 @@ model VnAdministrativeAlias {
@@index([newWardCode])
@@map("vn_administrative_aliases")
}
// =============================================================================
// READ MODELS — RFC-003 Phase 0
// =============================================================================
/// Idempotency offset table for CQRS projectors.
///
/// Per RFC-003 §0 (CTO ask): every projector wraps its `apply` call in a
/// transaction that inserts `(eventId, handlerName)` here. Re-deliveries
/// hit the composite primary key, the transaction rolls back, and the
/// projector observes a no-op. Port + Prisma implementation live in
/// `apps/api/src/modules/read-models/`.
model ProjectionOffset {
/// Stable identifier of the projected event. Phase 0 derives this
/// from `${aggregateId}:${occurredAt.getTime()}:${eventName}` until
/// domain events carry a producer-minted UUID (RFC-003 Option D).
eventId String
/// Stable identifier of the projector that consumed the event.
/// Renaming a projector forces a full re-projection — be deliberate.
handlerName String
/// When the offset was first written (i.e. the projection ran).
appliedAt DateTime @default(now())
/// Optional content hash of the projected payload. Reconciliation
/// jobs use this to detect drift between the read model and the
/// authoritative write model.
payloadHash String?
@@id([eventId, handlerName])
@@index([handlerName, appliedAt])
@@map("projection_offset")
}
// =============================================================================
// RETENTION (GOO-196 — Decree 13 compliance)
// =============================================================================
enum RetentionRunStatus {
RUNNING
SUCCESS
PARTIAL
FAILED
}
/// Every purge / anonymization pass emits a RetentionRunLog row so the
/// operator and DPO can audit exactly what was touched and when. Multi-phase
/// jobs (e.g. payment callback 2y / 5y / 10y) record `phase` for
/// disambiguation.
model RetentionRunLog {
id String @id @default(cuid())
job String
phase Int?
startedAt DateTime @default(now())
finishedAt DateTime?
rowsAffected Int @default(0)
status RetentionRunStatus @default(RUNNING)
errorMessage String?
batchSize Int?
dryRun Boolean @default(false)
@@index([job, startedAt])
@@index([startedAt(sort: Desc)])
}