Files
nick-doc/09 - Audits/Doc vs Code Audit Report - 2026-05-29.md
Siavash Sameni 5113b0df23 docs: add doc vs code audit report and comprehensive UAT test plan (2026-05-29)
228 findings (35 critical, 123 major, 54 minor) across 8 domains.
513 UAT test cases (165 P0, 233 P1, 102 P2, 13 P3) across 9 domains.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:32:02 +04:00

223 KiB
Raw Blame History

Doc vs Code Audit Report — 2026-05-29

Scope: 8 domains audited · 228 total findings · 35 critical · 123 major · 54 minor/info Audit Date: 2026-05-29 Source: Automated doc-vs-code agent audit of nick-doc flow documentation against live codebase

Executive Summary

Doc vs Code Audit — Executive Summary

Scope: 8 domains · 228 findings · 35 critical · 123 major · 54 minor


1. Top 10 Critical Discrepancies — Must Fix Before UAT

These findings will cause testers to either file false bugs, miss real bugs, or test against endpoints that return 404.

1. Delivery code roles are completely reversed (delivery + purchase-request)

The doc says the seller generates the code and the buyer enters it. The code is the exact opposite: the buyer calls POST .../delivery-code/generate (buyer-only enforced), verbalises the code, and the seller submits it via POST .../delivery-code/verify (seller-only enforced). The documented endpoint POST .../verify-delivery does not exist — the real path is /delivery-code/verify. Every UAT test case built from the doc will test the wrong actor against the wrong endpoint.

2. Passkey attestation is fully implemented — doc says it is stubbed

The Passkey Flow doc claims verifyRegistration stores publicKey: 'simulated-public-key' and warns of a severe security risk. The backend calls verifyRegistrationResponse() from @simplewebauthn/server and stores the real COSE key. QA following the doc will skip attestation validation testing entirely.

3. deleteAccount frontend action calls DELETE /user/profile — no such route exists

action.ts line 144 sends DELETE /user/profile. The actual soft-delete route is DELETE /api/auth/account (requires password in body, runs deleteAccountValidation). Account deletion silently returns 404 from every UI path today.

4. PATCH /api/disputes/:id/status and POST /api/disputes/:id/resolve have no role guard

Any authenticated buyer or seller can change dispute status to resolved or closed, or post a resolution including action=ban_seller. The doc says both require Bearer JWT (admin). These are privilege-escalation bugs reachable today via the live API.

5. Three Trezor endpoints have no authentication despite the doc claiming admin-only

POST /api/payment/payments/:id/fetch-tx, POST /api/payment/payments/auto-fetch-missing, and GET /api/payment/payments/:id/debug have zero auth middleware. Any unauthenticated caller can read full payment internals or trigger on-chain state writes. GET /api/admin/scanner/status has the same problem.

6. DePay intent endpoint: /create does not exist — only /save is implemented

All doc references to POST /api/payment/decentralized/create return 404. The real endpoint is POST /api/payment/decentralized/save. Any test harness built from the DePay flow sequence diagram will fail at step one.

7. POST /api/notifications/read-all does not exist — correct endpoint is PATCH /notifications/mark-all-read

Both the step narrative and the API table are wrong on method (POST vs PATCH) and path (read-all vs mark-all-read). Every bulk-read test will 404.

8. Seller offer create endpoint is POST /purchase-requests/:id/offers, not POST /api/marketplace/offers

No flat /offers route exists. Any integration test or API client hitting the documented flat path will 404. The three documented GET endpoints (/offers/request/:requestId, /offers/seller/:sellerId) also do not exist — the buyer list route is scoped under /purchase-requests/:id/offers and the seller history route has no HTTP exposure at all.

9. POST /api/disputes/:id/resolve (dashboard) resolves the Dispute model record — it does NOT touch escrow

The API doc claims it "triggers refund/release/split escrow action." It does not. DisputeService.resolveDispute only updates the Dispute document. The separate POST /api/disputes/:purchaseRequestId/resolve (releaseHold router) is required to unblock escrow. Due to route shadowing (both routers mounted at /api/disputes), a POST /api/disputes/{purchaseRequestId}/resolve with a valid purchase-request ID will match the dashboard router first and execute the wrong handler.

10. Frontend updateUserStatus sends 'inactive'/'pending' — backend only accepts 'active'/'suspended'/'deleted'

The TypeScript union type in user.ts line 159 is 'active' | 'inactive' | 'pending'. The backend User.status enum is active | suspended | deleted. The value 'suspended' is absent from the frontend; 'inactive' and 'pending' will be silently rejected or ignored by the backend. Combined with the fact that updateUserStatus and updateUserRole send PUT while the backend registers PATCH, these admin actions are broken on two axes.


2. Top 5 Most Incomplete Flows

1. Trezor Safekeeping Flow — entirely backend-only

Zero frontend implementation exists for any Trezor endpoint. There is no registration page, no xpub input, no Trezor Connect SDK import, and no signing UI. confirmReleaseTx and confirmRefundTx post { txHash } with no trezor object. With TREZOR_SAFEKEEPING_REQUIRED=true every admin release/refund from the UI will be rejected by the backend assertTrezorSignatureForOperation guard. The flow describes a 12-step challenge-sign-submit sequence that has no frontend surface at any step.

2. Dispute Flow — all socket events are TODO stubs; resolve has no escrow side-effect

Every socket.io emit block in DisputeService is commented out as TODO. No real-time updates fire for dispute creation, admin assignment, status changes, evidence uploads, or resolution. The flow doc describes real-time presence as a feature. Additionally, POST /api/disputes/:id/assign has no role guard (any user can self-assign as admin), and the documented decision: buyer|seller|split resolve schema is completely wrong — the real schema uses action: refund|replacement|compensation|warning_seller|ban_seller|no_action.

3. Points/Referral Flow — five frontend pages do not exist

The following routes 404 today: /dashboard/points/referrals, /dashboard/points/transactions, /dashboard/points/levels. redeemPoints is never called from any component (no redemption UI). generateReferralCode is never called (no regenerate button). adminAddPoints has no admin UI page. The referral reward triggers only on 'completed' status despite the doc saying 'delivered or completed'. The leaderboard period filter (all|month|week) is silently ignored by the backend.

4. Payment Flow — multiple provider routing failures and stub endpoints

getProviderIntentEndpoint() in payment.ts always routes to request-network/intents regardless of the provider argument — a SHKeeper checkout will silently POST to the wrong backend service. GET /api/payment/shkeeper/status/:paymentId (documented as a polling endpoint) does not exist anywhere in the backend or frontend; status transitions are socket-only. Five endpoints called by the frontend have no backend implementation: /payment/history, /payment/methods, /payment/validate, /payment/transactions, /payment/escrow/balance. The PaymentProvider TypeScript type excludes 'shkeeper' and 'decentralized', causing provider-specific UI branches to fall through to unknown state for the two main payment providers used in production.

5. Seller Offer Flow — withdraw, offer history, and notifications are absent

POST /api/marketplace/offers/:id/withdraw is documented and has a service method (SellerOfferService.withdrawOffer) but no HTTP route. No frontend withdraw button or action exists. No seller offer history page exists at /dashboard/seller/marketplace/offers. When a buyer uses select-offer, no per-seller socket events or notifications are sent to winning or losing sellers (only a generic purchase-request-update to the request room fires). The 'active' status listed in the SellerOffer state machine does not exist in the schema enum — any attempt to set it will throw a Mongoose ValidationError.


3. Backend Endpoints With No Frontend Coverage

The following backend endpoints are implemented and accessible but have zero frontend action functions, pages, or components:

Auth/User

  • DELETE /api/auth/account (account deletion — frontend hits wrong path)
  • POST /api/user/profile/email/verify and POST /api/user/profile/email/resend-verification (email re-verification after profile change)
  • POST /api/user/wallet-address/ton-proof/challenge (TON wallet proof nonce)
  • PATCH /api/user/admin/:userId/password, POST /api/users/admin/:userId/resend-verification, PUT /api/users/admin/update/:email

Trezor (all four documented endpoints, plus two undocumented ones)

  • GET /api/trezor/registration-message, POST /api/trezor/register, POST /api/trezor/addresses/next, POST /api/trezor/operation-message, POST /api/trezor/verify-operation, GET /api/trezor/account

Points

  • POST /api/points/redeem (redemption UI does not exist)
  • POST /api/points/generate-referral-code (no regenerate button)
  • GET /api/points/levels (levels page does not exist)
  • GET /api/points/referrals (referrals page does not exist)
  • POST /api/points/admin/add (no admin points management page)

Dispute

  • POST /api/disputes/:purchaseRequestId/raise (no frontend action)
  • GET /api/disputes/:purchaseRequestId/status (no frontend action)

Chat

  • POST /api/chat/purchase-request (endpoint key in axios config but no action function)
  • PUT /api/chat/:id/participants/:participantId (role update — no backend route exists either, so double gap)

Admin/Payment

  • GET /api/admin/settings/aml, PATCH /api/admin/settings/aml
  • GET /api/admin/settings/confirmation-thresholds/history (frontend action defined but backend route absent)
  • All shkeeper admin release/refund/payout endpoints (POST /api/payment/:id/release, /release/confirm, /refund, /refund/confirm)
  • All data cleanup/GDPR endpoints (POST /api/admin/cleanup/clean, DELETE /api/admin/cleanup/user/:userId, POST /api/admin/cleanup/seed-*)

4. Doc-Described Flows With No Backend Implementation

These are flows the documentation describes as functional but for which the backend endpoint does not exist:

Documented Endpoint Doc Claims Reality
POST /api/marketplace/offers/:id/withdraw Seller withdraws pending offer No HTTP route; service method is dead code
POST /api/marketplace/purchase-requests/:id/delivery-code (bare POST) Admin code regeneration No such route; regenerate is also absent (/regenerate 404s)
GET /api/marketplace/purchase-requests/search Search endpoint No /search sub-path; use query params on list endpoint
GET /api/marketplace/purchase-requests/stats Marketplace statistics No /stats sub-path
GET /api/marketplace/offers/seller/:sellerId Seller offer history Route not registered; service method getOffersBySeller() unreachable via HTTP
GET /api/payment/shkeeper/status/:paymentId Frontend polls for SHKeeper status Endpoint absent; status is socket-only
GET /payment/:id/status and POST /payment/:id/confirm Payment status check and confirmation No /status or /confirm sub-routes on payment documents
DELETE /payment/:id Cancel payment No DELETE handler on any payment route
POST /api/payment/request-network/:id/payout/initiate and three related sub-paths Admin RN payout/release/refund None of the four sub-paths are implemented
POST /api/notifications/read-all Bulk mark-all-read Wrong method and path; real endpoint is PATCH /notifications/mark-all-read
POST /api/admin/rn/networks/reload and POST /api/admin/rn/networks/probe/:chainId Network registry management Only GET /api/admin/rn/networks exists
GET /api/admin/settings/confirmation-thresholds/history Change history Only current-values GET and per-chain PATCH exist

Execute in this order to unblock dependent flows and surface blockers earliest.

Phase 1 — Auth and User (prerequisite for all other flows) Test auth before everything else. Critical gaps: deleteAccount wrong endpoint, passkey attestation is live (test it), rate-limit counts all attempts not just failures, reset-password-with-code has no complexity validation. Verify updateUserStatus/updateUserRole with correct PATCH method and 'active'/'suspended' values. Confirm admin delete is soft vs hard. Block UAT on other domains until login, registration, and session refresh are verified end-to-end.

Phase 2 — Purchase Request and Delivery (core escrow lifecycle) These two domains share the status progression that gates every other flow. Verify the full status sequence including the undocumented pending_payment and active statuses. Test the delivery code flow with correct role assignments (buyer generates, seller verifies). Confirm PUT /delivery (not PATCH /:id) is the seller shipped action. Verify confirm-delivery authorization gap (any authenticated user can call it today).

Phase 3 — Seller Offer and Negotiation Test offer creation against POST /purchase-requests/:id/offers (not the documented flat route). Verify select-offer status cascade does not corrupt withdrawn offers. Confirm PUT vs PATCH method for offer updates. Document that withdraw is only accessible via PUT /offers/:id/status with status='withdrawn' and that no seller notification fires from the select-offer path.

Phase 4 — Payment Test both SHKeeper and DePay flows with network interception to verify actual endpoint paths hit. Confirm the SIM_ bypass is present and works in staging (document for production gating). Verify completed payments do not count as successfulPayments in stats. Test the unauthenticated debug/fetch-tx/auto-fetch-missing endpoints — these are security bugs that should be escalated before production go-live regardless of UAT phase.

Phase 5 — Dispute All socket events are absent — do not waste UAT time on real-time behavior. Focus on CRUD correctness and the two privilege escalation bugs: non-admin can change dispute status and resolve disputes. Test the route shadowing between the two routers mounted at /api/disputes. Verify the actual resolve body schema (action + notes, not decision + refundAmount).

Phase 6 — Chat and Notification Test file upload via POST /chat/:id/messages/file (frontend sends to wrong endpoint today). Verify archiveConversation uses PATCH not PUT. Test rate limiting (20 msgs/min) and 15-minute edit window. For notifications, verify PATCH /notifications/mark-all-read (not the documented POST). Confirm unread-count-update drives cross-tab badge sync (not the documented notification-read).

Phase 7 — Points/Referral This domain has the most missing UI pages. UAT scope is limited to: points balance display, referral attribution (signup + reward on completed only), and leaderboard. All redemption, levels, referral history, and transaction history features are untestable via UI — test their API endpoints directly.

Phase 8 — Trezor No frontend exists. UAT is API-only. Verify registration challenge-sign flow directly via curl/Postman. Confirm TREZOR_SAFEKEEPING_REQUIRED=false in staging so other payment tests are not blocked. Escalate the admin release/refund gap as a known production blocker if safekeeping is intended to be enabled.


6. Doc Update Priorities

Immediate (block UAT if not corrected):

  1. Delivery flow — swap all actor references (buyer↔seller) for code generation and verification; replace all documented endpoint paths with actual paths (/delivery-code/generate, /delivery-code/verify); remove the non-existent /verify-delivery and bare /delivery-code POST entries.
  2. Passkey flow — remove all stub/simulated-public-key language; replace with accurate description of @simplewebauthn/server integration; remove the false refresh-token gap edge case.
  3. Dispute resolve schema — replace decision: buyer|seller|split + refundAmount with action: refund|replacement|compensation|warning_seller|ban_seller|no_action + amount + notes; correct dispute categories (product_quality, delivery_delay, wrong_item, payment_issue, seller_behavior, other — not fraud); replace under_review with in_progress.
  4. Seller offer endpoints — replace all three wrong GET paths with GET /purchase-requests/:id/offers; replace POST /api/marketplace/offers with POST /purchase-requests/:id/offers; remove the non-existent withdraw route.
  5. Payment DePay flow — replace /decentralized/create with /decentralized/save everywhere; correct verify path to include :paymentId param; remove /shkeeper/status/:paymentId polling step.
  6. Notification endpoints — replace POST /api/notifications/read-all with PATCH /notifications/mark-all-read; replace POST /api/notifications/mark-read with PATCH /notifications/:id/read; add unread-count-update to socket events table; remove fictional notification-read event.
  7. Admin auth gaps — add explicit warning that fetch-tx, auto-fetch-missing, and debug payment endpoints currently have no authentication and are exploitable without credentials.

High priority (correct before handing doc to integration teams): 8. Purchase request status enum — add pending_payment and active to all status lists; remove finalized and archived if not present in frontend types. 9. Password reset code length — correct all 8-digit references to 6-digit in backend API notable logic and authController.ts comment. 10. Points redeem body schema — replace amount/purpose with pointsToUse/purchaseRequestId; correct response shape to { transaction, discount, remainingPoints }. 11. Delivery role clarification — confirm confirm-delivery authorization model; add note that any authenticated user can currently call it (authorization gap pending fix). 12. PointTransaction type enum — remove refund from status values list; the valid types are earn | spend | expire only.

Standard priority (before final doc release): 13. Add pending_payment and seller_paid to notification templates gap documentation. 14. Document 90-day TTL auto-deletion of notifications. 15. Document chat rate limits (20 msgs/min, 15-minute edit window, 5000-char limit). 16. Document escrowState: releasable and escrowState: releasing values. 17. Document AML settings runtime-only persistence (changes lost on restart). 18. Add unarchive behavior to chat archive endpoint documentation (toggle semantics). 19. Document markAsRead with empty messageIds marks all messages as read. 20. Add GET /api/trezor/account and POST /api/trezor/verify-operation to Trezor API table.


Finding Statistics

Domain Critical Major Minor Total
Authentication 3 6 4 15
User Management 3 9 4 18
Purchase Request 4 15 6 27
Delivery 3 8 4 16
Seller Offer 3 8 4 15
Payment 5 13 7 27
Dispute 3 8 5 18
Chat 2 13 5 23
Notification 2 8 4 16
Points/Referral 2 13 4 19
Trezor Safekeeping 2 5 3 10
Admin Operations 3 17 4 24
TOTAL 35 123 54 228

Critical Findings (Must Fix Before UAT)

These findings will cause testers to test wrong actors, wrong endpoints, or expose live security vulnerabilities.

Domain: Authentication

C1. Passkey: attestation stub claim is false — real @simplewebauthn/server is used

Description: The Passkey Flow doc (step 6, edge case, and docFlags) repeatedly states that verifyRegistration stores publicKey as the literal string 'simulated-public-key' and that attestation parsing is stubbed, warning of a severe security risk. In reality, /Users/manwe/CascadeProjects/escrow/backend/src/services/auth/passkeyService.ts imports verifyRegistrationResponse and verifyAuthenticationResponse from @simplewebauthn/server (line 2), calls verifyRegistrationResponse() to cryptographically validate the attestation (line 158), and stores the real COSE public key as Buffer.from(webAuthnCredential.publicKey).toString('base64url') (line 175). Testers relying on this doc claim will file false critical security bugs and skip real attestation testing.

Doc Claim: Step 6: 'appends { publicKey: 'simulated-public-key', ... }'. Edge case: 'Attestation validation is stubbed — publicKey is stored as literal string 'simulated-public-key', allowing a malicious client to register attacker-controlled credential IDs.'

Code Reality: passkeyService.ts:158 calls verifyRegistrationResponse() from @simplewebauthn/server; on success, stores Buffer.from(webAuthnCredential.publicKey).toString('base64url') as publicKey. The stub has been replaced entirely.

UAT Impact: QA must test that passkey registration rejects forged attestations and that the stored public key enables successful authentication on subsequent sign-ins. Do NOT skip attestation validation tests on the assumption the feature is stubbed.

C2. Passkey: refresh tokens ARE persisted to user.refreshTokens[] — doc claims they are not

Description: The Passkey Flow edge cases state 'Refresh-token rotation gap — passkey-issued refresh tokens are not added to user.refreshTokens[]. Standard /api/auth/refresh-token will reject them on next refresh.' This is false. passkeyService.ts:281 explicitly does user.refreshTokens.push(refreshToken) followed by user.save(). The standard token refresh endpoint will accept passkey-issued tokens.

Doc Claim: Edge case: 'Refresh-token rotation gap — passkey-issued refresh tokens are not added to user.refreshTokens[]. Standard /api/auth/refresh-token will reject them on next refresh. User must passkey-sign-in again after token expiry.'

Code Reality: passkeyService.ts lines 281-282: user.refreshTokens.push(refreshToken); await user.save(); — tokens are persisted normally.

UAT Impact: QA must verify that after a passkey sign-in the standard /api/auth/refresh-token endpoint successfully rotates the token without requiring a new biometric prompt.

C3. deleteAccount frontend action calls DELETE /user/profile which has no backend route

Description: The frontend deleteAccount action in /Users/manwe/CascadeProjects/escrow/frontend/src/actions/account.ts (line 144) calls axiosInstance.delete(endpoints.users.profile) which resolves to DELETE /user/profile. There is no DELETE handler on that path in the backend. The actual soft-delete route is DELETE /api/auth/account (authRoutes.ts:86-89), which requires a password in the body and runs deleteAccountValidation. Calling DELETE /user/profile will return 404 or 405, making account deletion silently broken.

Doc Claim: Frontend actions table: deleteAccount → apiPath: '/user/profile', method: DELETE

Code Reality: Backend DELETE /auth/account exists (authRoutes.ts:86-89). No DELETE /user/profile route exists. The frontend action.ts sends to the wrong path.

UAT Impact: QA must attempt to delete a test account from the UI and confirm the request reaches DELETE /api/auth/account (not /user/profile) and that the account is set to status=deleted in MongoDB.

Domain: Purchase Request

C4. Delivery code flow: buyer generates, doc says buyer receives; seller verifies, doc says buyer enters code

Description: The documented flow states the seller shares the 6-digit code verbally with the buyer at hand-off, and the buyer enters the code to confirm receipt (POST /api/marketplace/purchase-requests/:id/verify-delivery). In the actual backend, the buyer generates the code (POST .../delivery-code/generate, restricted to buyerId) and shares it with the seller, who then verifies it (POST .../delivery-code/verify, restricted to the selected seller). The frontend confirms this: step-5-receive-goods.tsx auto-generates and displays the code to the buyer, while delivery-code-verification.tsx is rendered for the seller to type the code in.

Doc Claim: Seller shares the 6-digit code verbally with the buyer at hand-off. Buyer enters the code via POST /api/marketplace/purchase-requests/:id/verify-delivery.

Code Reality: Buyer generates the code (POST .../delivery-code/generate), sees it in their dashboard, and verbally gives it to the seller. The seller enters it via POST .../delivery-code/verify. The backend enforces this: generate checks buyerId, verify checks selectedOffer.sellerId.

UAT Impact: QA must verify that (1) the buyer dashboard shows the delivery code in step-5-receive-goods, (2) the seller dashboard shows the code-entry field in delivery-code-verification, and (3) the backend returns 403 if either role tries the other's endpoint.

C5. Doc lists POST /api/marketplace/purchase-requests/:id/verify-delivery; actual endpoint is POST .../delivery-code/verify

Description: The Delivery Confirmation Flow documentation lists 'POST /api/marketplace/purchase-requests/:id/verify-delivery' as the endpoint for code verification. Neither controllerRoutes.ts nor routes.ts registers this path. The real endpoint is POST /api/marketplace/purchase-requests/:id/delivery-code/verify. The frontend correctly uses the real path via endpoints.delivery.verifyCode in src/lib/axios.ts.

Doc Claim: POST /api/marketplace/purchase-requests/:id/verify-delivery

Code Reality: POST /api/marketplace/purchase-requests/:id/delivery-code/verify (registered in routes.ts line 2790)

UAT Impact: Any test harness or API client using the documented path will receive 404. Verify the correct path is used end-to-end.

C6. Doc lists POST /api/marketplace/purchase-requests/:id/delivery-code for code regeneration; endpoint does not exist

Description: The Delivery Confirmation Flow lists 'POST /api/marketplace/purchase-requests/:id/delivery-code' as the admin regeneration endpoint. No such POST route exists in routes.ts or controllerRoutes.ts. The backend only has GET .../delivery-code (retrieve code), POST .../delivery-code/generate (create new code), and POST .../delivery-code/verify. The frontend's regenerateDeliveryCode action calls a non-existent '/delivery-code/regenerate' endpoint and falls back to generate. No POST to the bare /delivery-code path is registered anywhere.

Doc Claim: POST /api/marketplace/purchase-requests/:id/delivery-code — Manual code regeneration (admin)

Code Reality: No such endpoint exists. Only GET /delivery-code (retrieve), POST /delivery-code/generate (buyer), and POST /delivery-code/verify (seller) are registered.

UAT Impact: Admin code regeneration is broken. Testers must confirm that expired code recovery path is unavailable and document the workaround.

C7. Backend status enum includes 'pending_payment' and 'active'; these are absent from documented statuses

Description: The actual PurchaseRequest.status enum in the codebase (confirmed in both routes.ts logic and the frontend IPurchaseRequest type at src/types/marketplace.ts line 107-119) includes 'pending_payment' and 'active' as valid statuses. Neither appears in the documented Purchase Request Flow status list. The STATUS_PROGRESSION_ORDER noted in backend code also includes 'active' between 'pending' and 'received_offers'. Template batch-convert can create requests in 'pending_payment' or 'active' initial states.

Doc Claim: Statuses: pending, received_offers, in_negotiation, payment, processing, delivery, delivered, confirming, seller_paid, completed, finalized, archived, cancelled

