feat: add sales return functionality

- Introduced Sales Return types and interfaces in the frontend for managing sales returns.
- Implemented SalesReturnsController in the backend to handle sales return endpoints.
- Created SalesReturn and SalesReturnLine entities to represent sales return data.
- Added DTOs for sales return responses and requests.
- Configured Entity Framework for SalesReturn and SalesReturnLine entities.
- Developed ISalesReturnService interface and its implementation for business logic.
- Added API methods for listing sales returns, creating new returns, and fetching remaining returnable quantities.
- Created frontend components for creating and listing sales returns, including validation logic.
- Implemented UI for selecting invoices, reason codes, and managing return lines.
This commit is contained in:
2026-08-12 10:18:56 +05:30
parent d37824cecc
commit d16a227b54
18 changed files with 1005 additions and 6 deletions
@@ -0,0 +1,38 @@
// One typed client method per Sales Return endpoint. Auto-posts an inbound
// FIFO movement on create, mirroring lib/api/purchase-returns.ts with the
// direction reversed.
import { apiRequest, buildQuery } from "@/lib/api-client"
import { PagedResponse } from "@/types/common"
import { CreateSalesReturnRequest, SalesInvoiceLineRemaining, SalesReturn, SalesReturnSummary } from "@/types/sales"
export interface ListSalesReturnsParams {
page?: number
pageSize?: number
q?: string
customerId?: number
warehouseId?: number
sort?: string
}
export const salesReturnsApi = {
list(params: ListSalesReturnsParams = {}): Promise<PagedResponse<SalesReturnSummary>> {
return apiRequest<PagedResponse<SalesReturnSummary>>(`/sales-returns${buildQuery(params)}`)
},
get(returnId: number): Promise<SalesReturn> {
return apiRequest<SalesReturn>(`/sales-returns/${returnId}`)
},
/** Remaining returnable qty per line of one sales invoice (invoiced qty minus already-returned). */
getRemaining(salesInvoiceId: number): Promise<SalesInvoiceLineRemaining[]> {
return apiRequest<SalesInvoiceLineRemaining[]>(`/sales-returns/remaining${buildQuery({ salesInvoiceId })}`)
},
/**
* 400 REASON_CODE_REQUIRED without a reason; 422 if it is not a Return-context reason;
* 409 STOCK_NEGATIVE_BLOCKED-equivalent errors do not apply here (inbound movement).
*/
create(request: CreateSalesReturnRequest): Promise<SalesReturn> {
return apiRequest<SalesReturn>("/sales-returns", { method: "POST", body: request })
},
}
@@ -0,0 +1,18 @@
// Client-side UX validation only — required fields, format/range checks the
// browser can already see. Server-authoritative rules (referential existence,
// concurrency, reason-code context) are never re-implemented here. Same
// pattern as lib/validations/procurement.ts's validateReturnLine.
export function validateSalesReturnLine(input: { salesInvoiceLineId: number | null; qty: string; maxQty: number | null }): Record<string, string> {
const errors: Record<string, string> = {}
if (!input.salesInvoiceLineId) errors.salesInvoiceLineId = "Select an invoiced line"
const qty = Number(input.qty)
if (!input.qty || Number.isNaN(qty) || qty <= 0) errors.qty = "Quantity must be greater than 0"
// Client-side sanity bound on the remaining returnable qty — the server remains authoritative.
if (input.maxQty !== null && qty > input.maxQty) {
errors.qty = input.maxQty <= 0
? "This line has already been fully returned"
: `Insufficient quantity — only ${input.maxQty} remain returnable`
}
return errors
}