Files
ERP_Distribution/Distribution/docs/12_store_management.md
T

10 KiB

12 — Store Management (Beyond CRUD)

Status: Done — migration applied to the database Depends On: 00 — Existing Gap Fixes, 10 — Region Management, 11 — Route & Sales Management


Goal

Store CRUD already exists (Features/Store). This step adds everything in mainPlan.md §4 that isn't plain CRUD: credit control, KYC verification, segmentation, outstanding balance, visit history, scheme eligibility, and a sales dashboard.


Background / Context

Store currently has no credit limit, outstanding balance, segment, or KYC fields — flagged as gap #8 in 00_existing_gaps.md. Credit fields exist informally today as OneTimeAmount / CreditAmount, which stay as-is; this step adds the real credit-limit + outstanding-balance + KYC-gated-ordering flow on top.

getStoreVisitHistory and checkStoreSchemeEligibility are read-only integration points into modules that don't exist yet (11_route_sales_management.md for visits, 17_promotions_schemes.md for schemes). Implement them now with the query shape defined below; wire the actual join once those modules land — return an empty list / eligible: true (no active schemes to check) until then, guarded by a NotImplementedYet style short-circuit, not a broken query.


Data Model Changes

Store — New Fields

Store (existing, add columns)
├── CreditLimit (decimal)          — default 0. Max outstanding allowed before new orders are blocked.
├── OutstandingBalance (decimal)   — default 0. Cached/denormalized. Updated by Finance module
│                                     (step 19) on invoice/payment events via UpdateOutstandingBalanceAsync.
│                                     Until Finance exists, only moved by manual adjustment endpoint.
├── Segment (string?)              — free text, e.g. "Gold", "Silver", "Key Account".
├── KycStatus (enum)               — Pending | Verified | Rejected. Default Pending.

New Enum — KycStatus

public enum KycStatus { Pending, Verified, Rejected }

New Entity — StoreKycDocument

StoreKycDocument
├── Id (Guid)                  — PK, from Template
├── FK_StoreId (Guid)          — required
├── DocumentType (string)      — free text, e.g. "GST Certificate", "PAN Card", "Shop License"
├── FileReference (string)     — relative path under wwwroot/uploads/kyc/{storeId}/
├── UploadedAt (DateTime)
├── IsActive, CreatedAt        — from Template

AppSettings — New Config Key

StoreManagement:UnverifiedKycOrderThreshold (decimal) — default 0.
Max single-order value a store with KycStatus != Verified may place.
0 disables ordering entirely for unverified stores; set higher to allow small orders through.

Migrations

One migration needed:

  1. AddStoreCreditAndKyc — adds CreditLimit, OutstandingBalance, Segment, KycStatus columns to Stores; creates StoreKycDocuments table.

API Endpoints

Existing (unchanged)

Method Route Permission Notes
GET /api/stores stores.view
GET /api/stores/{id} stores.view
POST /api/stores stores.create
PUT /api/stores/{id} stores.update
DELETE /api/stores/{id} stores.delete Soft delete

New

Method Route Permission Notes
POST /api/stores/{id}/kyc-documents stores.update Multipart upload, appends a KYC document
GET /api/stores/{id}/kyc-documents stores.view Lists documents for a store
PUT /api/stores/{id}/kyc/verify stores.kyc.verify Sets KycStatus, body carries status (Verified/Rejected) + optional notes
PUT /api/stores/{id}/credit-limit stores.credit.update Requires Supervisor (roleLevel ≥ 20) or above
PUT /api/stores/{id}/outstanding-balance stores.credit.update Manual adjustment until Finance owns this; same permission as credit-limit
GET /api/stores/{id}/outstanding-balance stores.view Returns cached value + CreditLimit + available credit
PUT /api/stores/{id}/segment stores.update Body: { "segment": "Gold" }
GET /api/stores/{id}/visit-history stores.view Paginated. Empty list until Step 11 wiring lands
GET /api/stores/{id}/schemes/{schemeId}/eligibility stores.view Returns { eligible: true, reason: null } until Step 17 wiring lands
GET /api/stores/{id}/dashboard?period=... stores.view Aggregates order count/value for the period from existing Order tables

Request / Response Shapes

POST /api/stores/{id}/kyc-documents (multipart/form-data)

Fields: documentType (string), file (binary).

Response:

{ "id": "...", "storeId": "...", "documentType": "GST Certificate", "fileReference": "uploads/kyc/{storeId}/{guid}.pdf", "uploadedAt": "2026-08-20T10:00:00" }

PUT /api/stores/{id}/kyc/verify request

{ "status": "Verified", "notes": "GST + PAN checked against ERP records" }

PUT /api/stores/{id}/credit-limit request

{ "newLimit": 50000, "approverId": "..." }

approverId must resolve to a user with RoleLevel >= 20 (Supervisor or above); enforced server-side from the authenticated caller, not trusted from the body — the field is echoed back for audit display only.

