# Average Cost and Selling Price from Batches (Implementation Approach)

## Goal

You want product-level pricing to come from all active batches, not only one batch.

- Product cost price should be the weighted average cost of active batch stock.
- Product selling price should also be derived from active batches (weighted average selling price).

## Short Answer: Backend or Frontend?

This must be handled in the backend.

- Backend is the source of truth for inventory and financial values.
- Frontend can show preview values, but final calculation must happen in backend services.
- If done only in frontend, values can become inconsistent between users, APIs, and reports.

## Calculation Rules

Use weighted average by available quantity (qtyAvailable):

- totalQty = sum(batch.qtyAvailable)
- avgCost = sum(batch.qtyAvailable * batch.costPrice) / totalQty
- avgSelling = sum(batch.qtyAvailable * effectiveBatchSellingPrice) / totalQty

Where:

- effectiveBatchSellingPrice = batch.sellingPrice if present, otherwise product.sellingPrice fallback (or 0 based on your policy).

Rounding policy:

- Keep internal calculations in full precision.
- Save product-level averages rounded to 2 decimals.

## Current Behavior in Your Codebase

Your current aggregate sync logic is in:

- src/modules/product-batches/product-batches.service.js

Today, syncProductAggregates picks the FIFO-first batch price, not weighted average.

## Modules That Should Change

### 1) product-batches (main logic)

Files:

- src/modules/product-batches/product-batches.service.js

Change:

- Update syncProductAggregates to compute weighted average cost and weighted average selling from all active batches (qtyAvailable > 0).
- Keep stock aggregation as is.
- Write computed values into Product.costPrice and Product.sellingPrice.

This is the central and most important change.

### 2) products (read model behavior)

Files:

- src/modules/products/products.model.js
- src/modules/products/products.controller.js (if any update endpoint sets cost/selling directly)
- src/modules/products/products.validation.js

Change:

- Decide if manual editing of Product.costPrice and Product.sellingPrice should be blocked or allowed.
- Recommended: treat these as derived fields and update them from batch sync only.
- If manual override is needed, add explicit override flags (for example pricingMode: auto|manual).

### 3) product-batches validation/controller

Files:

- src/modules/product-batches/product-batches.validation.js
- src/modules/product-batches/product-batches.controller.js

Change:

- Ensure batch create/edit accepts valid costPrice and optional sellingPrice.
- Trigger syncProductAggregates after any batch create/edit/delete or qty movement (already mostly done in service flow).

### 4) stock movement modules that consume or restore qty

Files to verify integration paths:

- src/modules/invoices/invoices.controller.js
- src/modules/orders/orders.controller.js
- src/modules/bills/bills.controller.js

Change:

- Confirm all stock consume/return flows call the product-batches service methods that eventually call syncProductAggregates.
- If any flow updates stock directly in Product without batch service, refactor to use batch service.

### 5) analytics/reports

Files:

- src/modules/analytics/
- src/modules/reports/

Change:

- Confirm reports that use product.costPrice/product.sellingPrice now reflect average-based values.
- If some reports need historical exact batch margins, compute from transaction allocations instead of only product snapshot fields.

## Recommended Backend Design

Use a single source function:

- syncProductAggregates(productId)

Responsibilities:

- Remove zero-qty batches (existing behavior).
- Aggregate active batches (qtyAvailable > 0).
- Compute:
  - stock
  - weighted average cost
  - weighted average selling
- Save into Product document in one update operation.

This keeps all entry points consistent: receiving stock, selling stock, returns, and batch edits.

## Edge Cases You Should Define

1. totalQty == 0
- Set product.stock = 0
- Keep last prices or set to 0/null based on business rule.

2. Missing batch sellingPrice
- Option A: ignore those batches in selling average denominator.
- Option B: fallback to current product.sellingPrice.
- Option C: fallback to costPrice plus margin rule.

3. Negative or invalid values
- Reject via validation in batch endpoints.

4. Concurrency
- Keep operations in transactions where you already use sessions.

## Frontend Role

Frontend should:

