Update docs for procurement and shop PO processes, including GRN rework functionality
This commit is contained in:
@@ -97,11 +97,18 @@ PurchaseOrderItem
|
||||
├── IsCancelled (bool) — true once this line is individually cancelled, default false
|
||||
```
|
||||
|
||||
> **Update (GRN rework):** a goods receipt no longer requires a PO — see "Direct goods receipt" and
|
||||
> the costing / free-issue / tax additions in
|
||||
> [refactor_docs/grn_costing_and_taxes.md](./refactor_docs/grn_costing_and_taxes.md). The tree
|
||||
> below shows the original design plus those changes, marked *(later)*.
|
||||
|
||||
### GoodsReceipt
|
||||
```
|
||||
GoodsReceipt
|
||||
├── Id (Guid) — PK, from Template
|
||||
├── FK_PurchaseOrderId (Guid) — required
|
||||
├── FK_PurchaseOrderId (Guid?) — nullable *(later)*: null = direct receipt (no PO)
|
||||
├── FK_SupplierId (Guid?) — *(later)* set on a direct receipt; a PO receipt reaches the supplier via the PO
|
||||
├── DiscountPercent, VatPercent, NbtPercent (decimal 5,2) — *(later)* GRN-wide defaults for lines
|
||||
├── FK_ReceivedByUserId (Guid) — required
|
||||
├── FK_WarehouseId (Guid) — required; the warehouse the accepted quantities are received into.
|
||||
│ Not anticipated in the original design — added because
|
||||
@@ -116,9 +123,13 @@ GoodsReceipt
|
||||
GoodsReceiptItem
|
||||
├── Id (Guid) — PK, from Template
|
||||
├── FK_GoodsReceiptId (Guid) — required
|
||||
├── FK_PurchaseOrderItemId (Guid) — required, ties the receipt line back to its ordered line
|
||||
├── FK_PurchaseOrderItemId (Guid?) — ties the receipt line back to its ordered line; null on a direct receipt
|
||||
├── FK_ItemId (Guid?) — *(later)* the item, on a direct receipt line
|
||||
├── AcceptedQuantity (int) — added to warehouse stock
|
||||
├── RejectedQuantity (int) — damaged/short at receipt, default 0, not added to stock
|
||||
├── FreeQuantity (int) — *(later)* free-issue units: added to stock, not costed, not counted against the PO
|
||||
├── UnitCost (decimal 18,2) — *(later)* cost per unit; PO receipts default to the PO line's UnitCost
|
||||
├── DiscountPercent, NbtPercent, VatPercent (decimal 5,2) — *(later)* per-line rates
|
||||
```
|
||||
|
||||
### PurchaseReturn
|
||||
@@ -154,11 +165,16 @@ gives one receiving warehouse per PO in practice; see Open Questions for the mul
|
||||
|
||||
## Migrations
|
||||
|
||||
One migration, applied:
|
||||
1. **AddProcurement** (`20260824110925_AddProcurement`) — created `PurchaseOrders`,
|
||||
`PurchaseOrderItems`, `GoodsReceipts`, `GoodsReceiptItems`, `PurchaseReturns`,
|
||||
`PurchaseReturnItems`, with FKs to `Suppliers`/`Items`/`Warehouses` and indexes on every FK
|
||||
column. Applied via `dotnet ef database update`.
|
||||
2. **AddDirectGoodsReceipt** (`20261006070042`) — made the PO links nullable and added the supplier /
|
||||
item links for direct receipts.
|
||||
3. **AddGrnPricing**, **AddGrnFreeQuantity**, **AddGrnNbt** (`20261007…`, `20261008…`) — GRN costing,
|
||||
free issues and NBT. Pricing and free quantity are applied; **AddGrnNbt was still pending** when
|
||||
last checked, and GRN reads/writes fail until it is applied — see
|
||||
[refactor_docs/grn_costing_and_taxes.md](./refactor_docs/grn_costing_and_taxes.md) §7.
|
||||
|
||||
---
|
||||
|
||||
@@ -182,11 +198,17 @@ One migration, applied:
|
||||
| `resolveInvoiceVariance(poId, variance, resolution)` | `POST /api/purchase-orders/{id}/resolve-variance` |
|
||||
|
||||
### Goods Receipt (`Features/Grn`)
|
||||
> A PO is **not mandatory** for a goods receipt: `POST /api/goods-receipts/direct` receives stock
|
||||
> with no PO (no ordered-quantity limit, no PO status, no purchase returns / invoice matching for
|
||||
> it). See [refactor_docs/grn_costing_and_taxes.md](./refactor_docs/grn_costing_and_taxes.md) §1 for
|
||||
> when to use which.
|
||||
| Function reference | Endpoint |
|
||||
|---|---|
|
||||
| `createGoodsReceipt(poId, warehouseId, items[])` | `POST /api/purchase-orders/{id}/goods-receipts` |
|
||||
| `createGoodsReceipt(poId, warehouseId, items[])` | `POST /api/purchase-orders/{id}/goods-receipts` (also `POST /api/goods-receipts` with `purchaseOrderId` in the body) |
|
||||
| `createDirectGoodsReceipt(warehouseId, supplierId?, items[])` | `POST /api/goods-receipts/direct` |
|
||||
| `getGoodsReceipt(grnId)` | `GET /api/goods-receipts/{id}` |
|
||||
| `listGoodsReceipts(poId)` | `GET /api/purchase-orders/{id}/goods-receipts` |
|
||||
| `listGoodsReceipts(page)` | `GET /api/goods-receipts` |
|
||||
|
||||
### Purchase Returns (`Features/PurchaseReturn`)
|
||||
| Function reference | Endpoint |
|
||||
@@ -215,8 +237,12 @@ One migration, applied:
|
||||
{ "purchaseOrderItemId": "...", "acceptedQuantity": 95, "rejectedQuantity": 5 }
|
||||
] }
|
||||
```
|
||||
`rejectedQuantity` defaults to 0 if omitted; only `acceptedQuantity` is credited to
|
||||
`StockBalance`.
|
||||
`rejectedQuantity` defaults to 0 if omitted. `acceptedQuantity` **plus `freeQuantity`** (free-issue
|
||||
units, default 0) is credited to `StockBalance`; only accepted units are costed and only accepted
|
||||
units advance the PO line's `ReceivedQuantity`. Optional pricing fields (`unitCost`,
|
||||
`discountPercent`, `vatPercent`, `nbtPercent` per line; `discountPercent`, `vatPercent`,
|
||||
`nbtPercent` for the whole GRN) are described in
|
||||
[refactor_docs/grn_costing_and_taxes.md](./refactor_docs/grn_costing_and_taxes.md).
|
||||
|
||||
### POST `/api/purchase-orders/{id}/returns`
|
||||
```json
|
||||
@@ -297,9 +323,10 @@ above) per 14's existing target design.
|
||||
- Where should cost-vs-sale-price margin reporting (`PurchaseOrderItem.UnitCost` vs
|
||||
`PriceListItem.Price`/`Item.SalePrice`) live — a read-only endpoint/report in this step, or a
|
||||
later analytics/reporting step? Not a blocker for the core PO→GRN flow.
|
||||
- Landed cost (freight, duty, taxes allocated onto received stock) is out of scope —
|
||||
`PurchaseOrderItem.UnitCost` is the only cost basis captured. Defer unless accurate per-unit cost
|
||||
is needed sooner.
|
||||
- Landed cost (freight, duty allocated onto received stock) is out of scope. The GRN now records
|
||||
unit cost, discount, NBT and VAT per line (see
|
||||
[refactor_docs/grn_costing_and_taxes.md](./refactor_docs/grn_costing_and_taxes.md)), but these do
|
||||
not feed stock valuation or `Item.CostPrice`, and the PO itself carries no discount or tax.
|
||||
- Reorder triggers (`setReorderPoint`/`sendLowStockAlert`) are not wired to procurement — restocking
|
||||
stays fully manual until Notifications & Alerts exists.
|
||||
- `createPurchaseReturn` assumes a single receiving warehouse per PO (uses the most recent
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Shop PO → Invoice → Operations — Actual Flow (as implemented)
|
||||
|
||||
**Status**: Reference — describes current code behavior, not a plan
|
||||
**Related docs**: 03_ride_lifecycle.md, 04_loading_event.md, 05_delivery_ride.md, 06_sale_ride.md (original specs — some details below diverge from those specs; this doc reflects what the code actually does today)
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Trace the end-to-end lifecycle of a customer order from the moment a Shop PO is raised through
|
||||
billing (Invoice) and physical fulfillment (Ride → Loading Sheet → Delivery Task), including every
|
||||
status transition and stock movement along the way.
|
||||
|
||||
---
|
||||
|
||||
## 1. Shop PO (raised by a sales agent, on behalf of a store)
|
||||
|
||||
**Pages**: `/sales` (list), `/sales/new` (create), `/sales/[id]` (detail)
|
||||
**Backend**: `Features/ShopPO`
|
||||
|
||||
- **Create** (`ShopPOService.CreateAsync`): agent picks a store + items. Unit price resolves from
|
||||
the active `PriceList` for that store's region, falling back to `Item.SalePrice` if none is
|
||||
active. Status starts `Pending`.
|
||||
- **Confirm** (`ConfirmAsync`): sets `ConfirmedAt`/`ConfirmedBy`. Status **stays `Pending`** —
|
||||
confirming does not change `Status`, it only marks the PO as ready to act on (load/invoice).
|
||||
- **Cancel**: only allowed while `Status == Pending` (not `PartiallyFulfilled`/`Fulfilled`).
|
||||
- A confirmed PO with no invoice yet (`ConfirmedAt != null && InvoiceId == null`) is the exact set
|
||||
`GetConfirmedWithoutInvoiceAsync` and the Loading Sheet picker (`GET /shop-pos?confirmed=true`)
|
||||
query for.
|
||||
|
||||
## 2. Invoice (billing document, one per Shop PO)
|
||||
|
||||
**Page**: `/sales/orders` (labeled "Orders" in the nav, but it is actually the Invoices list)
|
||||
**Backend**: `Features/Invoice`
|
||||
|
||||
- Created via `ShopPOService.ConfirmAndCreateInvoiceAsync` — confirms the PO **and** creates an
|
||||
`Invoice` (`Status: Draft`, `PaymentStatus: Pending`) in one step, linking `ShopPO.InvoiceId`.
|
||||
(No frontend UI currently calls this endpoint — only the plain `Confirm` action is wired up.)
|
||||
- Lifecycle: `Draft → Printed → Finalized`.
|
||||
- Finalizing can adjust the billed quantity per line (capped between `0` and the ordered
|
||||
quantity) and recomputes `TotalAmount` from the adjusted lines.
|
||||
- Payments (`payments` API) apply against the invoice, moving `PaymentStatus`:
|
||||
`Pending → PartiallyPaid → Paid`.
|
||||
|
||||
## 3. Operations: Ride → Loading Sheet → Confirm → Delivery Tasks
|
||||
|
||||
**Pages**: `/operations/rides`, `/operations/loading`, `/operations/loading/new`
|
||||
**Backend**: `Features/Ride`, `Features/LoadingEvent`, `Features/DeliveryRide`
|
||||
|
||||
1. **Ride** created with `Status: Open`. A Ride is just a Vehicle + Driver pairing — it has **no**
|
||||
Route field in the current data model (unlike the original 03/06 specs' "ride type" framing).
|
||||
2. **Loading Sheet** created against an Open ride (`/operations/loading/new`):
|
||||
- Pick a **Route** (required, first) → confirmed Shop POs for stores on that route load
|
||||
(`ConfirmedAt != null`, status `Pending`/`PartiallyFulfilled`, remaining quantity > 0).
|
||||
- Optionally narrow by **Shop** (store) within that route.
|
||||
- Pick items/quantities to load, a **Warehouse** (single warehouse per sheet), and finally the
|
||||
**Ride** to load onto.
|
||||
- Sheet starts `Status: Planned`.
|
||||
3. **Set items** (`PUT /loading-sheets/{id}/items`) — editable only while `Planned`. Behavior is
|
||||
**additive/upsert**, keyed by `(FK_ItemId, FK_WarehouseId, FK_ShopPOItemId)`: a matching row's
|
||||
`QuantityDispatched` is updated in place, a new combination is added, and rows for combinations
|
||||
not present in the request are left untouched. This lets a shortfall in one warehouse be topped
|
||||
up from a second warehouse in a later save without losing the first line.
|
||||
4. **Confirm sheet** (`LoadingSheetService.ConfirmAsync`): for every line, deducts stock from its
|
||||
Warehouse and credits it to the Ride's Vehicle (`StockMovement.Type = Load`,
|
||||
`Warehouse → Vehicle`). Sheet → `Status: Confirmed`. This is the only point stock actually
|
||||
leaves the warehouse. (Note: unlike the 04_loading_event.md spec, confirming a sheet does **not**
|
||||
transition the Ride's own status — `RideStatus` only has `Open`/`Cancelled`/`Closed`, no
|
||||
`Active`.)
|
||||
5. **Generate delivery tasks** (`DeliveryTaskService.GenerateAsync`): once a ride has at least one
|
||||
`Confirmed` loading sheet, one `DeliveryTask` (`Status: Pending`) is created per distinct Shop PO
|
||||
that sheet's `LoadingSheetOrder` rows reference.
|
||||
6. **Set quantities** on each task (`SetQuantitiesAsync`) — how much of each `ShopPOItem` was
|
||||
actually delivered, capped at the item's ordered quantity. Only allowed while the task is
|
||||
`Pending`.
|
||||
7. **Confirm task** (`DeliveryTaskService.ConfirmAsync`) — closes the loop back to the Shop PO:
|
||||
- Moves stock **off the vehicle** (`StockMovement.Type = Sale`, `Vehicle → External`) for
|
||||
whatever was fulfilled, failing if the vehicle doesn't actually have enough stock.
|
||||
- Task → `Status: Delivered` (all items fully fulfilled) or `PartiallyDelivered`.
|
||||
- Writes back to the Shop PO: `Status = Fulfilled`/`PartiallyFulfilled`, `FulfilledAt` set, and
|
||||
each `ShopPOItem.QuantityFulfilled` bumped. This is exactly the field the Loading Sheet
|
||||
picker's "remaining quantity" (`quantity - quantityFulfilled`) depends on — a fully fulfilled
|
||||
PO drops out of the picker entirely.
|
||||
8. **Fail task**: marks `Status: Failed`. No stock movement, no Shop PO status change.
|
||||
|
||||
---
|
||||
|
||||
## Stock ledger across the whole flow
|
||||
|
||||
```
|
||||
Warehouse --(Loading Sheet confirm, Type=Load)--> Vehicle --(Delivery Task confirm, Type=Sale)--> External/Customer
|
||||
```
|
||||
|
||||
Every step writes a `StockMovement` row, so `StockBalance` for each Warehouse/Vehicle stays
|
||||
reconciled from Shop PO creation all the way through delivery.
|
||||
|
||||
---
|
||||
|
||||
## Known gaps at time of writing
|
||||
|
||||
- No frontend UI calls `ConfirmAndCreateInvoiceAsync` — only the plain `Confirm` action (no
|
||||
invoice) is wired up on the Shop PO screens.
|
||||
- `ShopPOController`/`LoadingSheetController` Create/Confirm endpoints, like most other
|
||||
create/confirm endpoints in this codebase, rely on the TEMPORARY `currentUser.UserId ?? request.XxxUserId`
|
||||
fallback pattern documented in `testbug.md` (#28) until the real auth middleware is fixed.
|
||||
@@ -54,6 +54,7 @@ Running log of backend API failures found during testing and the corresponding f
|
||||
| 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](./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` | 2026-09-09 |
|
||||
| 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 |
|
||||
@@ -75,6 +76,7 @@ Everything below is a stopgap that only exists because `Distribution/Middleware/
|
||||
11. **`CreateSaleReturnRequest.CreatedByUserId`** (`ReturnDTOs.cs`) and the "Created By" dropdown in the Record Order Return dialog (#23) — remove once `ReturnController.Create` can resolve `currentUser.UserId` on its own.
|
||||
12. **`SettleClaimRequest.SettledByUserId`** (`ClaimDTOs.cs`) and the "Settled By" dropdown in the Settle Claim dialog (#23) — remove once `ClaimController.Settle` can resolve `currentUser.UserId` on its own.
|
||||
13. **`CreateSupplierClaimRequest.RaisedByUserId`/`ApproveSupplierClaimRequest.ApprovedByUserId`/`RejectSupplierClaimRequest.RejectedByUserId`** (`SupplierClaimDTOs.cs`) and the corresponding user dropdowns in the Raise/Approve/Reject Supplier Claim dialogs (#23) — remove once `SupplierClaimController.Create`/`Approve`/`Reject` can resolve `currentUser.UserId` on their own.
|
||||
14. **`SetSysConfigRequest.UserId`** (`SysConfigDTOs.cs`) and the `userId` argument threaded through `sysConfig.set()` from the Product Configuration toggles page (`app/(dashboard)/configuration/products/page.tsx`, #49) — remove once `SysConfigController.Set` can resolve `currentUser.UserId` on its own; drop the `?? request.UserId` fallback and the `BadRequest` guard around it.
|
||||
|
||||
## Implementation Guide
|
||||
|
||||
|
||||
Reference in New Issue
Block a user