Nửa mà ai cũng biết rồi
Giá không phải là float. Lưu số tiền dưới dạng số nguyên theo đơn vị nhỏ nhất của loại tiền đó, để mã tiền tệ nằm ngay cạnh, và đừng bao giờ để một phân số nhị phân bén mảng tới con số mà khách sẽ đọc. Nói ra thì chẳng ai cãi. Gọi đó là nửa dễ: một quyết định về schema, một cái helper, vài tuần đi comment review. Trang này không phản đối gì chỗ đó, và cũng không nói về chỗ đó. Nửa đắt tiền là: làm tròn không phải một bước. Nó là một tập các chỗ, và câu hỏi quyết định giá của bạn có đúng hay không là một số tiền đi qua bao nhiêu chỗ trong đó trước khi lên tới màn hình.
Markup theo từng phòng, hay markup trên tổng
Một ngã ba rất cụ thể, lấy từ offer khách sạn. Bên mình có hai pattern supplier đã viết thành tài liệu: một là supplier trả về đúng một token đặt N phòng và báo một giá cho cả cụm. Hai là giá mỗi phòng độc lập với nhau, và cái offer mình hiển thị là một tổ hợp được dựng ra — về mặt cấu trúc, giá của nó là tổng của các mức giá phòng độc lập.
Markup của một tenant có hai chỗ đặt đều hợp lý. Áp vào giá từng phòng, làm tròn từng kết quả, rồi cộng. Hoặc cộng giá gốc trước, áp markup, làm tròn một lần. Cả hai đều hợp lý, cả hai không ra cùng một con số, và không cái nào là một con bug bạn chỉ tay vào được.
Trên một booking, chênh lệch là một đơn vị làm tròn, đôi khi vài đơn vị — nhìn trong review thì vô hình. Cộng qua một tháng thì nó không có trần: nó lớn theo sản lượng và không tự triệt tiêu, vì làm tròn half-up không đối xứng quanh một phép cộng. Bản nhìn thấy được còn tệ hơn bản kế toán. Khách đọc bảng chi tiết theo phòng, cộng các dòng lại, rồi ra một số khác với cái tổng nằm ngay dưới — đó không còn là cuộc trao đổi về làm tròn với bộ phận tài chính nữa, mà là cuộc trao đổi về niềm tin với support.
Luật làm tròn: chọn một lần, và viết ra
Cái mình ngại không phải là chọn sai luật. Là không chọn gì cả. Chẳng ai viết
ra "half-up, tới đơn vị nhỏ nhất của loại tiền" — nó cứ thế xảy ra, thừa hưởng
từ cái formatter nào chạy sau cùng: một toFixed trong component UI, một
helper trong template, một cột có scale cố định, hoặc một response của supplier
vốn đã được làm tròn theo quy ước của họ trước khi mình kịp nhìn thấy. Mỗi chỗ
đó là một quyết định về tiền, do một người lúc ấy không hề nghĩ về tiền đưa ra.
Và ngay khi danh sách tenant có thêm một loại tiền không có phần lẻ, hoặc có ba
chữ số lẻ, thì cái mặc định thừa hưởng kia sai ở đâu đó, và sai một cách im
lặng.
Nên luật được phát biểu đúng một lần — mode và độ chính xác, theo từng loại tiền — trong một hàm, và số chỗ được phép gọi hàm đó ít tới mức liệt kê ra được.
Cái hiển thị và cái thu tiền
Kỷ luật thứ hai: làm tròn để hiển thị là một phép chiếu, và phép chiếu thì không bao giờ chảy ngược. Con số khách bị thu là con số nguyên đang lưu. Dáng bug cần canh là một giá trị đã làm tròn để hiển thị bị đọc ngược lại làm đầu vào cho một phép tính: ngay lúc đó, tầng giao diện đã trở thành một bên tham gia vào việc tính giá, mà phía dưới không ai biết.
Quy đổi là chỗ thứ ba, và nó có timestamp
Supplier báo giá bằng tiền của họ; tenant bán bằng một loại tiền khác. Quy đổi có làm tròn, nên nó là chỗ thứ ba. Quy đổi còn dùng một tỷ giá, mà tỷ giá thì gắn với một thời điểm. Lưu số tiền đã quy đổi mà không lưu kèm tỷ giá và thời điểm sinh ra nó, thì tính lại booking đó sau này là ra giá khác — cùng một booking trả lời khác nhau ở hai ngày, và không có cách nào phân biệt bug với thị trường. Nên số tiền đã quy đổi được lưu như một dữ kiện, kèm tỷ giá và timestamp, chứ không phải một thứ dẫn xuất chờ được tính lại. Khách trả bao nhiêu và nợ supplier bao nhiêu là hai dữ kiện ở hai loại tiền; bán xong rồi thì không bên nào tính ra bên nào nữa.
Con số mình không có
Trang này không có con số đối soát nào. Mình chưa từng chạy bản diff cả tháng giữa markup theo dòng và markup trên tổng, nên mình không nói được chênh lệch là bao nhiêu, theo chiều nào. Lập luận ở đây là về kỷ luật, không phải về một khoản tiền thu hồi được. Ai bảo họ sửa xong chuyện làm tròn rồi đưa bạn một con số, thì họ đã đo. Mình thì chưa.
Luật
Tiền cần một cách biểu diễn, một luật làm tròn, và một chỗ duy nhất áp luật đó. Trên site này có một bài khác nói về ba service không thống nhất với nhau về một cái giá. Bài này nhỏ hơn và ngượng hơn: một service không thống nhất với chính nó, vì cùng một số tiền đi hai đường khác nhau qua khâu làm tròn, mà cả hai đường đều do những người viết đúng viết ra.