Code Reality: Actual statuses include pending_payment and active (confirmed in IPurchaseRequest type and routes.ts workflow-steps handling for status:'pending_payment' and status:'active' branches). 'finalized' and 'archived' appear only in the doc but not in the frontend type definition.

UAT Impact: Test cases for status-based visibility, progression guards, and UI step rendering must cover 'pending_payment' and 'active'. Missing 'finalized'/'archived' in frontend types means those statuses cannot be rendered.

Domain: Seller Offer

C8. Doc claims POST /api/marketplace/offers creates an offer; actual endpoint is POST /api/marketplace/purchase-requests/:id/offers

Description: The documented API table lists 'POST /api/marketplace/offers' as the create-offer endpoint. No such flat route exists in the backend. The real endpoint registered in routes.ts line 1163 and marketplaceController.ts line 881 is POST /api/marketplace/purchase-requests/:id/offers, where :id is the purchaseRequestId path parameter. The frontend correctly uses the scoped path (src/lib/axios.ts endpoints.marketplace.requests.offers), so the mismatch is between doc and code, not frontend and backend.

Doc Claim: POST /api/marketplace/offers — Create offer

Code Reality: Route is POST /api/marketplace/purchase-requests/:id/offers (purchaseRequestId in path, not in body). Defined in routes.ts and marketplaceController.ts; frontend uses endpoints.marketplace.requests.offers which maps to this path.

UAT Impact: Any tester or integration client hitting POST /api/marketplace/offers will get a 404. Verify that the create-offer call goes to /purchase-requests/:id/offers.

C9. POST /api/marketplace/offers/:id/withdraw endpoint documented but does not exist

Description: The flow at step 16 and the API table both document 'POST /api/marketplace/offers/:id/withdraw — Seller withdraws'. The SellerOfferService.withdrawOffer() method exists (SellerOfferService.ts lines 427-443), but searching all of routes.ts and marketplaceController.ts finds zero route that calls it. The only way to achieve a 'withdrawn' status today is via PUT /api/marketplace/offers/:id/status with { status: 'withdrawn' }, which is accessible to seller, buyer, or admin without status guard. The dedicated withdraw route is entirely absent.

Doc Claim: POST /api/marketplace/offers/:id/withdraw — Seller withdraws; blocked once accepted or rejected.

Code Reality: No HTTP route for /offers/:id/withdraw exists. withdrawOffer() service method is dead code from the API surface. PUT /offers/:id/status accepts 'withdrawn' as a value but applies no pending-only guard at the route level (only the service-level SellerOfferService.updateOfferStatus does not guard status transitions either).

UAT Impact: The documented seller withdraw flow cannot be tested as documented. Testers must use PUT /offers/:id/status with { status: 'withdrawn' } and verify the pending-only guard is NOT enforced at the route level (regression risk).

C10. Doc lists GET /api/marketplace/offers/request/:requestId and GET /api/marketplace/offers/seller/:sellerId — neither route exists

Description: The API table documents two GET endpoints: 'GET /api/marketplace/offers/request/:requestId — Buyer view of offers on a request' and 'GET /api/marketplace/offers/seller/:sellerId — Seller's own offer history'. Neither route is registered in routes.ts or marketplaceController.ts. The actual routes are GET /api/marketplace/purchase-requests/:id/offers (list offers for a request) and there is no seller-history endpoint at all (getOffersBySeller() service method exists but is unexposed). The frontend uses the purchase-requests-scoped path correctly.

Doc Claim: GET /api/marketplace/offers/request/:requestId and GET /api/marketplace/offers/seller/:sellerId are documented as valid API endpoints.

Code Reality: GET /api/marketplace/purchase-requests/:id/offers is the real list endpoint (routes.ts line 1223). No route exists for /offers/seller/:sellerId; getOffersBySeller() in SellerOfferService is unreachable via HTTP.

UAT Impact: Hitting the documented GET paths returns 404. Verify buyer offer listing uses /purchase-requests/:id/offers. No seller offer history endpoint can be tested at all.

Domain: Payment

C11. DePay flow: /api/payment/decentralized/create does not exist; only /save is implemented

Description: The sequence diagram and API table in the DePay/Web3 flow documentation both reference POST /api/payment/decentralized/create as the intent-creation endpoint. The actual backend code exposes only POST /api/payment/decentralized/save for this purpose. There is no /create route in decentralizedPaymentRoutes. The frontend axios config in src/lib/axios.ts also only defines endpoints.payments.decentralized.save. Any external documentation or integration test that calls /create will receive a 404.

Doc Claim: POST /api/payment/decentralized/create (sequence diagram and API table) creates the payment intent

Code Reality: Only POST /api/payment/decentralized/save exists in both the backend route list and frontend endpoint config. No /create route is registered.

UAT Impact: QA must verify that the intent-creation step POSTs to /api/payment/decentralized/save and NOT /create. Any test harness using /create will silently fail with 404.

C12. DePay verify path mismatch: step narrative says :paymentId path param; API table says no path param

Description: The DePay flow step narrative specifies POST /api/payment/decentralized/verify/:paymentId with the payment ID as a path parameter. The API table documents POST /api/payment/decentralized/verify with no path param. The backend code exposes POST /api/payment/decentralized/verify/:paymentId. The frontend paymentBackendService.ts calls /api/payment/decentralized/verify/${paymentId} as a path param. The API docs table entry is wrong and will mislead QA and API consumers.

Doc Claim: API table: POST /api/payment/decentralized/verify (no path param). Step narrative: POST /api/payment/decentralized/verify/:paymentId

Code Reality: Backend route is POST /api/payment/decentralized/verify/:paymentId. Frontend src/web3/paymentBackendService.ts line 427 calls this with paymentId in the path.

UAT Impact: Tester must call POST /api/payment/decentralized/verify/:paymentId. Calling /verify without a path param will return 404.

C13. Frontend calls GET /payment/:id/status and POST /payment/:id/confirm — neither endpoint exists on backend

Description: The frontend actions getPaymentStatus() and getPaymentStatus()/confirmPayment() build URLs as /payment/:id/status and /payment/:id/confirm respectively. Neither endpoint is registered in the backend route list. The backend has no /status sub-route and no /confirm sub-route on individual payment documents. The getPaymentStatus action is actively called from the dispute payment-details-card component (src/sections/dispute/components/payment-details-card.tsx line 101), meaning the 'Verify' button in the dispute panel will always return a 404.

Doc Claim: API docs list GET /api/payment/:id with a payment document response but no /status or /confirm sub-routes. Frontend actions docs claim apiPath: /payment/:id/status and /payment/:id/confirm.

Code Reality: Backend routes: GET /api/payment/payments/:id (with auth) and GET /api/payment/:id (controller). No /status or /confirm sub-routes exist. dispute/payment-details-card.tsx actively calls getPaymentStatus() which hits the non-existent /status path.

UAT Impact: QA must verify: (1) the 'Verify' button in the dispute payment card returns a 404 in practice; (2) confirmPayment() is not reachable via the normal checkout UI, check whether any live component actually calls it.

C14. Frontend calls DELETE /payment/:id to cancel payment — no DELETE route exists

Description: cancelPayment() in src/actions/payment.ts sends a DELETE request to /payment/:id. The backend code exposes no DELETE method on any payment route. The backend only has GET and PUT on that path. cancelPayment() is locally defined in the web3 context as a UI state reset (no HTTP call), but the action-layer version makes a real HTTP DELETE that will 404.

Doc Claim: Frontend actions list cancelPayment with apiPath: /payment/:id and method: DELETE.

Code Reality: Backend has no DELETE handler on /api/payment/:id or /api/payment/payments/:id. The cancelPayment used in web3-provider.tsx is a local state reset and does not call the action.

UAT Impact: QA must confirm that no UI flow calls the action-layer cancelPayment(). If any component imports and invokes it, it will 404.

C15. SHKeeper flow documents GET /api/payment/shkeeper/status/:paymentId — endpoint does not exist

Description: The SHKeeper flow step 32 states the frontend polls GET /api/payment/shkeeper/status/:paymentId to transition to 'Payment received'. This endpoint is listed in the flow's apiEndpoints. The actual backend code and API docs do not register this route. The frontend also has no call to this path in axios config or component code. The frontend actually relies on socket events (payment-update) and the template-checkout-payment-confirmed custom event for status transitions, not polling.

Doc Claim: GET /api/payment/shkeeper/status/:paymentId is listed as an API endpoint used by the frontend checkout page for polling.

Code Reality: No such route exists in the backend code. Frontend checkout components listen to socket events (payment-update, template-checkout-payment-confirmed) rather than polling any shkeeper status endpoint.

UAT Impact: QA must verify that the SHKeeper checkout page does not call this path. Status transitions should be tested via socket event reception, not HTTP polling.

Domain: Dispute

C16. PATCH /api/disputes/:id/status has no role guard — any authenticated user can change dispute status

Description: The updateStatus controller method calls DisputeService.updateStatus with no call to authorizeRoles. Any logged-in buyer or seller can therefore PATCH /api/disputes/:id/status to set status=resolved, status=closed, or even status=rejected, completely bypassing admin authority. The dashboard router in /backend/src/routes/disputeRoutes.ts attaches only authenticateToken.

Doc Claim: API docs state 'Bearer JWT (admin)' is required for PATCH /api/disputes/:id/status. Flow step 11 describes this as an admin-only action.

Code Reality: No authorizeRoles guard exists in DisputeController.updateStatus or in the route definition. Any authenticated user with the dispute ID can transition the dispute to any status including resolved or closed.

UAT Impact: QA must verify that a buyer or seller token (non-admin) is rejected with 403 when calling PATCH /api/disputes/:id/status. Currently it succeeds with 200, which is a privilege-escalation bug in production.

C17. POST /api/disputes/:id/resolve (dashboard) has no role guard — any user can resolve a dispute

Description: The dashboard resolveDispute controller in /backend/src/controllers/disputeController.ts does not call authorizeRoles('admin'). Only authenticateToken is applied on the router. Any authenticated buyer, seller, or third party who knows the dispute _id can post a resolution including action=ban_seller.

Doc Claim: API docs state 'Bearer JWT (admin)' is required for POST /api/disputes/:id/resolve. Flow step 12 treats resolution as an admin action.

Code Reality: No authorizeRoles guard in the controller or route. Any authenticated user can call POST /api/disputes/:id/resolve with any action value. The releaseHold resolve endpoint (/api/disputes/:purchaseRequestId/resolve) correctly uses authorizeRoles('admin'), but the dashboard router does not.

UAT Impact: QA must verify that POST /api/disputes/:disputeId/resolve returns 403 for non-admin tokens. Currently returns 200 and persists the resolution, including destructive actions like ban_seller.

C18. Route shadowing: /:purchaseRequestId/raise and /:purchaseRequestId/resolve may collide with /:id routes

Description: Both /api/disputes route handlers (dashboardDisputeRoutes at line 521 and releaseHold disputeRoutes at line 585 in /backend/src/app.ts) are mounted on the same path /api/disputes. Express processes them in registration order. POST /api/disputes/:purchaseRequestId/resolve in the second router can potentially shadow POST /api/disputes/:id/resolve in the first router if the param patterns match. GET /api/disputes/:purchaseRequestId/status is defined only in the second router, but a request like GET /api/disputes/abc123/status could match GET /api/disputes/:id in the first router and return a 404 or wrong response before reaching the status handler — depending on Express route matching order.

Doc Claim: The doc's docFlags flag this as a potential problem: 'Second mount's paths overlap with :id routes from the first mount — potential route shadowing risk.'

Code Reality: Confirmed in app.ts: dashboardDisputeRoutes is mounted first (line 521); releaseHold disputeRoutes is mounted second (line 585). Express evaluates routes in order, so for POST /api/disputes/:purchaseRequestId/resolve the dashboard router's POST /:id/resolve will match first, executing the Dispute CRUD resolve instead of the purchaseRequest hold-clear logic.

UAT Impact: QA must test POST /api/disputes/{purchaseRequestId}/resolve with a valid purchaseRequestId to confirm whether the hold-clearing logic fires or the Dispute model resolve fires. The outcome depends on whether the ID happens to match a Dispute _id. This is non-deterministic and high-severity.

Domain: Chat

C19. sendFileMessage posts to wrong endpoint — missing /file suffix

Description: The frontend sendFileMessage action in /frontend/src/actions/chat.ts (line 386) sends multipart form data to endpoints.chat.sendMessage which resolves to POST /api/chat/:id/messages. The actual file upload endpoint on the backend is POST /api/chat/:id/messages/file. The API docs also list the correct path as POST /api/chat/:id/messages/file. As a result, file uploads hit the text-message handler, which expects a JSON body with a string content field, not a multipart file payload — the upload will fail or silently discard the attachment.

Doc Claim: Flow doc step 13 and API docs specify POST /api/chat/:chatId/upload (flow doc) or POST /api/chat/:id/messages/file (API docs) for file uploads.

Code Reality: Frontend sendFileMessage POSTs multipart/form-data to /chat/:id/messages (same endpoint as text messages). The axios endpoints object has no entry for /chat/:id/messages/file — only sendMessage: '/chat/:id/messages'.

UAT Impact: QA must verify: pick a file in the chat input, send it, and confirm the backend receives it at the /messages/file route and returns a message with an attachments array. Expect this to fail with the current code.

C20. archiveConversation uses PUT but backend exposes PATCH /api/chat/:id/archive

Description: The frontend archiveConversation action (/frontend/src/actions/chat.ts line 289) calls axiosInstance.put(...), and the axios endpoints config entry is '/chat/:id/archive'. The backend registers this as PATCH /api/chat/:id/archive and the API docs also document it as PATCH. HTTP method mismatch means the frontend will receive a 404 or 405 from the backend on every archive attempt.

Doc Claim: API docs list PATCH /api/chat/:id/archive. Backend code registers PATCH /api/chat/:id/archive.

Code Reality: Frontend action calls axiosInstance.put(endpoints.chat.archive...) — HTTP verb is PUT, not PATCH.

UAT Impact: QA must verify: attempt to archive a chat from the UI and confirm the backend receives a PATCH request and returns success. With the current code this will fail.

Domain: Notification

C21. POST /api/notifications/read-all does not exist — correct method is PATCH

Description: The flow doc's step 8 narrative references 'POST /api/notifications/read-all' for bulk marking. The API table also lists 'POST /api/notifications/read-all'. In reality the backend exposes this as PATCH /notifications/mark-all-read (not POST, and not at the /read-all path). The frontend correctly uses PATCH /notifications/mark-all-read (as seen in axios.ts endpoints.notifications.markAllRead and the actions/notification.ts markAllNotificationsAsRead function). Any test or integration that POSTs to /notifications/read-all will receive a 404.

Doc Claim: POST /api/notifications/read-all (mark all notifications read)

Code Reality: PATCH /notifications/mark-all-read — method is PATCH not POST, and the path segment is 'mark-all-read' not 'read-all'

UAT Impact: Verify that clicking 'Mark all read' in the notifications drawer sends PATCH /notifications/mark-all-read and returns modifiedCount. A POST to /notifications/read-all must 404.

C22. GET /notifications/:id is a broken workaround — only returns the user's most-recent notification

Description: The backend's getNotificationById controller does not perform a direct DB lookup. Instead it calls getUserNotifications(userId, 1, 1) — fetching page 1 with limit 1 — and then does an in-memory _id string match. This means any notification that is not the single most-recent record for that user will always return 404, regardless of ownership. This endpoint is completely unreliable for direct notification lookup by ID. The flow documentation does not mention this endpoint at all.

Doc Claim: No mention of GET /notifications/:id in the flow doc

Code Reality: GET /notifications/:id exists but silently fails for all but the user's most-recent notification due to a pagination bug in the controller

UAT Impact: Attempt to fetch a notification by its ID when it is not the user's latest notification — expect 404 erroneously. Verify this regression before exposing the endpoint to consumers.

Domain: Points/Referral

C23. Referral flow doc claims 'referral-signup' socket event; backend never emits it from PointsService

Description: The Referral Flow doc lists 'referral-signup' as a PointsService-emitted socket event. In reality, PointsService.processReferralReward emits 'referral-reward', not 'referral-signup'. The 'referral-signup' event is only emitted from authController.ts (lines 704, 1132) during the sign-up attribution step — not from PointsService. The frontend points-main-view.tsx listens for both 'referral-signup' and 'referral-reward' and handles them separately, so wiring is functional, but the doc incorrectly attributes 'referral-signup' as a PointsService/points-domain event rather than an auth-domain event.

Doc Claim: Referral Flow step 7: 'emits referral-signup to user-{referrerId} with the referee's name, email, and updated total'. Socket events section lists 'referral-signup — emitted to user-{referrerId} on referee creation'.

Code Reality: PointsService only emits 'referral-reward' (PointsService.ts:417). 'referral-signup' is emitted in authController.ts (lines 704, 1132) and is an auth-domain event, not a points-domain event.

UAT Impact: Verify that when a new user registers with a referral code, the referrer's dashboard receives a toast notification. Then verify separately that when that referred user completes a purchase and the request is marked 'completed', the referrer receives a second 'referral-reward' toast with points earned.

C24. PointTransaction type 'refund' in doc does not exist; actual types are 'earn', 'spend', 'expire'

Description: The Referral Flow doc lists 'PointTransaction.type: refund' as a valid status value. The actual Mongoose schema enum for PointTransaction.type is ['earn', 'spend', 'expire']. There is no 'refund' type. The frontend IPointTransaction interface also correctly uses 'earn' | 'spend' | 'expire'. Any attempt to create a refund-type transaction will fail schema validation.

Doc Claim: Referral Flow statuses list 'PointTransaction.type: refund' as a valid type.

Code Reality: PointTransaction model (schema enum): ['earn', 'spend', 'expire']. No 'refund' type exists anywhere in the schema or service code.

UAT Impact: Confirm that there is no refund/reversal mechanism for points. If a purchase is cancelled after points were redeemed, check whether points are restored — if they are, verify the mechanism used (it should create an 'earn' transaction, not a 'refund' type).

Domain: User Management