- Display product average cost/selling from API.
- Show per-batch cost/selling input fields.
- Optionally show estimated average before submit (for UX only).

Frontend should not be the final calculator for persisted prices.

## Rollout Plan

1. Update syncProductAggregates in product-batches service.
2. Add unit tests for weighted average math.
3. Add integration tests for receive/consume/restock/edit flows.
4. Review product update endpoints for manual override policy.
5. Validate reports after pricing logic switch.

## Testing Scenarios (Minimum)

1. Receive 2 batches with different cost/selling and qty; verify product averages.
2. Sell qty from first batch; verify averages recalculated with remaining qty.
3. Return qty to original allocations; verify averages go back correctly.
4. Edit batch cost/selling; verify product prices update.
5. Deplete all batches; verify stock 0 behavior.

## Suggested Decision (Practical)

For your system, implement weighted average in backend now and keep frontend as display + optional preview.

- This gives consistent accounting behavior.
- It avoids mismatched values across modules.
- It fits your existing architecture because product-batches service already centralizes stock sync.

## Implemented Now (Backend)

The backend is now updated to use weighted averages from active batches.

- Product costPrice is now calculated as weighted average from batch qtyAvailable and batch costPrice.
- Product sellingPrice is now calculated as weighted average from batch qtyAvailable and batch sellingPrice (only batches that have sellingPrice).
- Product stock remains sum of batch qtyAvailable.
- Direct product update requests can no longer overwrite costPrice or sellingPrice.

Code changed in:

- src/modules/product-batches/product-batches.service.js
- src/modules/products/products.controller.js

## Exact Frontend Changes Required

### 1) Stop sending product prices in product update calls

Endpoint:

- PUT/PATCH /api/v1/product/:id

Frontend action:

- Remove costPrice/mainprice and sellingPrice/sellingprice from edit product payload.
- Keep product metadata only (name, category, description, image, status, etc).

Reason:

- Backend now treats product prices as derived from batches and ignores direct price updates.

### 2) Use batch endpoints for any price change

Endpoints:

- POST /api/v1/product-batches/receive
- PATCH /api/v1/product-batches/:batchId
- GET /api/v1/product-batches/product/:productId

Frontend action:

- To change effective product cost/selling, add or edit batch costPrice/sellingPrice.
- After successful mutation, refresh product details and batch list.

### 3) Update price preview/read flow

Endpoint:

- GET /api/v1/product/:id/current-prices

Now returns average-based data:

- data.pricingMode = "batch-weighted-average"
- data.costPrice = weighted average cost of active batches
- data.sellingPrice = weighted average selling of batches that have sellingPrice
- data.batch = FIFO-first batch preview (kept for compatibility)

Frontend action:

- Show these values as system-calculated prices.
- Label UI as "Auto from batches".
- Do not offer manual product-level price editing unless you introduce a manual override feature.

### 4) Product create form behavior

Endpoint:

- POST /api/v1/product

Frontend action:

- Keep initial costPrice and sellingPrice fields for product creation.
- These seed the first initial batch and become the first computed averages.

### 5) Suggested UX text

- "Main Price is auto-calculated from active batches. Edit batch prices to change it."
- "Selling Price is auto-calculated from active batches with selling price set."

---

# Module API Audit: Pagination, Search, Filters (May 24, 2026)

This section documents which modules already support pagination/search/filter features and which modules need improvements.

## Legend

- Pagination: whether list endpoint supports paging (`page/limit` or `skip/limit`).
- Items per page: default items returned when `limit` is not provided.
- Needed features: important missing capabilities for production usage.

## Core Module Matrix

