Chuyển tới nội dung chính

Points wallet

Bối cảnh​

Trên Shukah, app đọc số dư điểm bằng GET /maeda/client/points, bảng tỷ lệ tích điểm bằng GET /maeda/point-rate, và đổi một gift-card code bằng GET /maeda/giftcard-code/use. Locate có một route cho mỗi cái, và đợt kiểm tra tương đương xếp cả ba là drop-in: không cái nào cần app đổi gì ngoài base URL và path. TL;DR của từng cái vẫn có các dòng cần lưu ý, vì method, token hoặc dữ liệu vẫn có thể khác:

  • Balance (drop-in). GET /api/v2/client/points trên order-v2 chuyển lời gọi sang point service. Nó trả về cùng bốn key như Shukah. Cần lưu ý: points có thể có bốn chữ số thập phân, trên dev số dư và ngày hết hạn không giống của Shukah (2026-10-01 và 2026-10-02), và trên dev hiện nay nó chỉ chấp nhận token Shukah, không chấp nhận JWT Locate.
  • Tỷ lệ tích điểm và gift-card code (drop-in). order-v2 tự phục vụ hai cái này. Nó không chuyển chúng sang point service. Cần lưu ý: bảng tỷ lệ tích điểm trên dev không phải của Shukah, và gift-card code giờ là một POST với code nằm trong body, một lời gọi app đã có sẵn. App chỉ gọi cả hai ở chế độ Merchant.

Tính năng​

Wallet là màn hình nơi client xem số dư điểm, các giao dịch điểm gần nhất, một ô để nhập gift-card code, và FAQ về điểm. Trong snapshot app ngày 2026-02-26 các lời gọi xảy ra ở đây:

Lời gọiỞ đâu trong app
Balance (#points)Khi app tải thông tin của client (lúc khởi động và sau khi đăng nhập), và khi checkout nhà hàng mở phần chọn phương thức thanh toán (để xem client có thể trả bằng điểm không). Màn hình Wallet hiển thị số điểm, giá trị của chúng theo SAR và ngày hết hạn.
Tỷ lệ tích điểm (#point-rates)Trong thẻ "Earn Points" của giỏ hàng nhà hàng và giỏ hàng merchant, hiển thị tổng giỏ hàng tích được bao nhiêu.
Gift-card code (#gift-card-code)Khi client nhập một code vào ô trên màn hình Wallet. Khi thành công app hiển thị một thông báo với code và số điểm, rồi tải lại số dư và lịch sử. Khi có lỗi nó hiển thị thông báo với text lỗi.

Balance và gift-card code cần token của chính client. Bảng tỷ lệ tích điểm là public. Với balance, hiện nay trên dev chỉ có token Shukah chạy: một JWT Locate nhận 401 vào 2026-10-02, dù cả hai token đều chạy vào 2026-10-01 (xem "Token nào chạy được" ở mục Chi tiết). Gift-card code dùng cùng cách kiểm tra token (qua /api/v1/users), nên nó cũng cần token Shukah (đọc code; chưa gọi với token, vì nó cộng điểm).

Chế độ Merchant. App chỉ gọi các route Locate cho tỷ lệ tích điểm và gift-card code khi client settings ghi enableMerchantService: true. Nếu không app vẫn gọi Shukah cho cả hai. App đọc setting đó từ Locate client-settings service khi nó khởi động. Lời gọi balance không có biến thể Locate trong app hiện nay: nó luôn gọi Shukah, ở cả hai chế độ, nên app phải được sửa để gọi GET /api/v2/client/points.

Hai số dư. Số dư mà route này trả về đến từ point service. Khi client trả một order bằng điểm, order-v2 vẫn kiểm tra số dư trên Shukah (GET /maeda/client/points với token của client), không phải cái này. Trong giai đoạn chạy song song hai số có thể khác nhau, nên màn hình Wallet và checkout có thể hiển thị các con số khác nhau. Có chủ đích hay không: Chưa được tài liệu hóa.

Việc tiêu điểm không được đồng bộ. Hai ledger đã không theo nhau khi điểm được tiêu (trong vài giây của lần chạy; việc nó có tới sau đó hay không thì chưa được kiểm tra). Trên dev ngày 2026-10-02, một order quà tặng trả bằng điểm trên Shukah làm số dư Shukah giảm 110 và để nguyên số dư Locate; cùng order đó trên Locate, vài giây sau, chỉ làm giảm số dư Locate (xem Point orders). Nếu app tạo point order ở một bên, nó phải đọc số dư và lịch sử từ chính bên đó, nếu không màn hình Wallet hiển thị sai số.

GET /api/v2/client/points​

Thay thế ShukahGET /maeda/client/points✅ Đã thay thế
Thay thẳng0 breaking · 4 cần lưu ý
  • 🟡 JWT của Locate nhận 401 "Invalid or expired token" trên dev; hãy gửi token của Shukah.
  • 🟡 Số dư "514405.6788" so với "514853.64" của Shukah (Locate thấp hơn 447.96); hết hạn 2026-11-28 so với 2026-11-30.
  • 🟡 points có tới 4 chữ số thập phân; app dùng Number(points) || 0 nên parse được; màn hình phải làm tròn khi hiển thị.
  • 🟡 Body 401 là {success,error:{code,message}} không có message ở cấp cao nhất; app sẽ hiển thị chuỗi JSON thô.

Request​

FieldVị tríKiểuBắt buộcRàng buộcMô tả
AuthorizationheaderstringcóBearer <token>. Một token Shukah chạy được trên dev. Một JWT Locate nhận 401 Invalid or expired token (capture trên dev, 2026-10-02; nó chạy vào 2026-10-01): xem "Token nào chạy được" ở mục Chi tiết.Token của chính client. Client id lấy từ nó, nên app không bao giờ gửi một id.
Bất kỳ query key nàoquery—khôngKhông được đọc. order-v2 không forward query trên route này, nên language mà app thêm vào bị bỏ.—

Response​

200 với một object trần (không có wrapper status hay data):

FieldKiểuNullableĐịnh dạng / ràng buộcMô tả
idstringkhôngClient id dạng text, ví dụ "356259".Client id.
user_idstringkhôngCùng giá trị với id.Client id.
pointsstringkhôngMột số thập phân dạng text. Các số 0 ở cuối và dấu chấm ở cuối bị bỏ, và tối đa bốn chữ số thập phân được giữ: "514405.6788". "0" khi client không có tài khoản điểm. Có thể âm: inbound sync từ Shukah không kiểm tra overdraft.Số dư hiện tại.
pointsExpirationDatestringcóYYYY-MM-DD, một ngày theo Asia/Riyadh. null trừ khi quy tắc hết hạn bên dưới áp dụng.Ngày số dư hết hạn do không hoạt động.

Các key đến theo thứ tự: id, user_id, points, pointsExpirationDate.

{
"id": "356259",
"user_id": "356259",
"points": "514405.6788",
"pointsExpirationDate": "2026-11-28"
}

Capture trên dev, 2026-10-01 (client 356259), với một token Shukah. Cùng lời gọi với một JWT Locate trả về cùng body vào ngày đó. Vào 2026-10-02 token Shukah nhận cùng các giá trị, còn JWT Locate nhận 401.

Định dạng points. Locate giữ điểm với bốn chữ số thập phân và không làm tròn chúng. Formatter chỉ bỏ các số 0 ở cuối, nên "511712.9600" thành "511712.96" và "0.0000" thành "0". Trên dev cùng client và cùng phút cho "514405.6788" trên Locate và "128712.41" trên Shukah vào 2026-10-01, và "514405.6788" và "514853.64" vào 2026-10-02. App đọc giá trị bằng Number(points) || 0, nên cả hai định dạng đều parse được.

Quy tắc pointsExpirationDate. Ngày là null khi point settings nói hết hạn là never, khi settings chưa được đặt, hoặc khi client không có dòng earn được tính nào. Nếu không thì nó là ngày của dòng earn được tính mới nhất của client cộng với số ngày không hoạt động đã cấu hình, theo Asia/Riyadh. Locate chỉ thực thi ngày đó khi expiry job của nó được bật (xem Chi tiết). Dev trả về 2026-11-28.

Lỗi​

HTTPBodyKhi nào
401{"success":false,"error":{"code":"UNAUTHORIZED","message":"Missing or invalid authorization header"}}Không có header Authorization, hoặc nó không bắt đầu bằng Bearer . Đã capture trên dev, 2026-10-01, khi không có token.
401{"success":false,"error":{"code":"UNAUTHORIZED","message":"Invalid or expired token"}}, hoặc cùng shape với message là auth service unreachable: <error text> hoặc auth response missing numeric client id.Auth service từ chối token, không kết nối được (sự cố auth trông giống token xấu), hoặc trả lời mà không có client id. Body đầu tiên đã được capture trên dev, 2026-10-02, với một JWT Locate. Hai body còn lại chưa được capture.
500{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Internal server error"}}Bất kỳ lỗi không mong đợi nào trong point service, ví dụ lỗi database. Message là AUTH_SERVICE_URL not set khi service bị cấu hình sai. Chưa capture.
502{"success":false,"error":{"code":"POINT_SERVICE_UNAVAILABLE","message":"Point service unavailable: <error text>"}}order-v2 không kết nối được point service. Body này đến từ order-v2, không phải từ point service. Chưa capture.
504{"success":false,"error":{"code":"POINT_SERVICE_TIMEOUT","message":"Point service did not respond in time; the request outcome is unknown"}}Point service không trả lời trong 10 giây. Body này đến từ order-v2. Chưa capture.

Mọi lỗi từ point service đều có envelope {"success":false,"error":{"code":"…","message":"…"}} (một key details được thêm cho lỗi validation). Body thành công không có envelope.

Thử ngay​

export ORDER_BASE_URL=https://dev.v2.api.order.locate.sa
export SHUKAH_TOKEN='<shukah-token>' # a Locate JWT gets 401 here today; see "Which tokens work" in Details
curl -s "$ORDER_BASE_URL/api/v2/client/points" -H "Authorization: Bearer $SHUKAH_TOKEN"

Kỳ vọng: 200 và object như ở mục Response. Hãy dùng token Shukah trên dev hiện nay: một JWT Locate nhận 401 Invalid or expired token như ở mục Lỗi (2026-10-02). Không có token bạn nhận 401 Missing or invalid authorization header.

Khác biệt so với Shukah​

Khía cạnhShukahLocateTác động lên app
URLGET $SHUKAH_BASE_URL/maeda/client/pointsGET $ORDER_BASE_URL/api/v2/client/pointsĐổi base URL và path.
TokenToken opaque của Shukah.Chỉ token Shukah trên dev hiện nay. Một JWT Locate nhận 401 (capture trên dev, 2026-10-02); vào 2026-10-01 cả hai đều nhận 200.App phải tiếp tục gửi token Shukah ở đây, ngay cả khi nó đã giữ một JWT Locate cho auth service. Với các môi trường khác: Chưa được tài liệu hóa.
Định dạng pointsHai chữ số thập phân: "514853.64" (capture trên dev, 2026-10-02).Tối đa bốn chữ số thập phân: "514405.6788" (cùng client, cùng ngày).App parse bằng Number(points) || 0, nên nó chạy được. Mọi màn hình giả định hai chữ số thập phân phải làm tròn.
Số dư của client 356259"514853.64" vào 2026-10-02 ("128712.41" vào 2026-10-01: dữ liệu Shukah dev đã thay đổi trong khoảng đó)."514405.6788" ở cả hai ngày: thấp hơn Shukah 447.96 điểm, khoảng 0.09%, vào 2026-10-02.Số dư trên màn hình Wallet sẽ giảm một chút khi chuyển. Dòng ledger mới nhất của Locate cũ hơn của Shukah khoảng hai ngày, khớp với khoảng chênh (suy luận). Hãy kiểm tra feed từ Shukah có cập nhật trước khi chuyển.
pointsExpirationDate"2026-11-30"."2026-11-28". Mỗi ngày là 60 ngày sau dòng ledger mới nhất ở phía đó, và dòng mới nhất của Locate cũ hơn hai ngày (suy luận).Ngày mà app hiển thị có thể lệch vài ngày. Locate có chạy expiry trên dev hay không: Chưa được tài liệu hóa.
Body lỗi{"name":"Unauthorized","message":"Your request was made with invalid credentials.","code":0,"status":401,"type":"Illuminate\\Auth\\AuthenticationException"} (capture trên dev, 2026-10-01, không token).Envelope của point-service ở mục Lỗi.Mọi chỗ so khớp theo text lỗi, name hay type đều phải đổi.
Content-Typeapplication/jsonapplication/json; charset=utf-8Không có.

GET /api/v2/client/system_configs/point_rates​

Thay thế ShukahGET /maeda/point-rate✅ Đã thay thế🚩 enableMerchantService
Thay thẳng0 breaking · 4 cần lưu ý
  • 🟡 Các bậc khác nhau: Locate 70-140:2, 140-251:4, 251-451:10, 451+:16; Shukah 0-139:2 ... 451+:30 (5 dòng).
  • 🟡 amount_to của Locate bằng amount_from kế tiếp (140, 251, 451); Shukah kết thúc ở 139, 153, 250, 450.
  • 🟡 Dòng đầu của Locate bắt đầu từ 70, của Shukah từ 0; dưới 70, dòng đầu do app thêm cho 0 điểm.
  • 🟡 Công khai ở cả hai phía (200, không cần token); app chỉ gọi route Locate khi enableMerchantService là true.

Công tắc phía app: app chỉ gọi route Locate này ở chế độ Merchant (client settings enableMerchantService: true); nếu không, app vẫn gọi Shukah.

order-v2 tự phục vụ route này và không chuyển nó sang point service. Nên các quy tắc proxy và các lỗi 502 và 504 của các trang điểm khác không áp dụng cho nó. App chỉ gọi nó ở chế độ Merchant.

Request​

FieldVị tríKiểuBắt buộcRàng buộcMô tả
AuthorizationheaderstringkhôngKhông cần: dev trả 200 khi không có token và khi có token. Token được gửi không được đọc.—
Bất kỳ query key nàoquery—khôngĐược chấp nhận và bỏ qua.—

Response​

200 với một mảng trần, sắp theo amount_from từ thấp đến cao (một null tính là 0). Nó là [] khi tùy chọn rate chưa được đặt:

FieldKiểuNullableĐịnh dạng / ràng buộcMô tả
amount_fromnumbercóTổng order mà dòng bắt đầu.Đầu của khoảng, theo SAR.
amount_tonumbercónull ở dòng cuối. Trên dev mỗi dòng kết thúc ở giá trị mà dòng kế tiếp bắt đầu (140, 251, 451).Cuối của khoảng, theo SAR.
point_earnnumberkhôngMột số nguyên trên dev.Điểm tích được khi tổng order rơi vào khoảng.
[
{"amount_from": 70, "amount_to": 140, "point_earn": 2},
{"amount_from": 140, "amount_to": 251, "point_earn": 4},
{"amount_from": 251, "amount_to": 451, "point_earn": 10},
{"amount_from": 451, "amount_to": null, "point_earn": 16}
]

Capture trên dev, 2026-10-01, không có token (và lần nữa với một token Shukah).

App khớp một tổng order với một dòng như thế nào, và nó có coi amount_to là bao gồm hay không: Chưa được tài liệu hóa, vì code của app chưa được đọc cho việc này.

Lỗi​

HTTPBodyKhi nào
500Một body lỗi của Nest. Các key chính xác của nó chưa được capture.Không đọc được tùy chọn rate, hoặc giá trị của nó không phải một danh sách. Chưa capture.

Không có 401: route không có guard. Các query key và header lạ bị bỏ qua.

Thử ngay​

export ORDER_BASE_URL=https://dev.v2.api.order.locate.sa
curl -s "$ORDER_BASE_URL/api/v2/client/system_configs/point_rates"

Kỳ vọng: 200 và mảng như ở mục Response. Không cần token.

Khác biệt so với Shukah​

Khía cạnhShukahLocateTác động lên app
URLGET $SHUKAH_BASE_URL/maeda/point-rateGET $ORDER_BASE_URL/api/v2/client/system_configs/point_ratesĐổi base URL và path. App đã có lời gọi này cho chế độ Merchant.
TokenKhông cần (200 khi không có token trên dev, 2026-10-01).Không cần (200 khi không có token trên dev, 2026-10-01).Không có.
Các dòng trên devNăm dòng: 0–139: 2, 140–153: 12, 154–250: 16, 251–450: 20, 451 and up: 30 (capture trên dev, 2026-10-01).Bốn dòng: 70–140: 2, 140–251: 4, 251–451: 10, 451 and up: 16.Thẻ "Earn Points" sẽ hiển thị các bậc khác sau khi chuyển. Bảng nào là đúng: Chưa được tài liệu hóa.
Cuối khoảngCác khoảng không chồng lên nhau: 139 rồi 140.Các khoảng dùng chung giá trị cuối: 140 kết thúc một dòng và bắt đầu dòng kế tiếp.Nếu app coi amount_to là bao gồm, một tổng chính xác 140 hoặc 251 khớp hai dòng.
Dòng đầuBắt đầu từ 0.Bắt đầu từ 70. App thêm một dòng cho các tổng dưới dòng đầu, với point_earn 0.Một tổng dưới 70 khớp dòng được thêm đó, nên thẻ hiển thị 0 điểm trên Locate.
Câu trả lời rỗngChưa được tài liệu hóa.[] khi tùy chọn chưa được đặt.Code của app đọc dòng đầu mà không kiểm tra, nên một mảng rỗng làm lời gọi lỗi với lỗi 422 của chính app.
Quy tắc creditKhông áp dụng.order-v2 vẫn quyết định số điểm một order tích được từ bảng của Shukah, không phải bảng này.Thẻ có thể hiển thị một tỷ lệ khác với số điểm order thực sự tích được.

POST /api/v2/client/gift-card-code​

Thay thế ShukahGET /maeda/giftcard-code/use✅ Đã thay thế🚩 enableMerchantService
Thay thẳng0 breaking · 5 cần lưu ý
  • 🟡 GET ?code= thành POST {code}; app đã có cả hai lệnh gọi và chọn theo chế độ (points.service.ts:128).
  • 🟡 Kiểm tra token giống các route points (user-profile-auth.guard.ts:60-62), nên app chỉ có JWT sẽ nhận 401.
  • 🟡 Chỉ các code do order-v2 giữ mới hợp lệ; code do Shukah phát hành có thể nhận 404 "The code not exist".
  • 🟡 Điểm được cộng qua add-point của Shukah trừ khi usePointService bật, nên số dư Locate thay đổi muộn hơn.
  • 🟡 Lỗi có dạng Nest {message,statusCode}; app hiển thị message nên nội dung phải phù hợp với người dùng.

Công tắc phía app: app chỉ gọi route Locate này ở chế độ Merchant (client settings enableMerchantService: true); nếu không, app vẫn gọi Shukah.

Locate nhận POST với {code} thay vì GET ?code= và kiểm tra code với các gift-card code do chính Locate giữ (chưa rõ code do Shukah phát hành có tồn tại ở đó không); app đã gọi route này ở chế độ Merchant. 0 lượt gọi trong bảng traffic production ngày 2026-10-02 (3 ngày).

order-v2 tự phục vụ route này và không chuyển nó sang point service. Nên các quy tắc proxy và các lỗi 502 và 504 của các trang điểm khác không áp dụng cho nó. App chỉ gọi nó ở chế độ Merchant.

Request​

FieldVị tríKiểuBắt buộcRàng buộcMô tả
AuthorizationheaderstringcóBearer <token>. Dev trả 401 khi không có token. Nó dùng cùng cách kiểm tra token như các route điểm (qua /api/v1/users, với x-auth-from: SHUKAH), nên trên dev hiện nay token Shukah chạy còn JWT Locate nhận 401 (đọc code; chưa gọi với token).Token của chính client. Client id lấy từ nó.
Content-Typeheaderstringcóapplication/json.Body là JSON.
codebodystringcóMột chuỗi không rỗng. Một code thiếu, rỗng hoặc không phải string bị bước validation từ chối với 400.Gift-card code.

Response​

201 được kỳ vọng khi thành công, nhưng chưa được capture: lời gọi tiêu một code và cộng điểm, nên nó chưa được chạy. Handler không set status nào khác, và mặc định của framework cho một POST là 201. Body có sáu key:

FieldKiểuNullableĐịnh dạng / ràng buộcMô tả
total_pointsnumberkhôngSố tiền của code nhân với tỷ giá. Tỷ giá là setting exchanged_points (point_rate chia cho riyad_rate), hoặc 4 khi setting đó thiếu hay không đọc được.Điểm được cộng.
date_createdstringkhôngyyyy-mm-dd hh:mm:ss theo giờ Ả Rập (UTC+3).Khi code được đổi.
descriptionstringkhôngPoints received for the <code> gift card.Text cho lịch sử.
messagestringkhôngText tiếng Ả Rập cố định تم شحن الرصيد بنجاح.Thông báo thành công.
points_typestringkhôngLuôn là earn.Loại dòng.
trans_typestringkhôngLuôn là adjustment.Loại giao dịch.
{
"total_points": 40,
"date_created": "2026-10-01 12:00:00",
"description": "Points received for the ABC12345 gift card",
"message": "تم شحن الرصيد بنجاح",
"points_type": "earn",
"trans_type": "adjustment"
}

Minh họa, không capture: dựng từ response type trong locate-order-v2-api@d9e4e5f6. Một lời gọi live sẽ dùng hết một gift-card code và cộng điểm. Các giá trị là bịa.

Lỗi​

HTTPBodyKhi nào
401{"message":"Unauthorized","statusCode":401}Không có token dùng được. Đã capture trên dev, 2026-10-01, khi không có token. Đây là shape của Nest, không phải envelope của point-service.
400Một lỗi validation của Nest. Chưa capture.code thiếu, rỗng hoặc không phải string.
404Message The code not exist, theo shape lỗi của Nest. Chưa capture.Không có code nào với giá trị đó, hoặc code vẫn là draft.
400Message This code has already been used, theo shape lỗi của Nest. Chưa capture.Code đã có client hoặc được đánh dấu đã dùng.
400Message The code has expired, theo shape lỗi của Nest. Chưa capture.Thời điểm hiện tại đã qua 24:00 giờ Ả Rập (21:00 UTC) của ngày expired_at của code.
500Shape lỗi của Nest. Chưa capture.Việc cộng điểm thất bại ba lần liên tiếp, cách nhau một giây.

Thử ngay​

Lời gọi này tiêu một gift-card code và cộng điểm. Hãy dùng một code được tạo để test.

export ORDER_BASE_URL=https://dev.v2.api.order.locate.sa
export SHUKAH_TOKEN='<shukah-token>' # a Locate JWT gets 401 here today; see "Which tokens work" in Details
curl -s -X POST "$ORDER_BASE_URL/api/v2/client/gift-card-code" -H "Authorization: Bearer $SHUKAH_TOKEN" -H "Content-Type: application/json" -d '{"code":"<test-code>"}'

Kỳ vọng: body sáu key như ở mục Response. Hãy dùng token Shukah trên dev hiện nay: một JWT Locate bị từ chối bởi cùng bước kiểm tra như các route điểm (đọc code). Không có token bạn nhận 401 như ở mục Lỗi.

Khác biệt so với Shukah​

Khía cạnhShukahLocateTác động lên app
Method và codeGET $SHUKAH_BASE_URL/maeda/giftcard-code/use?code=<code>.POST $ORDER_BASE_URL/api/v2/client/gift-card-code với {"code":"<code>"} trong body.Đổi method, URL và nơi đặt code. App đã có cả hai lời gọi và chọn một theo chế độ app.
TokenToken opaque của Shukah.Cùng cách kiểm tra token như các route điểm (qua /api/v1/users), nên là token Shukah trên dev hiện nay; một JWT Locate nhận 401 (đọc code; chưa gọi với token).App đã gửi token Shukah của nó ở chế độ Merchant. Hãy giữ nó cho đến khi bước kiểm tra token chấp nhận một JWT Locate.
Kho codeCác gift-card code của chính Shukah.Các code trong merchant database của order-v2. Các code do Shukah phát hành có ở đó không: Chưa được tài liệu hóa.Một code chạy được trên Shukah có thể bị Locate từ chối.
ResponseChưa được tài liệu hóa: không có capture Shukah nào, vì lời gọi cộng điểm.Sáu key: total_points, date_created, description, message, points_type, trans_type.App parse cùng sáu key cho cả hai chế độ. Hãy so sánh các giá trị trước khi chuyển.
Khoản credit đáp xuống đâuKhông áp dụng.Shukah hoặc point service, tùy theo một server flag. Nó gửi credit sang Shukah khi flag tắt (mặc định của nó). Giá trị trên dev: Chưa được tài liệu hóa.Với flag tắt, credit tới Shukah trước, nên số dư Locate chỉ thay đổi sau khi sync.
Body lỗiChưa được tài liệu hóa.401 là {"message":"Unauthorized","statusCode":401} của Nest.Mọi chỗ so khớp theo text lỗi đều phải đổi.

Lưu ý​

  • Số dư điểm cần token Shukah trên dev hiện nay (một JWT Locate nhận 401), con số của nó có bốn chữ số thập phân, và vào 2026-10-02 nó thấp hơn của Shukah 447.96 điểm cho cùng một client.
  • Checkout vẫn kiểm tra số dư Shukah khi client trả một order bằng điểm, và điểm tiêu ở một bên không bị trừ ở bên kia (dev, 2026-10-02, trong vài giây; về sau chưa kiểm tra): màn hình Wallet và checkout có thể mâu thuẫn nhau, nên hãy đọc số dư ở bên tạo order.
  • Bảng tỷ lệ tích điểm trên dev khác của Shukah, và code của app lỗi với một mảng rỗng, mà route này trả về khi tùy chọn bị thiếu.

Chi tiết (cho backend)​

Chỉ kiểm tra dev, và chỉ qua order-v2 tại https://dev.v2.api.order.locate.sa, prefix /api/v2/client. Point service nằm ở https://dev-point-api.locate.sa (được nêu trong tài liệu của chính point service). Việc order-v2 trên dev dùng host đó chưa được chứng minh: các capture cho thấy một point service đang chạy phía sau nó, và bản thân URL là Chưa được tài liệu hóa. Host UAT và production: Chưa được tài liệu hóa.

Trong mục này OV2: nghĩa là locate-order-v2-api@d9e4e5f6 (2026-10-01), PS: nghĩa là locate-point-service@2c44ce3 (2026-10-01) và AUTH: nghĩa là locate-authentication-service@6757aa4 (cả ba đều là origin/develop, được đọc lại vào 2026-10-02). Các trích dẫn mobile nêu đầy đủ tên repo.

Các lời gọi proxy hoạt động thế nào​

Mục này áp dụng cho lời gọi balance và, với các route khác, cho các trang điểm khác của module này. Các route tỷ lệ tích điểm và gift-card bên dưới nó không được proxy.

  • Route. App gọi order-v2, vốn không có guard, không có validation và không có body type trên các route này. Nó chuyển lời gọi sang point service và trả status và body không đổi (OV2: src/modules/client/point/point.controller.ts:17-29, OV2: src/modules/client/point/point.service.ts:64). Lời gọi balance đi tới GET /api/client/points trên point service, với giới hạn 10 giây (PS: services/api/src/routes/client.routes.ts:17, PS: services/api/src/handlers/client-points.ts:6-10).
  • Cái gì được forward. Chỉ Authorization (như đã nhận), Accept: application/json và, với POST, Content-Type: application/json. Không được forward: x-from-application, from-application, x-from-version, Accept-Language, device id và cookie, dù app gửi một vài trong số đó (OV2: src/modules/client/point/point.service.ts:54-60, mobile/locate-client@e960c3a: src/shared/api/helpers/getCommonHeaders.ts:4-11). Lời gọi balance không forward query nào.
  • Hai lỗi do order-v2 tạo ra. Một lỗi transport với code ECONNABORTED hoặc ETIMEDOUT thành 504. Bất kỳ lỗi transport nào khác thành 502. Nếu POINT_SERVICE_URL chưa được set, mọi lời gọi đều là 502 (OV2: src/modules/client/point/point.service.ts:11-16,74-96). Body 502 cũng có một message, mà tài liệu tham khảo của order-v2 bỏ sót (OV2: docs/locate-point-client-api-reference.md:15-16).
  • Lỗi của framework trước khi proxy chạy (từ một lần tái hiện local trên Nest 10.4.22; phiên bản runtime của dev chưa biết): một JSON body sai định dạng là 400 theo shape của Nest {"message":"…","error":"Bad Request","statusCode":400}, và một body trên khoảng 100 kB là 413. Đây không phải envelope của point-service.
  • Kiểm tra token. Point service yêu cầu Authorization: Bearer <token> và gọi GET {AUTH_SERVICE_URL}/api/v1/users với token đó và một giá trị x-auth-from mặc định là SHUKAH và có thể đổi bằng setting AUTH_VERIFY_APPLICATION. x-auth-from của chính caller không bao giờ được forward. Mọi câu trả lời không phải 2xx là 401. Client id là giá trị đầu tiên trong client_id, user_id và id của câu trả lời, và nó phải là số nguyên dương (PS: services/api/src/middleware/client-auth.ts:7-18, PS: services/api/src/services/client-auth.service.ts:15-37). Một auth service không kết nối được cũng là 401 (auth service unreachable: …), không phải 5xx, dù tài liệu tham khảo của order-v2 nói 500 (PS: services/api/src/services/client-auth.service.ts:26-28, OV2: docs/locate-point-client-api-reference.md:201).
  • Request body được log. order-v2 log request body mà không che, kể cả cvv của lời gọi trả bằng thẻ (OV2: src/main.ts:38, OV2: src/shared/lib/base.lib.ts:32-43).

Error envelope. Mọi lỗi của point-service là {"success":false,"error":{"code":…,"message":…,"details"?:…}}. Với VALIDATION_ERROR từ schema, details là {"formErrors":[…],"fieldErrors":{"<field>":["<message>"]}}.

error.codeHTTP
VALIDATION_ERROR400
UNAUTHORIZED401
FORBIDDEN403
NOT_FOUND404
CONFLICT409
INSUFFICIENT_POINTS422
INTERNAL_ERROR500

(PS: packages/core/src/errors/error.ts:19-26,42-47.) Bất kỳ exception nào không phải AppError đều thành INTERNAL_ERROR Internal server error (PS: services/api/src/middleware/error-handler.ts:18-22).

Token nào chạy được​

Trên dev, vào 2026-10-02, GET /api/v2/client/points trả 200 cho một token opaque của Shukah và 401 {"success":false,"error":{"code":"UNAUTHORIZED","message":"Invalid or expired token"}} cho một JWT Locate. Các route history, offers, occasions và FAQ cho cùng một cặp kết quả. Route earn-rate trả 200 cho cả hai, vì nó không có guard. Vào 2026-10-01 route balance đã trả 200 với body giống hệt cho cả hai token, và GET /api/v1/users với x-auth-from: SHUKAH cũng vậy.

Vì sao JWT Locate giờ lỗi:

  1. order-v2 không kiểm tra token trên các route được proxy. Nó forward Authorization (OV2: src/modules/client/point/point.controller.ts:17-29, OV2: src/modules/client/point/point.service.ts:54-60).
  2. Point service hỏi auth service GET /api/v1/users với x-auth-from: SHUKAH, trừ khi AUTH_VERIFY_APPLICATION được đặt, và biến mọi câu trả lời không phải 2xx thành 401 Invalid or expired token (PS: services/api/src/services/client-auth.service.ts:17,22-29). Message khớp với capture, nên việc từ chối đến từ auth service, không phải do thiếu header. Guard riêng của order-v2 trên route gift-card thực hiện cùng lời gọi với SHUKAH được hard-code (OV2: src/auth/guards/user-profile-auth.guard.ts:60-62).
  3. Với x-auth-from: SHUKAH auth service đọc auth_source (AUTH: src/middlewares/user.js:35-37,101; AUTH: src/services/setting.js:46-50). Client settings công khai của dev trả "auth_source":"shukah" vào 2026-10-02. Một biến môi trường AUTH_SOURCE sẽ ghi đè nó và không thể đọc từ bên ngoài, nên điều này rất có khả năng đúng, chưa được chứng minh. Ở nhánh đó token được chuyển tiếp tới /maeda/api/verify-token của Shukah, và mọi lỗi đều là 401 (AUTH: src/middlewares/user.js:132-136,178-185). Shukah dev chấp nhận token Shukah và từ chối JWT Locate vào 2026-10-02 (suy luận: verify-token của Shukah không được gọi với chính JWT). Vào 2026-10-01 nó vẫn chấp nhận JWT, có lẽ qua việc push các JWT Locate mới sang Shukah (xem User detail).
  4. Với auth_source đặt là locate, mọi JWT Locate cũng sẽ nhận 401: AUTH: src/services/client.js:12-17 gọi .cacheQuery trên Promise mà Client.findByToken kiểu async trả về (AUTH: src/models/client.js:163).

Vậy một JWT Locate chỉ chạy trên các route này nếu Shukah dev lại chấp nhận nó, hoặc nếu auth_source trở thành locate và defect đó được sửa. Cho đến lúc đó app cần một token Shukah cho mọi route có guard trên các trang điểm. Các route này chấp nhận token nào ở UAT và production: Chưa được tài liệu hóa.

Route balance​

  • Nó đọc ba thứ cùng lúc: tài khoản điểm của client, point settings, và dòng earn được tính mới nhất của client. Nó không ghi gì, không gửi push và không gọi Shukah (PS: services/api/src/services/client-points.service.ts:14-24).
  • Vì sao bốn chữ số thập phân. Ledger lưu điểm ở bốn chữ số thập phân và số dư được ghi dưới dạng text với bốn chữ số thập phân. formatShukahPoints chỉ bỏ các số 0 ở cuối và dấu chấm ở cuối, nên nó không bao giờ làm tròn (PS: packages/core/src/money/minor-units.ts:3-4,21-23, PS: services/api/src/services/client-points.mappers.ts:3-8; được chốt bởi PS: services/api/src/services/client-points.mappers.test.ts:31-41). Giá trị "514405.6788" là số dư đã lưu đúng như nó. Những dòng ledger nào tạo ra phần .6788: Chưa được tài liệu hóa.
  • Khoảng cách với Shukah. Vào 2026-10-01 số dư khác nhau theo hệ số khoảng 3.9966 (514405.6788 so với 128712.41). Vào 2026-10-02 Shukah trả 514853.64, nên dữ liệu Shukah dev đã thay đổi, và Locate thấp hơn 447.96 điểm. Dòng earn mới nhất của Locate (shukah_id 1351722, 2026-09-29 12:45:07) chậm 45 id so với của Shukah (1351767, 2026-10-01 17:00:01), và phần đuôi bị thiếu khớp với khoảng chênh (suy luận). Các báo cáo parity tháng 7 có cả hai phía ở khoảng 511,700 (PS: docs/reports/2026-07-17-client-api-parity.md:39-40).
  • Quy tắc hết hạn. null trừ khi settings nói inactivity_days và tồn tại một dòng earn được tính. Khi đó ngày là thời điểm tạo của dòng earn được tính mới nhất cộng expiration_inactivity_days ngày, hiển thị dưới dạng ngày Asia/Riyadh (PS: services/api/src/services/client-points.mappers.ts:30-39; test PS: services/api/src/services/client-points.mappers.test.ts:18-29: 2026-07-16 cộng 60 ngày cho 2026-09-14). Các dòng hết hạn cũ mà Shukah ghi dưới dạng earn âm không bị loại khỏi "earn mới nhất", nên một dòng như vậy có thể đẩy ngày ra xa. Các lần hết hạn mới được lưu dưới dạng expense và không như vậy (PS: services/api/src/services/client-points.service.ts:17-21, PS: packages/core/src/points/sync-shukah-points.ts:97-102).
  • Settings của dev (suy luận). Dev trả 2026-11-28. Dòng ledger Locate mới nhất của client là 2026-09-29 và 2026-09-29 cộng 60 ngày là 2026-11-28, nên dev hành xử như 60 ngày không hoạt động. Document settings thật chỉ đọc được qua route admin với một service key: Chưa được tài liệu hóa. Các settings được cache khoảng 10 giây (PS: packages/core/src/db/point-config.singleton.ts:20,89).
  • Ngày có được thực thi không? Expiry job là dry run (nó mô phỏng và không ghi gì) khi nguồn điểm không phải merchant, khi loại hết hạn không phải inactivity_days, hoặc khi cửa sổ là dưới 1 ngày. Nếu nguồn là merchant và MERCHANT_FIRST_ENABLED không phải true, job không mô phỏng: nó từ chối chạy và lỗi với INTERNAL_ERROR (PS: packages/core/src/points/expire-points.ts:245-254, PS: packages/core/src/points/merchant-first-guard.ts:21-28). Shukah vẫn ghi các lần hết hạn của chính nó. Khi job áp dụng, một số dư hết hạn khi ngày là hôm nay hoặc sớm hơn theo Asia/Riyadh, và job chạy hằng ngày lúc 01:00 Asia/Riyadh (PS: packages/core/src/points/expire-points.ts:63-66, PS: docs/plans/2026-09-16-point-expiration-job.md:6). Nó có áp dụng trên dev hay không: Chưa được tài liệu hóa.
  • Hai số dư. Với việc tính giá giỏ hàng và checkout order khi cửa hàng chấp nhận trả bằng điểm, order-v2 gọi GET {SHUKAH_BASE_URL}/maeda/client/points với token của client. Nó dùng 0 nếu ngày hết hạn của Shukah đã qua hoặc lời gọi lỗi. Điều này không phụ thuộc vào flag usePointService (OV2: src/shared/lib/handle-points.lib.ts:326-350, OV2: src/modules/client/orders/orders.service.ts:1128-1136,5036-5047).
  • Việc tiêu điểm không được đồng bộ. Lời gọi thật trên dev, 2026-10-02 (client 356259): Shukah 514853.64 và Locate 514405.6788 trước; 514743.64 và 514405.6788 sau một order point trên Shukah (order 1257); 514743.64 và 514295.6788 sau cùng order đó trên Locate (order 100012). Mỗi bên chỉ trừ purchased_point_amount (110) trên ledger của chính nó. Một order point của Locate xếp các chân Shukah vào một outbox mà hiện nay không có gì drain trên k8s (xem Point orders, Details). Việc tiêu điểm trên Shukah có tới Locate sau đó qua luồng đồng bộ vào hay không thì chưa được kiểm tra.

Route tỷ lệ tích điểm​

  • Controller là @Controller('system_configs') với @Get('/point_rates') và không có guard (OV2: src/modules/client/system-configuration-options/system-configs.controller.ts:7-13).
  • Service đọc option point_rates cho merchantId: 0 từ merchant-v2 database, qua một Redis hash cache. Nó parse JSON, sắp theo amount_from (một null tính là 0), và trả [] khi option bị thiếu. Text không phải JSON hợp lệ cũng cho [], vì parser fallback về một danh sách rỗng. Một 500 cần một lỗi đọc, hoặc JSON không phải danh sách (nên sort throw) (OV2: src/modules/client/system-configuration-options/system-configs.service.ts:18-40, OV2: src/services/redis/system-config-cache.service.ts:35-62, OV2: src/shared/lib/base.lib.ts:58-75). Nó không gọi point service cũng không gọi Shukah.
  • Nó không phải bảng mà order-v2 credit theo. getPointsEarn đọc /maeda/point-rate của Shukah (OV2: src/shared/lib/handle-points.lib.ts:25-43,98-107, OV2: src/services/points/points.service.ts:160). Point service không có route rate, chỉ có một model point_rates không dùng (PS: packages/core/src/db/models/point-settings.model.ts:14-19).
  • Một bộ bậc thứ ba. Text FAQ về điểm trên cả hai service nói 35–69 SR được 1 điểm, 70–139 được 2, 140–250 được 4, 251–450 được 8 và từ 451 trở lên được 16. Nó không khớp bảng nào (xem Points FAQ). Bảng nào là đúng: Chưa được tài liệu hóa.
  • App. Ở chế độ Merchant app gọi route này; cùng bước hậu xử lý chạy trên cả hai bảng. Nó thêm một dòng đầu với amount_from null và amount_to bằng amount_from của dòng đầu trừ một. Phép kiểm tra cho dòng đó luôn đúng (nó được viết bằng ||), nên dòng luôn được thêm. Nó đọc response.data?.[0].amount_from mà không có guard, nên [] throw (mobile/locate-client@e960c3a: src/entities/points/model/api/services/points.service.ts:78-126).

Route gift-card​

  • @UseGuards(UserProfileGuard) và @Post(), với body type {code} (@IsString(), @IsNotEmpty()) (OV2: src/modules/client/gift-card-code/gift-card-code.controller.ts:8-24, OV2: src/modules/client/gift-card-code/dtos/apply-code.dto.ts:4-9). Guard thực hiện cùng lời gọi auth như các route điểm, với x-auth-from hard-code là SHUKAH, và token thiếu cho {"message":"Unauthorized","statusCode":401} của Nest (OV2: src/auth/guards/user-profile-auth.guard.ts:58-72).
  • applyCode tra code trong merchant-v2 database với status không phải DRAFT. Một code không tồn tại là 404. Một code đã có client hoặc status USED là 400. Một code hết hạn từ 24:00 giờ Ả Rập (21:00 UTC) của ngày expired_at của nó, và đó là 400. Số điểm là số tiền nhân với tỷ giá, với tỷ giá từ setting exchanged_points hoặc 4. Credit đi qua PointsService.addPoints, tối đa ba lần thử cách nhau một giây, rồi 500. Sau đó code được đánh dấu là đã dùng (OV2: src/modules/core/gift-card-code/gift-card-code.service.ts:40-94,96-114, OV2: src/modules/core/gift-card-code/constants/exchange-point-rate.ts:1).
  • Credit đi đâu. addPoints gửi nó tới /maeda/client/add-point của Shukah khi flag usePointService là false (mặc định, cũng dùng khi không đọc được flag), và tới point service khi nó là true (OV2: src/services/points/points.service.ts:29-73, OV2: src/services/user-points/user-points.service.ts:39-52). Tài liệu của point service nói flag là false ở mọi môi trường và production không có kế hoạch bật nó (PS: docs/integrations/2026-07-30-use-point-service-flag-routing.md:13-16,37). Giá trị của dev chưa được đọc: Chưa được tài liệu hóa.
  • Point service không có route gift-card, và tài liệu của nó không nhắc tới.
  • Response phản chiếu shape của Shukah: app parse cùng sáu key ở cả hai chế độ (mobile/locate-client@e960c3a: src/entities/points/model/api/dto/points.dto.ts:215-222). App gọi Shukah bằng GET và code trong query, và Locate bằng POST và {code}, chọn theo chế độ app (mobile/locate-client@e960c3a: src/entities/points/model/api/services/points.service.ts:128-168).

Khi tài liệu và code mâu thuẫn (code thắng)​

  • Các tài liệu service nói points là số dư như đã lưu, và nó là "Shukah-formatted" (PS: docs/integrations/2026-07-16-client-facing-point-apis.md:92, OV2: docs/locate-point-client-api-reference.md:97). Điều đó chỉ đúng với các số 0 ở cuối: code giữ bốn chữ số thập phân.
  • Cả hai tài liệu đều nói pointsExpirationDate là null cho đến khi point settings được đặt (PS: docs/integrations/2026-07-16-client-facing-point-apis.md:463-467, OV2: docs/locate-point-client-api-reference.md:90,98,222-224). Dev giờ trả về một ngày.
  • Tài liệu point service nói một token opaque luôn được forward sang Shukah (PS: docs/integrations/2026-07-16-client-facing-point-apis.md:56-58). Trong code auth đó chỉ là một nhánh (xem "Token nào chạy được").
  • Hướng dẫn manual-test 2026-09-21 nói order-v2 dev gọi một point service trên localhost:4010, nên mọi lời gọi sẽ là 502 (PS: docs/plans/2026-09-21-point-order-manual-test-guide.md:24-32). Vào 2026-10-01 proxy dev trả 200 với dữ liệu ledger, nên hướng dẫn đã lỗi thời.
  • 2026-10-02: trang này nói một token Shukah và một JWT Locate đều chạy trên route balance, và mục Thử ngay của nó dùng một JWT Locate. Vào 2026-10-02 JWT nhận 401; trang giờ nói lý do, và mục Thử ngay dùng một token Shukah.
  • 2026-10-02: trang này nói số dư của Locate gấp khoảng bốn lần của Shukah ("128712.41"). Số dư Shukah dev giờ là "514853.64", cao hơn của Locate 447.96 điểm.

Sources​

  • locate-order-v2-api@d9e4e5f6: src/modules/client/point/point.controller.ts:17-29, src/modules/client/point/point.service.ts:11-16,54-60,64,74-96, src/main.ts:38, src/shared/lib/base.lib.ts:32-43,58-75, src/modules/client/system-configuration-options/system-configs.controller.ts:7-13, src/modules/client/system-configuration-options/system-configs.service.ts:18-40, src/services/redis/system-config-cache.service.ts:35-62, src/modules/client/gift-card-code/gift-card-code.controller.ts:8-24, src/modules/client/gift-card-code/dtos/apply-code.dto.ts:4-9, src/modules/core/gift-card-code/gift-card-code.service.ts:40-114, src/modules/core/gift-card-code/constants/exchange-point-rate.ts:1, src/auth/guards/user-profile-auth.guard.ts:58-72, src/services/points/points.service.ts:29-73,160, src/services/user-points/user-points.service.ts:39-52, src/shared/lib/handle-points.lib.ts:25-43,98-107,326-350, src/modules/client/orders/orders.service.ts:1128-1136,5036-5047
  • Tài liệu của locate-order-v2-api@d9e4e5f6: docs/locate-point-client-api-reference.md:15-16,90,97,98,201,222-224
  • locate-point-service@2c44ce3: services/api/src/routes/client.routes.ts:17, services/api/src/handlers/client-points.ts:6-10, services/api/src/middleware/client-auth.ts:7-18, services/api/src/services/client-auth.service.ts:15-37, services/api/src/middleware/error-handler.ts:18-22, packages/core/src/errors/error.ts:19-26,42-47, services/api/src/services/client-points.service.ts:14-24, services/api/src/services/client-points.mappers.ts:3-8,30-39, services/api/src/services/client-points.mappers.test.ts:18-29,31-41, packages/core/src/money/minor-units.ts:3-4,21-23, packages/core/src/db/point-config.singleton.ts:20,89, packages/core/src/points/expire-points.ts:63-66,245-254, packages/core/src/points/merchant-first-guard.ts:21-28, packages/core/src/points/sync-shukah-points.ts:97-102, packages/core/src/db/models/point-settings.model.ts:14-19
  • Tài liệu của locate-point-service@2c44ce3: docs/integrations/2026-07-16-client-facing-point-apis.md:56-58,92,463-467, docs/integrations/2026-07-30-use-point-service-flag-routing.md:13-16,37, docs/plans/2026-09-16-point-expiration-job.md:6, docs/plans/2026-09-21-point-order-manual-test-guide.md:24-32, docs/reports/2026-07-17-client-api-parity.md:39-40
  • Snapshot mobile mobile/locate-client@e960c3a (branch development, 2026-02-26; code của chính app, không phải Locate): platforms/mobile/products/locate/screens/home/mainInner/Wallet/ui/Wallet/Wallet.tsx:78-85, src/features/PointsWallet/ui/PointsWallet/PointsWallet.tsx:29, src/entities/user/model/services/fetchUserInfo/fetchUserInfo.ts:55, src/features/selectRestaurantPaymentMethod/ui/SelectRestaurantPaymentMethod/SelectRestaurantPaymentMethod.container.tsx:64, platforms/mobile/products/locate/screens/restaurant/RestaurantCart/ui/RestaurantCart/RestaurantCart.tsx:301, platforms/mobile/shared/modules/core/cart/components/public/MerchantCartGeneralInfo/components/MerchantCartGeneralInfo/MerchantCartGeneralInfo.tsx:66, src/widgets/ApplyPointsPromoCode/model/services/applyPointsPromoCode.ts:27-50, src/entities/points/model/api/services/points.service.ts:30-44,78-168, src/entities/points/model/api/controllers/points.controller.ts:95-126, src/entities/points/model/api/dto/points.dto.ts:90-102,185-222, src/entities/settings/model/services/getServiceAvailable/getServiceAvailable.ts:48-54, src/shared/features/AppModeConfigs.ts:5-62, src/shared/api/helpers/getCommonHeaders.ts:4-11
  • locate-authentication-service@6757aa4: src/middlewares/user.js:35-37,101,132-136,178-185, src/services/setting.js:46-50, src/services/client.js:12-17, src/models/client.js:163
  • locate-point-service@2c44ce3, kiểm tra token: services/api/src/services/client-auth.service.ts:17,22-29; locate-order-v2-api@d9e4e5f6: src/auth/guards/user-profile-auth.guard.ts:60-62
  • Capture dev, 2026-10-02 (parity audit): points--shukah, points--locate-st, points--locate-jwt, point-rate--shukah, point-rate--locate-st, point-rate--locate-jwt; client settings dev được đọc từ https://dev.go.locate.sa/api/v1/settings/client
  • Capture dev, 2026-10-01: locate-points, locate-points-locate-token, shukah-points, probe-locate-points, probe-shukah-points, locate-point-rates, probe-locate-point-rates, shukah-point-rate, probe-shukah-point-rate, probe-locate-gift-card-code, locate-points-faq, shukah-points-faq