erp-retail-backend-v2/src/modules/pos/controllers/pos.controller.ts
rckrdmrd a6186c4022 Migración desde erp-retail/backend - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:11:34 -06:00

606 lines
18 KiB
TypeScript

import { Response, NextFunction } from 'express';
import { BaseController } from '../../../shared/controllers/base.controller';
import { AuthenticatedRequest } from '../../../shared/types';
import { posSessionService } from '../services/pos-session.service';
import { posOrderService } from '../services/pos-order.service';
class POSController extends BaseController {
// ==================== SESSION ENDPOINTS ====================
/**
* POST /pos/sessions/open - Open a new session
*/
async openSession(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const userId = this.getUserId(req);
const branchId = this.getBranchId(req);
if (!branchId) {
return this.error(res, 'BRANCH_REQUIRED', 'X-Branch-ID header is required');
}
const { registerId, openingCash, openingNotes } = req.body;
if (!registerId) {
return this.validationError(res, { registerId: 'Required' });
}
const result = await posSessionService.openSession(tenantId, {
branchId,
registerId,
userId,
openingCash: openingCash || 0,
openingNotes,
});
if (!result.success) {
return this.error(res, result.error.code, result.error.message);
}
return this.success(res, result.data, 201);
} catch (error) {
next(error);
}
}
/**
* POST /pos/sessions/:id/close - Close a session
*/
async closeSession(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const userId = this.getUserId(req);
const { id } = req.params;
const { closingCashCounted, closingNotes, cashCountDetail } = req.body;
if (closingCashCounted === undefined) {
return this.validationError(res, { closingCashCounted: 'Required' });
}
const result = await posSessionService.closeSession(tenantId, id, {
closedBy: userId,
closingCashCounted,
closingNotes,
cashCountDetail,
});
if (!result.success) {
return this.error(res, result.error.code, result.error.message);
}
return this.success(res, result.data);
} catch (error) {
next(error);
}
}
/**
* GET /pos/sessions/active - Get active session for current user
*/
async getActiveSession(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const userId = this.getUserId(req);
const session = await posSessionService.getActiveSession(tenantId, userId);
if (!session) {
return this.notFound(res, 'Active session');
}
return this.success(res, session);
} catch (error) {
next(error);
}
}
/**
* GET /pos/sessions/:id - Get session by ID
*/
async getSession(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const { withOrders } = req.query;
const session = withOrders
? await posSessionService.getSessionWithOrders(tenantId, id)
: await posSessionService.findById(tenantId, id);
if (!session) {
return this.notFound(res, 'Session');
}
return this.success(res, session);
} catch (error) {
next(error);
}
}
/**
* GET /pos/sessions - List sessions for branch
*/
async listSessions(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const branchId = this.getBranchId(req);
if (!branchId) {
return this.error(res, 'BRANCH_REQUIRED', 'X-Branch-ID header is required');
}
const pagination = this.parsePagination(req.query);
const { status, userId, startDate, endDate } = req.query;
const result = await posSessionService.findAll(tenantId, {
pagination,
filters: [
{ field: 'branchId', operator: 'eq', value: branchId },
...(status ? [{ field: 'status', operator: 'eq' as const, value: status }] : []),
...(userId ? [{ field: 'userId', operator: 'eq' as const, value: userId }] : []),
],
});
return this.paginated(res, result);
} catch (error) {
next(error);
}
}
/**
* GET /pos/sessions/daily-summary - Get daily session summary
*/
async getDailySummary(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const branchId = this.getBranchId(req);
if (!branchId) {
return this.error(res, 'BRANCH_REQUIRED', 'X-Branch-ID header is required');
}
const dateParam = req.query.date as string;
const date = dateParam ? new Date(dateParam) : new Date();
const summary = await posSessionService.getDailySummary(tenantId, branchId, date);
return this.success(res, summary);
} catch (error) {
next(error);
}
}
// ==================== ORDER ENDPOINTS ====================
/**
* POST /pos/orders - Create a new order
*/
async createOrder(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const userId = this.getUserId(req);
const branchId = this.getBranchId(req);
const branchCode = req.branch?.branchCode || 'UNK';
if (!branchId) {
return this.error(res, 'BRANCH_REQUIRED', 'X-Branch-ID header is required');
}
const { sessionId, registerId, lines, ...orderData } = req.body;
if (!sessionId || !registerId || !lines || lines.length === 0) {
return this.validationError(res, {
sessionId: sessionId ? undefined : 'Required',
registerId: registerId ? undefined : 'Required',
lines: lines?.length ? undefined : 'At least one line is required',
});
}
const result = await posOrderService.createOrder(
tenantId,
{
sessionId,
branchId,
registerId,
userId,
lines,
...orderData,
},
branchCode
);
if (!result.success) {
return this.error(res, result.error.code, result.error.message);
}
return this.success(res, result.data, 201);
} catch (error) {
next(error);
}
}
/**
* POST /pos/orders/:id/payments - Add payment to order
*/
async addPayment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const userId = this.getUserId(req);
const { id } = req.params;
const { method, amount, amountReceived, ...paymentData } = req.body;
if (!method || amount === undefined) {
return this.validationError(res, {
method: method ? undefined : 'Required',
amount: amount !== undefined ? undefined : 'Required',
});
}
const result = await posOrderService.addPayment(
tenantId,
id,
{
method,
amount,
amountReceived: amountReceived || amount,
...paymentData,
},
userId
);
if (!result.success) {
return this.error(res, result.error.code, result.error.message);
}
return this.success(res, result.data);
} catch (error) {
next(error);
}
}
/**
* POST /pos/orders/:id/void - Void an order
*/
async voidOrder(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const userId = this.getUserId(req);
const { id } = req.params;
const { reason } = req.body;
if (!reason) {
return this.validationError(res, { reason: 'Required' });
}
const result = await posOrderService.voidOrder(tenantId, id, reason, userId);
if (!result.success) {
return this.error(res, result.error.code, result.error.message);
}
return this.success(res, result.data);
} catch (error) {
next(error);
}
}
/**
* GET /pos/orders/:id - Get order by ID
*/
async getOrder(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const order = await posOrderService.getOrderWithDetails(tenantId, id);
if (!order) {
return this.notFound(res, 'Order');
}
return this.success(res, order);
} catch (error) {
next(error);
}
}
/**
* GET /pos/orders - List orders
*/
async listOrders(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const branchId = this.getBranchId(req);
if (!branchId) {
return this.error(res, 'BRANCH_REQUIRED', 'X-Branch-ID header is required');
}
const pagination = this.parsePagination(req.query);
const { sessionId, status, customerId, startDate, endDate, search } = req.query;
// If search term provided, use search method
if (search) {
const orders = await posOrderService.searchOrders(
tenantId,
branchId,
search as string,
pagination.limit
);
return this.success(res, orders);
}
const result = await posOrderService.findAll(tenantId, {
pagination,
filters: [
{ field: 'branchId', operator: 'eq', value: branchId },
...(sessionId ? [{ field: 'sessionId', operator: 'eq' as const, value: sessionId }] : []),
...(status ? [{ field: 'status', operator: 'eq' as const, value: status }] : []),
...(customerId ? [{ field: 'customerId', operator: 'eq' as const, value: customerId }] : []),
],
});
return this.paginated(res, result);
} catch (error) {
next(error);
}
}
/**
* GET /pos/orders/session/:sessionId - Get orders by session
*/
async getOrdersBySession(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const { sessionId } = req.params;
const orders = await posOrderService.getOrdersBySession(tenantId, sessionId);
return this.success(res, orders);
} catch (error) {
next(error);
}
}
// ==================== REFUND ENDPOINTS ====================
/**
* POST /pos/refunds - Create a refund
*/
async createRefund(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const userId = this.getUserId(req);
const branchId = this.getBranchId(req);
const branchCode = req.branch?.branchCode || 'UNK';
if (!branchId) {
return this.error(res, 'BRANCH_REQUIRED', 'X-Branch-ID header is required');
}
const { originalOrderId, sessionId, registerId, lines, refundReason } = req.body;
if (!originalOrderId || !sessionId || !registerId || !lines || lines.length === 0) {
return this.validationError(res, {
originalOrderId: originalOrderId ? undefined : 'Required',
sessionId: sessionId ? undefined : 'Required',
registerId: registerId ? undefined : 'Required',
lines: lines?.length ? undefined : 'At least one line is required',
});
}
const result = await posOrderService.createRefund(
tenantId,
{
originalOrderId,
sessionId,
branchId,
registerId,
userId,
lines,
refundReason: refundReason || 'Customer refund',
},
branchCode
);
if (!result.success) {
return this.error(res, result.error.code, result.error.message);
}
return this.success(res, result.data, 201);
} catch (error) {
next(error);
}
}
/**
* POST /pos/refunds/:id/process - Process refund payment
*/
async processRefund(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const userId = this.getUserId(req);
const { id } = req.params;
const { method, amount } = req.body;
if (!method || amount === undefined) {
return this.validationError(res, {
method: method ? undefined : 'Required',
amount: amount !== undefined ? undefined : 'Required',
});
}
const result = await posOrderService.processRefundPayment(tenantId, id, { method, amount }, userId);
if (!result.success) {
return this.error(res, result.error.code, result.error.message);
}
return this.success(res, result.data);
} catch (error) {
next(error);
}
}
// ==================== HOLD/RECALL ENDPOINTS ====================
/**
* POST /pos/orders/:id/hold - Hold an order
*/
async holdOrder(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const { holdName } = req.body;
const result = await posOrderService.holdOrder(tenantId, id, holdName || 'Unnamed');
if (!result.success) {
return this.error(res, result.error.code, result.error.message);
}
return this.success(res, result.data);
} catch (error) {
next(error);
}
}
/**
* GET /pos/orders/held - Get held orders
*/
async getHeldOrders(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const branchId = this.getBranchId(req);
if (!branchId) {
return this.error(res, 'BRANCH_REQUIRED', 'X-Branch-ID header is required');
}
const orders = await posOrderService.getHeldOrders(tenantId, branchId);
return this.success(res, orders);
} catch (error) {
next(error);
}
}
/**
* POST /pos/orders/:id/recall - Recall a held order
*/
async recallOrder(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const userId = this.getUserId(req);
const { id } = req.params;
const { sessionId, registerId } = req.body;
if (!sessionId || !registerId) {
return this.validationError(res, {
sessionId: sessionId ? undefined : 'Required',
registerId: registerId ? undefined : 'Required',
});
}
const result = await posOrderService.recallOrder(tenantId, id, sessionId, registerId, userId);
if (!result.success) {
return this.error(res, result.error.code, result.error.message);
}
return this.success(res, result.data);
} catch (error) {
next(error);
}
}
// ==================== ADDITIONAL ENDPOINTS ====================
/**
* POST /pos/orders/:id/coupon - Apply coupon to order
*/
async applyCoupon(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const branchId = this.getBranchId(req);
const { id } = req.params;
const { couponCode } = req.body;
if (!branchId) {
return this.error(res, 'BRANCH_REQUIRED', 'X-Branch-ID header is required');
}
if (!couponCode) {
return this.validationError(res, { couponCode: 'Required' });
}
const result = await posOrderService.applyCouponToOrder(tenantId, id, couponCode, branchId);
if (!result.success) {
return this.error(res, result.error.code, result.error.message);
}
return this.success(res, result.data);
} catch (error) {
next(error);
}
}
/**
* POST /pos/orders/:id/receipt/print - Mark receipt as printed
*/
async markReceiptPrinted(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
await posOrderService.markReceiptPrinted(tenantId, id);
return this.success(res, { message: 'Receipt marked as printed' });
} catch (error) {
next(error);
}
}
/**
* POST /pos/orders/:id/receipt/send - Send receipt by email
*/
async sendReceipt(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const { email } = req.body;
if (!email) {
return this.validationError(res, { email: 'Required' });
}
const result = await posOrderService.sendReceiptEmail(tenantId, id, email);
if (!result.success) {
return this.error(res, result.error.code, result.error.message);
}
return this.success(res, { message: 'Receipt sent successfully' });
} catch (error) {
next(error);
}
}
/**
* GET /pos/sessions/:id/stats - Get session statistics
*/
async getSessionStats(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<Response | void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const stats = await posOrderService.getSessionStats(tenantId, id);
return this.success(res, stats);
} catch (error) {
next(error);
}
}
}
export const posController = new POSController();