| Module | Main List Endpoint | Pagination | Items Per Page (Default) | Search | Filters | Needed Features |
|---|---|---|---|---|---|---|
| Products | `getAllproducts` | Yes (`limit` + `skip`) | `100` (or `1` for barcode mode) | Yes (`search`) | `category`, `lowStock`, `status`, `barcode` | Add page-based pagination (`page`) and optional max limit cap |
| Categories | `getAllCategories` | Yes (`page/limit`) | `20` (max `200`) | Yes (`search` by `name/code`) | `parent` | Pagination added |
| Medicines | `getMedicines` | Yes (`page/limit`, optional `skip`) | `20` (max `200`) | Yes (`search`) | `status`, `medicineForm`, `model`, `strength`, `inStock`, sorting | Good coverage; mostly complete |
| Bills | `getAllBills` | Yes (`page/limit`) | `20` | Yes (`billNumber`) | `date`, `sortBy`, `order` | Add unified `search` key for consistency |
| Invoices | `getAllInvoices` | Yes (`page/limit`) | `50` | No | `status`, `startDate`, `endDate` | Add search (invoice number, account, salesperson) |
| Orders | `getAllOrders` | Yes (`page/limit`) | `20` | Yes (`search`) | `status`, `condition`, `date`, `startDate`, `endDate` | Add user-controlled sort fields/order |
| Expenses | `getAllExpenses` | Yes (`page/limit`) | `20` | Yes (`search`) | `category`, `status`, `date`, `startDate`, `endDate`, `dateFilter`, `datePreset`, `timeRange`, `accountId` | Good coverage |
| Expense Categories | `getAllExpenseCategories` | Yes (`page/limit`) | `50` | Yes (`search`) | `accountId` | Could add max limit cap |
| Expense Accounts | `getAllExpenseAccounts` | Yes (`page/limit`) | `20` | Yes (`search`) | `isActive`, `type`, `code`, `categoryName` | Good coverage |
| Accounts Receivable | `getAllAccounts` | Yes (`page/limit`) | `20` | Yes (`search`) | `status` | Good coverage |
| Accounts Payable | `getAllAccountsPayable` | Yes (`page/limit`) | `20` | Yes (`search`) | `status`, `type` | Good coverage |
| Receivable Records | `getAllReceivableRecords` | Yes (`page/limit`) | `20` | Yes (`search`) | `accountId`, `status`, `loanDirection` | Good coverage |
| Payable Records | `getAllPayableRecords` | Yes (`page/limit`) | `20` | Yes (`search`) | `accountId`, `status`, `direction` | Good coverage |
| Receivable Payments | `getAllReceivablePayments` | Yes (`page/limit`) | `20` (max `200`) | Yes (`search`) | `receivableRecordId`, `date`, `startDate`, `endDate` | Pagination/search/date filters added |
| Payable Payments | `getAllPayablePayments` | Yes (`page/limit`) | `20` (max `200`) | Yes (`search`) | `accountPayableId`, `date`, `startDate`, `endDate` | Pagination/search/date filters added |
| HR Employees | `getEmployees` | Yes (`page/limit`) | `20` (max `200`) | Yes (in filter builder) | `status`, `department`, `position` | Good coverage |
| HR Payrolls | `getEmployeePayrolls` | Yes (`page/limit`) | `20` (max `200`) | Yes (note/fields via filter builder) | `status`, `method`, `paymentType`, date and amount ranges, `currency`, `deductFromNetProfit` | Good coverage |
| User Details | `getAllUserDetails` | Yes (`page/limit`) | `20` | Yes (via filter builder) | `userId`, phone/id-card style filters | Could add max limit cap |
| User Payrolls | `getAllPayrolls` | Yes (`page/limit`) | `20` | Yes (via filter builder) | status/method/date/amount/currency filters | Could add max limit cap |
| Users | `getAllUsers` | Yes (`page/limit`) | `20` (max `200`) | Yes (`search`) | `role` | Pagination/search/role filter added |
| Cash In/Cash Out | `getAllTransactions` | Yes (`page/limit`, optional `skip`) | `20` (max `200`) | Yes (`search`) | `type`, `category`, `date`, `startDate`, `endDate` | Pagination added |
| Product Batches | `getProductBatches` | Yes (`page/limit`, optional `skip`) | `20` (max `200`) | No | `currency` | Pagination added |

## Standardization Recommendations

1. Standardize all list endpoints to `page` + `limit` (keep `skip` only as optional backward compatibility).
2. Add max limit cap where missing (for example: `Math.min(limit, 200)`).
3. Add missing pagination for heavy-growth modules: categories, users, receivable-payments, payable-payments, cash-in-cash-out, product-batches.
4. Add missing search to invoices and users.
5. Use one consistent search key (`search`) and one consistent filter naming style across modules.

