erp-retail-backend/src/modules/pricing/controllers/pricing.controller.ts

986 lines
20 KiB
TypeScript

import { Request, Response, NextFunction } from 'express';
import { PriceEngineService } from '../services/price-engine.service';
import { PromotionService } from '../services/promotion.service';
import { CouponService } from '../services/coupon.service';
import {
CreatePromotionInput,
UpdatePromotionInput,
CreateCouponInput,
UpdateCouponInput,
CalculatePricesInput,
RedeemCouponInput,
ListPromotionsQuery,
ListCouponsQuery,
PromotionProductInput,
GenerateBulkCouponsInput,
} from '../validation/pricing.schema';
import { AppDataSource } from '../../../config/database';
import { Promotion } from '../entities/promotion.entity';
import { Coupon } from '../entities/coupon.entity';
const priceEngineService = new PriceEngineService(AppDataSource);
const promotionService = new PromotionService(
AppDataSource,
AppDataSource.getRepository(Promotion)
);
const couponService = new CouponService(
AppDataSource,
AppDataSource.getRepository(Coupon)
);
// ==================== PRICE ENGINE ====================
/**
* Calculate prices for cart/order items
* POST /api/pricing/calculate
*/
export const calculatePrices = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const input = req.body as CalculatePricesInput;
const context = {
tenantId,
branchId: input.branchId,
customerId: input.customerId,
customerLevelId: input.customerLevelId,
isNewCustomer: input.isNewCustomer || false,
isLoyaltyMember: input.isLoyaltyMember || false,
isFirstOrder: input.isFirstOrder || false,
channel: input.channel,
};
const result = await priceEngineService.calculatePrices(
context,
input.lines,
input.couponCode
);
res.json({
success: true,
data: result,
});
} catch (error) {
next(error);
}
};
/**
* Validate a coupon code
* POST /api/pricing/validate-coupon
*/
export const validateCoupon = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const branchId = req.headers['x-branch-id'] as string;
const { code, orderAmount } = req.body;
const context = {
tenantId,
branchId,
channel: 'pos' as const,
isNewCustomer: false,
isLoyaltyMember: false,
isFirstOrder: false,
};
const result = await priceEngineService.validateCoupon(
context,
code,
orderAmount
);
res.json({
success: true,
data: result,
});
} catch (error) {
next(error);
}
};
/**
* Get active promotions for a branch
* GET /api/pricing/active-promotions
*/
export const getActivePromotions = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const branchId = req.headers['x-branch-id'] as string;
const channel = (req.query.channel as 'pos' | 'ecommerce' | 'mobile') || 'pos';
const promotions = await priceEngineService.getActivePromotions(
tenantId,
branchId,
channel
);
res.json({
success: true,
data: promotions,
meta: {
count: promotions.length,
},
});
} catch (error) {
next(error);
}
};
// ==================== PROMOTIONS ====================
/**
* List promotions with filters
* GET /api/pricing/promotions
*/
export const listPromotions = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const query = req.query as unknown as ListPromotionsQuery;
const result = await promotionService.listPromotions(tenantId, query);
res.json({
success: true,
data: result.data,
meta: result.meta,
});
} catch (error) {
next(error);
}
};
/**
* Get promotion by ID
* GET /api/pricing/promotions/:id
*/
export const getPromotion = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const { id } = req.params;
const result = await promotionService.getById(tenantId, id, {
relations: ['products'],
});
if (!result.success) {
res.status(404).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Create a new promotion
* POST /api/pricing/promotions
*/
export const createPromotion = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const input = req.body as CreatePromotionInput;
const result = await promotionService.createPromotion(tenantId, input, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.status(201).json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Update a promotion
* PUT /api/pricing/promotions/:id
*/
export const updatePromotion = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const input = req.body as UpdatePromotionInput;
const result = await promotionService.updatePromotion(tenantId, id, input, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Activate a promotion
* POST /api/pricing/promotions/:id/activate
*/
export const activatePromotion = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const result = await promotionService.activatePromotion(tenantId, id, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Pause a promotion
* POST /api/pricing/promotions/:id/pause
*/
export const pausePromotion = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const result = await promotionService.pausePromotion(tenantId, id, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* End a promotion
* POST /api/pricing/promotions/:id/end
*/
export const endPromotion = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const result = await promotionService.endPromotion(tenantId, id, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Cancel a promotion
* POST /api/pricing/promotions/:id/cancel
*/
export const cancelPromotion = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const result = await promotionService.cancelPromotion(tenantId, id, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Add products to a promotion
* POST /api/pricing/promotions/:id/products
*/
export const addPromotionProducts = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const { products } = req.body as { products: PromotionProductInput[] };
const result = await promotionService.addProducts(tenantId, id, products, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.status(201).json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Remove product from a promotion
* DELETE /api/pricing/promotions/:id/products/:productId
*/
export const removePromotionProduct = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id, productId } = req.params;
const result = await promotionService.removeProduct(tenantId, id, productId, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
message: 'Product removed from promotion',
});
} catch (error) {
next(error);
}
};
/**
* Delete a promotion
* DELETE /api/pricing/promotions/:id
*/
export const deletePromotion = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const result = await promotionService.softDelete(tenantId, id, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
message: 'Promotion deleted',
});
} catch (error) {
next(error);
}
};
// ==================== COUPONS ====================
/**
* List coupons with filters
* GET /api/pricing/coupons
*/
export const listCoupons = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const query = req.query as unknown as ListCouponsQuery;
const result = await couponService.listCoupons(tenantId, query);
res.json({
success: true,
data: result.data,
meta: result.meta,
});
} catch (error) {
next(error);
}
};
/**
* Get coupon by ID
* GET /api/pricing/coupons/:id
*/
export const getCoupon = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const { id } = req.params;
const result = await couponService.getById(tenantId, id, {
relations: ['redemptions'],
});
if (!result.success) {
res.status(404).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Get coupon by code
* GET /api/pricing/coupons/code/:code
*/
export const getCouponByCode = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const { code } = req.params;
const result = await couponService.getByCode(tenantId, code);
if (!result.success) {
res.status(404).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Create a new coupon
* POST /api/pricing/coupons
*/
export const createCoupon = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const input = req.body as CreateCouponInput;
const result = await couponService.createCoupon(tenantId, input, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.status(201).json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Generate bulk coupons
* POST /api/pricing/coupons/bulk
*/
export const generateBulkCoupons = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { quantity, prefix, baseInput } = req.body as GenerateBulkCouponsInput;
const result = await couponService.generateBulkCoupons(
tenantId,
baseInput,
quantity,
prefix,
userId
);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.status(201).json({
success: true,
data: result.data,
meta: {
count: result.data?.length || 0,
},
});
} catch (error) {
next(error);
}
};
/**
* Update a coupon
* PUT /api/pricing/coupons/:id
*/
export const updateCoupon = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const input = req.body as UpdateCouponInput;
const result = await couponService.updateCoupon(tenantId, id, input, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Activate a coupon
* POST /api/pricing/coupons/:id/activate
*/
export const activateCoupon = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const result = await couponService.activateCoupon(tenantId, id, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Deactivate a coupon
* POST /api/pricing/coupons/:id/deactivate
*/
export const deactivateCoupon = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const result = await couponService.deactivateCoupon(tenantId, id, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Redeem a coupon
* POST /api/pricing/coupons/:id/redeem
*/
export const redeemCoupon = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const input = req.body as RedeemCouponInput;
// Get coupon code from ID
const couponResult = await couponService.getById(tenantId, id);
if (!couponResult.success || !couponResult.data) {
res.status(404).json({
success: false,
error: 'Coupon not found',
});
return;
}
const result = await couponService.redeemCoupon(
tenantId,
couponResult.data.code,
input,
userId
);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.status(201).json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Redeem coupon by code
* POST /api/pricing/coupons/redeem
*/
export const redeemCouponByCode = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { code, ...input } = req.body as RedeemCouponInput & { code: string };
const result = await couponService.redeemCoupon(tenantId, code, input, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.status(201).json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Reverse a coupon redemption
* POST /api/pricing/coupons/redemptions/:redemptionId/reverse
*/
export const reverseRedemption = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { redemptionId } = req.params;
const { reason } = req.body as { reason: string };
const result = await couponService.reverseRedemption(
tenantId,
redemptionId,
reason,
userId
);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};
/**
* Get coupon redemption history
* GET /api/pricing/coupons/:id/redemptions
*/
export const getCouponRedemptions = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const { id } = req.params;
const { page = 1, limit = 20 } = req.query;
const result = await couponService.getRedemptionHistory(
tenantId,
id,
Number(page),
Number(limit)
);
res.json({
success: true,
data: result.data,
meta: result.meta,
});
} catch (error) {
next(error);
}
};
/**
* Delete a coupon
* DELETE /api/pricing/coupons/:id
*/
export const deleteCoupon = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id as string;
const { id } = req.params;
const result = await couponService.softDelete(tenantId, id, userId);
if (!result.success) {
res.status(400).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
message: 'Coupon deleted',
});
} catch (error) {
next(error);
}
};
/**
* Get coupon statistics
* GET /api/pricing/coupons/:id/stats
*/
export const getCouponStats = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const { id } = req.params;
const result = await couponService.getCouponStats(tenantId, id);
if (!result.success) {
res.status(404).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
data: result.data,
});
} catch (error) {
next(error);
}
};