GET /api/stores/{id}/outstanding-balance response

{ "storeId": "...", "creditLimit": 50000, "outstandingBalance": 32000, "availableCredit": 18000 }

GET /api/stores/{id}/dashboard response

{
  "storeId": "...",
  "period": "2026-08",
  "orderCount": 12,
  "totalOrderValue": 145000,
  "lastOrderDate": "2026-08-18T09:00:00"
}

Business Rules

  1. updateStoreCreditLimit requires the authenticated caller to have RoleLevel >= 20 (Supervisor or above) — enforced by the stores.credit.update policy plus a role-level check in the service, same pattern as StoreService.HasGlobalScope.
  2. A store with KycStatus != Verified may only place orders up to StoreManagement:UnverifiedKycOrderThreshold. OrderService must check this at order creation time; orders exceeding the threshold are rejected with a 400. This mirrors the stock-ledger pattern of rejecting at the point of the write, not after.
  3. deactivateStore (existing DELETE /api/stores/{id}) blocks new orders against the store, matching the existing IsActive soft-delete filter already used in OrderService. No separate reason field is added — soft delete already covers it via the existing Template.IsActive flag.
  4. OutstandingBalance is never edited directly by client-supplied arbitrary values outside the dedicated endpoint — always through UpdateOutstandingBalanceAsync, so Finance (step 19) has a single integration point to call into later.
  5. KYC documents are append-only from the API's perspective — no update endpoint. To replace a document, upload a new one; old ones stay for audit trail (soft-deletable via IsActive if ever needed).
  6. Scope rules for all new endpoints follow the existing StoreService pattern: global-scope roles (RoleLevel >= 50) see everything, others are checked via IScopeAccessService.HasStoreScopeAsync.

Permissions to Add to Seed

New permission codes to add to the existing seed:

stores.kyc.verify, stores.credit.update

(stores.view, stores.create, stores.update, stores.delete already exist.)

Role assignments:

  • SalesRep (10): stores.view (unchanged)
  • Supervisor (20): stores.view, stores.update, stores.credit.update
  • AreaManager (30): stores.view, stores.update, stores.credit.update, stores.kyc.verify
  • AreaSupervisor (40): all store permissions
  • RegionalManager (50): all store permissions
  • RegionalAgency (60): all store permissions

Open Questions

  • None. Resolved in planning discussion (2026-08-20): OutstandingBalance is cached/denormalized; KYC document type is free text with local disk storage; unverified stores are gated by a configurable order-value threshold rather than a hard block.

Implementation Notes

File structure created / touched

Features/
  Store/
    StoreController.cs        (extended — credit-limit, balance, kyc/verify, segment,
                                visit-history, scheme-eligibility, dashboard endpoints)
    StoreDTOs.cs               (extended — new request/response records)
    StoreService.cs            (extended — new methods, RoleLevel >= 20 credit-approver check)
    IStoreRepository.cs        (extended — GetOrdersForDashboardAsync)
    StoreRepository.cs         (extended)
    Kyc/
      StoreKycDocumentController.cs
      StoreKycDocumentDTOs.cs
      StoreKycDocumentService.cs
      IStoreKycDocumentRepository.cs
      StoreKycDocumentRepository.cs
  Order/
    OrderService.cs            (extended — KycStatus + UnverifiedKycOrderThreshold gate in CreateAsync)

Models/
  Store.cs              (add CreditLimit, OutstandingBalance, Segment, KycStatus columns)
  StoreKycDocument.cs    (new)
  Enums.cs               (add KycStatus enum)

Migrations/
  AddStoreCreditAndKyc

appsettings.json
  add "StoreManagement": { "UnverifiedKycOrderThreshold": 0 }

Program.cs
  register IStoreKycDocumentRepository, StoreKycDocumentRepository, StoreKycDocumentService

KYC documents live as a sub-namespace Features/Store/Kyc/ rather than a top-level feature, matching the one-feature-per-folder convention (Features/Store, Features/Stock) — they only ever belong to a Store and have no independent lifecycle.

KYC upload logic (pseudo)

POST /api/stores/{id}/kyc-documents — multipart (documentType, file):
  1. Load Store by id, reject if missing/inactive or caller lacks store scope
  2. Ensure wwwroot/uploads/kyc/{storeId}/ exists
  3. Save file as {guid}.{ext}, record FileReference = "uploads/kyc/{storeId}/{guid}.{ext}"
  4. Insert StoreKycDocument row, commit in the same UoW transaction

Order-creation KYC gate (pseudo)

OrderService.CreateAsync — before inserting the order:
  1. Load Store by request.StoreId
  2. If Store.KycStatus != Verified:
       threshold = config["StoreManagement:UnverifiedKycOrderThreshold"]
       if request.TotalAmount > threshold: reject (return null → 403 at controller)