## Quick Priority Backlog

1. Invoices: add `search`.
2. Add max limit caps for endpoints that still have uncapped `limit`.
3. Standardize all modules to return a unified pagination object shape.
4. Make products sorting query-driven (`sortBy/sortOrder`) where needed.
5. Add integration tests for new pagination metadata (`hasMore`, `nextSkip`).

---

# New Changes: Global Pagination Rollout (May 24, 2026)

Implemented backend pagination in modules that were missing it, and updated inventory API for scroll-based loading.

## Backend Changes Applied

1. Categories
- Endpoint: `getAllCategories`
- Added: `page`, `limit` (default `20`, max `200`)
- Response now includes: `results`, `totalRecords`, `page`, `limit`, `totalPages`

2. Users
- Endpoint: `getAllUsers`
- Added: `page`, `limit` (default `20`, max `200`)
- Added search/filter: `search` (name/email), `role`
- Response now includes: `results`, `totalRecords`, `page`, `limit`, `totalPages`

3. Receivable Payments
- Endpoint: `getAllReceivablePayments`
- Added: `page`, `limit` (default `20`, max `200`)
- Added filters: `search`, `date`, `startDate`, `endDate`
- Response now includes: `results`, `totalRecords`, `page`, `limit`, `totalPages`

4. Payable Payments
- Endpoint: `getAllPayablePayments`
- Added: `page`, `limit` (default `20`, max `200`)
- Added filters: `search`, `date`, `startDate`, `endDate`
- Response now includes: `results`, `totalRecords`, `page`, `limit`, `totalPages`
- Also added pagination to `getPaymentsForPayableRecord`

5. Cash In / Cash Out Transactions
- Endpoint: `getAllTransactions`
- Added: `page`, `limit` (default `20`, max `200`), optional `skip`
- Response now includes: `page`, `limit`, `skip`, `totalPages`, `hasMore`, `nextSkip`
- Existing filters remain: `type`, `search`, `category`, `date`, `startDate`, `endDate`

6. Product Batches
- Endpoint: `getProductBatches`
- Added: `page`, `limit` (default `20`, max `200`), optional `skip`
- Response now includes: `totalRecords`, `page`, `limit`, `skip`, `totalPages`, `hasMore`, `nextSkip`
- Aggregate summary (`totalAvailable`, averages) still computed from all active batches

7. Inventory (Products) - Scroll Based
- Endpoint: `getAllproducts`
- Added support for: `page` (while keeping existing `skip` + `limit`)
- Added response block:
  - `pagination.mode = "scroll"`
  - `pagination.limit`
  - `pagination.skip`
  - `pagination.hasMore`
  - `pagination.nextSkip`

## Frontend Contract (Must Follow)

Frontend must use pagination values from backend response, not hardcoded limits.

1. Category Dropdowns (Parent/Child)
- Parent categories request:
  - `GET /category?parent=null&page=1&limit=20`
- Child categories request:
  - `GET /category?parent=<parentId>&page=1&limit=20`
- Search in categories:
  - `GET /category?search=dell&page=1&limit=20`
- Use backend values for pagination UI:
  - `totalRecords`, `page`, `limit`, `totalPages`

2. Inventory List (Products) = Infinite Scroll
- Use `GET /product?limit=<n>&skip=<currentSkip>`
- Append incoming items to current list.
- Stop fetching when `pagination.hasMore === false`.
- Next request must use `pagination.nextSkip`.
- Do not calculate limits/pages locally if backend returns pagination metadata.

3. Users / Payments / Transactions / Batches
- Always send `page` + `limit` for table views.
- For scroll-based views, use `skip` where endpoint supports it.
- Always read and respect backend `totalRecords/totalPages/hasMore/nextSkip` metadata.

## Note

This change keeps old behavior backward-compatible where possible (for example products still accept `skip`), but frontend should migrate to backend-driven pagination metadata immediately.
