606 lines
18 KiB
TypeScript
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();
|