Hai thứ khác nhau xài chung một chữ
Ngày check-in là một sự thật về một toà nhà. Khách sạn nói phòng là của bạn vào ngày mùng 4; điều đó đúng tại khách sạn, theo lịch của khách sạn, và nó không biến thành một ngày khác chỉ vì lúc đọc thì người khách đang ngồi ở múi giờ khác. Giờ bay cũng cùng loại sự thật đó: giờ địa phương ở sân bay đi. Cả hai đều không phải một điểm trên trục thời gian toàn cầu — chúng là ngày dân sự và giờ địa phương, và chỗ duy nhất chúng chạm vào UTC là khi có ai đó đem đi convert, thường là do vô tình.
Có những giá trị trong du lịch thì đúng là instant thật. "Giá này được báo lúc 14:02:11" là một điểm thời gian. "Supplier xác nhận booking lúc..." cũng vậy. Sai không nằm ở việc dùng instant. Sai nằm ở việc dùng chung một cách biểu diễn cho cả hai loại giá trị rồi hy vọng khác biệt đó không quan trọng.
Hỏng ở đâu
Cái bẫy thứ nhất là kiểu dữ liệu. Một giá trị chỉ-có-ngày rơi vào một cột hay
một field kiểu timestamp, và ngay lúc đó phải có thứ gì đó bịa ra giờ và múi
giờ cho nó. Thường là nửa đêm UTC. Thế là ngày check-in mùng 4 thành
2026-06-04T00:00:00Z, và một client render nó ở múi giờ lùi so với UTC sẽ
hiện ra mùng 3. Chẳng có gì hỏng cả. Có một cái giờ được bịa ra, rồi được tôn
trọng.
Cái thứ hai là chữ nửa đêm. Nửa đêm chỉ là nửa đêm ở đâu đó. Giờ cắt, mốc chia ngày, "đơn đặt hôm nay" — mỗi cái đều đang lặng lẽ chọn một múi giờ, và nếu không ai gọi tên nó ra thì múi giờ đó là múi giờ mà process tình cờ đang chạy.
Cái thứ ba là cái tốn tiền: cửa sổ huỷ được tính theo múi giờ của server. Supplier nói JSON và SOAP/XML, và khá nhiều bên trả hạn huỷ về dưới dạng một chuỗi giờ địa phương trần, không kèm offset nào — cái field nói khi nào, còn vị trí khách sạn mới nói ở đâu. Parse chuỗi đó như thể nó là UTC, hoặc như thể nó là múi giờ của server, thì cái hạn huỷ miễn phí bạn hiện cho khách lệch vài tiếng so với cái hạn mà supplier sẽ áp. Quy trình test của chính tụi mình dựa vào chỗ này: booking test luôn chọn offer rẻ nhất có huỷ miễn phí, để một lần test thành công có thể tháo ra mà không mất đồng nào. Cái bảo đảm đó chỉ chắc bằng đúng phép tính ngày giờ nằm sau nó.
Cái thứ tư là phép cộng. Cộng 24 tiếng không phải là cộng một ngày. Vắt qua mốc DST thì một vòng lặp "theo đêm" đẻ ra một đêm dài 23 hoặc 25 tiếng, và một kỳ lưu trú vắt qua đó ra sai số đêm — rồi tính sai giá, theo kiểu trông giống supplier báo giá lệch chứ không giống một cái bug ngày giờ.
Vì sao nó sống sót qua review
Vì phần lớn thời gian, với phần lớn mọi người, nó đúng. Người viết code, dữ liệu test, server staging và người review thường cùng nằm trong một múi giờ, mà trong cùng một múi giờ thì mọi bug kiểu này triệt tiêu nhau sạch sẽ. Nó chỉ hỏng với khách ở múi giờ khác, vào những ngày sát mốc, với những kỳ lưu trú vắt qua đổi giờ. Nhìn trong review thì nó chỉ là một cái ngày được truyền qua lại. Không có gì sai lộ ra để chỉ tay vào, nên câu hỏi này phải được hỏi chứ không thể trông vào việc nhìn ra.
Đã từng có bao nhiêu booking hiển thị sai ngày thì mình không biết. Lệch một ngày không ném exception, không hiện trên dashboard lỗi nào — nó tới, nếu có tới, dưới dạng một ticket support của người tình cờ để ý. Không có con số nào để đăng lên đây.
Cái đã thay đổi
Ba thứ, và không thứ nào là một thư viện.
Giá trị chỉ-có-ngày giữ nguyên chỉ-có-ngày từ đầu tới cuối: kiểu date dưới
storage, YYYY-MM-DD trên đường truyền, không có constructor nào âm thầm gắn
giờ vào nó. Giá trị nào đúng là instant thì mang theo múi giờ của nó, thay vì
bị chuẩn hoá lúc nhận rồi dựng lại sau bằng một phỏng đoán. Và convert chỉ xảy
ra ở đúng một tầng — tầng hiển thị — để mọi tầng còn lại giữ giá trị đúng theo
cái nghĩa supplier muốn nói.
Thứ tư là tài liệu, và đó mới là thứ thật sự chặn tái phát: ngay cạnh mỗi field ngày, một câu nói rõ nó thuộc loại nào trong hai loại.
Cái rút ra
Với mỗi field ngày, quyết định xem nó là ngày dân sự hay là instant, rồi ghi điều đó ngay cạnh field. Quyết định đó tốn một phút lúc thiết kế. Đi khôi phục nó về sau nghĩa là đọc hết mọi chỗ sinh ra và mọi chỗ tiêu thụ cái field đó rồi suy ra từng bên đã ngầm hiểu thế nào — và các bên đã không ngầm hiểu giống nhau, đó chính là lý do bạn đang ngồi đây.