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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user