Skip to content
GitLab
Projects Groups Topics Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Register
  • Sign in
  • A Ahad Mobile
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributor statistics
    • Graph
    • Compare revisions
  • Issues 0
    • Issues 0
    • List
    • Boards
    • Service Desk
    • Milestones
  • Merge requests 1
    • Merge requests 1
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Terraform modules
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • holatech
  • Ahad Mobile
  • Merge requests
  • !12

fix(home): show the fiat wallet total as the headline figure

  • Review changes

  • Download
  • Patches
  • Plain diff
Merged Khanh Pham requested to merge feature/deposit-realtime-refresh into develop Aug 21, 2026
  • Overview 0
  • Commits 9
  • Pipelines 2
  • Changes 32

Vấn đề

Khi user nạp tiền mặt vào Airwallex Global Account, webhook deposit.settled về backend và số dư được ghi vào sổ cái fiat — nhưng không có gì báo cho app. Người dùng phải rời màn hình Home rồi quay lại (navigation.addListener('focus')) thì số mới đổi. Nhánh này làm màn hình cập nhật ngay lúc tiền vào.

Cách làm

Backend đã có sẵn SocketEventsGateway (xác thực handshake bằng JWT, map userId → socketIds trong Redis) nhưng chưa từng có ai gọi. Nhánh này là consumer đầu tiên của nó ở cả hai phía.

Ba tầng:

  1. AirwallexDepositsService phát domain event sau khi DB transaction commit — từ cả luồng webhook (handleSettled) lẫn luồng gán tay deposit treo (assignUnmatched). postDepositToLedger trả về event thay vì tự phát, nên về mặt cấu trúc không có cách nào phát từ trong transaction mà không đổi chữ ký hàm.
  2. Hai listener best-effort tiêu thụ event: một đẩy khung Socket.IO deposit.settled tới các thiết bị của user, một ghi notification và bắn FCM push. Cả hai bọc try/catch riêng và chỉ log — tiền đã chuyển rồi, một khung socket rơi không được phép làm hỏng webhook và kích hoạt Airwallex gửi lại.
  3. App mở socket có xác thực khi đã đăng nhập, và khi nhận khung thì invalidate đúng các query mà màn hình Home đọc. Danh sách invalidate viết một chỗ, dùng chung với handler FCM onMessage.

Gateway học thêm cách đọc token từ handshake.auth (client React Native pin transports: ['websocket'] không đặt được request header một cách tin cậy), vẫn fallback về header Authorization để client cũ không gãy.

Chiều ngược lại cũng được xử lý: handleReversed phát AirwallexDepositEvent.REVERSED, nên khi tiền bị thu hồi màn hình cũng cập nhật ngay — trước đó tiền vào thì realtime còn tiền ra thì không.

Một lỗi Critical đã bắt và sửa trong nhánh này

Bản đầu của thay đổi gateway viết (client.handshake.auth?.token as string | undefined)?.replace(...). handshake.auth là data của gói CONNECT, socket.io lưu không kiểm tra gì. Cast as string bị xoá lúc runtime và ?. chỉ chắn null/undefined, nên io(url, { auth: { token: 12345 } }) ném TypeError trước khối try, trong một async method mà Nest gọi không await — unhandled rejection, Node 24 mặc định --unhandled-rejections=throw, process chết. Gateway nghe cùng cổng với REST API và cors: true, tức là DoS bằng đúng một gói tin, không cần đăng nhập.

Đã sửa bằng cách narrow kiểu trước khi dùng (38402d8), kèm test cho token kiểu số và kiểu object. Bốn test cũ đều truyền string — đó là lý do lỗ hổng lọt qua mọi cổng kiểm tra.

Kiểm chứng

Chạy thật trên stack local, không chạm vào màn hình lần nào:

Bước Fiat trên Home Sổ cái
Trước 200,300.01 USD —
Sau deposit.settled 42 USD 200,342.01 USD DEPOSIT debit 42 nhà / credit 42 user
Sau deposit.reversed 200,300.01 USD DEPOSIT_REVERSAL debit 42 user / credit 42 nhà

Log Metro có 🔌 Socket connected: TnuScZ1JqB_z_pl0AAAB, xác nhận handshake và xác thực qua handshake.auth.token chạy đúng. Socket còn tự reconnect sau khi rebuild container API giữa chừng.

Test: BE 182 test / 18 suite + yarn build; FE pnpm test src/lib/socket 6/6, type-check, lint sạch.

⚠️ Cần làm khi deploy

Nhánh này là consumer đầu tiên của gateway, nên /socket.io chưa từng được chứng minh là sống sót qua proxy đứng trước api.steellabs.tech. WebSocket khởi đầu bằng một HTTP GET mang Connection: Upgrade / Upgrade: websocket — đây là hop-by-hop header, nginx mặc định không forward. Thiếu cấu hình dưới đây thì request bị xử lý như HTTP thường, không bao giờ có 101 Switching Protocols, và vì client pin transports: ['websocket'] nên không có đường lùi: feature im lặng không chạy trong khi mọi test vẫn xanh.

location /socket.io/ {
    proxy_pass http://api;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 3600s;
}

Local không dính vì Metro nói thẳng với Nest, không có proxy ở giữa.

Ghi chú về lịch sử commit

  • c342512 là việc trước đó (hiện số dư fiat thành dòng riêng trên Home) — đã review xong, được commit trước để diff của feature này sạch.
  • 7055a16 do một session khác tạo trên cùng nhánh, không liên quan tới feature này.

Những khoản nợ kỹ thuật cố ý để lại

  • moanapay:sockets:clients:<userId> là set Redis không có TTL; handleDisconnect chỉ xoá khi ngắt sạch, nên mỗi lần server restart để lại thành viên mồ côi vĩnh viễn. Vô hại từng khung tin (emit tới socket chết là no-op) nhưng key phình dần. Có từ trước, nhánh này làm nó thành load-bearing.
  • Gateway không kiểm tra lại token sau handshake, nên một phiên hết hạn vẫn giữ kênh đẩy tới khi socket rớt. Đổi lại, refresh token không làm rớt socket giữa chừng.
  • Mọi FCM push giờ đều invalidate query balance, không chỉ push deposit.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MKC4HFXNN7AzPhk6x4c6fY

Assignee
Assign to
Reviewers
Request review from
Time tracking
Source branch: feature/deposit-realtime-refresh