Timezone Rules (BẮT BUỘC)
Vi phạm timezone rules = data corruption. Không có ngoại lệ.
1. Nguyên tắc cốt lõi
┌─────────────────────────────────────────────────────────────┐
│ Mọi thời gian user nhìn thấy = giờ SALON (Europe/Oslo) │
│ Mọi thời gian lưu DB = UTC │
│ Timezone của user (browser) = KHÔNG BAO GIỜ được dùng │
└─────────────────────────────────────────────────────────────┘
2. Data Types & Storage
| Data | Kiểu | Ví dụ | Timezone |
|---|---|---|---|
| Business hours | String "HH:mm" |
"09:00" |
Salon local — KHÔNG convert |
| Resource schedule | String "HH:mm" |
"09:00" |
Salon local — KHÔNG convert |
| Schedule override time | String "HH:mm" nullable |
"10:00" / null |
Salon local |
| Schedule override date | @db.Date |
2026-04-14 |
Calendar date (no tz) |
| Booking startTime/endTime | DateTime |
2026-04-14T07:00:00Z |
UTC |
| Tenant timezone | String IANA | "Europe/Oslo" |
Config — source of truth |
3. Conversion Rules
Khi LƯU booking (frontend → API)
// Frontend: user chọn 09:00 ngày 14/04 → convert salon local → UTC
import { toUTC } from '@/lib/timezone';
const utcISO = toUTC('2026-04-14', '09:00', 'Europe/Oslo');
// → "2026-04-14T07:00:00.000Z" (Oslo UTC+2 summer)
Khi HIỂN THỊ booking (API → frontend)
// DB trả về UTC → convert sang salon local để hiển thị
import { formatTimeInZone } from '@/lib/timezone';
formatTimeInZone('2026-04-14T07:00:00Z', 'Europe/Oslo');
// → "09:00"
Khi tính AVAILABILITY (API)
// Schedule "09:00" là salon local → convert sang UTC để so sánh với bookings
import { localToUTC } from '../../common/timezone.util';
localToUTC('2026-04-14', '09:00', 'Europe/Oslo');
// → Date(2026-04-14T07:00:00Z)
4. KHÔNG BAO GIỜ làm
// ❌ KHÔNG: Coi schedule time là UTC
new Date(`${date}T${time}:00Z`) // Z = UTC! Schedule "09:00" ≠ 09:00 UTC
// ❌ KHÔNG: Dùng toISOString() để lấy date string
d.toISOString().split('T')[0] // Lệch ngày nếu local ≠ UTC
// ❌ KHÔNG: Dùng new Date(dateStr) không có time component
new Date('2026-04-14') // JS parse as UTC midnight → getDay() có thể lệch
// ❌ KHÔNG: Dùng getHours(), getDay() etc. trên UTC dates
booking.startTime.getHours() // Server timezone, không phải salon timezone
// ❌ KHÔNG: Dùng browser timezone cho bất cứ logic nào
new Date().getDay() // Browser timezone, không phải salon timezone
// (Trừ khi chỉ để hiển thị week navigation UI — không liên quan booking logic)
5. LUÔN LUÔN làm
// ✅ ĐÚNG: Date string từ Date object → dùng local methods
function toDateString(d: Date): string {
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}
// ✅ ĐÚNG: DayOfWeek từ date string → dùng noon UTC (tránh lệch ngày)
function getDayOfWeek(date: string): number {
return new Date(`${date}T12:00:00Z`).getUTCDay();
}
// ✅ ĐÚNG: Hiển thị thời gian booking → dùng salon timezone
formatTimeInZone(booking.startTime, tenant.settings.timezone);
// ✅ ĐÚNG: So sánh business hours → convert UTC sang salon local
const parts = getPartsInZone(utcDate, salonTimezone);
// → { dayOfWeek, hour, minute } in salon local time
6. Utility Files
| File | Vị trí | Mục đích |
|---|---|---|
timezone.ts |
booking-web/src/lib/ |
Frontend: toUTC, toSalonTime, formatTimeInZone, formatDateTimeInZone, getPartsInZone, createDateTimeFormatterInZone |
timezone.util.ts |
booking-api/src/common/ |
API: localToUTC, getDayOfWeek, getPartsInZone |
7. Flow Diagram
sequenceDiagram
participant Owner
participant DB
participant Customer
participant API
participant FE
Note over Owner,DB: Salon setup
Owner->>DB: schedule "09:00-17:00"<br/>(plain string, salon local — KHÔNG convert)
Note over DB: Hiển thị: as-is, no conversion
Note over Customer,FE: Customer xem available slots (date: 2026-04-14)
Customer->>API: GET /availability?date=2026-04-14
API->>API: localToUTC("2026-04-14","09:00","Europe/Oslo") = 07:00Z
API->>API: localToUTC("2026-04-14","17:00","Europe/Oslo") = 15:00Z
API->>API: Generate slots: 07:00Z, 07:15Z, ..., 14:45Z
API-->>FE: slots as UTC ISO strings
FE->>FE: formatTimeInZone("07:00Z","Europe/Oslo") = "09:00"
FE-->>Customer: 09:00, 09:15, 09:30 ... 16:45 (salon time)
Note over Customer,DB: Customer book slot "09:00" salon time
Customer->>FE: pick 09:00 on 2026-04-14
FE->>FE: toUTC("2026-04-14","09:00","Europe/Oslo") = "2026-04-14T07:00:00Z"
FE->>API: POST /bookings { startTime: "...07:00:00Z" }
API->>DB: INSERT booking.startTime = 2026-04-14T07:00:00Z (UTC)
Note over DB: Calendar render: formatTimeInZone("07:00Z","Europe/Oslo") = "09:00" ✓
8. Display snapshot — bảo vệ historic bookings
BookingService.create ghi booking.metadata.displaySnapshot = { timezone, currency, locale } (frozen snapshot từ tenant settings tại lúc tạo). FE render booking time / money từ snapshot này, KHÔNG phải tenant setting hiện tại — tránh tenant đổi timezone về sau retroactively shift historical bookings.
Customer portal: GET /customer/me/bookings flatten booking.tenantTimezone ở DTO boundary để FE render formatDateTimeInZone(startTime, tenantTimezone) không cần re-fetch tenant.
9. Tham khảo
docs/rules/development-rules.md— Phần "Date & Time" link sang doc nàybooking-web/src/lib/timezone.ts— FE utilitiesbooking-api/src/common/timezone.util.ts— API utilitiesdocs/flows/booking-flow.md— Booking creation flow áp dụng các rules này