C25. Frontend admin actions use /users/admin/* (legacy) instead of /user/admin/* (new controller)

Description: All admin mutation endpoints in user.ts (createUser, updateUser, deleteUser, updateUserStatus, updateUserRole, toggleUserStatus, getUserDependencies, getAllUsers) resolve via endpoints.users.admin.* which map to /users/admin/... paths (the legacy router). The new controller routes live under /api/user/admin/... (singular). The two controllers have meaningfully different behavior: the legacy DELETE does a hard findByIdAndDelete, whereas the new DELETE does a soft status='deleted'. The legacy status update uses an isActive boolean; the new controller accepts a status string (active/suspended/deleted). Sending traffic to the wrong router produces incorrect behavior silently.

Doc Claim: API doc lists both /api/user/admin/... (primary) and /api/users/admin/... (legacy alias) endpoints. Frontend action doc says createUser -> /users/admin/create, deleteUser -> /users/admin/:id (soft delete).

Code Reality: axios.ts maps endpoints.users.admin.* to /users/admin/* (legacy). The legacy DELETE is a hard findByIdAndDelete; the new /api/user/admin/:userId DELETE is a soft delete setting status='deleted'. Frontend action comment says 'soft delete' but calls the hard-delete legacy route.

UAT Impact: QA must verify: (1) deleteUser actually soft-deletes (status='deleted') vs hard-deletes the record. (2) updateUserStatus sends correct payload format — frontend sends { status } string but legacy endpoint may expect { isActive } boolean. (3) createUser, toggleUserStatus, getUserDependencies, getAllUsers — confirm they hit the intended controller by checking response shape and DB state.

C26. updateUserStatus and updateUserRole use PUT but backend only accepts PATCH

Description: In user.ts, updateUserStatus calls axiosInstance.put(...) and updateUserRole calls axiosInstance.put(...). The backend exposes these as PATCH /api/users/admin/:userId/status and PATCH /api/users/admin/:userId/role (both legacy and new controllers). PUT is not registered for these sub-routes; the calls will receive 404 or 405 in production.

Doc Claim: API doc specifies PATCH /api/user/admin/:userId/status and PATCH /api/user/admin/:userId/role.

Code Reality: user.ts line 162: axiosInstance.put(endpoints.users.admin.status...). user.ts line 175: axiosInstance.put(endpoints.users.admin.role...). Backend registers PATCH, not PUT.

UAT Impact: QA must exercise the 'Update Status' and 'Update Role' admin actions and confirm HTTP 200 is returned, not 404/405. Test should also verify the DB record is actually updated.

C27. Frontend updateUserStatus sends wrong status values

Description: The TypeScript signature for updateUserStatus accepts 'active' | 'inactive' | 'pending' and sends the value as-is in the request body. The backend (both controllers) only recognises 'active', 'suspended', and 'deleted' as valid status values. The values 'inactive' and 'pending' are not valid on the backend and will be rejected or silently ignored.

Doc Claim: API doc states PATCH /api/user/admin/:userId/status accepts isActive boolean (new controller) or status string active/suspended/deleted.

Code Reality: user.ts line 157-168: status typed as 'active' | 'inactive' | 'pending'. Backend User.status enum: active, suspended, deleted.

UAT Impact: QA must attempt to set a user status to 'inactive' and 'pending' via the admin UI and confirm these are rejected by the backend. Only 'active' and 'suspended' (and 'deleted') should be accepted.

Domain: Admin Operations

C28. fetch-tx and auto-fetch-missing have no authentication despite doc claiming admin-only

Description: The API doc lists POST /api/payment/payments/:id/fetch-tx and POST /api/payment/payments/auto-fetch-missing as admin-protected endpoints requiring Bearer JWT with role=admin. The actual backend registers both with NO authentication middleware at all — any unauthenticated caller can trigger on-chain fetches or read full payment state. The GET /api/payment/payments/:id/debug endpoint has the same problem (also has no auth).

Doc Claim: POST /api/payment/payments/:id/fetch-tx and POST /api/payment/payments/auto-fetch-missing require Bearer JWT, role=admin

Code Reality: Both endpoints are mounted with no authenticateToken middleware. The backend notableLogic explicitly flags this: 'UNPROTECTED DEBUG/UTILITY ENDPOINTS: ... have NO authentication middleware — any unauthenticated caller can read full payment internals or trigger on-chain fetches.'

UAT Impact: QA must verify: (1) call POST /api/payment/payments/test123/fetch-tx without an Authorization header — it should return 401 but currently returns 200. (2) Same for auto-fetch-missing. (3) Same for GET /api/payment/payments/:id/debug. All three are exploitable in production without credentials.

C29. GET /api/admin/scanner/status has no authentication middleware despite being under /api/admin/

Description: The scanner status proxy endpoint sits under the /api/admin/ route prefix, which would conventionally imply admin auth. The API doc lists it as requiring Bearer JWT role=admin. But the backend explicitly has no authenticateToken middleware on this route — it proxies directly to AMN_SCANNER_URL without any auth check.

Doc Claim: GET /api/admin/scanner/status requires Bearer JWT, role=admin

Code Reality: Backend notes: 'SCANNER STATUS PROXY: GET /api/admin/scanner/status has no authenticateToken middleware despite being an /api/admin/ route.' Any unauthenticated request can query the scanner.

UAT Impact: QA must verify: call GET /api/admin/scanner/status without Authorization header and confirm it currently returns scanner data (200). It should return 401.

C30. Shkeeper release/refund doc paths do not match backend paths

Description: The API doc describes four admin shkeeper endpoints under the /api/payment/shkeeper/:id/ prefix: release, release/confirm, refund, and refund/confirm. The actual backend routes these under /api/payment/:id/ (without the /shkeeper/ segment). Any client built against the documented paths will receive 404 errors.

Doc Claim: POST /api/payment/shkeeper/:id/release, POST /api/payment/shkeeper/:id/release/confirm, POST /api/payment/shkeeper/:id/refund, POST /api/payment/shkeeper/:id/refund/confirm

Code Reality: Backend registers: POST /api/payment/:id/release, POST /api/payment/:id/release/confirm, POST /api/payment/:id/refund, POST /api/payment/:id/refund/confirm. The /shkeeper/ path segment is absent. The frontend payment actions also use the correct /payment/:id/* path (via endpoints.payments.details), confirming the doc is wrong.

UAT Impact: QA must verify: POST /api/payment/shkeeper/{id}/release returns 404. POST /api/payment/{id}/release with valid admin token returns the expected escrow-release transaction.

Domain: Trezor Safekeeping

C31. No frontend implementation for any Trezor API endpoint

Description: A comprehensive search across all .ts and .tsx files in /Users/manwe/CascadeProjects/escrow/frontend/src finds zero calls to any of the four documented Trezor endpoints (GET /api/trezor/registration-message, POST /api/trezor/register, POST /api/trezor/addresses/next, POST /api/trezor/operation-message) and also zero calls to the undocumented POST /api/trezor/verify-operation and GET /api/trezor/account endpoints that exist in the backend. The only Trezor reference in the entire frontend src tree is a brand logo entry in wallet-icons.ts. There is no Trezor registration page, no xpub input, no signing UI, and no admin Trezor safekeeping panel anywhere in the frontend.

Doc Claim: Step 1: 'User connects a Trezor in the frontend and exports an Ethereum account xpub.' Steps 2-4 describe a frontend-driven registration challenge-sign-submit flow. Steps 9-11 describe admin Trezor signing for operation approval.

Code Reality: No frontend page, component, action, or hook references any /api/trezor/* endpoint. The Trezor Connect SDK is not imported anywhere. The frontend actions/payment.ts confirmReleaseTx and confirmRefundTx only send { txHash } with no trezor object (message+signature).

UAT Impact: The entire Trezor registration flow is untestable via the frontend UI. There is no page to navigate to for xpub registration. The admin Trezor sign-before-release flow is also untestable. QA must verify whether any external (non-Next.js) admin tool handles this, or whether this feature is entirely backend-only at this stage.

C32. Release/refund confirmation does not include Trezor signature payload

Description: When TREZOR_SAFEKEEPING_REQUIRED=true the backend assertTrezorSignatureForOperation guard blocks release/refund unless the request body includes a valid trezor object (message + signature). The frontend confirmReleaseTx and confirmRefundTx in /Users/manwe/CascadeProjects/escrow/frontend/src/actions/payment.ts post only { txHash, ...extra } — there is no code anywhere in the frontend that builds an operation-message request, prompts for Trezor signing, or appends the resulting signature to the release/refund confirmation body. In safekeeping-enforced mode, every admin release/refund attempt from the frontend will be rejected by the backend.

Doc Claim: Steps 9-12: 'Admin requests the exact operation message via POST /api/trezor/operation-message, signs the operation message with the Trezor, submits release/refund confirmation including txHash and trezor object (message + signature).'

Code Reality: confirmReleaseTx (line 487) and confirmRefundTx (line 503) in src/actions/payment.ts post { txHash, ...extra } — no trezor field is ever constructed or passed. No component in the dispute or admin sections calls POST /api/trezor/operation-message or POST /api/trezor/verify-operation.

UAT Impact: QA must verify: (1) with TREZOR_SAFEKEEPING_REQUIRED=true, admin release/refund from the UI fails with a backend 4xx; (2) with TREZOR_SAFEKEEPING_REQUIRED=false, release/refund works normally. Confirm whether the gap is intentional (feature flagged off) or a missing implementation.

Domain: Delivery

C33. Delivery code is generated by the buyer, not the seller/admin

Description: The documentation step-by-step narrative describes the seller clicking 'Mark as shipped' which triggers code generation as a backend side-effect. In reality, code generation is a separate buyer-initiated action via POST /api/marketplace/purchase-requests/:id/delivery-code/generate. Both the legacy routes.ts (line 2738) and the marketplaceController (line 1403) enforce that only the buyer (request.buyerId === userId) may call this endpoint. The seller has no role in triggering code generation.

Doc Claim: Step 4 states: 'Backend invokes DeliveryService.generateDeliveryCode(requestId)' as an automatic side-effect of the seller marking shipment. The API table lists 'POST /api/marketplace/purchase-requests/:id/delivery-code — Manual code regeneration (admin)'.

Code Reality: POST /api/marketplace/purchase-requests/:id/delivery-code/generate is restricted to the buyer (buyerId check), requires status='delivery', and is a manually triggered action. There is no automatic code generation when the seller marks shipped. The endpoint listed in the doc (/delivery-code without the /generate suffix) does not exist in the backend; the actual path is /delivery-code/generate.

UAT Impact: QA must verify: (1) seller cannot call POST /delivery-code/generate — must receive 403; (2) admin cannot call it either — must receive 403; (3) buyer must explicitly call the endpoint after status reaches 'delivery' to get a code; (4) marking shipped does NOT auto-generate a code.

C34. Delivery code is verified by the seller, not the buyer

Description: The documentation states 'Buyer enters the code in the dashboard' and 'Frontend sends POST /api/marketplace/purchase-requests/:id/verify-delivery with {code}'. In reality the code is entered and submitted by the seller. Both routes.ts (line 2790) and marketplaceController (line 1447) check that the authenticated user is the selectedOffer.sellerId. The buyer generates the code; the seller submits it at handoff.

Doc Claim: Steps 7-9: 'Seller shares the 6-digit code verbally with the buyer at hand-off' then 'Buyer enters the code in the dashboard' and 'Frontend sends POST .../verify-delivery'.

Code Reality: POST /api/marketplace/purchase-requests/:id/delivery-code/verify enforces that req.user.id === selectedOffer.sellerId. The buyer has no access to submit a code. The correct endpoint path is /delivery-code/verify, not /verify-delivery as the doc states.

UAT Impact: QA must verify: (1) buyer cannot call POST /delivery-code/verify — must receive 403; (2) seller submitting correct code succeeds and transitions status to 'delivered'; (3) buyer calling the documented /verify-delivery path returns 404.

C35. Documented API endpoint paths do not match actual backend routes

Description: The documentation API table lists these endpoints which do not exist: 'POST /api/marketplace/purchase-requests/:id/delivery-code' (for manual code regeneration) and 'POST /api/marketplace/purchase-requests/:id/verify-delivery' (for buyer code submission). The actual backend endpoints are /delivery-code/generate and /delivery-code/verify respectively. There is no regenerate endpoint registered in any backend router; the frontend action falls back to /generate on 404.

Doc Claim: API endpoints table: 'POST /:id/delivery-code — Manual code regeneration (admin)' and 'POST /:id/verify-delivery — Buyer confirms with code'.

Code Reality: Registered routes: POST /:id/delivery-code/generate (buyer only), POST /:id/delivery-code/verify (seller only), GET /:id/delivery-code (buyer+seller), GET /:id/delivery-code/status (buyer+seller). No /verify-delivery or bare /delivery-code POST routes exist. Frontend axios.ts lists /delivery-code/regenerate as the regenerate endpoint but the backend has no such route.

UAT Impact: QA must confirm: POST /delivery-code and POST /verify-delivery both return 404. The correct working endpoints are /delivery-code/generate and /delivery-code/verify.


Major Findings

Major findings will cause test failures, wrong behavior, or documentation-code mismatches that affect integration teams.

Domain: Authentication

M1. Axios interceptor only handles 401, not 403, for token refresh — doc says both

Description: The Authentication Flow doc step 17 says the Axios interceptor 'handles 401/403 by triggering the refresh flow'. The actual interceptor in /Users/manwe/CascadeProjects/escrow/frontend/src/lib/axios.ts line 105 only triggers the refresh flow for status === 401. 403 responses (for example from EMAIL_NOT_VERIFIED or a blocked account) are not handled by the refresh flow and will propagate as errors.

Doc Claim: Step 17: 'Axios interceptor attaches Authorization: Bearer ${accessToken} to every subsequent request and handles 401/403 by triggering the refresh flow.'

Code Reality: axios.ts:105: if (status === 401 && !isAuthRoute && !originalRequest?._retry) — 403 is not included.

UAT Impact: QA must verify that a 403 response from the backend (e.g., email not verified) is surfaced as an error to the user rather than silently attempting a token refresh and looping.

M2. Password reset code is 6 digits, not 8 — backend API doc and controller comment are wrong

Description: The backend API docs notableLogic entry states 'Generates 8-digit code, 1-hour TTL' for the request-password-reset endpoint. The authController.ts comment at line 838 also says '// Generate reset code (8 digits)'. However, authService.generateVerificationCode() (authService.ts:226-228) always generates a 6-digit code via Math.floor(100000 + Math.random() * 900000), and isValidVerificationCode() validates with /^\d{6}$/ (authService.ts:237). The Password Reset Flow doc correctly says '6-digit code' but it conflicts with the API-level description. An 8-digit code will always fail validation.

Doc Claim: Backend API notable logic: 'Generates 8-digit code'. authController.ts comment: '// Generate reset code (8 digits)'.

Code Reality: authService.ts:226-228 generates exactly 6 digits. isValidVerificationCode() at line 237 validates /^\d{6}$/. Any 8-digit code submitted to reset-password-with-code returns 400 Invalid code format.

UAT Impact: QA must confirm that the password reset email delivers a 6-digit code (not 8) and that submitting it to POST /api/auth/reset-password-with-code succeeds.

M3. Login rate limit counts all attempts, not only failures — doc says '5 failures'

Description: The Authentication Flow doc step 5 and edge case describe the limit as '5 failures in 15 min'. The actual implementation calls rateLimitService.checkLoginAttempts() (which calls checkLimit(), which increments on every invocation) BEFORE the password comparison. This means every login attempt — including ones with correct credentials — increments the counter. A successful login does reset the counter via resetLoginAttempts() afterward, but if a user makes 4 correct logins followed by 1 failed attempt without a reset in between, they can be locked out faster than the doc implies.

Doc Claim: Step 5: '5 failures in 15 min → 429 TOO_MANY_ATTEMPTS'. Edge case: '5 failed login attempts within 15 minutes → 429 TOO_MANY_ATTEMPTS'.

Code Reality: rateLimitService.checkLimit() increments the counter on every call (line 49: redisService.incr). The check is made before password validation. The counter is only reset on a successful full login. 5 total attempts (not 5 failures) within the window will trigger the lock.

UAT Impact: QA must verify rate-limiting with a mix of correct and incorrect passwords to confirm the lockout triggers on attempt count, not failure count only.

M4. changePassword action is defined but never wired to any page or UI component

Description: The changePassword action is implemented in action.ts (line 263) and the endpoint POST /api/auth/change-password exists in the backend, but no dashboard page or view component calls it. The frontend actions audit also flags this. The changePasswordValidation middleware enforces password complexity (uppercase, lowercase, digit required). There is no 'Change Password' UI anywhere under /dashboard.

Doc Claim: Frontend actions: changePassword → apiPath: /auth/change-password, method: POST (implied to be accessible from the UI).

Code Reality: changePassword() function exists in action.ts:263 but grep across all .tsx files finds no call site. No dashboard page under /dashboard renders a change-password form.

UAT Impact: QA cannot test password change from the UI because there is no UI for it. Verify the endpoint directly via API. This is also an incomplete feature that should be tracked.

M5. Passkey sign-in calls Next.js /api/auth/passkey/* paths directly — no Next.js route handlers exist

Description: The frontend actions table notes passkey actions as 'Next.js route handler, not direct backend'. However there are no Next.js API route files under /Users/manwe/CascadeProjects/escrow/frontend/src/app/api/ for passkey paths (only /api/health exists). PasskeySignIn.tsx calls fetch('/api/auth/passkey/authenticate/challenge') which is resolved by the Next.js rewrite rule in next.config.ts (source: '/api/:path*' → backend) — it goes directly to the backend. There are no intermediate Next.js server-side handlers.

Doc Claim: Frontend actions: registerPasskey (WebAuthn challenge/complete) and authenticateWithPasskey — notes say 'Next.js route handler, not direct backend'.

Code Reality: No Next.js route.ts files exist for passkey paths. next.config.ts rewrites /api/:path* to the backend. All passkey API calls proxy directly to the Express backend.

UAT Impact: QA must configure CORS and ensure the backend PASSKEY_RP_ORIGIN matches the Next.js frontend URL, since there is no Next.js intermediary to mutate headers or credentials.

M6. reset-password-with-code has no password complexity validation middleware — reset-password (token) does

Description: POST /api/auth/reset-password is wired with passwordResetValidation middleware which enforces 6+ chars, uppercase+lowercase+digit. POST /api/auth/reset-password-with-code has no validation middleware at all (authRoutes.ts:54-56: no middleware between route and controller). The controller only validates email/code/password existence and the 6-digit code format. A new password of '123456' or 'aaaaaa' is accepted on reset-with-code but would be rejected on token-based reset.

Doc Claim: API docs: reset-password-with-code body: 'email, code, password'. No note about differing complexity requirements.

Code Reality: authRoutes.ts:49-53: reset-password uses passwordResetValidation. authRoutes.ts:54-57: reset-password-with-code has no validation middleware. authValidation.ts:44-54: passwordResetValidation requires /^(?=.[a-z])(?=.[A-Z])(?=.*\d)/.

UAT Impact: QA must test that reset-password-with-code accepts weak passwords that would be rejected by the token-based reset, and decide if this is an intentional design or a validation gap to fix.

Domain: Purchase Request

M7. Negotiation Flow: PATCH /api/marketplace/offers/:id documented but backend only has PATCH in routes.ts, not in controllerRoutes.ts; frontend uses PUT

Description: The Negotiation Flow lists PATCH /api/marketplace/offers/:id for offer edits. The legacy routes.ts registers PATCH /offers/:id (line 1260). However, the controllerRoutes.ts (the primary router) registers PUT /api/marketplace/offers/:id/status — not a bare PATCH on /offers/:id. The frontend updateOffer action uses axiosInstance.put against endpoints.marketplace.offers.update ('/marketplace/offers/:id'), which does not match any known route in controllerRoutes.ts. Both the doc and the frontend are misaligned with the active controller.

Doc Claim: PATCH /api/marketplace/offers/:id

Code Reality: routes.ts registers PATCH /offers/:id (legacy, auth required). controllerRoutes.ts has no offer update route other than PUT /offers/:id/status. Frontend uses PUT /marketplace/offers/:id which maps to neither registered path in the controller router.

UAT Impact: Verify that offer price/ETA edits from the buyer or seller actually reach the backend without 404. Confirm which router (legacy or controller) handles the request in the mounted app.

M8. Negotiation Flow: POST /api/marketplace/offers/:id/withdraw documented; no such route exists in backend

Description: The Negotiation Flow documents 'POST /api/marketplace/offers/:id/withdraw' as the seller withdrawal endpoint. Neither controllerRoutes.ts nor routes.ts contains a /withdraw sub-path on offers. The documented axios endpoint list in the frontend also has no withdraw action. Withdrawal is instead handled via PUT /marketplace/offers/:id/status with body {status:'withdrawn'}, which routes.ts line 1914 handles.

Doc Claim: POST /api/marketplace/offers/:id/withdraw

Code Reality: No /withdraw endpoint exists. Withdrawal is done via PUT /offers/:id/status with status='withdrawn' (routes.ts line 1914). Frontend has no dedicated withdraw action either.

UAT Impact: Any test that posts to the withdraw path will get 404. Correct test must use PUT /offers/:id/status with {status:'withdrawn'}.

M9. Purchase Request Flow step 4: sellers fetched from GET /api/users/sellers; actual endpoint is GET /api/marketplace/sellers

Description: The Purchase Request Flow step 4 states the buyer selects preferred sellers via a typeahead bound to GET /api/users/sellers. In reality, the sellers endpoint is GET /api/marketplace/sellers (registered in both controllerRoutes.ts line 183 and routes.ts line 1441). The frontend getSellers action calls endpoints.marketplace.sellers ('/marketplace/sellers'), confirming the real path.

Doc Claim: Typeahead bound to GET /api/users/sellers

Code Reality: GET /api/marketplace/sellers (controllerRoutes.ts line 183, frontend src/lib/axios.ts line 268)

UAT Impact: API tests hitting /api/users/sellers will get 404. Integration tests should use /api/marketplace/sellers.

M10. Purchase Request Flow step 5: attachments uploaded via POST /api/files/upload; actual endpoint is POST /api/marketplace/purchase-requests/:id/attachments

Description: The documented flow says optional attachments on the Review step are uploaded via POST /api/files/upload. The frontend uploadRequestAttachment action posts to endpoints.marketplace.requests.attachments = '/marketplace/purchase-requests/:id/attachments'. No general /api/files/upload endpoint is used in the wizard.

Doc Claim: Step 5 — uploads optional attachments via POST /api/files/upload

Code Reality: Frontend uses POST /marketplace/purchase-requests/:id/attachments (src/actions/marketplace.ts line 326-339)

UAT Impact: Attachment upload tests must target the correct endpoint. Verify the backend registers /purchase-requests/:id/attachments and returns the expected shape.

M11. Delivery Confirmation doc says status goes delivery→delivered via buyer code entry; backend code flow reverses actors

Description: The doc says backend sets status 'delivered' after the buyer enters the delivery code successfully. In the actual backend, the seller calls POST .../delivery-code/verify, which triggers updatePurchaseRequestStatus(id, 'delivered') (routes.ts line 2828). The buyer's confirmDelivery endpoint (PATCH .../confirm-delivery in controllerRoutes.ts) is a separate fast-track path that also moves to 'delivered'. These are two distinct paths to 'delivered' that are conflated in the doc.

Doc Claim: Buyer enters code → POST .../verify-delivery → backend flips status to 'delivered'

Code Reality: Seller calls POST .../delivery-code/verify with the code the buyer gave them → backend sets 'delivered'. Separately, PATCH .../confirm-delivery is a buyer fast-track that also sets 'delivered' without requiring a code.

UAT Impact: Test both paths independently: (1) seller verifies code → request becomes 'delivered', (2) buyer confirm-delivery without code → request becomes 'delivered'. Confirm neither path is blocked when the other was already used.

M12. Documented socket event 'request-cancelled' not emitted or listened to anywhere in codebase

Description: The Purchase Request Flow documents a 'request-cancelled' socket event emitted to user-{buyerId} and user-{sellerId} when the buyer cancels. A grep of the entire frontend socket layer and backend routes shows no emission or listener for this event name. The backend routes.ts emits purchase-request-update with eventType 'status-changed' for all status transitions, including cancellation.

Doc Claim: request-cancelled (emitted to user-{buyerId} and user-{sellerId} when buyer cancels)

Code Reality: No emission of 'request-cancelled' exists in any backend file. Cancellation emits purchase-request-update with eventType:'status-changed'.

UAT Impact: Any frontend component or test listening for 'request-cancelled' will never receive it. Cancellation UI must be verified via the 'purchase-request-update' + 'status-changed' path instead.

M13. Documented 'join-request-room' emitted by frontend on detail page; actual rooms use seller-specific rooms via 'join-seller-room'

Description: The Purchase Request Flow lists 'join-request-room' as a client-to-server event emitted on detail page mount. The socket-context.tsx confirms this exists. However, the useSellerMarketplaceSocket hook emits 'join-seller-room' / 'leave-seller-room' (use-marketplace-socket.ts lines 339, 344) which is not documented. The backend app.ts presumably handles both but the doc only mentions one.

Doc Claim: join-request-room (emitted by frontend on detail page mount to subscribe buyer to request-{id} room)

Code Reality: Frontend also emits 'join-seller-room'/'leave-seller-room' for sellers and 'join-buyer-room'/'leave-buyer-room' for buyers (use-marketplace-socket.ts). These room joins are undocumented.

UAT Impact: Verify that seller real-time events (new offers, offer updates, payment events) arrive correctly via the seller room, not just the request room.

M14. Backend emits 'new-purchase-request' to room 'sellers'; doc describes 'new-notification' to each seller via user-{sellerId}

Description: The Purchase Request Flow step 11 says the backend fans out notifications to sellers via Socket.IO by emitting to 'user-{sellerId}' for each seller. The actual backend code emits 'new-purchase-request' to the shared 'sellers' room (confirmed in backend socket events list). The frontend useSellerMarketplaceSocket listens for 'new-purchase-request' (use-marketplace-socket.ts line 392), not per-user notifications.

Doc Claim: Backend emits via Socket.IO to user-{sellerId} for each notified seller

Code Reality: Backend emits 'new-purchase-request' to room 'sellers' (all sellers) for public requests. Per-seller notifications use 'seller-offer-update' and 'new-notification' separately.

UAT Impact: Verify new public requests appear in real time on all connected seller dashboards. Verify private requests (with specific preferredSellerIds) do not appear on other sellers' dashboards.

M15. Frontend actions for delivery-code/regenerate, delivery-code/attempts, and /delivery/stats call endpoints that do not exist in backend

Description: The frontend delivery.ts defines regenerateDeliveryCode (calls /delivery-code/regenerate), getDeliveryAttempts (calls /delivery-code/attempts), and getDeliveryStats (calls /delivery/stats). None of these paths are registered in routes.ts or controllerRoutes.ts. regenerateDeliveryCode catches the 404 and falls back to generateDeliveryCode as a workaround, but the fallback silently ignores the missing endpoint.

Doc Claim: Frontend actions list includes: regenerateDeliveryCode, getDeliveryAttempts, getDeliveryStats

Code Reality: Backend has no routes for /delivery-code/regenerate, /delivery-code/attempts, or /delivery/stats. These are phantom endpoints.

UAT Impact: Test that regenerateDeliveryCode falls back to generate without visible error. getDeliveryAttempts and getDeliveryStats will return 404 and throw — any UI calling them will fail unless errors are caught.

M16. Frontend searchPurchaseRequests calls /marketplace/purchase-requests/search which is not a backend endpoint

Description: The frontend defines searchPurchaseRequests pointing to /marketplace/purchase-requests/search. Neither controllerRoutes.ts nor routes.ts registers a /search sub-path. Search is handled via query parameters on the list endpoint GET /purchase-requests. The missingFrontendFeatures note also confirms there is no standalone search page.

Doc Claim: Frontend action: searchPurchaseRequests → GET /marketplace/purchase-requests/search

Code Reality: No /search endpoint exists in the backend. Search/filter is via query params on GET /marketplace/purchase-requests.

UAT Impact: Calling searchPurchaseRequests will produce a 404. Verify search functionality uses getPurchaseRequests with filter params instead.

M17. Frontend getMarketplaceStats calls /marketplace/purchase-requests/stats which has no backend handler

Description: The frontend defines getMarketplaceStats calling endpoints.marketplace.requests.stats = '/marketplace/purchase-requests/stats'. Neither controllerRoutes.ts nor routes.ts registers a /stats sub-path under purchase-requests. This will 404 in production.

Doc Claim: Frontend action: getMarketplaceStats → GET /marketplace/purchase-requests/stats

Code Reality: No /stats endpoint under /marketplace/purchase-requests is registered in the backend.

UAT Impact: Any dashboard page that calls getMarketplaceStats will receive a 404. Confirm no production UI currently depends on this.

M18. updatePurchaseRequest uses PUT in frontend but backend only registers PATCH /purchase-requests/:id

Description: The frontend updatePurchaseRequest action (marketplace.ts line 71) calls axiosInstance.put against endpoints.marketplace.requests.update = '/marketplace/purchase-requests/:id'. The backend controllerRoutes.ts and routes.ts register PATCH (not PUT) on /purchase-requests/:id. Sending PUT will result in 404 from the controller router.

Doc Claim: Frontend action updatePurchaseRequest → PUT /marketplace/purchase-requests/:id

Code Reality: Backend registers PATCH /purchase-requests/:id (controllerRoutes.ts registers nothing; routes.ts line 1007 has router.patch). Frontend sends PUT, causing a method mismatch.

UAT Impact: Test editing a purchase request from the buyer edit view. A PUT request should return 404/405; only PATCH should succeed.

M19. Purchase Request Flow: description minimum is documented as 20 chars; frontend schema enforces 5 chars minimum

Description: The Purchase Request Flow documents the description field as '202000 chars'. The frontend RequestFormSchema (request-form-wizard.tsx line 94-98) sets the minimum to 5 characters, and the field is marked as optional. The two constraints are inconsistent.

Doc Claim: Step 2 — description (202000 chars)

Code Reality: Frontend zod schema: description is optional; if provided, minimum is 5 characters (request-form-wizard.tsx lines 92-108).

UAT Impact: Test submitting a description of 619 characters. Frontend should accept it. Verify whether the backend also enforces a minimum and which value takes precedence.

M20. Purchase Request Flow: step 3 shows urgency values low/medium/high; 'urgent' also exists

Description: The documented Budget step lists urgency options as low/medium/high. The actual frontend schema (request-form-wizard.tsx line 281) and IPurchaseRequest type also include 'urgent' as a valid urgency value. The backend statusValues list also includes 'urgent'. The doc is incomplete.

Doc Claim: urgency (low/medium/high)

Code Reality: urgency enum includes 'urgent' as a fourth value (request-form-wizard.tsx, IPurchaseRequest type, backend statusValues).

UAT Impact: Test submitting a request with urgency='urgent'. It should be accepted by both frontend validation and backend.

M21. Escrow Flow: doc lists GET /api/payment/:id but backend payment routes use /api/payment/:id (not /api/marketplace/payments/:paymentId)

Description: The Escrow Flow documents endpoint GET /api/payment/:id for fetching payment details. The actual Payment routes are under /api/marketplace/payments/:paymentId (controllerRoutes.ts and routes.ts). The standalone /api/payment/ path may refer to a separate payment service router, but this is not confirmed in the backend endpoints provided.

Doc Claim: GET /api/payment/:id

Code Reality: Backend marketplace router registers GET /marketplace/payments/:paymentId (controllerRoutes.ts line 53, routes.ts line 206). A separate /payment/* router may also exist (referenced in frontend endpoints.payments).

UAT Impact: Confirm which router serves /api/payment/:id and whether it is the same as /api/marketplace/payments/:paymentId. Test payment detail fetching from the buyer and admin views.

Domain: Seller Offer

M22. 'active' status is documented for SellerOffer but absent from the schema enum

Description: The flow document lists 'active (SellerOffer — optional manual seller activation)' as a valid SellerOffer status and mentions it in the docFlags. The SellerOffer Mongoose schema (SellerOffer.ts line 80) and TypeScript interface (line 17) enumerate only 'pending | accepted | rejected | withdrawn'. The service methods at SellerOfferService.ts line 307 also use only these four values. The 'active' status would be rejected by Mongoose on save. The createOffer() status gate at line 83 does allow PurchaseRequest.status='active' (not SellerOffer.status='active'), which is a separate concept.

Doc Claim: SellerOffer status 'active' exists as an optional manual seller activation state.

Code Reality: SellerOffer.status enum: ['pending', 'accepted', 'rejected', 'withdrawn']. No 'active' value. Any attempt to save status='active' on a SellerOffer will throw a Mongoose ValidationError.

UAT Impact: Do not attempt to set a SellerOffer to 'active' — it will fail. The state machine diagram showing 'active' as reachable is incorrect and should not guide test case design.

M23. select-offer cascade rejects ALL competing offers regardless of current status, not just pending/active

Description: The flow doc says at step 15 that 'all other offers on same request → rejected via SellerOffer.updateMany'. The SellerOfferService.acceptOffer() method (lines 376-387) correctly filters by status: { $in: ['pending', 'active'] }. However, the direct route handler for POST /purchase-requests/:id/select-offer (routes.ts lines 1386-1395) uses updateMany with only { purchaseRequestId, _id: { $ne: offerId } } — no status filter. This means selecting an offer via select-offer can overwrite already-withdrawn or previously-rejected offers back to 'rejected', corrupting their status history.

Doc Claim: Step 15: 'all other offers on same request → rejected via SellerOffer.updateMany'. The acceptOffer cascade description says 'rejects all competing pending/active offers'.

Code Reality: POST /purchase-requests/:id/select-offer route (routes.ts:1386) calls updateMany without a status filter, affecting offers in any status. Only the SellerOfferService.acceptOffer() used by POST /offers/:id/accept filters by ['pending', 'active'].

UAT Impact: Create a request with one withdrawn offer and one pending offer. Select the pending offer via select-offer. Verify whether the withdrawn offer's status is corrupted to 'rejected'. This is a data integrity regression.

M24. Doc missing: new-offer socket event emitted to buyer-{buyerId} room on offer creation

Description: The backend marketplaceController.ts (lines 47-56, 931-936) emits a 'new-offer' event to the room buyer-{buyerId} whenever a seller creates an offer via the controller's createOffer route. The socket events table in the doc only lists 'seller-offer-update (eventType: new-offer) → seller-{sellerId}' and 'new-notification → user-{buyerId}'. The direct 'new-offer' event to the buyer room is not documented. The backend socket events list in the provided code data does list 'EMIT new-offer — room: buyer-{buyerId}', confirming the omission is in the flow doc's socket section.

Doc Claim: Socket events: 'seller-offer-update (eventType: new-offer) → seller-{sellerId}' and 'new-notification → user-{buyerId}'. No mention of a direct new-offer event to the buyer.

Code Reality: marketplaceController.ts emits 'new-offer' directly to buyer-{buyerId} room in addition to the seller-specific event and the notification. The frontend use-marketplace-socket.ts (lines 300, 497) listens on 'new-offer'.

UAT Impact: Buyer dashboard real-time offer arrival must be tested by listening on the buyer-{buyerId} room for the 'new-offer' event, not only new-notification. Verify both events fire when a seller submits a proposal.

M25. select-offer does not emit per-seller socket events or notifications to losing sellers

Description: The flow doc at step 15 states 'notifications sent to winning seller (notifyOfferAccepted) and losing sellers; socket events emitted to winner and losers'. The POST /purchase-requests/:id/select-offer route handler (routes.ts lines 1300-1438) emits only a single purchase-request-update event to the request room with eventType 'offer-selected'. It does NOT call notifyOfferAccepted, does NOT call notifyOfferRejected for losing sellers, and does NOT emit seller-offer-update events to individual seller rooms. Those notifications are only sent via SellerOfferService.updateOfferStatus() (which fires when using PUT /offers/:id/accept or PUT /offers/:id/status), not via the select-offer path.

Doc Claim: Step 15: 'notifications sent to winning seller (notifyOfferAccepted) and losing sellers; socket events emitted to winner and losers'.

Code Reality: POST /purchase-requests/:id/select-offer emits only purchase-request-update to the request room. No per-seller notifications or socket events are sent to winning or losing sellers from this path.

UAT Impact: After a buyer selects an offer via the select-offer flow, verify whether the winning seller receives a notification and whether losing sellers are notified. Expected result per doc: yes. Actual result per code: no notifications are sent.

M26. No frontend withdraw offer action or UI — POST /api/marketplace/offers/:id/withdraw has no frontend coverage

Description: The flow documents a seller withdraw path at step 16 and the rejectOffer() frontend action (src/actions/marketplace.ts lines 299-308) only sends status='rejected', never 'withdrawn'. There is no withdrawOffer() action, no withdraw button in any seller step component, and the axios endpoint map marks the withdraw-capable /offers/:id/status endpoint only as 'withdraw/update offer status' without a dedicated frontend action using it for withdrawal. Confirmed by the missingFrontendFeatures list: 'No frontend action or UI for withdrawing an offer'.

Doc Claim: Step 16: 'Seller can withdraw their pending offer via /dashboard/seller/marketplace/offers/{offerId} → withdrawOffer'.

Code Reality: No withdraw button, no withdrawOffer() action exists in frontend. rejectOffer() always sends { status: 'rejected' }. The route /dashboard/seller/marketplace/offers/{offerId} does not appear to exist as a page.

UAT Impact: There is no UI path to test seller offer withdrawal. This feature is completely untestable from the frontend. Backend-only testing via PUT /offers/:id/status with status='withdrawn' is the only path.

M27. No seller 'My Offers' page — GET /api/marketplace/offers/seller/:sellerId has no frontend page or action

Description: The doc lists GET /api/marketplace/offers/seller/:sellerId and the flow implies a seller can review their offer history. The frontend has no page at /dashboard/seller/marketplace/offers (referenced in backend notification actionUrl), no getSellerOffers() action, and the missingFrontendFeatures list confirms 'No frontend page or action for listing a seller's own offers'. The backend service method getOffersBySeller() also has no HTTP route, compounding the gap.

Doc Claim: GET /api/marketplace/offers/seller/:sellerId — Seller's own offer history

Code Reality: No HTTP route exposes getOffersBySeller(). No frontend page /dashboard/seller/marketplace/offers exists. Notification actionUrls pointing to this path are broken links.

UAT Impact: Seller offer history view cannot be tested. Notification links to /dashboard/seller/marketplace/offers will produce a 404 page. Both the missing route and missing page need to be verified.

M28. Frontend updateOffer uses PUT /marketplace/offers/:id but backend registers PATCH /offers/:id

Description: The frontend updateOffer action (src/actions/marketplace.ts line 289) uses axiosInstance.put() against endpoints.marketplace.offers.update which maps to /marketplace/offers/:id. The backend registers this as router.patch('/offers/:id', ...) at routes.ts line 1260. PUT vs PATCH is a method mismatch. Many HTTP routers treat them as distinct methods; Express does not alias them. The frontend step-1-send-proposal.tsx calls updateOffer() for existing offer edits, so this path is actively exercised.

Doc Claim: PATCH /api/marketplace/offers/:id — Update price / ETA / notes (seller, while pending)

Code Reality: Backend: router.patch('/offers/:id'). Frontend: axiosInstance.put(). Method mismatch means the PUT request may match the wrong route or return 404 depending on Express routing order.

UAT Impact: Edit an existing offer from the seller proposal form and verify the network request method is PUT. Then verify the backend accepts PUT (it registers PATCH). If Express does not match PUT to PATCH, the update will silently fail or 404.

M29. createOffer() allows PurchaseRequest in 'active' status but doc step 6 only mentions 'pending' or 'received_offers'

Description: SellerOfferService.createOffer() at line 83 checks that PurchaseRequest.status is in ['pending', 'active', 'received_offers']. The flow doc step 6 states 'validates purchase request status is pending or received_offers'. The 'active' PurchaseRequest status is accepted by the backend but not documented in the narrative, leaving testers unaware that offers can be submitted against active requests. The PurchaseRequest status enum does include 'active' as a valid value (confirmed in the status list).

Doc Claim: Step 6: 'validates purchase request status is pending or received_offers'.

Code Reality: SellerOfferService.createOffer() line 83: status !== 'pending' && status !== 'active' && status !== 'received_offers' — three statuses are allowed.

UAT Impact: Testers should also verify that a seller can submit an offer against a PurchaseRequest in 'active' status. The doc-based test cases for the status gate are incomplete.

Domain: Payment

M30. API docs path prefix mismatch: /api/payment/stats vs /api/payment/payments/stats

Description: The API docs list GET /api/payment/stats and GET /api/payment/stats/:userId without the /payments/ infix. The backend code registers these under GET /api/payment/payments/stats and GET /api/payment/payments/stats/:userId (with the /payments/ segment). A duplicate controller-pattern route exists at /api/payment/stats (no /payments/) but requires admin role vs the /payments/ version. The frontend axios config defines endpoints.payments.stats as '/payment/stats' (without /payments/), which routes to the controller pattern endpoint.

Doc Claim: API docs: GET /api/payment/stats (auth: Bearer JWT) and GET /api/payment/stats/:userId

Code Reality: Two parallel implementations: /api/payment/payments/stats (admin-gated, strict) and /api/payment/stats (controller-pattern, authenticateToken only). Frontend hits the controller-pattern route. Auth level differs between the two.

UAT Impact: QA must check which stats route the admin dashboard calls. The /payments/stats route requires admin role and will 403 for non-admins; the /stats route does not enforce this, creating a privilege gap.

M31. API docs path prefix mismatch: /api/payment/export vs /api/payment/payments/export

Description: Same dual-path pattern as stats. API docs list /api/payment/export and /api/payment/export/:userId. Backend registers both /api/payment/payments/export (admin-only) and /api/payment/export (controller-pattern, no admin guard — 'uses role-based filter in service'). The frontend axios config uses '/payment/export', hitting the controller-pattern route that has no admin guard at the router level.

Doc Claim: API docs: GET /api/payment/export with auth: Bearer JWT (admin). GET /api/payment/export/:userId

Code Reality: GET /api/payment/payments/export has admin role guard; GET /api/payment/export (controller route) has only authenticateToken — no admin guard at the route level. Frontend hits the non-admin-gated path.

UAT Impact: QA must verify that non-admin buyers cannot export all payment data. Test with a buyer JWT against GET /api/payment/export.

M32. API docs say GET /api/payment/fetch-tx/:paymentId; backend is POST /api/payment/payments/:id/fetch-tx

Description: The DePay flow API endpoints table lists GET /api/payment/fetch-tx/:paymentId as the manual rechecker. The actual backend route is POST /api/payment/payments/:id/fetch-tx (POST method, /payments/ infix). The frontend fetchTransactionHashFromBlockchain() correctly calls POST /payment/payments/${paymentId}/fetch-tx (line 693 of payment.ts), confirming the backend reality. The flow doc is wrong on both the method (GET vs POST) and the path.

Doc Claim: DePay flow apiEndpoints: GET /api/payment/fetch-tx/:paymentId

Code Reality: Backend: POST /api/payment/payments/:id/fetch-tx (no auth). Frontend action calls POST /payment/payments/${paymentId}/fetch-tx.

UAT Impact: QA must use POST, not GET, when manually invoking the tx-hash rechecker. Sending a GET will 404.

M33. Frontend defines createDePayIntent calling /payment/depay/intents — no such backend route

Description: src/actions/payment.ts exports createDePayIntent() which posts to /payment/depay/intents (or a fallback hardcoded string). No such route exists in the backend endpoint list. The function comment says it is 'used by tests', but the endpoint is not present. The frontend axios endpoints object also has no payments.depay key, so the fallback hardcoded string is always used.

Doc Claim: Frontend actions list createDePayIntent with apiPath: /payment/depay/intents

Code Reality: No /depay/intents route on backend. Function uses a fallback hardcoded URL '/payment/depay/intents'. Will always 404 if called.

UAT Impact: QA must confirm createDePayIntent() is not called in any production flow. If a test path invokes it, it will receive 404.

M34. Frontend actions for Request Network payout/release/refund confirm point to non-existent routes

Description: src/actions/payment.ts exports initiateRequestNetworkPayout(), confirmRequestNetworkPayout(), confirmRequestNetworkRelease(), and confirmRequestNetworkRefund() hitting /api/payment/request-network/:id/payout/initiate, /payout/confirm, /release/confirm, and /refund/confirm. None of these sub-paths appear in the backend endpoint list. The backend only documents POST /api/payment/request-network/intents, GET /api/payment/request-network/:paymentId/checkout, and POST /api/payment/request-network/webhook.

Doc Claim: Frontend actions docs list these as valid endpoints for admin Request Network payout/release/refund operations.

Code Reality: Backend code does not expose any of the four Request Network payout/release/refund sub-routes. These actions will return 404 when called.

UAT Impact: QA must test the admin release/refund flow for Request Network payments. All four actions are currently broken and will 404.

M35. Multiple frontend stub endpoints have no backend implementation: /payment/history, /payment/methods, /payment/validate, /payment/transactions, /payment/escrow/balance

Description: The frontend axios.ts registers endpoints for /payment/history, /payment/methods, /payment/validate, /payment/transactions, and /payment/escrow/balance. None of these routes exist in the backend. The corresponding frontend actions (getPaymentHistory, getPaymentMethods, validatePayment, getTransactionHistory, getEscrowBalance) will all receive 404 responses. These appear to be placeholder definitions for planned features.

Doc Claim: Frontend actions docs list these as available API paths with GET/POST methods.

Code Reality: No matching routes in backend. Calls will 404.

UAT Impact: QA must confirm none of these actions are invoked from live UI components. If any dashboard widget calls them, it will silently fail or show empty state due to caught errors.

M36. 'completed' status is not counted as successful in payment stats aggregate — only 'confirmed' is

Description: The backend notable logic states: 'Payment.status confirmed counts as successfulPayments in stats; completed is not counted in successfulPayments'. The flow docs for both SHKeeper and DePay describe the final successful terminal state as 'completed'. If the admin dashboard displays successfulPayments from the stats endpoint, payments that followed the standard 'completed' terminal path will not be counted. The SHKeeper flow maps PAID to 'completed', so most successful SHKeeper payments will be invisible in the success count.

Doc Claim: Both payment flows document 'completed' as the terminal success status. API docs describe GET /api/payment/stats as returning aggregated counts per status.

Code Reality: paymentService.getPaymentStats aggregate counts only 'confirmed' as successfulPayments; 'completed' is excluded.

UAT Impact: QA must run a SHKeeper payment to completion, then check GET /api/payment/stats. The successful payment should appear in completed count but NOT in successfulPayments. Verify the dashboard does not mislead admins by showing an artificially low success count.

M37. PaymentProvider type in frontend excludes 'shkeeper' and 'decentralized' — only 'request.network', 'test', 'other'

Description: src/types/payment.ts defines PaymentProvider as 'request.network' | 'test' | 'other'. The SHKeeper and DePay flows both create Payment records with provider values that are not in this union type. The backend accepts 'shkeeper', 'decentralized', and 'other' as provider values. Frontend TypeScript code that reads payment.provider and switches on PaymentProvider type will miss shkeeper and decentralized payments, causing UI components to fall through to a default/unknown state.

Doc Claim: DePay flow doc says provider is 'other' or 'decentralized'. SHKeeper flow implies provider is 'shkeeper'. Frontend type definition says only 'request.network' | 'test' | 'other'.

Code Reality: src/types/payment.ts line 15: PaymentProvider = 'request.network' | 'test' | 'other'. No 'shkeeper' or 'decentralized' variant.

UAT Impact: QA must verify that the payment list and payment details views correctly display and label SHKeeper and DePay payments. Any provider-based conditional rendering may show incorrect labels or skip those payment records.

M38. createProviderPaymentIntent always routes to request-network/intents regardless of provider argument

Description: src/actions/payment.ts getProviderIntentEndpoint() ignores the provider parameter and always returns endpoints.payments.requestNetwork.intents ('/payment/request-network/intents'). If any UI component passes provider='shkeeper' to createProviderPaymentIntent(), the call will go to the Request Network endpoint instead of /payment/shkeeper/intents, creating a silent routing failure. The SHKeeper intents endpoint (/payment/shkeeper/intents) is defined in axios.ts but is never routed to by the provider intent factory.

Doc Claim: Frontend actions list createProviderPaymentIntent as a provider-agnostic factory that routes to the appropriate provider endpoint.

Code Reality: getProviderIntentEndpoint() at line 444 of payment.ts always returns requestNetwork.intents, ignoring the provider argument. The shkeeper.intents endpoint defined in axios.ts is never called by this factory.

UAT Impact: QA must verify that any checkout path intended to use SHKeeper actually POSTs to /payment/shkeeper/intents and not /payment/request-network/intents. Test by intercepting network requests during a SHKeeper checkout.

M39. Simulated transaction bypass (SIM_ prefix) is active in production frontend code with no environment guard

Description: src/web3/context/web3-provider.tsx lines 225 and 232 generate SIM_ prefixed transaction hashes when wallet connection fails, and these are passed to the backend. The backend notable logic confirms: paymentHash starting with 'SIM_' skips on-chain verification — controlled only by hash prefix, not an environment flag. The frontend generates SIM_ hashes in an error fallback path that can be triggered in production when the wallet connection attempt fails, not just in development.

Doc Claim: Backend notable logic notes: 'Simulated transaction bypass: paymentHash starting with SIM_ ... intended for dev but controlled only by hash prefix, not environment flag'

Code Reality: Frontend web3-provider.tsx returns SIM_ hashes on wallet connection failure with no process.env.NODE_ENV check. This can occur in production if wallet connection times out or throws.

UAT Impact: QA must test the payment flow with a wallet connection failure in a staging environment and confirm that the resulting SIM_ hash does not successfully create a completed payment record in the database.

M40. API docs list auth for /api/payment/payments/:id/debug as 'Bearer JWT' — backend has NO auth middleware

Description: The API docs table lists GET /api/payment/:id/debug with auth: Bearer JWT. The backend code explicitly notes: 'GET /api/payment/payments/:id/debug — SECURITY: returns payment + walletMonitor status without authentication'. The route has no auth middleware applied. Any unauthenticated caller can read full payment documents including blockchain metadata and wallet monitor state.

Doc Claim: API docs: GET /api/payment/:id/debug with auth: Bearer JWT

Code Reality: Backend code comment explicitly flags this as having no auth middleware. Full payment data exposed without authentication.

UAT Impact: QA must verify that the debug endpoint returns data when called without an Authorization header. If confirmed, this is a data exposure vulnerability requiring an auth middleware fix before production.

M41. API docs list auth for POST /api/payment/payments/auto-fetch-missing as 'Bearer JWT' — backend has NO auth

Description: The API docs table lists POST /api/payment/payments/auto-fetch-missing with auth: Bearer JWT. The backend code notes this endpoint has no authentication middleware. Any unauthenticated caller can trigger batch blockchain tx-hash lookups and cause writes to payment records.

Doc Claim: API docs: POST /api/payment/payments/auto-fetch-missing with auth: Bearer JWT

Code Reality: Backend code: 'POST /api/payment/payments/auto-fetch-missing — Auto-fetches missing tx hashes for completed payments; no auth'

UAT Impact: QA must call this endpoint without an Authorization header and confirm it either rejects the request or is intentionally public. If unintentionally public, it represents a state-mutation endpoint with no access control.

M42. SHKeeper flow documents 'payment-created' as emitted on intent creation — backend only emits it after admin-payout and Request Network pay-in

Description: The SHKeeper flow step 12 states 'Emit payment-created globally via emitGlobalEvent so admin dashboard sees the new pending payment in real time'. However, the backend socket events list specifies: 'payment-created (server→client, global): emitted after admin-payout save and after Request Network pay-in creation'. The SHKeeper create handler is not mentioned as a source of this event. If the SHKeeper intent creation does not emit payment-created, the admin dashboard real-time view will not show new SHKeeper payments.

Doc Claim: SHKeeper flow step 12: payment-created is emitted globally on SHKeeper intent creation.

Code Reality: Backend socket events documentation attributes payment-created only to admin-payout and Request Network pay-in. SHKeeper create is not listed.

UAT Impact: QA must create a SHKeeper payment intent while the admin dashboard is open and verify whether a payment-created socket event is received and the new payment appears in real time.

Domain: Dispute

M43. All Socket.IO events for disputes are unimplemented — every socket claim in docs is a TODO stub

Description: The flow documentation lists three socket events: new-message (active via ChatService), new-notification (planned), and dispute-updated (planned). The code reality is that no Socket.IO emit occurs anywhere in DisputeService or the dispute controllers. All notification emit blocks are commented out as TODO. The new-message event only fires if ChatService.sendMessage is called separately; DisputeService.createDispute inserts the system message directly into the Chat document without calling ChatService.sendMessage, so no socket event fires even for that path.

Doc Claim: Flow step 6: 'chat creation provides real-time presence via new-message socket emit'. Steps 5 and 14 note new-notification as TODO. dispute-updated is listed as planned.

Code Reality: DisputeService.createDispute, assignAdmin, updateStatus, resolveDispute, and addEvidence all contain only commented-out TODO blocks for socket notifications. No socket.io emit calls exist in any dispute service or controller file.

UAT Impact: QA must verify: (1) No real-time update appears in the buyer/seller browser when a dispute is created, an admin is assigned, status changes, evidence is added, or resolution is posted. All real-time feedback is absent. Chat participants do not receive a system message notification on dispute creation.

M44. API docs use 'under_review' status; code uses 'in_progress'

Description: The API docs for POST /api/disputes/:id/assign state it 'transitions status to under_review'. The Dispute model, DisputeService, and all frontend code use in_progress as the status value when an admin is assigned. There is no under_review value in the status enum anywhere in the codebase.

Doc Claim: API docs: 'Sets assignedAdminId, transitions status to under_review, notifies participants'.

Code Reality: Dispute model enum: ['pending', 'in_progress', 'waiting_response', 'resolved', 'rejected', 'closed']. DisputeService.assignAdmin sets dispute.status = 'in_progress'. No under_review value exists.

UAT Impact: QA calling POST /api/disputes/:id/assign and checking the returned dispute.status should expect in_progress, not under_review. API doc consumers will be misled.

M45. API docs describe buyer/seller/split decision model for resolve; code uses refund/replacement/compensation/warning_seller/ban_seller/no_action

Description: The API docs for POST /api/disputes/:id/resolve specify a decision field with values 'buyer'|'seller'|'split' and refundAmount/releaseAmount. The backend and frontend use an action field with values 'refund'|'replacement'|'compensation'|'warning_seller'|'ban_seller'|'no_action' and an optional amount field. The two schemas are completely different.

Doc Claim: API docs body: 'decision: buyer|seller|split; refundAmount?: number (required for split); releaseAmount?: number (required for split); reasoning: string'.

Code Reality: DisputeController.resolveDispute reads { action, amount, currency, notes } from req.body. Dispute model resolution.action enum: ['refund', 'replacement', 'compensation', 'warning_seller', 'ban_seller', 'no_action']. Frontend ResolveDisputeData interface: { action: DisputeResolutionAction; amount?: number; currency?: string; notes?: string }.

UAT Impact: QA following the API docs will send wrong field names and receive no validation error (the service will accept it but store undefined). Integration tests or clients built from the docs will be broken.

M46. Flow doc says dispute categories are delivery/payment/quality/fraud/other; code uses a different enum

Description: Flow step 2 describes the category options as 'delivery, payment, quality, fraud, other'. The actual Dispute model and frontend define six different categories: product_quality, delivery_delay, wrong_item, payment_issue, seller_behavior, other. 'fraud' does not exist in the code; 'delivery' maps to 'delivery_delay'; 'quality' maps to 'product_quality'; 'wrong_item' and 'seller_behavior' are absent from the doc.

Doc Claim: Flow step 2: 'Initiator selects a category (delivery, payment, quality, fraud, other)'.

Code Reality: Dispute model enum and frontend constants: ['product_quality', 'delivery_delay', 'wrong_item', 'payment_issue', 'seller_behavior', 'other']. No 'fraud' category exists.

UAT Impact: QA testing category filtering or creation with 'fraud' will get a validation error. The list of available categories visible in the UI also does not match the doc, which will confuse testers using the doc as a reference.

M47. Statistics endpoint omits waiting_response, rejected, and closed from counts

Description: DisputeService.getStatistics only counts pending, in_progress, and resolved disputes. The model supports six statuses. The frontend dispute list view uses the statistics counts to populate tab badges, meaning the 'all' tab shows an accurate total but the status-specific tabs only cover three of six statuses. Disputes in waiting_response, rejected, or closed are invisible in the stats summary.

Doc Claim: API docs describe 200 response as '{ success, data: { open, byReason, avgResolutionHours, ... } }' suggesting a broader stats shape.

Code Reality: DisputeService.getStatistics returns { total, pending, inProgress, resolved, byCategory, byPriority }. No counts for waiting_response, rejected, closed, or avgResolutionHours.

UAT Impact: QA should verify that closed and rejected disputes do not appear in any statistics panel. They would also note no avgResolutionHours despite docs claiming it is returned.

M48. POST /api/disputes/:id/assign lacks a role guard but flow and docs say admin-only

Description: The route for assign admin is documented as admin-only and the flow step 8 shows only an admin picking up a dispute. However, the dashboard router applies only authenticateToken. The controller reads req.user.id and uses that as the admin ID when assignToSelf=true. A buyer could self-assign as 'admin' to their own dispute, bypassing the intended workflow.

Doc Claim: API docs: 'Bearer JWT (admin)'. Flow step 8: 'Admin clicks Pick up'.

Code Reality: Backend router: router.post('/:id/assign', DisputeController.assignAdmin) — only authenticateToken, no authorizeRoles guard. Any user with assignToSelf=true will be set as dispute.adminId.

UAT Impact: QA must test POST /api/disputes/:id/assign with a buyer token and assignToSelf=true — currently succeeds. Expected behavior: 403.

M49. Resolve dispute does not trigger financial side effects — escrow state is unchanged

Description: DisputeService.resolveDispute only updates the Dispute document status and resolution fields. It does not call releaseHoldService.resolveDispute, does not interact with Payment or PurchaseRequest models, and does not initiate any refund or release. The admin must separately call POST /api/disputes/:purchaseRequestId/resolve (releaseHold router) to unblock the escrow hold, and then separately trigger a payout or refund through the payment system.

Doc Claim: Flow step 14: 'Depending on the resolution action, the admin manually triggers the financial side-effect'. API docs for POST /api/disputes/:id/resolve state 'triggers refund/release/split escrow action'.

Code Reality: DisputeService.resolveDispute modifies only the Dispute document. No escrow interaction, no Payment update, no releaseHold call. The API docs claim is false — no escrow action is triggered automatically.

UAT Impact: QA resolving a dispute through POST /api/disputes/:id/resolve must confirm that the payment escrow state is NOT automatically changed. A separate call to the hold-clear endpoint is required. The API doc claim that it 'triggers refund/release/split escrow action' will cause integration misuse.

M50. Dispute timeline initialised twice: pre('save') middleware adds dispute_created, but service also sets timeline: []

Description: DisputeService.createDispute passes timeline: [] when creating the Dispute document. The Dispute model's pre('save') middleware then fires on the new document and pushes a dispute_created entry. This means the timeline always has exactly one entry after creation — the one added by the middleware — but any evidence items or other entries passed in the initial create call would be discarded because timeline: [] is explicitly set. This interaction is not documented.

Doc Claim: Flow step 4 says 'creates the Dispute with ... empty timeline[]'. No mention of middleware auto-populating it.

Code Reality: Dispute.pre('save') in /backend/src/models/Dispute.ts line 226: if (this.isNew) { this.timeline.push({ action: 'dispute_created', ... }) }. The service sets timeline: [] at creation, and the middleware appends to that empty array, resulting in one entry.

UAT Impact: QA fetching a newly-created dispute via GET /api/disputes/:id should confirm timeline has exactly one entry (dispute_created). If timeline has zero entries, the middleware is not firing. If it has more, there is an unexpected duplicate.

Domain: Chat

M51. leaveConversation frontend action calls non-existent backend endpoint PUT /api/chat/:id/leave

Description: The frontend defines a leaveConversation action (chat.ts line 298) that calls PUT /chat/:id/leave. Neither the backend code summary nor the API docs list any endpoint at this path. The backend removes participants via DELETE /api/chat/:id/participants/:participantId. The leave endpoint is registered in the axios config (endpoints.chat.leave: '/chat/:id/leave') but has no backend handler.

Doc Claim: Flow doc does not document a leave endpoint. API docs do not list it. Backend code does not expose it.

Code Reality: Frontend has a dedicated leaveConversation action pointing to PUT /chat/:id/leave which does not exist on the backend.

UAT Impact: QA must verify: when a user attempts to leave a group chat, the action must call DELETE /chat/:id/participants/:participantId instead. The current leave action will return 404.

M52. GET /api/chat/:id/participants has no backend implementation

Description: The frontend getParticipants action (chat.ts line 445) calls GET /chat/:id/participants. Neither the backend code summary nor the API docs list a GET endpoint at that path. The backend exposes POST and DELETE at that path, but no GET. This means the frontend function will always 404.

Doc Claim: API docs list POST /api/chat/:id/participants and DELETE /api/chat/:id/participants/:participantId. No GET is listed.

Code Reality: Frontend defines getParticipants calling GET /chat/:id/participants. Backend has no such route.

UAT Impact: QA must verify: any UI that calls getParticipants to refresh the participant list will fail. Confirm participant data is only loaded through GET /chat/:id/info (which returns participants in the chat metadata).

M53. PUT /api/chat/:id/participants/:participantId (role update) has no backend implementation

Description: The frontend updateParticipantRole action (chat.ts line 456) calls PUT /chat/:id/participants/:participantId. The backend does not expose this route. The backend only has POST (add) and DELETE (remove) for participants. There is no role-update endpoint defined in backend code or API docs.

Doc Claim: API docs do not document a role-update endpoint for participants.

Code Reality: Frontend defines updateParticipantRole calling PUT /chat/:id/participants/:participantId — backend has no such route.

UAT Impact: QA must verify: any admin UI for changing participant roles will silently fail with a 404 or 405.

M54. editMessage sends field 'text' but backend expects field 'content'

Description: The frontend editMessage action (chat.ts line 400) sends { text: string } as the request body. The backend PUT /api/chat/:id/messages/:messageId handler expects { content: string } (max 5000 chars) and will treat 'text' as an unrecognised field, resulting in a validation error or empty content update.

Doc Claim: API docs specify body: content: string for PUT /api/chat/:id/messages/:messageId.

Code Reality: Frontend editMessage sends { text: '...' } — field name is 'text', not 'content'.

UAT Impact: QA must verify: edit a message in the chat UI, save, and confirm the message content is updated. Expect the backend to reject or ignore the 'text' field.

M55. Flow doc states markAsRead is POST but backend and API docs define it as PATCH

Description: The flow doc step 14 says 'frontend POSTs POST /api/chat/:chatId/read'. The actual backend endpoint is PATCH /api/chat/:id/messages/read. The API docs and backend code both agree on PATCH. The frontend correctly uses axiosInstance.patch() and the path /chat/:id/messages/read, so the implementation is correct, but the flow document has the wrong HTTP method and path.

Doc Claim: Flow doc step 14: 'frontend POSTs POST /api/chat/:chatId/read'.

Code Reality: Backend: PATCH /api/chat/:id/messages/read. Frontend markAsRead action (chat.ts line 476) correctly uses axiosInstance.patch() to /chat/:id/messages/read.

UAT Impact: No runtime impact since the frontend is correct. Developers following the flow doc may write incorrect test scripts or integrations using POST — verify any external API clients use PATCH.

M56. Flow doc describes POST /api/chat/:chatId/upload; this endpoint does not exist

Description: Flow doc step 13 references 'POST /api/chat/:chatId/upload' and the docFlags section says the doc uses 'chatService.uploadChatFile(chatId, file) or the equivalent POST ...'. Neither the backend code nor the API docs expose /api/chat/:chatId/upload. The correct file endpoint is POST /api/chat/:id/messages/file. The misleading path in the doc led the frontend to also use the wrong endpoint (see the sendFileMessage finding).

Doc Claim: Flow doc step 13: 'frontend POSTs POST /api/chat/:chatId/upload'.

Code Reality: Backend exposes POST /api/chat/:id/messages/file. No /upload route exists for the chat domain.

UAT Impact: Any integration test or API script that follows the flow doc and calls /api/chat/:chatId/upload will get a 404. Use /api/chat/:id/messages/file instead.

M57. markAsRead with empty messageIds marks all unread — behavior undocumented

Description: The flow doc step 14 mentions 'optionally with messageIds array' but does not state what happens when the array is omitted or empty. The backend note explicitly says 'omit to mark all'. The frontend clickConversation helper calls markAsRead(conversationId, []) (empty array) as a side-effect of opening a conversation, which will silently mark all messages read. This is a significant implicit behavior that is not documented.

Doc Claim: Flow doc states the messageIds parameter is optional but does not describe the 'mark all' fallback behavior.

Code Reality: Backend PATCH /api/chat/:id/messages/read: when messageIds is omitted or empty, all messages are marked as read. Frontend clickConversation (chat.ts line 493) always passes an empty array.

UAT Impact: QA must verify: open a conversation with multiple unread messages, confirm all are marked read without supplying explicit messageIds. Also test that passing a specific messageIds array marks only those messages.

M58. Flow doc lists 'user-online' as a client-to-server socket event; backend joins user room via 'join-user-room' not 'user-online'

Description: The flow doc step 7 says 'frontend emits socket.emit(user-online, userId)'. The backend code shows that user-online is handled separately from room joining — user-online broadcasts a status change to all clients, while join-user-room actually joins the socket to the user-{userId} room. The frontend socket context (socket-context.tsx line 198) emits join-user-room for room joining, and setUserOnline (line 238) emits user-online for the online broadcast. The flow doc conflates these two distinct events.

Doc Claim: Flow doc step 7: 'frontend emits socket.emit(user-online, userId) so other clients see green status'. Implies user-online is what joins the user room.

Code Reality: Backend listens to join-user-room to add socket to room user-{userId}. user-online is a separate event that broadcasts user-status-change to all. They are distinct — room joining and online broadcasting are separate operations.

UAT Impact: QA must verify: on login/app load, the frontend emits both join-user-room and user-online, and other online users see the green status indicator update.

M59. disconnect does not emit offline status — doc implies it does

Description: The documented socket event 'user-status-change' is described as 'emitted when user-online is received', which implies an online/offline toggle. The backend code note explicitly states: 'disconnect: logs disconnection (no offline-status broadcast in current implementation)'. So when a user disconnects, no user-status-change event is broadcast to other clients. Other users will never see the user go offline.

Doc Claim: Flow doc implies user-status-change covers both online and offline transitions via socket events.

Code Reality: Backend only emits user-status-change on user-online. On disconnect it only logs; no offline broadcast occurs.

UAT Impact: QA must verify: have two users in a chat, one disconnects (closes tab/network loss), confirm the other user's UI does NOT update the online indicator to offline. This is a known gap — verify it does not cause stale 'online' indicators that mislead users.

M60. POST /api/chat/purchase-request has no frontend UI or action wiring

Description: The backend exposes POST /api/chat/purchase-request to create a direct chat linked to a PurchaseRequest. The axios endpoints config registers endpoints.chat.purchaseRequest = '/chat/purchase-request'. However, the frontend actions/chat.ts has no exported action function for this endpoint, and per the frontend actions audit there is no frontend UI that calls it. The flow doc describes a post-payment auto-chat mechanism but the manual trigger path has no frontend surface.

Doc Claim: Flow doc step 5 describes a post-payment auto-chat; the API docs list POST /api/chat/purchase-request as a supported endpoint.

Code Reality: No action function exists in chat.ts for the purchase-request endpoint. The axios endpoint key exists (endpoints.chat.purchaseRequest) but is unused by any exported action.

UAT Impact: QA must verify: after payment is confirmed, confirm a direct chat between buyer and seller is automatically created (server-side). The manual creation path via this endpoint has no UI to test.

M61. Backend enforces rate limiting (20 msgs/min) and message deduplication — not documented in flow

Description: The backend uses Redis-based rate limiting (20 messages per user per chat per 60 seconds, 5 typing indicators per 10 seconds) and message deduplication via Redis (5-minute window). Neither the flow doc nor the API docs mention these constraints. Users and QA testers will encounter unexplained 429-style errors when hitting the rate limit.

Doc Claim: Flow doc and API docs have no mention of rate limiting or message deduplication.

Code Reality: Backend ChatRateLimiter enforces 20 msgs/min per user per chat. Typing indicators are rate-limited to 5 per user per 10 seconds. Message deduplication via Redis for 5 minutes.

UAT Impact: QA must verify: rapidly send more than 20 messages in a minute in a single chat and confirm a meaningful error is returned. Verify the frontend handles this error gracefully (shows a user-facing message rather than silently failing).

M62. Edit message has a 15-minute time window constraint not documented in flow doc

Description: The backend enforces that messages can only be edited within 15 minutes of their original timestamp. After 15 minutes, a 400 error is returned. This constraint is not mentioned in the flow doc or API docs. The frontend editMessage action has no awareness of this limit and will fail silently after the window expires.

Doc Claim: Flow doc and API docs do not mention any time restriction on message editing.

Code Reality: Backend: 'messages can only be edited within 15 minutes of their original timestamp; attempts after that return a 400 validation error'.

UAT Impact: QA must verify: attempt to edit a message sent more than 15 minutes ago and confirm the backend returns an appropriate error, and the frontend displays it to the user rather than silently failing.

M63. Soft-delete on message DELETE and participant removal not documented in flow

Description: The flow doc describes message deletion as 'rare' with no endpoint or narrative step. The backend actually soft-deletes messages (sets deletedAt, clears content) and emits message-deleted. Similarly participant removal sets isActive=false and records leftAt instead of removing the subdocument. Neither behavior is documented. Frontend message-deleted socket handling exists in use-chat-socket.ts (line 264) but the soft-delete semantics (content cleared, deletedAt set) are not reflected in the frontend data model.

Doc Claim: Flow doc says deletion is 'rare' with no step. API docs say 'Soft-delete: sets deletedAt, clears content' but flow doc narrative is silent.

Code Reality: Backend deleteMessage soft-deletes (sets deletedAt, clears content, repairs lastMessage). removeParticipant sets participant.isActive=false with leftAt timestamp. Frontend listens to message-deleted socket event.

UAT Impact: QA must verify: delete a message, confirm it disappears from the UI (message-deleted socket event fires), and confirm the backend record shows deletedAt set and content cleared. Verify the same for participant removal (isActive=false, leftAt set).

Domain: Notification

M64. PATCH /notifications/bulk/mark-read and DELETE /notifications/bulk/delete are undocumented

Description: The backend exposes two bulk endpoints not present anywhere in the flow doc or API table: PATCH /notifications/bulk/mark-read (accepts { notificationIds: string[] }, loops individually) and DELETE /notifications/bulk/delete (accepts { notificationIds: string[] }, loops individually). Neither endpoint is listed in the documented API, and neither has a corresponding action function in the frontend notification.ts or an entry in axios.ts endpoints. These endpoints are unreachable from the frontend today.

Doc Claim: API table lists only: GET /notifications, GET /notifications/unread-count, PATCH /notifications/:id/read, POST /notifications/read-all, DELETE /notifications/:id

Code Reality: Backend also exposes PATCH /notifications/bulk/mark-read and DELETE /notifications/bulk/delete; neither is in the doc or accessible from the frontend

UAT Impact: Test bulk mark-read and bulk delete via direct API calls. Confirm partial failure behavior (per-ID success/failure array) and that no atomic rollback occurs.

M65. POST /api/notifications/mark-read (narrative step 8) matches no real endpoint

Description: Step 8 of the flow narrative says 'frontend calls POST /api/notifications/mark-read' for per-item reading. The API table does not list this endpoint. The real single-notification read endpoint is PATCH /notifications/:id/read. The frontend correctly uses PATCH, but the narrative text creates confusion for QA and integrators who may expect a POST route at /mark-read.

Doc Claim: Step 8: 'frontend calls POST /api/notifications/mark-read (or POST /api/notifications/read-all for bulk)'

Code Reality: PATCH /notifications/:id/read for single, PATCH /notifications/mark-all-read for bulk. No POST /notifications/mark-read route exists.

UAT Impact: Confirm that single-notification read uses PATCH /notifications/:id/read and returns the updated notification object with isRead=true and readAt set.

M66. unread-count-update socket event is undocumented but actively used

Description: The backend emits 'unread-count-update' to user-{userId} after createNotification, markAsRead, and markAllAsRead. The frontend notification-context.tsx explicitly listens for this event and calls setUnreadCount(data.unreadCount). The flow doc mentions only 'new-notification' and vaguely references 'notification-read' for syncing. The actual unread sync mechanism is 'unread-count-update', which is entirely absent from the documented socket events table.

Doc Claim: Socket events table lists 'notification-read → user-{userId}' as the sync mechanism after markAsRead

Code Reality: Backend emits 'unread-count-update' (payload: { unreadCount, timestamp }) after markAsRead, markAllAsRead, and createNotification. Frontend listens for 'unread-count-update', not 'notification-read'.

UAT Impact: Open two browser tabs, mark a notification read in one tab, and verify the badge in the second tab updates automatically via the unread-count-update socket event without a page refresh.

M67. notification-read socket event does not exist in the backend or frontend

Description: The flow doc lists 'notification-read → user-{userId}' as a socket event emitted after markAsRead for cross-tab sync. Neither the backend socket event list nor the frontend socket-context.tsx nor notification-context.tsx registers any listener or emitter for 'notification-read'. This event is entirely fictional in the current implementation; the real sync is done via 'unread-count-update'.

Doc Claim: Socket events: 'notification-read → user-{userId} (or unread count recomputed; emitted after markAsRead so other tabs sync)'

Code Reality: No 'notification-read' event exists in backend or frontend. Cross-tab sync uses 'unread-count-update' instead.

UAT Impact: Do not write test hooks for 'notification-read'. Verify cross-tab sync via 'unread-count-update' only.

M68. useNotifications hook in use-notifications.ts has fetchNotifications stubbed out (TODO comment)

Description: The hook at src/socket/hooks/use-notifications.ts has a fetchNotifications function that is entirely commented out with a 'TODO: Implement getNotifications' comment. The hook initialises an empty notifications array and never fetches real data from the API. It also assigns a temporary fake _id (Date.now()) to incoming socket notifications. The actual working implementation lives in the NotificationProvider context (notification-context.tsx), but any component using the use-notifications.ts hook directly will get empty state and malformed IDs.

Doc Claim: Flow doc step 7: 'User opens bell-icon dropdown — frontend fetches GET /api/notifications for paginated history'

Code Reality: src/socket/hooks/use-notifications.ts fetchNotifications body is a no-op TODO; real fetch only happens in NotificationProvider. Socket notifications receive fake string IDs.

UAT Impact: Identify all components using useNotifications from src/socket/hooks/use-notifications.ts. Verify they display real data and not an empty list. Verify that incoming socket notifications show correct _id values (not timestamp-based strings).

M69. GET /notifications/settings is wired in axios endpoints but has no backend route

Description: The axios endpoints config at src/lib/axios.ts defines endpoints.notifications.settings = '/notifications/settings'. The frontend notes also flag this: 'the endpoint is defined in axios endpoints config but has no corresponding action function in notification.ts and no dedicated settings page'. There is no backend route for GET /notifications/settings in the backend endpoint list, and no action function in actions/notification.ts. The endpoint is dead at both ends.

Doc Claim: Flow doc does not mention a /notifications/settings endpoint

Code Reality: endpoints.notifications.settings = '/notifications/settings' exists in axios.ts but is never called by any action, has no backend handler, and has no frontend page

UAT Impact: Confirm GET /notifications/settings returns 404. Remove or implement this endpoint before exposing settings UI.

M70. Dual router registration creates ambiguity about which controller handles /notifications

Description: The backend mounts notification routes from two separate files (notificationControllerRoutes.ts and routes.ts) both at the same /notifications prefix. Depending on app.ts mount order, one set of handlers will shadow the other. The flow doc and API table describe a single canonical set of endpoints without acknowledging this conflict. UAT cannot determine which controller's behaviour is authoritative without inspecting app.ts mount order.

Doc Claim: API table presents a single clean set of notification endpoints with no mention of dual registration

Code Reality: Two router registrations exist for /notifications. One may shadow the other depending on mount order.

UAT Impact: Inspect app.ts to determine mount order. Run all notification endpoint tests and confirm which router's response headers/body are returned. Ensure the intended controller handles all routes.

M71. Notification category 'general' is used in code but not listed in documented category enum

Description: The backend POST /notifications endpoint docs state the default category is 'general'. The use-notifications.ts hook also hardcodes category: 'system' as a fallback for socket notifications. The flow doc lists valid category values as: purchase_request | offer | payment | delivery | system. The value 'general' is not in this list. If the schema enforces an enum, 'general' will cause validation errors on default-category notifications.

Doc Claim: category values: purchase_request | offer | payment | delivery | system

Code Reality: Backend POST /notifications defaults category to 'general'; socket hook hardcodes 'system'. Neither 'general' appears in the documented enum.

UAT Impact: Create a notification via POST /notifications without specifying a category. Verify the saved document has a valid category value and does not fail schema validation.

Domain: Points/Referral

M72. API doc claims GET /points/levels is public (no auth); backend requires authenticateToken

Description: The API doc marks GET /points/levels with 'Bearer JWT' auth. The flow doc describes it as 'public info'. The backend pointsRoutes.ts applies router.use(authenticateToken) globally to ALL routes including /levels — there is no unauthenticated access. The API spec says auth is required which matches the code, but the flow doc's description of it as 'public' is misleading.

Doc Claim: Referral Flow: GET /api/points/levels is listed as 'GET /api/points/levels — level config (public)'.

Code Reality: pointsRoutes.ts line 8: router.use(authenticateToken) applies to all routes including /levels. The route comment says 'public info' but auth is enforced.

UAT Impact: Verify that an unauthenticated GET /api/points/levels returns 401. Confirm any marketing page or public-facing level display that needs to show tiers without login has a separate unauthenticated mechanism or is served from static config.

M73. POST /points/redeem request/response shape mismatch between API doc and actual code

Description: The API doc describes the redeem endpoint body as: amount (required), purpose (wallet_credit|discount_code, optional), purchaseRequestId (optional). The actual controller expects: purchaseRequestId (required) and pointsToUse (required) — 'amount' is not used, 'purpose' is not accepted. The response shape also differs: doc claims redemption object with creditAmount/currency/code; code returns discount (numeric, always pointsToUse * 1000 IRR) and remainingPoints. The 'purpose' field controlling wallet_credit vs discount_code behavior does not exist in the code.

Doc Claim: POST /api/points/redeem body: amount (number, required), purpose (wallet_credit|discount_code, optional), purchaseRequestId (optional). Response: transaction, redemption (creditAmount/currency/code), newBalance.

Code Reality: pointsController.ts lines 128-135: expects purchaseRequestId (required) and pointsToUse (required). No 'amount' field. No 'purpose' field. Response: { transaction, discount (pointsToUse * 1000), remainingPoints }. No 'newBalance' or 'redemption' object.

UAT Impact: Test POST /points/redeem with { amount: 100, purchaseRequestId: '...' } — it will fail because 'pointsToUse' is the required field name. Test with { pointsToUse: 100, purchaseRequestId: '...' } to confirm success. Verify discount calculation is always 1 point = 1000 IRR (no currency flexibility). Confirm purpose/wallet_credit/discount_code options are unavailable.

M74. POST /points/admin/add request body mismatch: doc requires 'reason', code accepts 'description' (and silently ignores it)

Description: The API doc specifies the admin add endpoint body as: userId (required), amount (required), reason (string, required), metadata (object, optional). The actual controller reads: userId, amount, description — no 'reason' field. Furthermore the 'description' value is read from req.body but never passed to PointsService.addPoints (the call at line 209 passes an empty metadata object {}). Both 'reason' and 'description' are effectively dropped.

Doc Claim: POST /api/points/admin/add body: userId (string, required), amount (number, required), reason (string, required), metadata (object, optional).

Code Reality: pointsController.ts lines 201-209: reads { userId, amount, description } — 'reason' is never read, 'description' is read but not passed to addPoints. The PointsService.addPoints call is addPoints(userId, amount, 'admin', {}) with empty metadata.

UAT Impact: Test POST /points/admin/add with a reason field — verify the reason does not appear in the PointTransaction record. Confirm that admin-granted points have no human-readable reason stored, which hinders audit trails.

M75. API doc claims leaderboard supports period filter (all|month|week); backend ignores it

Description: The API doc states GET /points/leaderboard accepts a 'period' query parameter (all|month|week). The actual controller only reads 'limit' from req.query and passes it to getLeaderboard(limitNum). PointsService.getLeaderboard only accepts a limit parameter — there is no period/date filtering in the aggregation pipeline. All leaderboard queries return all-time data regardless of any period parameter sent.

Doc Claim: GET /api/points/leaderboard query: limit (default 10), period (all|month|week).

Code Reality: pointsController.ts line 232: only reads 'limit' from req.query. PointsService.getLeaderboard(limit) only accepts limit. No period filtering exists.

UAT Impact: Test GET /points/leaderboard?period=week — confirm the response is identical to ?period=all, proving the filter is silently ignored. Document this as a known limitation for the leaderboard feature.

M76. POST /points/generate-referral-code: doc says 'force' param rotates existing code; code always overwrites

Description: The API doc lists a 'force' boolean parameter for generate-referral-code that implies conditional behavior (generate only if missing vs force-rotate). The actual controller does not read the 'force' param at all. PointsService.generateReferralCode always generates a new code and overwrites any existing one via User.findByIdAndUpdate. There is no 'force' conditional — the behavior is always to replace. The response also only returns { referralCode } — not { referralCode, link } as the doc states.

Doc Claim: POST /api/points/generate-referral-code body: force (boolean, optional). Response: referralCode, link.

Code Reality: pointsController.ts line 177: only calls PointsService.generateReferralCode(userId) with no force param. Response: { referralCode } only — no 'link' field returned.

UAT Impact: Verify that calling POST /points/generate-referral-code without force=true still always regenerates the code. Verify the response does not include a 'link' field. Check whether the frontend invite-friends component constructs the share URL client-side (it does: points-invite-friends.tsx line 36 builds the URL from NEXT_PUBLIC_API_URL).

M77. GET /points/transactions type filter: API doc accepts 'redeem|referral|purchase|review|admin_grant|admin_deduct'; backend only accepts 'earn|spend|expire'

Description: The API doc lists the transactions type filter as: earn|redeem|referral|purchase|review|admin_grant|admin_deduct. The actual backend and frontend action both use the PointTransaction.type enum which is earn|spend|expire. 'redeem', 'referral', 'purchase', 'review', 'admin_grant', 'admin_deduct' are all invalid type values that the backend will silently ignore (no match). The correct way to filter by referral transactions is by source (source='referral'), not by type.

Doc Claim: GET /api/points/transactions query type filter: earn|redeem|referral|purchase|review|admin_grant|admin_deduct.

Code Reality: PointsService.getTransactions filters by PointTransaction.type which is enum ['earn', 'spend', 'expire']. Frontend actions/points.ts also types it as 'earn' | 'spend' | 'expire'. No source-based filtering is supported by the transactions endpoint.

UAT Impact: Test GET /points/transactions?type=referral — verify it returns 0 results or all results (no filtering), not referral-source transactions. Test with type=earn to confirm it works. Verify there is no way to filter transactions by source (purchase, referral, admin) through the API.

M78. Referral reward triggered on 'completed' status only; doc says 'delivered or completed'

Description: The backend notable logic note states referral reward is awarded on 'status delivered or completed'. The actual code in marketplaceController.ts shows PointsService.processReferralReward is only called when newStatus === 'completed' (line 473). There is no 'delivered' trigger. If the flow ends at 'delivered' without reaching 'completed', the referrer never earns their commission.

Doc Claim: Backend notable logic: '2% of the selected offer price... awarded in points to the referrer on purchase completion (status delivered or completed)'.

Code Reality: marketplaceController.ts line 473: processReferralReward is called only inside if (newStatus === 'completed'). No 'delivered' trigger exists.

UAT Impact: Create a purchase request with a referred buyer, advance it to 'delivered' status, and verify no referral points are awarded. Then advance to 'completed' and verify points are awarded. Confirm this is the intended behavior or flag as a gap.

M79. Points redemption has no UI — redeemPoints action is wired to nothing

Description: The redeemPoints action is defined and correctly calls POST /points/redeem, but it is never invoked from any component or page in the frontend. There is no checkout integration, no discount code entry, and no redemption confirmation modal. Users cannot redeem points through the UI at all.

Doc Claim: Referral Flow step 12: 'Users view their balance at /dashboard/account/points and can spend points via POST /api/points/redeem (e.g. for service credit or discount codes)'.

Code Reality: grep across all frontend src files confirms redeemPoints is only defined in actions/points.ts and never called from any component or page.

UAT Impact: Confirm that on any purchase/checkout flow there is no 'use points' option. Confirm /dashboard/points has no redeem button or form. This is a complete missing feature that blocks the points-spend use-case entirely.

M80. Referrals list page does not exist despite route path being defined

Description: paths.ts defines /dashboard/points/referrals as a route. The getReferrals action is correctly defined. However no Next.js page file exists at /app/dashboard/points/referrals/ and getReferrals is never called from any component. The route path will 404.

Doc Claim: Referral Flow step 12 implies users can view referred users. GET /api/points/referrals is listed as a supported endpoint.

Code Reality: find /app/dashboard/points confirms only page.tsx, error.tsx, loading.tsx exist — no referrals/ subdirectory. getReferrals action is never imported or called anywhere outside actions/points.ts.

UAT Impact: Navigate to /dashboard/points/referrals — confirm it returns a 404 or renders the parent layout with no content. Verify there is no link to this page in the points dashboard nav.

M81. Full paginated transactions page does not exist; only first 5 items shown in overview

Description: The points main view fetches only { page: 1, limit: 5 } transactions for the 'recent transactions' widget and provides a 'view all' button that routes to /dashboard/points/transactions. However no Next.js page exists at /app/dashboard/points/transactions/. The route will 404.

Doc Claim: Points main view has a handleViewTransactions callback pushing to paths.dashboard.points.transactions.

Code Reality: find /app/dashboard/points shows no transactions/ subdirectory. The 'View All' button in points-main-view.tsx will navigate to a non-existent route.

UAT Impact: Click 'View All Transactions' on the points dashboard. Confirm the route 404s or shows an error page. Confirm there is no paginated history accessible to users.

M82. generateReferralCode action is never called; there is no regenerate button in the UI

Description: The PointsInviteFriends component displays the referral code passed as a prop (fetched once via getMyPoints) but has no button wired to generateReferralCode. The share UI is static — the code cannot be rotated from the UI. The POST /points/generate-referral-code endpoint is unused from the frontend.

Doc Claim: Referral Flow step 1-2: 'User opens /dashboard/account/referrals; if no code exists, clicks Generate code'. Frontend generates via POST /api/points/generate-referral-code.

Code Reality: points-invite-friends.tsx only displays referralCode prop — no button calls generateReferralCode. The action is defined in actions/points.ts but never invoked from any component.

UAT Impact: On the points dashboard, verify there is no 'Generate Code' or 'Regenerate Code' button. Confirm that if a user's referral code is auto-generated by getMyPoints lazy-bootstrap, there is no way to intentionally rotate it via the UI.

M83. Levels/tiers page does not exist and getLevels is never called from any component

Description: paths.ts defines /dashboard/points/levels but no page file exists at /app/dashboard/points/levels/. getLevels action is defined but never called from any component or page — users cannot see the loyalty tier structure, benefits, or thresholds.

Doc Claim: Frontend actions list getLevels as returning 'all loyalty level configurations (thresholds, benefits, icons)'.

Code Reality: No page at /app/dashboard/points/levels/. getLevels is never imported or called in any component outside actions/points.ts.

UAT Impact: Navigate to /dashboard/points/levels — expect 404. Confirm that the PointsLevelProgress component in the main view only shows current level vs next level (not the full tier ladder).

M84. Admin points management page does not exist; adminAddPoints action is never invoked

Description: No admin page exists under /app/dashboard/admin/ for managing user points. The adminAddPoints action is defined and the backend endpoint is functional, but there is no UI surface for admins to grant or deduct points.

Doc Claim: Frontend actions list adminAddPoints as 'Admin action to manually add points to any user account'. API doc specifies POST /api/points/admin/add with userId, amount, reason.

Code Reality: find /app/dashboard/admin confirms only confirmation-thresholds, derived-destinations, networks, payments-awaiting-confirmation subdirectories exist. No points-management page.

UAT Impact: Confirm there is no admin UI to manually adjust user point balances. Admins must use direct API calls or database access. Test that POST /points/admin/add works via curl/Postman with a valid admin JWT.

Domain: User Management

M85. No frontend action for wallet address read or update

Description: The backend exposes GET /api/user/wallet-address (returns walletAddress, type, provider) and PATCH /api/user/wallet-address (EVM signature-verified update, TON with optional TonProof). The axios endpoints object defines both endpoints.users.walletAddress and endpoints.users.updateWalletAddress, but no function in user.ts or any other action file consumes them. Users cannot view or update their wallet address through the frontend.

Doc Claim: API doc specifies GET and PATCH /api/user/wallet-address with full request/response shapes.

Code Reality: axios.ts lines 219-220 define the endpoints. user.ts has no getWalletAddress or updateWalletAddress function. missingFrontendFeatures list confirms this gap.

UAT Impact: QA must verify there is no wallet management UI. If wallet address display/edit is expected in the user profile or settings page, this is a feature gap that needs a frontend implementation.

M86. No frontend action for email verification after profile email change

Description: The backend automatically invalidates email verification when a user changes their email via PUT /api/user/profile and sends a 6-digit verification code. Two endpoints exist to support the follow-up flow: POST /api/user/profile/email/verify and POST /api/user/profile/email/resend-verification. Neither has a corresponding frontend action function, meaning users who change their email have no UI path to re-verify it.

Doc Claim: Backend notableLogic: 'Changing a user's email via PUT /api/user/profile automatically sets isEmailVerified=false and immediately dispatches a new 6-digit verification code.' Endpoints are documented in the API.

Code Reality: axios.ts defines endpoints.users.verifyProfileEmail and endpoints.users.resendProfileEmailVerification. user.ts has no verifyProfileEmail or resendProfileEmailVerification functions. missingFrontendFeatures list confirms this gap.

UAT Impact: QA must test changing an email address in profile settings. Verify whether a verification prompt appears and whether the user can complete verification. If not, the email change flow is incomplete.

M87. No frontend action for admin user stats endpoint

Description: The backend provides GET /api/users/admin/stats returning aggregated totals (total/active/verified counts, role distribution, 24h/7d/30d activity buckets). The endpoint is defined in axios.ts as endpoints.users.admin.stats but no function in user.ts or any admin action file fetches it. Admin analytics/dashboard cannot display these statistics.

Doc Claim: API doc specifies GET /api/users/admin/stats with full response shape. axios.ts line 223 defines the endpoint.

Code Reality: No function in user.ts or discovered action files calls endpoints.users.admin.stats. missingFrontendFeatures list confirms this gap.

UAT Impact: QA must check whether an admin statistics/analytics page exists. If it does, verify the data is populated. If it does not, treat as a missing feature.

M88. getUserAddresses calls /addresses (GET) but frontend docs describe endpoint as /addresses/list

Description: The getUserAddresses action calls endpoints.addresses.list which resolves to '/addresses' (GET). The frontend action documentation describes the apiPath as '/addresses/list'. The backend API doc lists GET /api/addresses (no /list suffix). The actual path in axios.ts is correct ('/addresses'), but the action documentation path '/addresses/list' is wrong and could mislead developers or QA.

Doc Claim: Frontend action doc for getUserAddresses: apiPath = '/addresses/list'.

Code Reality: axios.ts addresses.list = '/addresses'. user.ts calls endpoints.addresses.list which resolves to '/addresses'. Backend registers GET /api/addresses.

UAT Impact: QA should verify GET /api/addresses returns the user's address list correctly. No runtime bug expected since the code path is correct, but the documentation is misleading.

M89. API doc describes PATCH /api/user/admin/:userId/status body as isActive boolean; backend also accepts status string and isEmailVerified

Description: The API doc states the body for PATCH /api/user/admin/:userId/status is '{ isActive: boolean, reason?: string }'. The actual backend (new controller) accepts a status string ('active'/'suspended'/'deleted') and also accepts isEmailVerified as a boolean. The doc omits the status string form and the isEmailVerified field, which are actually implemented.

Doc Claim: API doc: body = 'isActive: boolean, reason?: string'.

Code Reality: Backend notes: 'Set status to active/suspended/deleted; also accepts isEmailVerified boolean'. The new controller uses a status string enum, not a boolean isActive.

UAT Impact: QA must test the status update endpoint with both the boolean isActive form and the string status form to determine which the new controller actually validates. Also verify isEmailVerified can be set through this endpoint.

M90. API doc states admin DELETE blocks admin-on-admin; new controller only blocks self-deletion

Description: The API doc states DELETE /api/user/admin/:userId returns '403 admin-on-admin' error when an admin tries to delete another admin. The actual new controller (DELETE /api/user/admin/:userId) only blocks self-deletion; it does not prevent deleting other admins. The legacy route (DELETE /api/users/admin/:userId) does block admin-on-admin deletion. These two routes now have divergent authorization logic.

Doc Claim: API doc: 'errors: 400 self-delete, 403 admin-on-admin, 404 not found'.

Code Reality: Backend notableLogic: 'Legacy admin DELETE /api/users/admin/:userId blocks deletion of users with role=admin; new controller DELETE /api/user/admin/:userId only blocks self-deletion.'

UAT Impact: QA must test deleting another admin account via the new controller endpoint and verify whether a 403 is returned. If no 403, an admin can delete other admin accounts — a significant privilege escalation issue.

M91. API doc states resend-verification generates 8-digit code; new controller generates 6-digit code

Description: The API doc for POST /api/users/admin/:userId/resend-verification states it 'Regenerates 8-digit email verification code'. The backend notableLogic explicitly documents a discrepancy: the new userController uses 6-digit codes while the legacy userRoutes uses 8-digit codes. The legacy endpoint path /api/users/admin/:userId/resend-verification uses 8-digit codes, but the new controller endpoint /api/user/admin (if it had a resend endpoint) would use 6-digit codes.

Doc Claim: API doc POST /api/users/admin/:userId/resend-verification: 'Regenerates 8-digit email verification code and re-sends email'.

Code Reality: Backend notableLogic: 'Email verification code is a 6-digit numeric code (userController) or 8-digit numeric code (legacy userRoutes). Both expire 15 minutes after generation. There is a discrepancy in code length.'

UAT Impact: QA must trigger admin resend-verification and check the actual code length in the email. Verify whether the UI input field accepts 6 or 8 digits and whether validation matches the code sent.

M92. TON wallet proof challenge endpoint not documented

Description: The backend exposes POST /api/user/wallet-address/ton-proof/challenge to generate a TON proof nonce for the current user. This endpoint is not listed in the API documentation at all. Any frontend wallet connection flow for TON wallets using TonProof requires this endpoint first.

Doc Claim: API doc does not mention POST /api/user/wallet-address/ton-proof/challenge.

Code Reality: Backend endpoint list includes: POST /api/user/wallet-address/ton-proof/challenge — 'Generate a TON proof nonce/challenge for the current user'.

UAT Impact: QA must check whether TON wallet connection is implemented in the frontend. If so, verify the challenge/nonce flow is called before submitting the TonProof. If not, this is a missing feature for TON wallet users.

M93. Profile email verification flow endpoints (POST /user/profile/email/verify and resend) not in API docs

Description: The backend implements two endpoints for re-verifying email after a profile email change: POST /api/user/profile/email/verify (accepts 6-digit code) and POST /api/user/profile/email/resend-verification. Neither endpoint appears in the API documentation. This makes the email change flow invisible to developers and QA.

Doc Claim: API doc does not list POST /api/user/profile/email/verify or POST /api/user/profile/email/resend-verification.

Code Reality: Backend endpoint list includes both endpoints. axios.ts defines endpoints.users.verifyProfileEmail and endpoints.users.resendProfileEmailVerification.

UAT Impact: QA must test the complete email-change flow: change email in profile, receive verification code email, enter code in UI (if UI exists), confirm isEmailVerified is set back to true.

Domain: Admin Operations

M94. User admin endpoint prefix inconsistency: doc mixes /api/user/ and /api/users/

Description: The API doc describes create, delete, status, toggle-status, role, list, and dependencies under /api/user/admin/* (singular). The stats, get-by-id, PUT update, PUT update/:email, password, and resend-verification are listed under /api/users/admin/* (plural). The frontend consistently calls /api/users/admin/* (plural) for all operations. The backend has two separate route groups (/api/user/* new controller, /api/users/* legacy), and the admin sub-routes are mounted on the legacy /api/users/* group.

Doc Claim: PATCH /api/user/admin/:userId/status, DELETE /api/user/admin/:userId, PATCH /api/user/admin/:userId/role, GET /api/user/admin/list, etc.

Code Reality: Frontend uses /api/users/admin/:id/status (plural), /api/users/admin/:id, etc. Backend mounts admin routes on /api/users/* (legacy). The singular /api/user/admin/* paths documented for create/delete/status/role/list are incorrect.

UAT Impact: QA must verify: PATCH /api/user/admin/:userId/status returns 404 while PATCH /api/users/admin/:userId/status returns 200 with valid admin token. Confirm all documented /api/user/admin/* singular paths are unreachable.

M95. updateUserStatus and updateUserRole use PUT in frontend but PATCH in API doc

Description: The API doc specifies PATCH /api/user/admin/:userId/status and PATCH /api/user/admin/:userId/role. The frontend user actions (src/actions/user.ts) call axiosInstance.put() for both updateUserStatus and updateUserRole. HTTP method semantics differ: PATCH is partial update, PUT is full replacement. If the backend only registers a PATCH handler, all PUT calls from the frontend will fail with 404 or 405.

Doc Claim: PATCH /api/user/admin/:userId/status, PATCH /api/user/admin/:userId/role

Code Reality: Frontend calls PUT /api/users/admin/:id/status and PUT /api/users/admin/:id/role. The frontend actions file at /Users/manwe/CascadeProjects/escrow/frontend/src/actions/user.ts lines 162 and 175 use axiosInstance.put().

UAT Impact: QA must verify: Using the admin UI to change a user status or role succeeds. If the backend only accepts PATCH, these actions will silently fail in production. Test both the network tab method and the server response.

M96. updateUserStatus frontend accepts 'inactive'/'pending' but API doc says 'active'/'suspended'

Description: The API doc for PATCH /api/user/admin/:userId/status states the body accepts status values of 'active' or 'suspended'. The frontend TypeScript type in updateUserStatus (src/actions/user.ts line 159) constrains the parameter to 'active' | 'inactive' | 'pending'. The value 'suspended' from the doc is absent from the frontend, and 'inactive'/'pending' are not mentioned in the doc. This creates a mismatch where the frontend may send values the backend does not recognize.

Doc Claim: body: status (active/suspended)

Code Reality: Frontend type: status: 'active' | 'inactive' | 'pending'. The value 'suspended' is not in the frontend union type.

UAT Impact: QA must verify: sending status='suspended' from a test client updates the user correctly. Verify what the backend model actually accepts. Test that the frontend status toggle UI sends a value the backend understands.

M97. POST /api/admin/cleanup/clean: confirm body param documented as optional but backend requires it for real deletions

Description: The API doc lists the confirm parameter for POST /api/admin/cleanup/clean with a question mark suffix, implying it is optional. The backend notableLogic explicitly states: 'Requires body.confirm="DELETE_ALL_DATA" and dryRun=false for actual deletion.' Omitting confirm will either silently do nothing or error, depending on implementation — not perform the deletion as a caller following the doc would expect.

Doc Claim: body: collections?, dryRun?, keepAdmins?, olderThanDays?, confirm? (all optional per doc notation)

Code Reality: Backend enforces confirm='DELETE_ALL_DATA' for any non-dry-run execution. The field is required to perform actual cleanup, not optional.

UAT Impact: QA must verify: POST /api/admin/cleanup/clean with dryRun=false but no confirm field — should be rejected, not execute deletions. Verify the API returns a clear error rather than silently skipping.

M98. AML settings endpoints entirely absent from API documentation

Description: The backend exposes GET and PATCH /api/admin/settings/aml to read and update the AML provider configuration at runtime. These are admin-only operations that mutate process.env (provider and cost) without persistence — changes are lost on server restart. Neither endpoint is mentioned in the API doc. Additionally there is no frontend page or action for AML settings management.

Doc Claim: No entry for /api/admin/settings/aml in the API doc

Code Reality: Backend registers GET /api/admin/settings/aml (returns provider=none|chainalysis and costUsd, never exposes API key) and PATCH /api/admin/settings/aml (updates process.env at runtime). The runtime-only nature of the PATCH means a server restart silently reverts any change.

UAT Impact: QA must verify: GET /api/admin/settings/aml returns current AML config. PATCH updates the running config. After a server restart, confirm the change is lost (documents the persistence limitation). No frontend UI exists for this operation.

M99. Confirmation thresholds, awaiting-confirmation, and RN network registry endpoints are undocumented

Description: The backend exposes a suite of admin blockchain-monitoring endpoints that have corresponding frontend pages and actions but are absent from the API doc: GET /api/admin/settings/confirmation-thresholds, PATCH /api/admin/settings/confirmation-thresholds/:chainId, GET /api/admin/payments/awaiting-confirmation, and GET /api/admin/rn/networks. All four have frontend pages under /dashboard/admin/ and dedicated action files (confirmation-thresholds.ts, network-registry.ts).

Doc Claim: None of these endpoints appear in the API documentation

Code Reality: All four endpoints exist in backend, have admin auth, and are actively used by the frontend at /Users/manwe/CascadeProjects/escrow/frontend/src/app/dashboard/admin/

UAT Impact: QA must verify: all four endpoints return correct data with admin JWT. The awaiting-confirmation page shows payments with tx hash but not yet in funded/released state. The confirmation-thresholds page shows and allows updating per-chain values persisted to DB.

M100. Derived destinations and sweep endpoints are undocumented

Description: The backend exposes a full derived-destinations management suite (derive, list, by-id, balance, sweep, native-balance, sweep-native) all under /api/payment/derived-destinations/* with admin auth. A frontend page exists at /dashboard/admin/derived-destinations with corresponding actions in src/actions/derived-destinations.ts. None of these endpoints appear in the API doc.

Doc Claim: No /api/payment/derived-destinations/* endpoints in the API doc

Code Reality: Backend registers 7 derived-destinations endpoints. Frontend page at /Users/manwe/CascadeProjects/escrow/frontend/src/app/dashboard/admin/derived-destinations/ calls getDerivedDestinations, triggerSweep, triggerSingleSweep, getSweepCronStatus, startSweepCron, stopSweepCron.

UAT Impact: QA must verify the complete derived-destinations workflow: list all destinations, trigger a dry-run sweep, check cron status, start/stop the cron, and trigger a single-destination sweep.

M101. Frontend calls derived-destinations cron and single-sweep endpoints not present in backend data

Description: The frontend derived-destinations actions call four endpoints that do not appear in the backend endpoint inventory: GET /api/payment/derived-destinations/cron/status, POST /api/payment/derived-destinations/cron/start, POST /api/payment/derived-destinations/cron/stop, and POST /api/payment/derived-destinations/:id/sweep. The backend lists only the bulk POST /api/payment/derived-destinations/sweep and POST /api/payment/derived-destinations/:id/sweep-native. The cron management and per-id token sweep may be unimplemented.

Doc Claim: Frontend actions: getSweepCronStatus, startSweepCron, stopSweepCron, triggerSingleSweep (all defined in src/actions/derived-destinations.ts)

Code Reality: Backend data lists no cron/* endpoints and no plain /:id/sweep endpoint (only /:id/sweep-native). The derived-destinations UI page actively calls getSweepCronStatus on mount.

UAT Impact: QA must verify: open /dashboard/admin/derived-destinations and observe the network tab — check whether the cron status request returns 200 or 404. Attempt to start/stop the cron and trigger a single sweep; any 404 confirms the backend routes are missing.

M102. Frontend calls network registry reload and chain probe endpoints not in backend data

Description: The frontend network-registry actions call POST /api/admin/rn/networks/reload and POST /api/admin/rn/networks/probe/:chainId. The backend endpoint inventory lists only GET /api/admin/rn/networks. The reload and probe operations may not be implemented on the backend, causing the UI buttons for these actions to silently fail.

Doc Claim: Frontend actions reloadNetworkRegistry and probeChain defined in src/actions/network-registry.ts

Code Reality: Backend data shows only GET /api/admin/rn/networks. No reload or probe endpoints are registered.

UAT Impact: QA must verify: in the /dashboard/admin/networks page, click Reload Registry and Probe Chain buttons. Check network tab for 404 responses. Confirm whether these are implemented on the backend.

M103. Frontend calls GET /api/admin/settings/confirmation-thresholds/history which is not in backend data

Description: The frontend confirmation-thresholds action file defines getConfirmationThresholdHistory() which calls GET /api/admin/settings/confirmation-thresholds/history. The backend endpoint inventory lists only GET /api/admin/settings/confirmation-thresholds (returns current values) and PATCH /api/admin/settings/confirmation-thresholds/:chainId (update). A history endpoint is not registered.

Doc Claim: Frontend action getConfirmationThresholdHistory in src/actions/confirmation-thresholds.ts calls /admin/settings/confirmation-thresholds/history

Code Reality: Backend data lists no history endpoint for confirmation thresholds. Only the current-values GET and the per-chain PATCH exist.

UAT Impact: QA must verify: if the confirmation-thresholds UI page requests history, confirm the network request returns 404 or 200. Determine if history tracking was ever implemented.

M104. GET /api/disputes/statistics auth: doc claims admin-only, backend applies only authenticateToken

Description: The API doc lists GET /api/disputes/statistics as requiring Bearer JWT with role=admin. The backend registers this endpoint with only authenticateToken middleware, with no authorizeRoles(admin) guard. Any authenticated non-admin user can access aggregate dispute KPI data.

Doc Claim: GET /api/disputes/statistics requires Bearer JWT, role=admin

Code Reality: Backend registers GET /api/disputes/statistics with auth: authenticateToken only. No role restriction is enforced at the route or controller level per the backend data.

UAT Impact: QA must verify: call GET /api/disputes/statistics with a valid non-admin user JWT. If it returns 200 with statistics data, the endpoint is under-protected. It should return 403 for non-admin users.

M105. POST /api/payment/payments/cleanup-pending: doc claims admin middleware, backend enforces admin in handler only

Description: The API doc states cleanup-pending requires Bearer JWT with role=admin (implying middleware-level enforcement). The backend registers this with only authenticateToken at the route level; the admin check is performed inside the handler. Any authenticated non-admin who discovers this endpoint can attempt to call it; the in-handler check is the only defense.

Doc Claim: POST /api/payment/payments/cleanup-pending requires Bearer JWT, role=admin

Code Reality: Backend registers: auth: authenticateToken, notes: 'Admin check inside handler; deletes pending payments older than 2h'. No authorizeRoles(admin) middleware at the route level.

UAT Impact: QA must verify: call POST /api/payment/payments/cleanup-pending with a valid non-admin user JWT. Should return 403. Also verify the cleanup logic only deletes pending payments older than 2 hours.

M106. POST /api/points/admin/add: doc claims middleware-level admin auth, backend uses handler-level check

Description: The API doc lists POST /api/points/admin/add as requiring Bearer JWT with role=admin. The backend registers this with authenticateToken and a role check inside the handler, not via authorizeRoles middleware. The distinction matters because a middleware rejection returns 403 before any business logic runs; a handler check may have edge-case bypass risk.

Doc Claim: POST /api/points/admin/add requires Bearer JWT, role=admin

Code Reality: Backend: auth: authenticateToken + role check in handler. The /api/admin/ path prefix and the 'admin/add' route name imply admin-level protection but the middleware chain does not enforce it.

UAT Impact: QA must verify: send POST /api/points/admin/add with a non-admin JWT. Should return 403. Verify the role check fires before any points mutation occurs.

M107. No admin UI for shkeeper release, refund, payout, and webhook-stats operations

Description: The API doc defines admin-only shkeeper endpoints for triggering escrow releases, refunds, payout tasks, and reading webhook telemetry. No frontend page, action, or component exists for any of these operations. The frontend does have releasePayment and processRefund in src/actions/payment.ts but these call /api/payment/:id/release (not the documented shkeeper paths) and are invoked from payment detail views, not dedicated admin tooling.

Doc Claim: POST shkeeper release/confirm, POST shkeeper refund/confirm, POST shkeeper payout, GET shkeeper/webhook-stats all documented as admin endpoints

Code Reality: No frontend page exists under /dashboard/admin/ or elsewhere for these shkeeper admin operations. src/actions/payment.ts has releasePayment hitting /payment/:id/release (correct backend path) but no dedicated admin shkeeper management UI.

UAT Impact: QA must verify: admins have no in-app way to trigger shkeeper release/refund or create payout tasks. These operations must currently be done via API client or curl. Confirm whether this is intentional or a missing feature.

M108. No admin UI for data cleanup, seeder, and GDPR user-deletion operations

Description: The API doc defines a full cleanup/maintenance suite: stats, collections list, bulk clean, GDPR per-user deletion, temp cleanup, seed-templates, and seed-all. All are admin-only. No frontend page or action exists for any of these endpoints. They can only be called directly via API.

Doc Claim: GET /api/admin/cleanup/stats, GET /api/admin/cleanup/collections, POST /api/admin/cleanup/clean, DELETE /api/admin/cleanup/user/:userId, POST /api/admin/cleanup/temp, POST /api/admin/cleanup/seed-templates, POST /api/admin/cleanup/seed-all

Code Reality: No frontend page under /dashboard/admin/ handles any cleanup route. No action file imports or calls these endpoints. The backend has all routes with proper auth.

UAT Impact: QA must verify: admins cannot currently perform GDPR user data deletion, run data cleanup, or seed templates through the UI. These are operational gaps. Confirm if a separate admin tool or script covers these.

M109. No admin UI for user password reset, resend-verification, update-by-email, and user stats

Description: The API doc defines admin endpoints for resetting any user's password (wiping refresh tokens), resending verification email, updating user by email address, and viewing aggregate user statistics. None of these have frontend UI, action functions, or routes. The user management pages at /dashboard/user/ handle basic CRUD but lack these admin-specific operations.

Doc Claim: PATCH /api/users/admin/:userId/password, POST /api/users/admin/:userId/resend-verification, PUT /api/users/admin/update/:email, GET /api/users/admin/stats

Code Reality: src/actions/user.ts has no functions for password reset, resend-verification, or update-by-email. The axios endpoints config has stats: '/users/admin/stats' defined but the missingFrontendFeatures list confirms no UI page exists for it.

UAT Impact: QA must verify: admin cannot reset a user's password or resend verification through the UI. Test the raw endpoints directly: PATCH /api/users/admin/:userId/password should clear refresh tokens; POST /api/users/admin/:userId/resend-verification should queue an email.

M110. Blog admin CRUD endpoints are undocumented

Description: The backend exposes a complete admin blog management API: GET /api/blog/admin/posts (list), POST /api/blog/posts (create), GET /api/blog/admin/posts/:id (detail), PUT /api/blog/posts/:id (update), DELETE /api/blog/posts/:id (delete). All require admin auth. Frontend actions for getAdminBlogPosts and getBlogPostById exist (src/actions/blog.ts), and the missingFrontendFeatures note confirms no admin blog page exists. None of these endpoints appear in the API doc.

Doc Claim: No blog admin endpoints in the API documentation

Code Reality: Backend registers 5 blog admin endpoints with authorizeRoles(admin). Frontend has action functions that call them. No dedicated admin blog management UI page exists.

UAT Impact: QA must verify: GET /api/blog/admin/posts returns all posts for admin, not just published ones. POST /api/blog/posts creates a post. Confirm the blog post editor at /dashboard/post/ is restricted to admins.

Domain: Trezor Safekeeping

M111. GET /api/trezor/account endpoint not documented

Description: The backend exposes GET /api/trezor/account (authenticated) which returns the active Trezor account summary including xpubFingerprint, registrationAddress, basePath, nextAddressIndex, and addressCount (or {registered:false}). This endpoint is not listed in the documented API endpoints at all.

Doc Claim: Documented API endpoints are: GET /api/trezor/registration-message, POST /api/trezor/register, POST /api/trezor/addresses/next, POST /api/trezor/operation-message.

Code Reality: Backend also has GET /api/trezor/account (auth: authenticateToken) returning the account summary or {registered:false}.

UAT Impact: QA cannot verify the account status check flow. A frontend that eventually implements registration would use this endpoint to pre-check whether a user is already registered — the missing documentation means this was never wired up in the frontend either.

M112. POST /api/trezor/verify-operation endpoint not documented

Description: The backend exposes POST /api/trezor/verify-operation (admin-only: authenticateToken + authorizeRoles('admin')) which verifies a Trezor ECDSA signature for an operation payload against the admin's registered safekeeping address. This is a distinct step from operation-message generation and is not mentioned in the documented flow at all.

Doc Claim: Step 11 describes submitting release/refund confirmation with a trezor object, and step 12 references confirmReleaseRefundInstruction verifying the Trezor signature, but no separate /api/trezor/verify-operation endpoint is documented.

Code Reality: Backend has POST /api/trezor/verify-operation (admin-only) as a standalone verification endpoint separate from operation-message. The notable logic note confirms it checks the recovered signer against TrezorAccount.registrationAddress using a per-operation nonce.

UAT Impact: QA should test this endpoint directly: submit a valid operation payload with a correct admin Trezor signature and confirm 200 response; submit with a wrong signer and confirm rejection. Also verify whether the release/refund confirmation endpoints call this internally or require the frontend to call it first.

M113. Registration role ambiguity: doc says 'User' but operation endpoints are admin-only

Description: The flow says 'User (seller/buyer connecting Trezor)' registers in steps 1-6, but POST /api/trezor/operation-message and POST /api/trezor/verify-operation are both admin-only (authorizeRoles('admin')). The doc flags in the original document acknowledge this ambiguity. Given that the Trezor safekeeping guard is about admin authorization of releases/refunds, the registrant for the safekeeping address must be an admin account, not a buyer or seller.

Doc Claim: Actors list includes 'User (seller/buyer connecting Trezor)'. Steps 1-4 are attributed to the generic 'User'.

Code Reality: POST /api/trezor/register uses only authenticateToken (no role restriction), so any role can register. But operation-message and verify-operation are admin-only, meaning the Trezor used to authorize releases must belong to an admin account. The registrationAddress matched during verify-operation is from the admin's TrezorAccount.

UAT Impact: QA must clarify and test: (1) Can a buyer/seller register a Trezor and, if so, what does it enable? (2) Verify that an admin-registered Trezor address is what the safekeeping guard validates against — not a buyer's registered address.

M114. Upsert behavior on re-registration not documented

Description: The backend POST /api/trezor/register performs an upsert: if the user already has a TrezorAccount, it updates xpub, basePath, and deviceLabel but preserves nextAddressIndex and the existing addresses array via $setOnInsert. This means re-registering with a new xpub keeps old derived address records pointing to the previous xpub, which could cause address/xpub mismatches in accounting.

Doc Claim: Step 6 says 'Backend stores userId, xpub fingerprint, xpub, base derivation path, registrationAddress, next address index, and issued address records.' No mention of update/upsert behavior or what happens on re-registration.

Code Reality: notableLogic: 're-registering (POST /register) updates xpub/basePath/label on the existing TrezorAccount document but preserves nextAddressIndex and the addresses array via $setOnInsert.'

UAT Impact: QA should test: register with xpub-A, issue a deposit address, re-register with xpub-B, then call GET /api/trezor/account and POST /api/trezor/addresses/next to verify whether old addresses remain and whether new addresses are derived from xpub-B while old records reference xpub-A.

M115. Purpose field valid values not documented but are enumerated in the schema

Description: The doc notes that POST /api/trezor/addresses/next accepts a 'purpose' field but only shows 'deposit' as an example and explicitly flags it as unclear. The backend status values enumerate TrezorAccount.addresses[].purpose as: deposit, release, refund, other — meaning all four are valid schema values.

Doc Claim: Step 7: 'POST /api/trezor/addresses/next (purpose: deposit, paymentId)'. DocFlag: 'only deposit shown as example — unclear what other values are valid.'

Code Reality: statusValues: 'TrezorAccount.addresses[].purpose: deposit, release, refund, other' — four defined enum values.

UAT Impact: QA should test POST /api/trezor/addresses/next with each purpose value (deposit, release, refund, other) and confirm all are accepted. Also test an invalid purpose value to confirm rejection.

Domain: Delivery

M116. Seller marks shipped via PUT /delivery, not PATCH /:id with {status:'delivery'}

Description: The documentation step 2 states the frontend sends 'PATCH /api/marketplace/purchase-requests/:id with {status: delivery}' when the seller clicks 'Mark as shipped'. The actual frontend action is updateDelivery which calls PUT /api/marketplace/purchase-requests/:id/delivery. The controller's updateDeliveryInfo (PUT /delivery) is the shipping endpoint — it sets shippedAt, updates delivery date/time fields, and advances status to 'delivery' when the current status is in [processing, payment, delivery, confirming].

Doc Claim: Step 2: 'Frontend sends PATCH /api/marketplace/purchase-requests/:id with {status: delivery}'.

Code Reality: Frontend action updateDelivery calls PUT /marketplace/purchase-requests/:id/delivery. The marketplaceController.updateDeliveryInfo method at controllerRoutes.ts line 103 handles this endpoint and conditionally sets status='delivery'. The generic PATCH /:id endpoint also exists but does not trigger delivery-code generation.

UAT Impact: QA must verify: seller calling PUT /delivery with valid delivery date advances status to 'delivery' and sets shippedAt. Test that PATCH /:id with {status:'delivery'} does NOT auto-generate a delivery code or emit delivery-specific socket events.

M117. confirm-delivery endpoint is buyer-only, not admin-only, and has a buyer auth check

Description: The documentation is ambiguous about who can trigger confirm-delivery, stating in docFlags that it is 'unclear which authorization model (admin only vs. buyer self-service)'. The controller implementation confirms it is buyer self-service: it checks request.status === 'delivery' and does not restrict to admin. There is no admin-only guard — any authenticated user can call it as long as they know the request ID, because the controller's confirmDelivery method (line 782) does not verify that the caller is the buyer of the request.

Doc Claim: API table lists 'PATCH /:id/confirm-delivery — Buyer fast-track confirm (no code)'. docFlags say authorization model is unclear.

Code Reality: marketplaceController.confirmDelivery (line 782) checks: (1) dispute gate via isReleaseBlockedById, (2) status must be 'delivery'. It does NOT verify the caller is the buyer — any authenticated user can confirm delivery on any request in 'delivery' status. Sets deliveryConfirmed=true and deliveryConfirmedAt, transitions to 'delivered'.

UAT Impact: QA must test: (1) seller can incorrectly call PATCH /confirm-delivery and it succeeds — this is a security gap; (2) buyer can call it and confirm delivery without a code; (3) confirm that PurchaseRequestService.ts:631-641 notifyDeliveryConfirmed is NOT called from this endpoint (the doc says it should be).

M118. notifyDeliveryConfirmed called via socket events in DeliveryService, not PurchaseRequestService:631-641

Description: The documentation states 'Backend calls notifyDeliveryConfirmed for both buyer and seller (PurchaseRequestService.ts:631-641)' after successful code verification. In reality, DeliveryService.verifyDeliveryCode handles notifications directly by creating NotificationService records for both buyer and seller and emitting 'delivery-confirmed' + 'buyer-confirmed-delivery' socket events. There is no call to PurchaseRequestService at line 631-641 from the verification path.

Doc Claim: Step 13: 'Backend calls notifyDeliveryConfirmed for both buyer and seller (PurchaseRequestService.ts:631-641)'.

Code Reality: DeliveryService.verifyDeliveryCode (lines 180-212) sends in-app notifications to buyer and seller via NotificationService.createNotification. It emits 'delivery-confirmed' to request-{id} room and 'buyer-confirmed-delivery' to user-{sellerId} room. No call to PurchaseRequestService.

UAT Impact: QA must verify: after successful code verification, in-app notification appears for buyer AND seller. The 'delivery-confirmed' socket event fires on the request room. Confirm buyer-confirmed-delivery fires on the seller's user room.

M119. delivery-code-generated socket event broadcasts raw code to entire request room including seller

Description: DeliveryService.generateDeliveryCode emits 'delivery-code-generated' with the raw 6-digit code in the payload to the room request-{id}. Both buyer and seller are in this room. The documentation flags the security concern on the buyer notification side but the socket broadcast exposes the code to everyone in the room — including the seller — before physical handoff, defeating the purpose of the code.

Doc Claim: Socket events section: 'delivery-code-generated → room request-{id} (payload: code, expiresAt)'. Security note only mentions buyer notification side.

Code Reality: DeliveryService.ts line 55: global.io.to('request-{requestId}').emit('delivery-code-generated', { requestId, code, expiresAt, timestamp }). The full 6-digit code is in the payload to all room subscribers (buyer + seller + any admin listeners).

UAT Impact: QA/Security: verify that seller-side socket listener receives the delivery code via the 'delivery-code-generated' event payload. This means a malicious seller with socket access could intercept the code. Test whether a rate-limit or brute-force lockout exists on the verify endpoint (it does not — no attempt limit in code).

M120. No regenerate delivery code endpoint exists in the backend

Description: The frontend action regenerateDeliveryCode calls POST /marketplace/purchase-requests/:id/delivery-code/regenerate. This endpoint does not exist in either routes.ts or controllerRoutes.ts. The frontend has a catch fallback that calls generateDeliveryCode instead (POST /generate). DeliveryService.regenerateDeliveryCode() exists as a method but is not exposed via any route.

Doc Claim: Edge cases section: 'POST /:id/delivery-code regenerates a new 6-digit value, invalidates the old one, and re-notifies; access should be restricted to admin/seller to avoid abuse'.

Code Reality: No backend route for /delivery-code/regenerate exists. The frontend falls back silently to /delivery-code/generate on 404. The DeliveryService.regenerateDeliveryCode method exists (line 342) but is unreachable via HTTP.

UAT Impact: QA must confirm: POST /delivery-code/regenerate returns 404. The frontend silently falls back to generate — verify this actually creates a new code and whether the old code is properly invalidated (the fallback skips the regenerateDeliveryCode method's invalidation step).

M121. No backend endpoints for /delivery-code/attempts and /delivery/stats

Description: The frontend actions getDeliveryAttempts and getDeliveryStats call /marketplace/purchase-requests/:id/delivery-code/attempts and /delivery/stats respectively. Neither endpoint is registered anywhere in the backend. Delivery attempt data is stored in deliveryInfo.deliveryAttempts[] in MongoDB but there is no HTTP route to read it. Similarly, /delivery/stats has no backend handler.

Doc Claim: Not documented in the flow doc (these are from the frontend actions inventory).

Code Reality: No route for /delivery-code/attempts or /delivery/stats exists in routes.ts, controllerRoutes.ts, or any other backend file. DeliveryService has no method to aggregate stats. Failed attempt codes are stored to deliveryInfo.deliveryAttempts[] by logDeliveryAttempt() but no route exposes them.

UAT Impact: QA must confirm both endpoints return 404. Any UI that calls these will display no data or an error silently.

M122. All six delivery-code frontend actions have no UI surface in any dashboard page

Description: The frontend defines generateDeliveryCode, verifyDeliveryCode, getDeliveryCode, checkDeliveryCodeStatus, regenerateDeliveryCode, and getDeliveryAttempts in src/actions/delivery.ts, but no dashboard page under /dashboard/* imports or invokes them. The delivery-code-verification.tsx and step-4-waiting-for-confirmation.tsx components reference the axios endpoints directly, while the step-5-receive-goods.tsx component uses them, but there is no dedicated delivery management page and no code-generation button wired to generateDeliveryCode from the actions file.

Doc Claim: The flow doc references 'frontend/src/sections/request/components/seller-steps/delivery-code-verification.tsx or buyer-steps/step-5-receive-goods.tsx' as the UI for code entry.

Code Reality: src/actions/delivery.ts is imported only by the types file. The step components use axiosInstance directly rather than the actions layer. No dashboard page provides a code generation button, delivery attempt history, or delivery statistics view.

UAT Impact: QA must manually navigate to the step-5-receive-goods buyer step component and verify that: code entry UI exists, code submission reaches the correct /delivery-code/verify endpoint, and the POST is made by the seller (logged-in as seller) not the buyer.

M123. confirm-delivery does not call notifyDeliveryConfirmed and emits different socket event than documented

Description: The controller's confirmDelivery endpoint (PATCH /confirm-delivery) sets deliveryConfirmed=true, transitions status to 'delivered', and emits 'purchase-request-update' with eventType='status-changed'. It does NOT emit the 'delivery-confirmed' or 'buyer-confirmed-delivery' socket events that the document lists, nor does it call any notification service. The documented socket event 'purchase-request-update (status-changed)' is correct, but the absence of delivery-specific notifications means the buyer fast-track path is silent to the seller.

Doc Claim: Socket events section: 'purchase-request-update (status-changed) → room request-{id} on delivery→delivered transition'. Implied that notifyDeliveryConfirmed runs for both paths.

Code Reality: marketplaceController.confirmDelivery emits only 'purchase-request-update' with status-changed payload. No notification is sent to seller via NotificationService. The 'delivery-confirmed' and 'buyer-confirmed-delivery' events are only emitted by DeliveryService.verifyDeliveryCode (the code path), not the fast-track confirm-delivery path.

UAT Impact: QA must test the fast-track confirm-delivery path: confirm seller receives NO 'buyer-confirmed-delivery' socket event and NO in-app notification. The seller will not be proactively informed when buyer uses the no-code path.


Minor / Info Findings

Domain: Authentication

  • Sign-up view hardcodes password: '' when calling signUp() — password field missing from form — The jwt-sign-up-view.tsx onSubmit handler calls signUp({ ..., password: '', ... }) with an empty string (line 191, with comment 'You might need to add password field to form'). The signUp action sends this empty password to POST /api/auth/register. The backend ignores it (password is set at verif...
  • Google sign-in also filters by status:active — soft-deleted users get 404, not a distinct error — The Google OAuth Flow doc describes the sign-in 404 case as 'User missing during sign-in → 404' and the 409 case as 'User already exists during sign-up'. It does not mention that googleSignIn also queries with status:'active'. A user who registered via Google and was soft-deleted will receive 404...
  • Passkey challenge TTL is 5 minutes in code, but doc cites it as part of a 60-second timeout — The doc mentions the passkey registration challenge has a 'timeout: 60000' in the returned object (which is the WebAuthn browser timeout). The actual in-memory challenge expiry used for server-side validation is 300,000ms (5 minutes), set in passkeyService.ts:68 and 81. These serve different purp...
  • No UI path to verify POST /api/auth/reset-password (legacy token-based variant) — The Password Reset Flow doc lists POST /api/auth/reset-password as a legacy token-based variant and the frontend actions table includes a resetPassword action that calls this endpoint. However no view component wires to it — the reset-password and update-password views only use the code-based flo...
  • Login increments rate-limit counter before rate-limit reset on success — counter is transient during valid login — On a successful login, the counter increments to 1 (via checkLoginAttempts) and is then deleted (via resetLoginAttempts). A user who has 4 prior failures can still log in on the 5th attempt because checkLoginAttempts returns allowed:true when count < 5 and then increments to 5. On success the cou...
  • signUp action sends password to /auth/register but view always passes empty string — The signUp action signature accepts a password parameter and forwards it to the register endpoint. The sign-up view always passes password:'' (jwt-sign-up-view.tsx:191). The backend stores this in TempVerification.password which is unused (the real password comes from verify-email-code). There is...

Domain: Purchase Request

  • Delivery Confirmation doc lists two separate socket emits ('delivery-code-generated' and 'delivery-update'); backend only emits one event name — The Delivery Confirmation socket events section lists both 'delivery-code-generated' and 'delivery-update' as distinct events emitted to room request-{id}. The backend routes.ts delivery code generation handler (lines 2770-2777) does not emit any socket event directly; it returns the code in the ...
  • Delivery Confirmation doc: PATCH /confirm-delivery listed as buyer fast-track but backend auth does not restrict to buyer only — The Delivery Confirmation Flow lists PATCH /:id/confirm-delivery as a buyer fast-track. The backend controllerRoutes.ts line 114 registers it with only authenticateToken (no role or ownership check visible in the route definition). The doc is ambiguous about authorization model. A separate edge c...
  • Delivery code management has no dashboard page despite all six actions being defined — Six delivery-related frontend actions are defined in src/actions/delivery.ts (generateDeliveryCode, verifyDeliveryCode, getDeliveryCode, checkDeliveryCodeStatus, regenerateDeliveryCode, getDeliveryAttempts). The generate and verify functions are used inline within step components (step-5-receive-...
  • Workflow steps endpoint defined and backed but never called from the request detail page — GET /marketplace/purchase-requests/:id/workflow-steps is registered on the backend (both routers) and the frontend defines getWorkflowSteps in marketplace.ts. However, the frontend request detail views (buyer-request-details-view.tsx, seller-request-details-view.tsx, admin-request-details-view.ts...
  • Backend 'transaction-completed' socket event to buyer and seller rooms is undocumented — The backend socket events list notes 'Emit → room user-{buyerId}: transaction-completed' and 'Emit → room user-{sellerId}: transaction-completed' when request reaches 'completed'. These events are not mentioned in any of the four documented flows.
  • No brute-force protection for 6-digit delivery code verification is documented or visible in backend code — The delivery code is 6 digits (1,000,000 possible values). The backend verify endpoint (routes.ts lines 2790-2847) has no visible rate limiting, lockout counter, or attempt tracking. The doc flags this as a security gap but marks it as unresolved.
  • Backend registers PATCH /purchase-requests/:id in routes.ts (legacy) in addition to the controller PATCH /status — The legacy routes.ts registers a general PATCH /purchase-requests/:id that updates any field (line 1007). The controllerRoutes.ts registers a dedicated PATCH /purchase-requests/:id/status for status changes only. Both are active. The documented flows only describe the /status endpoint.
  • POST /api/marketplace/purchase-requests/:id/final-approval creates a dummy payment for testing if no payment exists — The final-approval endpoint in routes.ts (lines 1561-1592) contains logic that creates a dummy Payment document when no real payment is found and the request is in 'delivered' or 'delivery' status. This testing backdoor is not documented anywhere and bypasses the payment integrity check in produc...

Domain: Seller Offer

  • Doc step 6 says backend responds '200 { offer }' but createOffer throws duplicate error as generic 400, not 409 — The SellerOfferService.createOffer() catches the duplicate-offer case and throws a Persian error. The route handler in routes.ts (lines 1185-1195) catches errors and returns 409 only when error.message includes 'already has an offer' (the exact string from line 74 of SellerOfferService). However ...
  • Doc says validUntil=0 is rejected by schema validator at creation time; schema has no such validator — The doc edge-cases section states 'validUntil in the past at creation → schema-level validator should reject'. The SellerOffer Mongoose schema (SellerOffer.ts lines 90-92) defines validUntil as a plain Date with no custom validator checking whether it is in the future. A past date will be accepte...
  • HTTP status code for withdrawing a non-pending offer is undocumented — The doc flags section acknowledges 'The doc does not specify what HTTP status code is returned when a seller tries to withdraw a non-pending offer'. The withdrawOffer() service method returns null when the offer is not in pending status (findOneAndUpdate returns null). The only route that can tri...
  • Doc references seller marketplace dashboard at /dashboard/seller/marketplace with offer cards, but no dedicated seller dashboard page is confirmed in frontend — The flow step 1 says 'Seller opens /dashboard/seller/marketplace; page hits GET /api/marketplace/purchase-requests?sellerId={me}'. The missingFrontendFeatures list confirms 'No dedicated dashboard page for a seller to view and manage all their submitted offers — offer interactions are only embedd...

Domain: Payment

  • DePay flow references non-existent 'sellerOfferId' field in decentralized/save body — The DePay flow step 4 mentions sending 'sellerOfferId' to the save endpoint. The API docs list the body as: purchaseRequestId, buyerId, sellerId?, amount, currency?, transactionHash?, network?, token?, walletAddress?, metadata?. There is no sellerOfferId in the documented or implemented body sche...
  • SHKeeper flow references POST /api/payment/shkeeper/create — actual implemented intent path is /shkeeper/intents — The SHKeeper flow step 2 documents 'Frontend POSTs POST /api/payment/shkeeper/create'. The backend API docs and backend code list POST /api/payment/shkeeper/intents as the current intent creation endpoint. The /create path is also defined in the frontend axios config (endpoints.payments.shkeeper....
  • payout-completed socket event is emitted by backend but no frontend handler exists — The backend socket events list 'payout-completed (server→client, user-{sellerId} room): emitted after admin wallet payout to seller'. No frontend code was found listening for the payout-completed event. The seller dashboard will not receive a real-time notification when the admin pays out via the...
  • payment-received socket event emitted by Web3 verify — no frontend listener found — The backend socket events list 'payment-received (server→client, user-{sellerId} room): emitted when buyer completes Web3 payment verify'. No frontend component was found with socket.on('payment-received'). The seller will not receive real-time notification of a completed DePay/Web3 payment.
  • SHKeeper flow step 32: checkout page polls GET /api/payment/shkeeper/status/:paymentId — this endpoint is absent from the entire codebase — The SHKeeper flow explicitly states the frontend checkout page polls this endpoint alongside socket subscription. The endpoint is absent from both the backend code and the API docs. The frontend has no axios endpoint definition for it and no component code calling it. The actual mechanism is sock...
  • Backend escrowState values 'releasable' and 'releasing' not documented in either flow — The backend status values list includes escrowState:releasable and escrowState:releasing. Neither the SHKeeper flow nor the DePay flow documentation mentions these intermediate escrow states. UI components that render escrow state labels need to handle these values or they will display as unknown...
  • PurchaseRequest status 'pending_payment' not documented in either payment flow — The backend status values include PurchaseRequest:pending_payment as a distinct status. Neither the SHKeeper nor DePay flow documents this status or the transition into it. It is unclear when a request enters pending_payment vs the documented 'payment' status.
  • SHKeeper flow webhook response documented as 'success: true' — backend actually returns 202 Accepted — The SHKeeper flow step 31 and the API docs entry for POST /api/payment/shkeeper/webhook both say the response body is 'success: true'. The SHKeeper flow also explicitly states 'Respond 202 Accepted (SHKeeper retries on non-2xx)'. The actual status code is 202, not 200. The response body shape may...
  • Sweep cron auto-start behaviour and derived-destination sweep endpoints not covered by any flow document — The backend notable logic documents a sweep cron (DERIVED_DESTINATION_SWEEP_AUTOSTART=true) and the API docs list multiple /api/payment/derived-destinations endpoints for managing HD-wallet derived destination addresses. Neither the DePay flow nor the SHKeeper flow references this infrastructure....

Domain: Dispute

  • Flow doc says dispute chat opening message uses sellerId from 'explicit data.sellerId → selectedOffer.sellerId → first preferredSellerIds' but chat system message is sent with buyerId as senderId — Flow step 5 describes a system message 'اختلاف جدید ایجاد شد: {reason}' with type system. The DisputeService creates this message with senderId set to buyerId, not a system actor ID. This means the chat thread's first message is attributed to the buyer, not a neutral system account, which could c...
  • Dispute statistics page does not exist as a standalone route — The getDisputeStatistics action and the backend endpoint both exist. The list view (dispute-list-view.tsx) calls getDisputeStatistics and uses the counts inline to populate tab badges, but there is no dedicated /dashboard/disputes/statistics page or admin analytics view that surfaces the full sta...
  • Flow hardcoded resolve action in detail view — admin has no choice, always sends action=refund — The DisputeDetailsView.handleResolve in /frontend/src/sections/dispute/view/dispute-details-view.tsx hardcodes action: 'refund' when the 'حل اختلاف' button is clicked from the top-level actions area. The AdminActionsPanel component correctly offers a dropdown. But the detail view has a second res...
  • No endpoint or flow step documented for dispute reassignment (admin handover) — If an assigned admin becomes unavailable, there is no endpoint to change dispute.adminId to a new admin. POST /api/disputes/:id/assign checks if adminId is already set but does not block re-assignment — it will overwrite. This is not documented anywhere and the doc docFlags section calls it out a...
  • Frontend has no action for POST /api/disputes/:purchaseRequestId/raise or GET /api/disputes/:purchaseRequestId/status — The releaseHold dispute routes (raise, status) are implemented in the backend but there are no corresponding API calls in /frontend/src/actions/dispute.ts. The frontend cannot raise a hold-dispute or query release-block status from the escrow layer.
  • Dispute model has a messages sub-array that is never used — IDispute interface and the Mongoose schema include a messages array (senderId, content, timestamp, isRead, attachments). All chat communication goes through the Chat model referenced via chatId. The messages field on the Dispute document is declared but never written to or read in any service or ...
  • Dispute model has no uniqueness constraint on (purchaseRequestId, status) — duplicate disputes are possible — The doc's edgeCases section calls out that there is no uniqueness constraint preventing multiple open disputes for the same purchase request. Confirmed in the Dispute model schema: only single-field and compound indexes on status+priority, adminId+status. No unique index on purchaseRequestId with...

Domain: Chat

  • Flow doc CREATE CHAT body includes relatedTo field; backend API does not accept it at POST /api/chat — Flow doc step 1 shows the frontend posting { type: 'direct', participantIds: [...], relatedTo: { type: 'PurchaseRequest', id } }. The API docs for POST /api/chat do not list relatedTo as an accepted body field. The purchase-request-linked chat is handled by the dedicated POST /api/chat/purchase-r...
  • PATCH /api/chat/:id/archive toggles archived state — unarchive path is undocumented — The flow doc docFlags note that the unarchive path (Archived → Active) is mentioned in the state diagram but has no API endpoint or narrative step. The backend PATCH /api/chat/:id/archive toggles the isArchived flag (archive and unarchive via the same endpoint). This is not documented in the flow...
  • addParticipants frontend sends { participants } but backend expects { userId } (single user) — The frontend addParticipants action (chat.ts line 425) sends { participants: string[] } (an array) as the body. The API docs document POST /api/chat/:id/participants with body { userId: string } — a single user. The backend note also describes it as adding a participant (singular). There is a mis...
  • GET /api/chat/stats endpoint exists but has no dedicated dashboard UI — The backend exposes GET /api/chat/stats returning totalChats, unreadChats, and totalUnreadMessages. The frontend getChatStats action exists (chat.ts line 534) and the axios endpoint is configured (endpoints.chat.stats). However, the frontend actions audit notes there is no chat statistics dashboa...
  • getChatInfo returns only first 50 messages — not all messages — undocumented truncation — The backend GET /api/chat/:id/info reuses getChatMessages with page=1, limit=50. The flow doc does not mention this truncation. Consumers relying on getChatInfo to load full conversation history will silently receive only the first 50 messages, with no pagination metadata indicating more messages...
  • Backend enforces 5000-character message content limit — not documented in flow doc — The backend enforces a 5000-character maximum on message content at both the Mongoose schema and controller validation levels. The flow doc does not mention this constraint. The frontend has no visible character counter or validation, meaning users can type long messages and only discover the lim...
  • File uploads stored under uploads/chat/ with anonymous access — security concern not surfaced in flow doc — The flow doc edge cases note mentions 'sensitive attachments are unprotected — any user with the URL can fetch them'. The backend stores files under uploads/chat/ on disk. This is a known security gap. The flow doc flags it only as a recommendation callout, not as current behavior explicitly stat...
  • Direct chat participant count validation: exactly 1 external participantId required — not documented — The backend createChat controller validates that for direct chats, exactly 1 external participantId must be supplied (caller is auto-appended to make 2 total). If more or fewer are given, validation fails. The flow doc does not document this constraint, which could cause unexpected 400 errors for...

Domain: Notification

  • Doc lists referral-signup socket event; backend also emits referral-reward which is undocumented — The flow doc lists 'referral-signup → user-{referrerId}' as a socket event. The actual backend socket events also include 'referral-reward → user-{referrerId}' emitted when a referral reward is credited. This event is not in the doc's socket events table. The socket context does not expose a list...
  • 90-day TTL auto-deletion of notifications is not documented — The backend applies a MongoDB TTL index on Notification.createdAt with expireAfterSeconds = 7,776,000 (90 days), which hard-deletes old notifications automatically. This is not mentioned anywhere in the flow doc, edge cases, or status values. Users and QA may be surprised when old notifications d...
  • pending_payment and seller_paid statuses have no notification templates — The PurchaseRequest model includes status values 'pending_payment' and 'seller_paid'. The backend's notifyRequestStatusChanged function handles states: pending, active, received_offers, in_negotiation, payment, processing, delivery, delivered, confirming, completed, cancelled — but not 'pending_p...
  • level-up and referral-signup socket events have no persistence path documented — The socket events table lists level-up and referral-signup as emitted events, but the flow narrative never explains whether these create entries in the Notification collection. The notable logic confirms level-up comes from PointsService.addPoints and referral-signup from authController, but it i...
  • Doc says frontend uses React Query cache for notifications; actual implementation uses useState — The flow doc step 6 says 'prepends the entry into the React Query notifications cache'. The actual frontend implementation (notification-context.tsx and use-notifications.ts) uses plain React useState for the notifications array and unreadCount. There is no React Query (TanStack Query) usage for ...
  • getNotifications action passes userId as a query param; backend authenticates by token — The frontend actions/notification.ts passes userId as a query param to GET /notifications and GET /notifications/unread-count. The backend derives the authenticated user from the JWT token, not from a query param. Sending a userId query param that differs from the token user may be silently ignor...

Domain: Points/Referral

  • activeReferrals meaning changed: doc says 'never incremented', code now counts all referred users (not active buyers) — The Referral Flow doc flags activeReferrals as 'defined in schema but no code path currently increments it'. In reality, PointsService.processReferralReward (line 409) does set activeReferrals — but it counts ALL users with referredBy = referrer._id, not only those who have made a purchase. This ...
  • Self-referral prevention is absent from both the code and the doc's recommended fix — The doc flags self-referral as missing and recommends adding a guard in verifyEmailWithCode and googleSignUp. Inspection of authController.ts referral attribution logic (the two referral-signup emit sites at lines 704 and 1132) shows no self-referral check. Any user who obtains their own code and...
  • Point expiry: expiresAt field exists in model but no expiry enforcement exists in PointsService — PointTransaction has a sparse-indexed expiresAt field suggesting an expiry system was planned. PointsService has no cron job, TTL index, or expiry enforcement code. The 'expire' type in the transaction enum exists but is never created by any service method. This gap is noted in the backend code a...
  • Referral link uses NEXT_PUBLIC_API_URL (backend URL) as base, not a frontend marketing URL — The Referral Flow doc states the share URL is https://amn.gg/r/{code} and that the backend GET /r/:code redirects to ${FRONTEND_URL}/auth/jwt/sign-up?ref={code}. The actual frontend code in points-invite-friends.tsx builds the share link as ${NEXT_PUBLIC_API_URL}/r/${referralCode} — pointing to...

Domain: User Management

  • API doc says GET /api/user/wallet-address returns only walletAddress; backend returns type and provider too — The API doc for GET /api/user/wallet-address states the response is '{ success, data: { walletAddress: 0x... | null } }'. The actual backend also returns the wallet type (evm/ton) and provider fields.
  • API doc says PATCH /api/user/wallet-address only supports EVM; backend supports both EVM and TON — The API doc for PATCH /api/user/wallet-address describes only EVM semantics (0x-prefixed 40-hex address, EIP-191 signature). The actual backend also handles TON wallet addresses (regex validated) with optional TonProof verification, setting walletProofVerified and walletProofTimestamp when proof ...
  • No dedicated frontend page for user dependencies view — The getUserDependencies action exists and the backend endpoint returns dependency counts (templates, requests as buyer/seller, payments, chats). However, there is no dedicated frontend page at /dashboard/user/[id]/dependencies to surface this data to admins before deletion.
  • Soft-delete vs hard-delete behavior not verified by frontend flow — The frontend deleteUser function calls the legacy /users/admin/:id DELETE (hard delete via findByIdAndDelete), but the comment in user.ts says 'soft delete'. The new controller at /api/user/admin/:userId does a soft delete (status='deleted'). If the intent is soft-delete, the frontend is calling ...
  • Legacy /users/admin/ routes coexist with new /user/admin/ routes without deprecation notice** — Both sets of admin routes are mounted and reachable simultaneously. The documentation does not indicate which is canonical or provide a deprecation timeline for the legacy routes. This creates ambiguity for frontend developers and QA about which routes to target.
  • PUT /api/users/profile (legacy update) not surfaced in frontend actions — The backend provides PUT /api/users/profile as a legacy alias for updating the current user's profile. The frontend does not have a dedicated action for this endpoint. Profile updates go through the auth flow (endpoints.auth.updateProfile) or the new /api/user/profile route, not this legacy path.

Domain: Admin Operations

  • Dispute assign and resolve doc claims admin middleware; backend enforces in controller — POST /api/disputes/:id/assign is documented as requiring role=admin. The backend registers it with authenticateToken only; the admin check is inside the controller. POST /api/disputes/:id/resolve (DisputeController) is similarly controller-enforced. This is the same pattern as cleanup-pending and...
  • No admin UI for dispute statistics despite frontend action existing — A getDisputeStatistics action is defined in src/actions/dispute.ts calling GET /api/disputes/statistics. The missingFrontendFeatures list explicitly notes 'No admin page for dispute statistics'. No page exists under /dashboard/admin/ or /dashboard/disputes/ that displays these KPIs.
  • No admin UI for user statistics endpoint — GET /api/users/admin/stats is defined in the API doc and in the axios endpoints config (endpoints.users.admin.stats). The missingFrontendFeatures list confirms no UI page exists for it. No frontend action calls this endpoint.
  • AML runtime configuration is not persisted — server restart silently reverts admin changes — PATCH /api/admin/settings/aml updates process.env.TRANSACTION_SAFETY_AML_PROVIDER and process.env.AML_CHECK_COST_USD at runtime only. This behavior is documented in backend notableLogic but is not mentioned in the API doc, and there is no frontend warning. An admin who sets the AML provider via t...

Domain: Trezor Safekeeping

  • No socket events emitted for Trezor registration or address issuance — The documented flow has no socket events section (listed as empty array). The backend socket EMITTED events list does not include any trezor-specific event (no trezor-registered, no address-issued events). This confirms the doc's own flag — no real-time feedback is provided; the frontend must pol...
  • Per-operation nonce for replay prevention not documented — The backend notable logic states that operation signature verification uses a per-operation nonce to prevent replay attacks. The documented flow describes only the operation payload fields (operation, paymentId, transactionHash, amount, currency, provider) with no mention of a nonce field or how ...
  • Canonical message construction details not documented (stable JSON key order, ethers.getAddress normalization) — The backend applies stable JSON key ordering and ethers.getAddress (EIP-55 checksum) normalization to the operation payload before building the canonical message. This is required for deterministic signing — if a frontend constructs the payload in a different key order or uses a non-checksummed a...

Domain: Delivery

  • Doc lists 'delivery' status precondition as requiring PATCH /:id {status:'delivery'}, but multiple paths set this status — The documentation implies a single entry point (PATCH /:id with body {status:'delivery'}) sets the delivery status. In reality at least four separate code paths can set status='delivery': (1) PUT /delivery via updateDeliveryInfo, (2) POST /purchase-requests/:id/update-delivery (legacy), (3) PATCH...
  • Dual-router conflict: delivery-code endpoints exist in both routers with different authorization — The delivery code routes (generate, verify, get, status) are registered in both marketplaceRouter (routes.ts) and the controller (marketplaceController.ts), but the controller's GET /delivery-code only allows the buyer while routes.ts allows both buyer and seller (preferredSellerIds + selectedOff...
  • No rate-limiting or brute-force protection on verify-delivery code endpoint — The 6-digit delivery code has a 1-in-900,000 guess probability per attempt. The backend records failed attempts to deliveryInfo.deliveryAttempts[] but does not enforce any rate limit, lockout threshold, or attempt count maximum. A malicious actor could attempt all 900,000 combinations. The doc fl...
  • regenerateDeliveryCode creates a second deliveryCode field entry rather than updating in place — The documentation says regeneration 'invalidates the old one'. DeliveryService.regenerateDeliveryCode (line 342) first sets deliveryCodeUsed=true on the existing record, then calls generateDeliveryCode which overwrites the single deliveryInfo.deliveryCode field using $set. Since both the invalida...
  • Simulated payment bypass (SIM_ prefix) allows reaching 'delivery' status without real escrow — The backend POST /payments/verify treats any paymentHash starting with 'SIM_' or any short 0x hash as automatically verified. This backdoor exists in production code and allows the delivery flow to be entered without a real on-chain transaction, meaning the escrow is never actually funded. The de...

Doc Update Priorities

The following documents need updating most urgently. Extracted from the Executive Summary Section 6.

Immediate (Block UAT if Not Corrected)

  1. Delivery Flow (Delivery Confirmation Flow doc) — Swap all actor references (buyer↔seller) for code generation and verification; replace all documented endpoint paths with actual paths (/delivery-code/generate, /delivery-code/verify); remove the non-existent /verify-delivery and bare /delivery-code POST entries.
  2. Passkey Flow (Authentication Flow doc) — Remove all stub/simulated-public-key language; replace with accurate description of @simplewebauthn/server integration; remove the false refresh-token gap edge case.
  3. Dispute Resolve Schema (Dispute Flow doc) — Replace decision: buyer|seller|split + refundAmount with action: refund|replacement|compensation|warning_seller|ban_seller|no_action + amount + notes; correct dispute categories; replace under_review with in_progress.
  4. Seller Offer Endpoints (Seller Offer Flow doc) — Replace all three wrong GET paths; replace POST /api/marketplace/offers with POST /purchase-requests/:id/offers; remove the non-existent withdraw route.
  5. Payment DePay Flow (DePay/Web3 Payment Flow doc) — Replace /decentralized/create with /decentralized/save everywhere; correct verify path to include :paymentId param; remove /shkeeper/status/:paymentId polling step.
  6. Notification Endpoints (Notification Flow doc) — Replace POST /api/notifications/read-all with PATCH /notifications/mark-all-read; replace POST /api/notifications/mark-read with PATCH /notifications/:id/read; add unread-count-update to socket events; remove fictional notification-read event.
  7. Admin Auth Gaps — Add explicit warning that fetch-tx, auto-fetch-missing, and debug payment endpoints currently have no authentication and are exploitable without credentials.

High Priority (Correct Before Handing to Integration Teams)

  1. Purchase Request Status Enum — Add pending_payment and active to all status lists; remove finalized and archived if not present in frontend types.
  2. Password Reset Code Length — Correct all 8-digit references to 6-digit in backend API notable logic and authController.ts comment.
  3. Points Redeem Body Schema — Replace amount/purpose with pointsToUse/purchaseRequestId; correct response shape to { transaction, discount, remainingPoints }.
  4. Delivery Role Clarification — Confirm confirm-delivery authorization model; add note that any authenticated user can currently call it (authorization gap pending fix).
  5. PointTransaction Type Enum — Remove refund from status values list; valid types are earn | spend | expire only.

Standard Priority (Before Final Doc Release)

  1. Add pending_payment and seller_paid to notification templates gap documentation.
  2. Document 90-day TTL auto-deletion of notifications.
  3. Document chat rate limits (20 msgs/min, 15-minute edit window, 5000-char limit).
  4. Document escrowState: releasable and escrowState: releasing values.
  5. Document AML settings runtime-only persistence (changes lost on restart).
  6. Add unarchive behavior to chat archive endpoint documentation (toggle semantics).
  7. Document markAsRead with empty messageIds marks all messages as read.
  8. Add GET /api/trezor/account and POST /api/trezor/verify-operation to Trezor API table.