MannaHarvest Order Profit Tracker — đồng bộ Shopify để tính lợi nhuận theo đơn
Xây dựng cho MannaHarvest.com, một cửa hàng thương mại điện tử cần biết lợi nhuận thật theo từng đơn hàng, chứ không chỉ doanh thu. Đơn hàng tự động đồng bộ từ Shopify, nhân viên nhập chi phí sản phẩm và vận chuyển qua một web app nhỏ, và lợi nhuận theo đơn được tính ra từ một database view — không cần spreadsheet, không tính tay.
Kiến trúc
Ba service chạy trong Docker Compose trên một VPS duy nhất:
- Caddy — reverse proxy, tự động HTTPS.
- Fastify (Node.js) — API và một frontend vanilla-JS nhỏ, không cần build step.
- PostgreSQL — bảng orders, line items, và một view
order_financialstự động cascade chi phí/doanh thu từ line item → sản phẩm → đơn hàng → lợi nhuận.
Giữ đơn hàng luôn đồng bộ
Webhook của Shopify (orders/create, orders/updated,
orders/cancelled) đẩy thay đổi về theo thời gian thực, mỗi webhook được
xác thực bằng chữ ký HMAC trước khi xử lý. Webhook đôi khi bị rớt — lỗi
gửi, downtime — nên một job đối soát định kỳ quét lại các đơn gần đây mỗi
4 giờ như lớp dự phòng. Kết hợp cả hai, toàn bộ lịch sử đơn hàng luôn
được đồng bộ: webhook lo phần thời gian thực, đối soát bắt lại những gì
lọt lưới.
Đồng bộ phí: không có webhook, nên phải retry ba lớp
Shopify không phát webhook cho phí xử lý thanh toán — dữ liệu này chỉ xuất hiện sau khi giao dịch được settle bên phía Shopify, không cùng lúc với lúc đơn hàng được tạo. Một lần tra cứu là không đủ, nên việc đồng bộ phí được thiết kế ba lớp:
- Tra cứu ngay lập tức, cố gắng tốt nhất, chạy ngay khi
orders/createbắn ra. - Nếu giao dịch chưa settle, đơn hàng được đánh dấu và một job retry có mục tiêu sẽ kiểm tra lại mỗi giờ, tối đa 5 lần.
- Một lượt quét không giới hạn chạy mỗi 6 giờ như lớp dự phòng cuối cùng, không bao giờ bỏ cuộc với một đơn hàng.
Cả ba job đều chạy một lần ngay khi process khởi động, không chỉ theo
interval — vì setInterval bị reset mỗi lần deploy, nếu không có bước
này thì việc deploy liên tục trong giai đoạn phát triển có thể khiến đơn
hàng thiếu phí lâu hơn dự tính. (Một lỗi thật, đã phát hiện và sửa.)
Nhập chi phí với carry-forward
Shopify không lưu chi phí sản phẩm thật cho cửa hàng này, nên chi phí được nhập tay qua màn hình Order Detail. Để tránh phải gõ lại cùng một chi phí ở mọi đơn hàng, một khi chi phí của một SKU đã được xác nhận, nó sẽ tự điền (carry-forward) sang đơn tiếp theo có cùng SKU — nhưng vẫn được đánh dấu chờ xác nhận lại, vì chi phí có thể thay đổi, không nên tin tưởng ngầm định.
Bảo mật
- Mật khẩu hash bằng bcrypt; session dùng cookie
httpOnly+Secure+SameSite=Strictchứa JWT đã ký, JavaScript phía trang không đọc được. - Mọi route
/api/*đều yêu cầu xác thực — không có đăng ký công khai. - Login bị giới hạn tốc độ (rate-limited).
- Fastify bật
trustProxy, vì Caddy đứng phía trước — nếu không, mọi request đều trông như đến từ IP nội bộ của Caddy, âm thầm phá vỡ rate limiting theo từng khách truy cập.
Vì sao chọn stack này
- Fastify thay vì framework nặng hơn: bề mặt API nhỏ (đồng bộ, xác thực, vài endpoint báo cáo) — không cần nhiều cấu trúc hơn thế.
- Dùng thẳng Postgres view để tính lợi nhuận: cascade chi phí/doanh thu thuần là bài toán mô hình hoá dữ liệu; một SQL view giữ số liệu luôn đúng và tránh lặp lại phép tính trong code ứng dụng.
- Docker Compose trên một VPS: ba service, lưu lượng vừa phải —
không cần gì phức tạp hơn
docker compose up.