32 KiB
Test Bugs & Change Tracker
Running log of backend API failures found during testing and the corresponding frontend changes needed. Add one entry per issue found; move to "Resolved" once fixed and verified.
Open
| # | Area | Backend API Failure | Frontend Change Needed | Date |
|---|
| 11 | Procurement → GRN | Record Receipt throws var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); (same #28 pattern, not yet given a temp workaround) | Added a View icon (see Resolved #44). No Edit/Delete — a GRN has no backend update/delete endpoint by design, since it already wrote stock balance + a PurchaseReceipt movement + the Po's ReceivedQuantity on create; reversing one safely needs a dedicated endpoint, not a plain CRUD delete. Still pending: the broken-auth throw on Record Receipt | 2026-09-02 |
| 13 | Pricing → Schemes (Add Scheme modal) | | Add Scheme modal should show a rule-type-specific "value" input field that changes based on the selected Scheme Name/Rule Type (currently no dynamic value field for the chosen rule type) | 2026-09-02 |
| 15 | Driver Profiles | Delete doesn't work | Add a View icon to the action column in Driver Profiles table | 2026-09-02 |
| 16 | Users | Delete action not working | Add View icon | 2026-09-02 |
| 17 | Users → Roles | | For now, hide the Add Role icon and the Edit/Cancel (deactivate) icons in the action column; keep only the View icon | 2026-09-02 |
| 18 | Network → Regions | Adding a region that already exists is not blocked — no duplicate-name validation; deleting a region does not work (backend as well) | Add View action icon to the action column; when status is Inactive, region should actually be set to inactive and reflect that status in the frontend | 2026-09-02 |
| 19 | Network → Region Manage | | Remove the Rule Config JSON-type field from the frontend view; add a View icon to the action column; on Cancel, just set status to Inactive and reflect that in the UI | 2026-09-02 |
| 20 | Network → Routes | Inactive status doesn't work properly | Check Manage Owner and Manage Stores actions; add View icon to the action column; check Cancel action too | 2026-09-02 |
| 21 | Network → Vehicles | | Add a View icon to the action column; check Cancel action | 2026-09-02 |
| 22 | Network → Warehouses | Delete doesn't work | Add a View icon to the action column; check Inactive status | 2026-09-02 |
| 25 | Orders (CRUD gap) | Create/Update/Delete endpoints exist and work | Orders page is read-only in the frontend — only list/get are wired up; add Create/Update/Delete UI | 2026-09-02 |
| 26 | Pricing → Schemes (unused capability) | Eligibility check, spend/ROI analytics, and apply-scheme-to-order endpoints all exist | None of these are wired into the frontend — only basic scheme CRUD is; add UI for eligibility check, spend/ROI view, and applying a scheme to an order | 2026-09-02 |
| 27 | Misc missing Delete actions | Delete/soft-delete endpoints exist for Users, Stores, Price Lists, and Suppliers | No Delete UI wired up for these on their respective pages | 2026-09-02 |
| 28 | Auth (root cause of #10 and likely others) | The actually-registered middleware is Distribution/Middleware/AuthMiddleware.cs (app.UseMiddleware<AuthMiddleware>() in Program.cs) — a plain, symmetric-key JWT check, NOT the RSA-based Infra/Authorization/Middleware/AuthMiddleware.cs/JwtMiddleware.cs, which despite their doc comments claiming to be "reference-only," are actually unregistered/dead. The active middleware reads config["Jwt:Key"], but appsettings.json only defines Jwt:PublicKey (RSA XML, used by AuthHex-issued tokens) — so Jwt:Key is always null, the whole validation block is skipped, and context.User is never set for ANY request, valid token or not. It also never attaches role_id/role_level/username claims the way the dead Infra middleware does. Net effect: ICurrentUserService.UserId/RoleId are always null, so every currentUser.UserId ?? throw new UnauthorizedAccessException() check fails (this is the actual cause of bug #10, Procurement → Orders) and every permission/role-level check silently fails too | Decision: keep Distribution/Middleware/AuthMiddleware.cs (outside Infra) as the one true auth flow going forward; delete the dead unregistered duplicates Infra/Authorization/Middleware/AuthMiddleware.cs and JwtMiddleware.cs. Then fix the active middleware to validate against Jwt:PublicKey (RSA), reject (not silently swallow) invalid/missing tokens, and attach role_id/role_level/username claims from the local User/Role row after validation | 2026-09-02 |
Resolved
| # | Area | Backend API Failure | Frontend Change Needed | Fixed In | Date |
|---|---|---|---|---|---|
| 1 | Operations → Rides (Create Ride) | Create Ride failed with "Vehicle not found" | — | Fixed DTO field naming mismatch | 2026-09-02 |
| 29 | Operations → Rides | Cancel-request API routes mismatched | Added View, Edit, and Cancel icons to Rides table | Fixed API routes + added action icons | 2026-09-02 |
| 30 | Operations → Delivery Tasks | Delivery task status update API mismatched | Added View, Confirm Delivery, and Mark as Failed icons | Fixed API routes + added action icons | 2026-09-02 |
| 32 | Sales (Create Sale, van sale) | If any item was short on the vehicle, the whole sale was rejected instead of selling what was available | — | Van sale now sells whatever quantity is on the vehicle per item and is always a clean Completed sale (no partial status) | 2026-09-03 |
| 33 | Operations → Rides (Close Ride) | No validation that meter end >= meter start | Close Ride dialog now enforces meter end >= meter start | Added backend + frontend validation | 2026-09-03 |
| 34 | Sales | — | No action icons on the Sales table | Added View and Cancel icons | 2026-09-03 |
| 2 | Operations → Loading Sheets | — | Missing View icon | Added View icon + details modal (status, dispatch/receipt confirmer + timestamps, item-level dispatched/received/resolved breakdown) | 2026-09-02 |
| 35 | Sales/Orders/Stores (scope-denied paths) | Forbid() crashed with InvalidOperationException: No authenticationScheme was specified... since no auth scheme is registered in Program.cs |
— | Replaced all 6 Forbid() calls (SaleController, OrderController, StoreController) with StatusCode(403) |
2026-09-03 |
| 36 | Sales (Create Sale) — TEMPORARY workaround | Store-scope check and currentUser.UserId were always unresolved due to the broken auth middleware (#28), blocking every sale create |
Added a required "Created By" user dropdown to the Create Sale modal | TEMP: bypassed store-scope check and added CreatedByUserId to CreateSaleRequest so the caller supplies it explicitly. Revert once auth (#28) is fixed |
2026-09-03 |
| 37 | Sales (Create Sale) | Every business-rule failure (store inactive, KYC threshold, price resolution failed, ride not found) silently returned a bare 403 with no explanation | — | Each failure now throws a specific InvalidOperationException, surfaced as a clear 400 error message in the create-sale toast |
2026-09-03 |
| 38 | Sales (Create Sale modal) | — | "Sale Summary" total box was frozen at Rs. 0 — form.watch('items') called inline doesn't subscribe the component to re-render on field changes |
Replaced with useWatch({ control, name: 'items' }), which properly re-renders the total as items are entered |
2026-09-03 |
| 39 | Sales (Create Sale) | — | Moved Create Sale from a cramped modal to a dedicated /sales/new page: Sale Details and Items are each full-width, Items shown as an invoice-style table, and a compact Sale Summary card is right-aligned below the items |
2026-09-03 | |
| 40 | Sales (Cancel Sale) — TEMPORARY workaround | CancelAsync still had the same broken store-scope check as Create (#36) — HasStoreScopeAsync always false due to broken auth middleware (#28), so cancel silently 404'd |
— | TEMP: bypassed store-scope check in CancelAsync, same as Create. Revert once auth (#28) is fixed |
2026-09-03 |
| 3 | Sales (Create Sale) | Sale create always failed — saleDate was never sent, defaulted to year 0001, no price list ever active that date |
Added a required Sale Date field to the Create Sale modal | Sent saleDate in the create payload; summary total was already computing correctly, just never reached the backend |
2026-09-03 |
| 6 | Sales → Visits (Route dropdown) | Selecting a specific route in the route dropdown does not filter route sales — all routes still show; check-in also not working | Route filter and check-in now work on the Sales Visits page | Fixed route filter and check-in (see TEMP CheckedInByUserId workaround, #40-style, pending real auth) |
2026-09-08 |
| 7 | Catalogue → Categories (Subcategories) | Added subcategories don't display in the FE | Added a View icon to the action column in Categories table | Fixed subcategory list rendering — root cause was GetAllAsync returning CategorySummaryResponse, which had no subCategories field at all |
2026-09-08 |
| 42 | Procurement → Orders (Place Order UI) | — | Moved Create Purchase Order from the PurchaseOrderSheet modal to a dedicated /procurement/orders/new page, matching the /sales/new layout: Order Details full-width card, Items as an invoice-style table, and a right-aligned Order Summary card below |
Created procurement/orders/new/page.tsx, updated PurchaseOrdersClient's "New Order" button to navigate there, removed the now-unused purchase-order-sheet.tsx |
2026-09-08 |
| 43 | Procurement → Orders (action icons) | Cancel API call was broken: frontend sent PATCH /purchase-orders/{id}/cancel with body {cancelReason}, but the backend only registers POST .../cancel and expects {reason} — the wrong HTTP verb alone would 405 |
Added View (details modal, showing all lines with received/cancelled quantities) and Edit (a /procurement/orders/[id]/edit page, enabled only while the order is Issued and matching backend's UpdatePoRequest rule that already-received lines are locked). Delete was already present as the Cancel action (soft-cancel with a reason, per the no-hard-delete Implementation Guide rule) — relabeled "Delete order" |
Fixed the cancel API client to POST with {reason} matching CancelPoRequest; added purchaseOrders.update(); created PurchaseOrderViewSheet and the Edit page |
2026-09-08 |
| 44 | Procurement → GRN (action icons) | — | Added a View icon (details modal showing warehouse, received date, and each line's accepted/rejected quantity). No Delete — deliberate, not a gap: a goods receipt already applied its effect (stock balance + PurchaseReceipt movement + the Po's ReceivedQuantity) at create time, so a generic delete would silently desync stock; reversing one needs a dedicated, explicit endpoint, which hasn't been requested/built |
Created GrnViewSheet, wired a View row action into GrnClient |
2026-09-08 |
| 10 | Procurement (Create Purchase Order + Record Goods Receipt) | Both POST /purchase-orders and POST /goods-receipts (and the Po-scoped goods-receipts route) had var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); — with auth broken (#28) and no global exception handler registered, every Create Order / Record Receipt attempt crashed with an unhandled 500, not even a clean 401. This was a genuine, currently-broken business flow, not just a cosmetic gap |
Added a "Created By" dropdown to /procurement/orders/new and a "Received By" dropdown to the Record Receipt (GRN) sheet |
TEMP: added CreatedByUserId to CreatePoRequest and ReceivedByUserId to CreateGrnRequest/CreateFlatGrnRequest, same currentUser.UserId ?? request.XxxUserId fallback pattern as #36/#40/#8, with a clean BadRequest instead of the unhandled crash |
2026-09-08 |
| 23 | Returns & Claims Management | ReturnController, ClaimController, SupplierClaimController all exist and are fully implemented, but Create/Settle/Approve/Reject on all three had the same unhandled ?? throw new UnauthorizedAccessException() crash as #10; ClaimController and ReturnController also had no GetAll endpoint at all, only per-order/per-agency lookups, so there was no way to list existing records for a page. None of the three had an Update/edit endpoint either |
No frontend integration existed at all — built a new "Returns & Claims" nav group with 3 dedicated pages: Order Returns (/returns-claims/order-returns — list + View + Edit + a Record Return dialog that only allows returning items the selected order actually has, mode CreditDeduction/ItemExchange), Agency Claims (/returns-claims/claims — list + View + Edit (while Open) + Raise Claim + Settle), Supplier Claims (/returns-claims/supplier-claims — list + View + Edit (while Open) + Raise Claim scoped to a PO's received items + Approve/Reject). Edit scope per module: Order Return quantities/mode are applied to stock and the order total immediately on create with no approval step, so only each line's reason code is editable, not quantity/mode/items (same immutable-effect principle as Stock Movements, #9); Claims and Supplier Claims have no side effect until Settle/Approve, so their Details/Items are fully editable while still Open, locked out afterward. Purchase Returns (#24) already lives in-context inside the Purchase Order View sheet and was left as-is |
Backend: added GetAllAsync to IReturnRepository/IClaimRepository/ISupplierClaimRepository + a GET list endpoint on each controller; fixed all 5 broken-auth crash sites (ReturnController.Create, ClaimController.Settle, SupplierClaimController.Create/Approve/Reject) with the same CreatedByUserId/SettledByUserId/RaisedByUserId/ApprovedByUserId/RejectedByUserId fallback pattern; added PATCH /returns/{id}/reasons, PUT /claims/{id} (blocked once Settled), PUT /supplier-claims/{id} (blocked once resolved). Frontend: added saleReturns/claims/supplierClaims to a new lib/api/returns-claims.ts, plus types in types/api.ts |
2026-09-08 |
| 24 | Procurement → Purchase Returns | PurchaseReturnController.Create had the same unhandled ?? throw new UnauthorizedAccessException() crash as #10; also no GetAll endpoint exists — a return only ever makes sense in the context of one purchase order (must have a prior GoodsReceipt against it, and quantities are capped per-item at received-minus-already-returned), so there's no global "all returns" listing to build a standalone page around |
Built the module in-context: the Purchase Order View sheet now shows a "Purchase Returns" section (reason, date, line items for every return already recorded against that order) plus a "Return Items" button (shown only once the order has any received quantity) that opens a Return dialog — item dropdown capped to each item's real receivable-and-not-already-returned quantity, reason, and a temporary "Created By" user dropdown | TEMP: added CreatedByUserId to CreatePurchaseReturnRequest, same fallback pattern; added purchaseReturns API client (get, listByPurchaseOrder, create); created PurchaseReturnDialog, wired into PurchaseOrderViewSheet |
2026-09-08 |
| 45 | Pricing → Price Lists (action icons) | DeleteAsync did a hard delete (repo.Remove → EF Remove), violating the no-hard-delete Implementation Guide rule (the repo method itself was already misleadingly named Remove but did set IsActive = false — the actual bug was GetAllAsync filtering .Where(pl => pl.IsActive), hiding a deleted price list from the list entirely instead of showing it as Inactive, and GetByIdAsync/UpdateAsync blocking view/edit of an inactive one, matching the same recurring pattern as Stores/Items/Categories/Brands/Orders). Also GetAll returns a plain List<PriceListResponse> but the frontend was configured serverSide: true expecting a PaginatedResponse, so pagination silently did nothing |
Added a View icon (details modal — fetches the full record incl. items via GET /price-lists/{id} since the list endpoint returns rows with an empty items array) and a Delete icon (soft-deactivate, reusing the existing "Active" checkbox in the Edit sheet to reactivate). Edit was already wired up and already sent isActive in its request body — just needed the backend to actually accept it |
Removed the IsActive filters from GetAllAsync/GetByIdAsync/UpdateAsync; added IsActive to UpdatePriceListRequest; switched the frontend to client-side pagination (same fix as Orders/Brands) since the backend has no real paging; added priceLists.remove(); created PriceListViewSheet |
2026-09-08 |
| 46 | Pricing → Price Lists (item pricing) | PUT /price-lists/{id}/items/{sku} and DELETE /price-lists/{id}/items/{sku} were fully implemented and correctly validated (MinPrice ≤ Price, overlap check against other active lists) but no frontend code called either endpoint — the Create/Edit sheet only ever sent items: []. So every price list created through the UI was permanently empty, meaning SaleService.ResolveUnitPriceAsync could never resolve a real price-list price and always fell back to the flat Item.SalePrice — the whole point of the Price Lists module (region/date-scoped item pricing) was unreachable end-to-end |
Added an "Items" section to the Edit sheet: a table of already-priced items (inline edit price/min-price, remove) plus an "Add item" row (item picker + price + min price). Since the backend upserts one SKU at a time (not a bulk array), each row change is its own immediate API call, same as the existing GRN/PO-style line-item tables but per-row-saved instead of submitted together. Creating a new price list now keeps the dialog open afterward (switching in place to "edit" mode) so items can be added immediately instead of requiring a reopen | Created PriceListItemsSection, embedded in PriceListSheet (edit mode only); added priceLists.upsertItem()/priceLists.removeItem() to the API client |
2026-09-09 |
| 47 | Sales (Create Sale) — wrong price prefilled | Even with #46 fixed, /sales/new still prefilled unitPrice straight from item.salePrice (the flat catalogue price) — no frontend code ever called price-list resolution, so a rep would see the wrong default price for any item actually covered by an active region/date-scoped price list, and the sale would only succeed if they happened to retype the exact resolved price manually. PriceListService.ResolvePriceAsync (region price list → global price list → Item.SalePrice) existed but was only ever called from inside SaleService, with no public endpoint to query it ahead of submission |
Selecting an item now calls the new resolution endpoint (store → its route's region, so the exact same lookup SaleService uses) and prefills the real resolved price; changing the Store or Sale Date afterward re-resolves every already-selected line, since the applicable price list depends on both. When the resolved price has a MinPrice band, a "Negotiable down to Rs. X" hint shows under the field so the rep knows how far they can discount without the submit failing |
Added PriceListService.ResolveByStoreAsync(itemId, storeId, saleDate) (looks up the store's region via IStoreRepository.GetByIdWithRegionAsync, then reuses ResolvePriceAsync) and GET /price-lists/resolve?itemId=&storeId=&saleDate=; added priceLists.resolve() to the frontend API client and a PriceResolution type |
2026-09-09 |
| 12 | Pricing → Schemes (action icons) | Same recurring pattern as Price Lists (#45): DeleteAsync's repo.Remove already soft-deleted (IsActive = false), but GetAllAsync/GetByIdAsync/UpdateAsync filtered/blocked on IsActive, hiding a deleted scheme from the list entirely and blocking viewing/editing/reactivating it. EvaluateEligibilityAsync relied on that same repo-level filter to only ever consider active schemes as redemption candidates — removing the filter without care would have silently let inactive/deleted schemes be evaluated as eligible. Also GetAll returns a plain List<SchemeResponse> but the frontend was configured serverSide: true expecting a PaginatedResponse, so pagination silently did nothing |
Added View (details modal — rule type, region, segment, date range, trigger/free-goods payout, bundle components) and Delete (soft-deactivate, reuses the existing "Active" checkbox in the Edit form to reactivate) icons | Removed the IsActive filter from SchemeRepository.GetAllAsync; moved the active-only filter into SchemeService.EvaluateEligibilityAsync itself (.Where(s => s.IsActive) on the fetched candidates) so eligibility evaluation is unaffected; removed the IsActive blocks from GetByIdAsync/UpdateAsync; added IsActive to UpdateSchemeRequest; switched the frontend to client-side pagination (same fix as Price Lists/Orders/Brands); added schemes.remove(); created SchemeViewSheet |
2026-09-09 |
| 14 | Suppliers (action icons) | Exact same recurring pattern again: SupplierRepository.GetAllAsync filtered .Where(s => s.IsActive) (hiding a deleted supplier from the list instead of showing it Inactive), GetByIdAsync/UpdateAsync blocked viewing/editing an inactive one, and UpdateSupplierRequest had no IsActive field so the Edit form's existing "Active" checkbox couldn't reactivate one. Also suppliers.list() was typed as PaginatedResponse against a backend GetAll that returns a plain array, same pagination-shape bug as Price Lists/Schemes/Orders/Brands |
Added View (details modal — name, contact, status, performance score, created date) and Delete (soft-deactivate, reuses the existing "Active" checkbox in Edit to reactivate) icons | Removed the IsActive filter from SupplierRepository.GetAllAsync; removed the IsActive blocks from GetByIdAsync/UpdateAsync (kept on EvaluateAsync, a real business guard — no point recomputing a performance score for a deleted supplier); added IsActive to UpdateSupplierRequest; switched the frontend to client-side pagination; added suppliers.remove(); created SupplierViewSheet |
2026-09-09 |
| 48 | Pricing → Price Lists (new fields) | — | Added RetailPrice and AgencyMarginPercent to PriceListItem, alongside the existing Price/MinPrice band — see 14_pricing_and_suppliers.md for the full business rules. RetailPrice is the MRP the store is expected to charge the end consumer (must be >= Price); AgencyMarginPercent is the regional agency's commission % of Price (must be 0-100). Both are informational only — carried through PriceResolution for display/reporting, never enforced on order pricing |
Added both columns to the Items table in the Price List Edit/View pages (add-row + inline edit + view-only table), and a "Retail price: Rs. X" hint under the unit price on Create Sale when the resolved price came from a real price list | Added RetailPrice/AgencyMarginPercent to PriceListItem, migration AddRetailPriceAndAgencyMargin; extended UpsertPriceListItemRequest/PriceListItemResponse/PriceResolution with validation (RetailPrice >= Price, AgencyMarginPercent in [0,100]); updated PriceListItemsSection, the View page's items table, and priceLists.upsertItem(); updated docs/14_pricing_and_suppliers.md |
| 49 | Configuration → Products (Product Capabilities toggles) — TEMPORARY workaround | PUT /api/sys-config/{key} had the same unhandled var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); crash as #10/#36/#40/#8, surfaced the moment a toggle tried to save. A first attempt added Guid? UserId = null as a record constructor default, which broke System.Text.Json deserialization entirely (NotSupportedException — a record constructor parameter with a default value reads to STJ as an ambiguous/parameterless-like constructor) |
Rebuilt the Configuration area end to end: sidebar "Configuration" is now a standalone link (no sub-nav/collapsible) straight to /configuration, a card-grid landing page; its "Products" card opens /configuration/products, a redesigned "Product Capabilities" card (two-column toggle grid, ON/OFF Switch, no "Enable…" wording). Removed the page's "Save Changes" button entirely — each toggle (and the default-warranty-days input, on blur) now calls the API immediately on change, optimistically updating and reverting with a toast on failure. Also renamed "Items" → "Products" in the Catalogue nav label, page heading, and the Add/Edit/View dialogs (left every other module's "Item" line-item wording — Sales/Procurement/GRN/Loading Sheets/Returns — untouched, since those are document line items, not the catalogue) |
TEMP: added UserId (no default value, to keep STJ happy) to SetSysConfigRequest; SysConfigController.Set now does currentUser.UserId ?? request.UserId, returning a clean BadRequest instead of throwing when both are null. Frontend sysConfig.set() takes an optional userId and the Product Configuration page passes useAuth().user.id (the mock-auth user) on every toggle/save call. Revert once auth (#28) is fixed |
2026-10-02 |
| 8 | Stock Balances | No way to correct a balance — stock is architecturally never edited directly, only through movement transactions (load, sale, return, damage, adjustment); the POST /stock/adjustment endpoint existed but its controller action always returned Unauthorized() because currentUser.UserId is unset (#28) |
Added a View icon (details modal) and an Edit icon that opens an "Adjust Stock" dialog (signed quantity delta + reason + Adjusted By) which posts to /stock/adjustment — this is the correct way to change a balance, not a direct field edit |
TEMP: added AdjustedByUserId fallback to AdjustmentRequest, same pattern as #36/#40; wired StockBalanceViewSheet + StockAdjustDialog into Stock Balances table |
2026-09-08 |
| 9 | Stock Movements | Some rows missing From/To locations — every movement should always have both | Added View and Edit icons. Movements are an immutable audit-trail ledger by design — quantity, locations, and type can never change once recorded (the running balance is the sum of all movements), so Edit only lets you correct the Reason/note via a new PATCH /stock/movements/{id}/reason endpoint; a wrong quantity/location must be corrected with a new Adjustment (see #8), not by editing history |
Wired StockMovementViewSheet + StockMovementEditDialog into Stock Movements table |
2026-09-08 |
| 41 | Stock Balances / Stock Movements | Balances and Movements tables/view-sheets showed raw location GUIDs instead of names; Movements also had a genuinely missing side — Sale/PurchaseReceipt/PurchaseReturn/CustomerReturn/Damage/Adjustment each only ever wrote one of From/To, since the other side (customer, supplier, write-off, adjustment) has no real Warehouse/Vehicle record. Existing seeded movement rows still showed blank From/To after the code fix since it only affects newly-created movements | — | Added LocationName to StockBalanceResponse and FromLocationName/ToLocationName to StockMovementResponse, resolved server-side (StockService) by looking up the real Warehouse/Vehicle. Added a new LocationType.External plus a fixed Guid.Empty sentinel location so every movement's missing side is now always populated and labeled contextually ("Customer", "Supplier", "Written Off", "Stock Adjustment") instead of being left null. Updated all 6 movement-creation call sites (StockService Damage/CustomerReturn/Adjustment, SaleService, DeliveryTaskService, GrnService, PurchaseReturnService, ReturnService), and added migration FixSeedStockMovementExternalLocations to backfill the 2 affected seeded rows |
2026-09-08 |
Future Fixes — Pending Real Auth
Everything below is a stopgap that only exists because Distribution/Middleware/AuthMiddleware.cs (see #28) never actually authenticates a request — currentUser.UserId/RoleId/RoleLevel are always unset. Once #28 is properly fixed (validate against Jwt:PublicKey via RSA, reject bad/missing tokens instead of silently skipping validation, and attach role_id/role_level/username claims from the local User/Role row), all of the following must be reverted/re-enabled:
SaleService.CreateAsync— restore theHasStoreScopeAsync(request.StoreId)check that was bypassed in #36.SaleService.CancelAsync— restore theHasStoreScopeAsync(sale.FK_StoreId)check that was bypassed in #40.CreateSaleRequest.CreatedByUserId(SaleDTOs.cs) — remove this field;CreateAsyncshould go back to usingcurrentUser.UserIdonly (drop the?? request.CreatedByUserIdfallback and theInvalidOperationExceptionguard around it).- Create Sale page (
/sales/new) — remove the "Created By" user dropdown and its form field/validation entirely; the acting user should come from the session, not be picked manually. - Sweep for the same pattern elsewhere —
HasStoreScopeAsync/HasGlobalScope-style checks exist in other services too (Order, Store, etc., per #35's affected controllers) and likely have the same silent-fail behavior; audit and fix once auth is real, not just the Sales module. - Re-verify
StatusCode(403)usages from #35 — once real authentication exists, confirm whether any of those 6 call sites should become genuine ASP.NET[Authorize]-driven403/401responses instead of manual scope checks. AdjustmentRequest.AdjustedByUserId(StockDTOs.cs) and the "Adjusted By" dropdown in the Adjust Stock dialog (#8) — remove onceStockController.Adjustcan resolvecurrentUser.UserIdon its own.CreatePoRequest.CreatedByUserId(PoDTOs.cs) and the "Created By" dropdown on/procurement/orders/new— remove oncePoController.Createcan resolvecurrentUser.UserIdon its own.CreateGrnRequest.ReceivedByUserId/CreateFlatGrnRequest.ReceivedByUserId(GrnDTOs.cs) and the "Received By" dropdown in the Record Receipt sheet — remove onceGrnController.Create/CreateFlatcan resolvecurrentUser.UserIdon its own.CreatePurchaseReturnRequest.CreatedByUserId(PurchaseReturnDTOs.cs) and the "Created By" dropdown in the Return Items dialog (#24) — remove oncePurchaseReturnController.Createcan resolvecurrentUser.UserIdon its own.CreateSaleReturnRequest.CreatedByUserId(ReturnDTOs.cs) and the "Created By" dropdown in the Record Order Return dialog (#23) — remove onceReturnController.Createcan resolvecurrentUser.UserIdon its own.SettleClaimRequest.SettledByUserId(ClaimDTOs.cs) and the "Settled By" dropdown in the Settle Claim dialog (#23) — remove onceClaimController.Settlecan resolvecurrentUser.UserIdon its own.CreateSupplierClaimRequest.RaisedByUserId/ApproveSupplierClaimRequest.ApprovedByUserId/RejectSupplierClaimRequest.RejectedByUserId(SupplierClaimDTOs.cs) and the corresponding user dropdowns in the Raise/Approve/Reject Supplier Claim dialogs (#23) — remove onceSupplierClaimController.Create/Approve/Rejectcan resolvecurrentUser.UserIdon their own.SetSysConfigRequest.UserId(SysConfigDTOs.cs) and theuserIdargument threaded throughsysConfig.set()from the Product Configuration toggles page (app/(dashboard)/configuration/products/page.tsx, #49) — remove onceSysConfigController.Setcan resolvecurrentUser.UserIdon its own; drop the?? request.UserIdfallback and theBadRequestguard around it.
Implementation Guide
Cross-cutting rules to apply consistently across every module/page while working through the entries above:
- Cancel / Delete semantics — Cancel and Delete actions should never remove a row from the page. They should always set the record's status to Inactive (soft delete) and immediately reflect that updated status in the UI. The row must stay visible in the table after the action.
- Frontend validation — On every page, check that form validation is handled per the actual business scenario (required fields, duplicate checks, valid ranges/formats) before submitting to the backend.
- Summary calculations — Verify that any summary/total/aggregate calculations shown on a page (e.g. sale totals, stock summaries) compute and display correctly.
- UI consistency — Maintain consistent layout, spacing, and interaction patterns across all pages.
- Icon consistency — Use the correct, consistent icon set for actions (View/Edit/Delete/Cancel/etc.) across every page.
- Search & filters — Check that the search bar and filters actually filter the table results, and that dropdown filters fetch their options correctly from the backend.