erp-core-backend/src/modules/sales/customer-groups.service.ts
2025-12-12 14:39:29 -06:00

210 lines
6.1 KiB
TypeScript

import { query, queryOne } from '../../config/database.js';
import { NotFoundError, ConflictError } from '../../shared/errors/index.js';
export interface CustomerGroupMember {
id: string;
customer_group_id: string;
partner_id: string;
partner_name?: string;
joined_at: Date;
}
export interface CustomerGroup {
id: string;
tenant_id: string;
name: string;
description?: string;
discount_percentage: number;
members?: CustomerGroupMember[];
member_count?: number;
created_at: Date;
}
export interface CreateCustomerGroupDto {
name: string;
description?: string;
discount_percentage?: number;
}
export interface UpdateCustomerGroupDto {
name?: string;
description?: string | null;
discount_percentage?: number;
}
export interface CustomerGroupFilters {
search?: string;
page?: number;
limit?: number;
}
class CustomerGroupsService {
async findAll(tenantId: string, filters: CustomerGroupFilters = {}): Promise<{ data: CustomerGroup[]; total: number }> {
const { search, page = 1, limit = 20 } = filters;
const offset = (page - 1) * limit;
let whereClause = 'WHERE cg.tenant_id = $1';
const params: any[] = [tenantId];
let paramIndex = 2;
if (search) {
whereClause += ` AND (cg.name ILIKE $${paramIndex} OR cg.description ILIKE $${paramIndex})`;
params.push(`%${search}%`);
paramIndex++;
}
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM sales.customer_groups cg ${whereClause}`,
params
);
params.push(limit, offset);
const data = await query<CustomerGroup>(
`SELECT cg.*,
(SELECT COUNT(*) FROM sales.customer_group_members cgm WHERE cgm.customer_group_id = cg.id) as member_count
FROM sales.customer_groups cg
${whereClause}
ORDER BY cg.name
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
params
);
return {
data,
total: parseInt(countResult?.count || '0', 10),
};
}
async findById(id: string, tenantId: string): Promise<CustomerGroup> {
const group = await queryOne<CustomerGroup>(
`SELECT cg.*,
(SELECT COUNT(*) FROM sales.customer_group_members cgm WHERE cgm.customer_group_id = cg.id) as member_count
FROM sales.customer_groups cg
WHERE cg.id = $1 AND cg.tenant_id = $2`,
[id, tenantId]
);
if (!group) {
throw new NotFoundError('Grupo de clientes no encontrado');
}
// Get members
const members = await query<CustomerGroupMember>(
`SELECT cgm.*,
p.name as partner_name
FROM sales.customer_group_members cgm
LEFT JOIN core.partners p ON cgm.partner_id = p.id
WHERE cgm.customer_group_id = $1
ORDER BY p.name`,
[id]
);
group.members = members;
return group;
}
async create(dto: CreateCustomerGroupDto, tenantId: string, userId: string): Promise<CustomerGroup> {
// Check unique name
const existing = await queryOne(
`SELECT id FROM sales.customer_groups WHERE tenant_id = $1 AND name = $2`,
[tenantId, dto.name]
);
if (existing) {
throw new ConflictError('Ya existe un grupo de clientes con ese nombre');
}
const group = await queryOne<CustomerGroup>(
`INSERT INTO sales.customer_groups (tenant_id, name, description, discount_percentage, created_by)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`,
[tenantId, dto.name, dto.description, dto.discount_percentage || 0, userId]
);
return group!;
}
async update(id: string, dto: UpdateCustomerGroupDto, tenantId: string): Promise<CustomerGroup> {
await this.findById(id, tenantId);
const updateFields: string[] = [];
const values: any[] = [];
let paramIndex = 1;
if (dto.name !== undefined) {
// Check unique name
const existing = await queryOne(
`SELECT id FROM sales.customer_groups WHERE tenant_id = $1 AND name = $2 AND id != $3`,
[tenantId, dto.name, id]
);
if (existing) {
throw new ConflictError('Ya existe un grupo de clientes con ese nombre');
}
updateFields.push(`name = $${paramIndex++}`);
values.push(dto.name);
}
if (dto.description !== undefined) {
updateFields.push(`description = $${paramIndex++}`);
values.push(dto.description);
}
if (dto.discount_percentage !== undefined) {
updateFields.push(`discount_percentage = $${paramIndex++}`);
values.push(dto.discount_percentage);
}
values.push(id, tenantId);
await query(
`UPDATE sales.customer_groups SET ${updateFields.join(', ')}
WHERE id = $${paramIndex++} AND tenant_id = $${paramIndex}`,
values
);
return this.findById(id, tenantId);
}
async delete(id: string, tenantId: string): Promise<void> {
const group = await this.findById(id, tenantId);
if (group.member_count && group.member_count > 0) {
throw new ConflictError('No se puede eliminar un grupo con miembros');
}
await query(`DELETE FROM sales.customer_groups WHERE id = $1 AND tenant_id = $2`, [id, tenantId]);
}
async addMember(groupId: string, partnerId: string, tenantId: string): Promise<CustomerGroupMember> {
await this.findById(groupId, tenantId);
// Check if already member
const existing = await queryOne(
`SELECT id FROM sales.customer_group_members WHERE customer_group_id = $1 AND partner_id = $2`,
[groupId, partnerId]
);
if (existing) {
throw new ConflictError('El cliente ya es miembro de este grupo');
}
const member = await queryOne<CustomerGroupMember>(
`INSERT INTO sales.customer_group_members (customer_group_id, partner_id)
VALUES ($1, $2)
RETURNING *`,
[groupId, partnerId]
);
return member!;
}
async removeMember(groupId: string, memberId: string, tenantId: string): Promise<void> {
await this.findById(groupId, tenantId);
await query(
`DELETE FROM sales.customer_group_members WHERE id = $1 AND customer_group_id = $2`,
[memberId, groupId]
);
}
}
export const customerGroupsService = new CustomerGroupsService();