feat: Implementar servicios core críticos
- CartaPorteService: CRUD completo para Carta Porte 3.1 SAT - RolesService: Gestión de roles con aislamiento por tenant - PermissionsService: Gestión de permisos con bulk creation - TarifasService: Motor de cotización con múltiples tipos de tarifa - LanesService: Gestión de rutas origen-destino Total: ~1,800 líneas de código de servicios Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7d6d4b7caa
commit
6e89e7f4c5
@ -6,3 +6,9 @@ export { default as authRoutes } from './auth.routes.js';
|
||||
export * from './apiKeys.service.js';
|
||||
export * from './apiKeys.controller.js';
|
||||
export { default as apiKeysRoutes } from './apiKeys.routes.js';
|
||||
|
||||
// Entities
|
||||
export * from './entities/index.js';
|
||||
|
||||
// Additional Services (roles, permissions)
|
||||
export * from './services/index.js';
|
||||
|
||||
7
src/modules/auth/services/index.ts
Normal file
7
src/modules/auth/services/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Auth Services
|
||||
* Roles, permisos, autenticacion
|
||||
*/
|
||||
export * from './token.service.js';
|
||||
export * from './roles.service.js';
|
||||
export * from './permissions.service.js';
|
||||
292
src/modules/auth/services/permissions.service.ts
Normal file
292
src/modules/auth/services/permissions.service.ts
Normal file
@ -0,0 +1,292 @@
|
||||
import { Repository, FindOptionsWhere, ILike, In } from 'typeorm';
|
||||
import { Permission, PermissionAction, Role } from '../entities/index.js';
|
||||
|
||||
export interface PermissionSearchParams {
|
||||
search?: string;
|
||||
resource?: string;
|
||||
action?: PermissionAction;
|
||||
module?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface CreatePermissionDto {
|
||||
resource: string;
|
||||
action: PermissionAction;
|
||||
description?: string;
|
||||
module?: string;
|
||||
}
|
||||
|
||||
export interface UpdatePermissionDto {
|
||||
description?: string;
|
||||
module?: string;
|
||||
}
|
||||
|
||||
export interface BulkCreatePermissionsDto {
|
||||
resource: string;
|
||||
actions: PermissionAction[];
|
||||
module?: string;
|
||||
}
|
||||
|
||||
export class PermissionsService {
|
||||
constructor(
|
||||
private readonly permissionRepository: Repository<Permission>,
|
||||
private readonly roleRepository: Repository<Role>,
|
||||
) {}
|
||||
|
||||
async findAll(params: PermissionSearchParams): Promise<{ data: Permission[]; total: number }> {
|
||||
const {
|
||||
search,
|
||||
resource,
|
||||
action,
|
||||
module,
|
||||
limit = 100,
|
||||
offset = 0,
|
||||
} = params;
|
||||
|
||||
const where: FindOptionsWhere<Permission>[] = [];
|
||||
const baseWhere: FindOptionsWhere<Permission> = {};
|
||||
|
||||
if (resource) {
|
||||
baseWhere.resource = resource;
|
||||
}
|
||||
|
||||
if (action) {
|
||||
baseWhere.action = action;
|
||||
}
|
||||
|
||||
if (module) {
|
||||
baseWhere.module = module;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.push(
|
||||
{ ...baseWhere, resource: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, description: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, module: ILike(`%${search}%`) }
|
||||
);
|
||||
} else {
|
||||
where.push(baseWhere);
|
||||
}
|
||||
|
||||
const [data, total] = await this.permissionRepository.findAndCount({
|
||||
where,
|
||||
take: limit,
|
||||
skip: offset,
|
||||
order: { module: 'ASC', resource: 'ASC', action: 'ASC' },
|
||||
});
|
||||
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Permission | null> {
|
||||
return this.permissionRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['roles'],
|
||||
});
|
||||
}
|
||||
|
||||
async findByResourceAction(resource: string, action: PermissionAction): Promise<Permission | null> {
|
||||
return this.permissionRepository.findOne({
|
||||
where: { resource, action },
|
||||
});
|
||||
}
|
||||
|
||||
async findByModule(module: string): Promise<Permission[]> {
|
||||
return this.permissionRepository.find({
|
||||
where: { module },
|
||||
order: { resource: 'ASC', action: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findByResource(resource: string): Promise<Permission[]> {
|
||||
return this.permissionRepository.find({
|
||||
where: { resource },
|
||||
order: { action: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async create(dto: CreatePermissionDto): Promise<Permission> {
|
||||
// Check if permission already exists
|
||||
const existing = await this.findByResourceAction(dto.resource, dto.action);
|
||||
if (existing) {
|
||||
throw new Error(`Ya existe el permiso ${dto.action} para el recurso ${dto.resource}`);
|
||||
}
|
||||
|
||||
const permission = this.permissionRepository.create({
|
||||
resource: dto.resource,
|
||||
action: dto.action,
|
||||
description: dto.description ?? null,
|
||||
module: dto.module ?? null,
|
||||
});
|
||||
|
||||
return this.permissionRepository.save(permission);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdatePermissionDto): Promise<Permission | null> {
|
||||
const permission = await this.findOne(id);
|
||||
if (!permission) return null;
|
||||
|
||||
if (dto.description !== undefined) {
|
||||
permission.description = dto.description ?? null;
|
||||
}
|
||||
if (dto.module !== undefined) {
|
||||
permission.module = dto.module ?? null;
|
||||
}
|
||||
|
||||
return this.permissionRepository.save(permission);
|
||||
}
|
||||
|
||||
// Create multiple permissions for a resource (CRUD operations)
|
||||
async bulkCreateForResource(dto: BulkCreatePermissionsDto): Promise<Permission[]> {
|
||||
const created: Permission[] = [];
|
||||
|
||||
for (const action of dto.actions) {
|
||||
const existing = await this.findByResourceAction(dto.resource, action);
|
||||
if (!existing) {
|
||||
const permission = this.permissionRepository.create({
|
||||
resource: dto.resource,
|
||||
action,
|
||||
description: `${action} ${dto.resource}`,
|
||||
module: dto.module ?? null,
|
||||
});
|
||||
const saved = await this.permissionRepository.save(permission);
|
||||
created.push(saved);
|
||||
}
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
// Create standard CRUD permissions for a resource
|
||||
async createCrudPermissions(resource: string, module?: string): Promise<Permission[]> {
|
||||
const crudActions = [
|
||||
PermissionAction.CREATE,
|
||||
PermissionAction.READ,
|
||||
PermissionAction.UPDATE,
|
||||
PermissionAction.DELETE,
|
||||
];
|
||||
|
||||
return this.bulkCreateForResource({
|
||||
resource,
|
||||
actions: crudActions,
|
||||
module,
|
||||
});
|
||||
}
|
||||
|
||||
// Get all unique resources
|
||||
async getResources(): Promise<string[]> {
|
||||
const permissions = await this.permissionRepository
|
||||
.createQueryBuilder('p')
|
||||
.select('DISTINCT p.resource', 'resource')
|
||||
.orderBy('p.resource', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
return permissions.map(p => p.resource);
|
||||
}
|
||||
|
||||
// Get all unique modules
|
||||
async getModules(): Promise<string[]> {
|
||||
const permissions = await this.permissionRepository
|
||||
.createQueryBuilder('p')
|
||||
.select('DISTINCT p.module', 'module')
|
||||
.where('p.module IS NOT NULL')
|
||||
.orderBy('p.module', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
return permissions.map(p => p.module);
|
||||
}
|
||||
|
||||
// Get permissions grouped by module
|
||||
async getPermissionsByModule(): Promise<Map<string, Permission[]>> {
|
||||
const permissions = await this.permissionRepository.find({
|
||||
order: { module: 'ASC', resource: 'ASC', action: 'ASC' },
|
||||
});
|
||||
|
||||
const grouped = new Map<string, Permission[]>();
|
||||
|
||||
for (const permission of permissions) {
|
||||
const module = permission.module ?? 'other';
|
||||
if (!grouped.has(module)) {
|
||||
grouped.set(module, []);
|
||||
}
|
||||
grouped.get(module)!.push(permission);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// Get permissions grouped by resource
|
||||
async getPermissionsByResource(): Promise<Map<string, Permission[]>> {
|
||||
const permissions = await this.permissionRepository.find({
|
||||
order: { resource: 'ASC', action: 'ASC' },
|
||||
});
|
||||
|
||||
const grouped = new Map<string, Permission[]>();
|
||||
|
||||
for (const permission of permissions) {
|
||||
if (!grouped.has(permission.resource)) {
|
||||
grouped.set(permission.resource, []);
|
||||
}
|
||||
grouped.get(permission.resource)!.push(permission);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// Check if a role has a specific permission
|
||||
async roleHasPermission(
|
||||
roleId: string,
|
||||
resource: string,
|
||||
action: PermissionAction
|
||||
): Promise<boolean> {
|
||||
const permission = await this.findByResourceAction(resource, action);
|
||||
if (!permission) return false;
|
||||
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { id: roleId },
|
||||
relations: ['permissions'],
|
||||
});
|
||||
|
||||
if (!role) return false;
|
||||
|
||||
return role.permissions.some(p => p.id === permission.id);
|
||||
}
|
||||
|
||||
// Get roles that have a specific permission
|
||||
async getRolesWithPermission(permissionId: string): Promise<Role[]> {
|
||||
const permission = await this.findOne(permissionId);
|
||||
if (!permission) return [];
|
||||
|
||||
return permission.roles ?? [];
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<boolean> {
|
||||
const permission = await this.findOne(id);
|
||||
if (!permission) return false;
|
||||
|
||||
// Check if permission is assigned to any roles
|
||||
if (permission.roles && permission.roles.length > 0) {
|
||||
throw new Error('No se puede eliminar un permiso asignado a roles. Primero quite el permiso de los roles.');
|
||||
}
|
||||
|
||||
const result = await this.permissionRepository.delete(id);
|
||||
return (result.affected ?? 0) > 0;
|
||||
}
|
||||
|
||||
// Delete all permissions for a resource
|
||||
async deleteByResource(resource: string): Promise<number> {
|
||||
const permissions = await this.findByResource(resource);
|
||||
|
||||
// Check if any permission is assigned to roles
|
||||
for (const permission of permissions) {
|
||||
const fullPermission = await this.findOne(permission.id);
|
||||
if (fullPermission?.roles && fullPermission.roles.length > 0) {
|
||||
throw new Error(`El permiso ${permission.action} de ${resource} esta asignado a roles`);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.permissionRepository.delete({ resource });
|
||||
return result.affected ?? 0;
|
||||
}
|
||||
}
|
||||
302
src/modules/auth/services/roles.service.ts
Normal file
302
src/modules/auth/services/roles.service.ts
Normal file
@ -0,0 +1,302 @@
|
||||
import { Repository, FindOptionsWhere, ILike, In } from 'typeorm';
|
||||
import { Role, Permission, User } from '../entities/index.js';
|
||||
|
||||
export interface RoleSearchParams {
|
||||
tenantId: string;
|
||||
search?: string;
|
||||
isSystem?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface CreateRoleDto {
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
isSystem?: boolean;
|
||||
color?: string;
|
||||
permissionIds?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateRoleDto extends Partial<CreateRoleDto> {}
|
||||
|
||||
export interface AssignPermissionsDto {
|
||||
permissionIds: string[];
|
||||
}
|
||||
|
||||
export class RolesService {
|
||||
constructor(
|
||||
private readonly roleRepository: Repository<Role>,
|
||||
private readonly permissionRepository: Repository<Permission>,
|
||||
) {}
|
||||
|
||||
async findAll(params: RoleSearchParams): Promise<{ data: Role[]; total: number }> {
|
||||
const {
|
||||
tenantId,
|
||||
search,
|
||||
isSystem,
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
} = params;
|
||||
|
||||
const where: FindOptionsWhere<Role>[] = [];
|
||||
const baseWhere: FindOptionsWhere<Role> = { tenantId };
|
||||
|
||||
if (isSystem !== undefined) {
|
||||
baseWhere.isSystem = isSystem;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.push(
|
||||
{ ...baseWhere, name: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, code: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, description: ILike(`%${search}%`) }
|
||||
);
|
||||
} else {
|
||||
where.push(baseWhere);
|
||||
}
|
||||
|
||||
const [data, total] = await this.roleRepository.findAndCount({
|
||||
where,
|
||||
relations: ['permissions'],
|
||||
take: limit,
|
||||
skip: offset,
|
||||
order: { isSystem: 'DESC', name: 'ASC' },
|
||||
});
|
||||
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
async findOne(id: string, tenantId: string): Promise<Role | null> {
|
||||
return this.roleRepository.findOne({
|
||||
where: { id, tenantId },
|
||||
relations: ['permissions', 'users'],
|
||||
});
|
||||
}
|
||||
|
||||
async findByCode(code: string, tenantId: string): Promise<Role | null> {
|
||||
return this.roleRepository.findOne({
|
||||
where: { code, tenantId },
|
||||
relations: ['permissions'],
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateRoleDto, createdBy: string): Promise<Role> {
|
||||
// Check if code already exists
|
||||
const existingCode = await this.findByCode(dto.code, tenantId);
|
||||
if (existingCode) {
|
||||
throw new Error(`Ya existe un rol con el codigo "${dto.code}"`);
|
||||
}
|
||||
|
||||
const role = this.roleRepository.create({
|
||||
tenantId,
|
||||
name: dto.name,
|
||||
code: dto.code,
|
||||
description: dto.description ?? null,
|
||||
isSystem: dto.isSystem ?? false,
|
||||
color: dto.color ?? null,
|
||||
createdBy,
|
||||
});
|
||||
|
||||
// If permissions were provided, assign them
|
||||
if (dto.permissionIds && dto.permissionIds.length > 0) {
|
||||
const permissions = await this.permissionRepository.findBy({
|
||||
id: In(dto.permissionIds),
|
||||
});
|
||||
role.permissions = permissions;
|
||||
}
|
||||
|
||||
return this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
dto: UpdateRoleDto,
|
||||
updatedBy: string
|
||||
): Promise<Role | null> {
|
||||
const role = await this.findOne(id, tenantId);
|
||||
if (!role) return null;
|
||||
|
||||
// Cannot modify system roles
|
||||
if (role.isSystem && !dto.description && !dto.color) {
|
||||
throw new Error('Los roles del sistema solo permiten modificar descripcion y color');
|
||||
}
|
||||
|
||||
// Check for code uniqueness if changing code
|
||||
if (dto.code && dto.code !== role.code) {
|
||||
const existingCode = await this.findByCode(dto.code, tenantId);
|
||||
if (existingCode && existingCode.id !== id) {
|
||||
throw new Error(`Ya existe un rol con el codigo "${dto.code}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Update basic fields
|
||||
if (dto.name !== undefined && !role.isSystem) role.name = dto.name;
|
||||
if (dto.code !== undefined && !role.isSystem) role.code = dto.code;
|
||||
if (dto.description !== undefined) role.description = dto.description ?? null;
|
||||
if (dto.color !== undefined) role.color = dto.color ?? null;
|
||||
role.updatedBy = updatedBy;
|
||||
|
||||
// Update permissions if provided
|
||||
if (dto.permissionIds !== undefined) {
|
||||
const permissions = await this.permissionRepository.findBy({
|
||||
id: In(dto.permissionIds),
|
||||
});
|
||||
role.permissions = permissions;
|
||||
}
|
||||
|
||||
return this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
async assignPermissions(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
dto: AssignPermissionsDto,
|
||||
updatedBy: string
|
||||
): Promise<Role | null> {
|
||||
const role = await this.findOne(id, tenantId);
|
||||
if (!role) return null;
|
||||
|
||||
const permissions = await this.permissionRepository.findBy({
|
||||
id: In(dto.permissionIds),
|
||||
});
|
||||
|
||||
role.permissions = permissions;
|
||||
role.updatedBy = updatedBy;
|
||||
|
||||
return this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
async addPermission(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
permissionId: string,
|
||||
updatedBy: string
|
||||
): Promise<Role | null> {
|
||||
const role = await this.findOne(id, tenantId);
|
||||
if (!role) return null;
|
||||
|
||||
const permission = await this.permissionRepository.findOne({
|
||||
where: { id: permissionId },
|
||||
});
|
||||
if (!permission) {
|
||||
throw new Error('Permiso no encontrado');
|
||||
}
|
||||
|
||||
// Check if permission already assigned
|
||||
const hasPermission = role.permissions.some(p => p.id === permissionId);
|
||||
if (!hasPermission) {
|
||||
role.permissions.push(permission);
|
||||
role.updatedBy = updatedBy;
|
||||
return this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
async removePermission(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
permissionId: string,
|
||||
updatedBy: string
|
||||
): Promise<Role | null> {
|
||||
const role = await this.findOne(id, tenantId);
|
||||
if (!role) return null;
|
||||
|
||||
role.permissions = role.permissions.filter(p => p.id !== permissionId);
|
||||
role.updatedBy = updatedBy;
|
||||
|
||||
return this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
async getPermissions(id: string, tenantId: string): Promise<Permission[]> {
|
||||
const role = await this.findOne(id, tenantId);
|
||||
if (!role) return [];
|
||||
|
||||
return role.permissions;
|
||||
}
|
||||
|
||||
async getUsers(id: string, tenantId: string): Promise<User[]> {
|
||||
const role = await this.findOne(id, tenantId);
|
||||
if (!role) return [];
|
||||
|
||||
return role.users;
|
||||
}
|
||||
|
||||
async getUserCount(id: string, tenantId: string): Promise<number> {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { id, tenantId },
|
||||
relations: ['users'],
|
||||
});
|
||||
|
||||
return role?.users?.length ?? 0;
|
||||
}
|
||||
|
||||
// Get system roles (admin, user, etc.)
|
||||
async getSystemRoles(tenantId: string): Promise<Role[]> {
|
||||
return this.roleRepository.find({
|
||||
where: { tenantId, isSystem: true },
|
||||
relations: ['permissions'],
|
||||
order: { name: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
// Get custom roles (non-system)
|
||||
async getCustomRoles(tenantId: string): Promise<Role[]> {
|
||||
return this.roleRepository.find({
|
||||
where: { tenantId, isSystem: false },
|
||||
relations: ['permissions'],
|
||||
order: { name: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
// Clone a role
|
||||
async cloneRole(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
newCode: string,
|
||||
newName: string,
|
||||
createdBy: string
|
||||
): Promise<Role | null> {
|
||||
const sourceRole = await this.findOne(id, tenantId);
|
||||
if (!sourceRole) return null;
|
||||
|
||||
// Check if new code exists
|
||||
const existingCode = await this.findByCode(newCode, tenantId);
|
||||
if (existingCode) {
|
||||
throw new Error(`Ya existe un rol con el codigo "${newCode}"`);
|
||||
}
|
||||
|
||||
const newRole = this.roleRepository.create({
|
||||
tenantId,
|
||||
name: newName,
|
||||
code: newCode,
|
||||
description: sourceRole.description,
|
||||
isSystem: false, // Cloned roles are never system roles
|
||||
color: sourceRole.color,
|
||||
permissions: sourceRole.permissions,
|
||||
createdBy,
|
||||
});
|
||||
|
||||
return this.roleRepository.save(newRole);
|
||||
}
|
||||
|
||||
async delete(id: string, tenantId: string): Promise<boolean> {
|
||||
const role = await this.findOne(id, tenantId);
|
||||
if (!role) return false;
|
||||
|
||||
// Cannot delete system roles
|
||||
if (role.isSystem) {
|
||||
throw new Error('No se pueden eliminar roles del sistema');
|
||||
}
|
||||
|
||||
// Check if role has users
|
||||
if (role.users && role.users.length > 0) {
|
||||
throw new Error('No se puede eliminar un rol que tiene usuarios asignados');
|
||||
}
|
||||
|
||||
const result = await this.roleRepository.softDelete(id);
|
||||
return (result.affected ?? 0) > 0;
|
||||
}
|
||||
}
|
||||
446
src/modules/carta-porte/services/carta-porte.service.ts
Normal file
446
src/modules/carta-porte/services/carta-porte.service.ts
Normal file
@ -0,0 +1,446 @@
|
||||
import { Repository, FindOptionsWhere, ILike, In, Between } from 'typeorm';
|
||||
import {
|
||||
CartaPorte,
|
||||
EstadoCartaPorte,
|
||||
TipoCfdiCartaPorte,
|
||||
UbicacionCartaPorte,
|
||||
MercanciaCartaPorte,
|
||||
FiguraTransporte,
|
||||
AutotransporteCartaPorte,
|
||||
} from '../entities';
|
||||
|
||||
export interface CartaPorteSearchParams {
|
||||
tenantId: string;
|
||||
search?: string;
|
||||
estado?: EstadoCartaPorte;
|
||||
estados?: EstadoCartaPorte[];
|
||||
tipoCfdi?: TipoCfdiCartaPorte;
|
||||
viajeId?: string;
|
||||
emisorRfc?: string;
|
||||
receptorRfc?: string;
|
||||
fechaDesde?: Date;
|
||||
fechaHasta?: Date;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface CreateCartaPorteDto {
|
||||
viajeId: string;
|
||||
tipoCfdi: TipoCfdiCartaPorte;
|
||||
versionCartaPorte?: string;
|
||||
serie?: string;
|
||||
// Emisor
|
||||
emisorRfc: string;
|
||||
emisorNombre: string;
|
||||
emisorRegimenFiscal?: string;
|
||||
// Receptor
|
||||
receptorRfc: string;
|
||||
receptorNombre: string;
|
||||
receptorUsoCfdi?: string;
|
||||
receptorDomicilioFiscalCp?: string;
|
||||
// Totales
|
||||
subtotal?: number;
|
||||
total?: number;
|
||||
moneda?: string;
|
||||
// Transporte internacional
|
||||
transporteInternacional?: boolean;
|
||||
entradaSalidaMerc?: string;
|
||||
paisOrigenDestino?: string;
|
||||
// Datos autotransporte
|
||||
permisoSct?: string;
|
||||
numPermisoSct?: string;
|
||||
configVehicular?: string;
|
||||
pesoBrutoTotal?: number;
|
||||
unidadPeso?: string;
|
||||
numTotalMercancias?: number;
|
||||
// Seguros
|
||||
aseguraRespCivil?: string;
|
||||
polizaRespCivil?: string;
|
||||
aseguraMedAmbiente?: string;
|
||||
polizaMedAmbiente?: string;
|
||||
aseguraCarga?: string;
|
||||
polizaCarga?: string;
|
||||
primaSeguro?: number;
|
||||
}
|
||||
|
||||
export interface UpdateCartaPorteDto extends Partial<CreateCartaPorteDto> {
|
||||
estado?: EstadoCartaPorte;
|
||||
}
|
||||
|
||||
export interface TimbrarCartaPorteDto {
|
||||
uuidCfdi: string;
|
||||
fechaTimbrado: Date;
|
||||
xmlCfdi: string;
|
||||
xmlCartaPorte: string;
|
||||
pdfUrl?: string;
|
||||
qrUrl?: string;
|
||||
}
|
||||
|
||||
export interface CancelarCartaPorteDto {
|
||||
motivoCancelacion: string;
|
||||
uuidSustitucion?: string;
|
||||
}
|
||||
|
||||
export class CartaPorteService {
|
||||
constructor(
|
||||
private readonly cartaPorteRepository: Repository<CartaPorte>,
|
||||
private readonly ubicacionRepository: Repository<UbicacionCartaPorte>,
|
||||
private readonly mercanciaRepository: Repository<MercanciaCartaPorte>,
|
||||
private readonly figuraRepository: Repository<FiguraTransporte>,
|
||||
private readonly autotransporteRepository: Repository<AutotransporteCartaPorte>,
|
||||
) {}
|
||||
|
||||
async findAll(params: CartaPorteSearchParams): Promise<{ data: CartaPorte[]; total: number }> {
|
||||
const {
|
||||
tenantId,
|
||||
search,
|
||||
estado,
|
||||
estados,
|
||||
tipoCfdi,
|
||||
viajeId,
|
||||
emisorRfc,
|
||||
receptorRfc,
|
||||
fechaDesde,
|
||||
fechaHasta,
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
} = params;
|
||||
|
||||
const where: FindOptionsWhere<CartaPorte>[] = [];
|
||||
const baseWhere: FindOptionsWhere<CartaPorte> = { tenantId };
|
||||
|
||||
if (estado) {
|
||||
baseWhere.estado = estado;
|
||||
}
|
||||
|
||||
if (estados && estados.length > 0) {
|
||||
baseWhere.estado = In(estados);
|
||||
}
|
||||
|
||||
if (tipoCfdi) {
|
||||
baseWhere.tipoCfdi = tipoCfdi;
|
||||
}
|
||||
|
||||
if (viajeId) {
|
||||
baseWhere.viajeId = viajeId;
|
||||
}
|
||||
|
||||
if (emisorRfc) {
|
||||
baseWhere.emisorRfc = emisorRfc;
|
||||
}
|
||||
|
||||
if (receptorRfc) {
|
||||
baseWhere.receptorRfc = receptorRfc;
|
||||
}
|
||||
|
||||
if (fechaDesde && fechaHasta) {
|
||||
baseWhere.createdAt = Between(fechaDesde, fechaHasta);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.push(
|
||||
{ ...baseWhere, folio: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, serie: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, emisorNombre: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, receptorNombre: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, receptorRfc: ILike(`%${search}%`) }
|
||||
);
|
||||
} else {
|
||||
where.push(baseWhere);
|
||||
}
|
||||
|
||||
const [data, total] = await this.cartaPorteRepository.findAndCount({
|
||||
where,
|
||||
relations: ['ubicaciones', 'mercancias', 'figuras', 'autotransporte'],
|
||||
take: limit,
|
||||
skip: offset,
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
async findOne(id: string, tenantId: string): Promise<CartaPorte | null> {
|
||||
return this.cartaPorteRepository.findOne({
|
||||
where: { id, tenantId },
|
||||
relations: ['ubicaciones', 'mercancias', 'figuras', 'autotransporte'],
|
||||
});
|
||||
}
|
||||
|
||||
async findByViaje(viajeId: string, tenantId: string): Promise<CartaPorte[]> {
|
||||
return this.cartaPorteRepository.find({
|
||||
where: { viajeId, tenantId },
|
||||
relations: ['ubicaciones', 'mercancias', 'figuras', 'autotransporte'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findByUuid(uuidCfdi: string, tenantId: string): Promise<CartaPorte | null> {
|
||||
return this.cartaPorteRepository.findOne({
|
||||
where: { uuidCfdi, tenantId },
|
||||
relations: ['ubicaciones', 'mercancias', 'figuras', 'autotransporte'],
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateCartaPorteDto, createdBy: string): Promise<CartaPorte> {
|
||||
const cartaPorte = this.cartaPorteRepository.create({
|
||||
...dto,
|
||||
tenantId,
|
||||
estado: EstadoCartaPorte.BORRADOR,
|
||||
createdById: createdBy,
|
||||
});
|
||||
|
||||
return this.cartaPorteRepository.save(cartaPorte);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
dto: UpdateCartaPorteDto,
|
||||
updatedBy: string
|
||||
): Promise<CartaPorte | null> {
|
||||
const cartaPorte = await this.findOne(id, tenantId);
|
||||
if (!cartaPorte) return null;
|
||||
|
||||
// Cannot update if already timbrada or cancelada
|
||||
if (cartaPorte.estado === EstadoCartaPorte.TIMBRADA) {
|
||||
throw new Error('No se puede modificar una Carta Porte ya timbrada');
|
||||
}
|
||||
|
||||
if (cartaPorte.estado === EstadoCartaPorte.CANCELADA) {
|
||||
throw new Error('No se puede modificar una Carta Porte cancelada');
|
||||
}
|
||||
|
||||
Object.assign(cartaPorte, dto);
|
||||
return this.cartaPorteRepository.save(cartaPorte);
|
||||
}
|
||||
|
||||
async validar(id: string, tenantId: string): Promise<{ valid: boolean; errors: string[] }> {
|
||||
const cartaPorte = await this.findOne(id, tenantId);
|
||||
if (!cartaPorte) {
|
||||
return { valid: false, errors: ['Carta Porte no encontrada'] };
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validaciones requeridas para Carta Porte 3.1
|
||||
if (!cartaPorte.emisorRfc) errors.push('RFC del emisor es requerido');
|
||||
if (!cartaPorte.emisorNombre) errors.push('Nombre del emisor es requerido');
|
||||
if (!cartaPorte.receptorRfc) errors.push('RFC del receptor es requerido');
|
||||
if (!cartaPorte.receptorNombre) errors.push('Nombre del receptor es requerido');
|
||||
|
||||
// Validar ubicaciones (minimo origen y destino)
|
||||
if (!cartaPorte.ubicaciones || cartaPorte.ubicaciones.length < 2) {
|
||||
errors.push('Se requieren al menos 2 ubicaciones (origen y destino)');
|
||||
}
|
||||
|
||||
// Validar mercancias
|
||||
if (!cartaPorte.mercancias || cartaPorte.mercancias.length === 0) {
|
||||
errors.push('Se requiere al menos una mercancia');
|
||||
}
|
||||
|
||||
// Validar figuras de transporte
|
||||
if (!cartaPorte.figuras || cartaPorte.figuras.length === 0) {
|
||||
errors.push('Se requiere al menos una figura de transporte (operador)');
|
||||
}
|
||||
|
||||
// Validar autotransporte
|
||||
if (!cartaPorte.autotransporte || cartaPorte.autotransporte.length === 0) {
|
||||
errors.push('Se requiere informacion de autotransporte');
|
||||
}
|
||||
|
||||
// Validar datos de transporte federal
|
||||
if (!cartaPorte.permisoSct) errors.push('Tipo de permiso SCT es requerido');
|
||||
if (!cartaPorte.numPermisoSct) errors.push('Numero de permiso SCT es requerido');
|
||||
if (!cartaPorte.configVehicular) errors.push('Configuracion vehicular es requerida');
|
||||
|
||||
// Si es CFDI de ingreso, validar totales
|
||||
if (cartaPorte.tipoCfdi === TipoCfdiCartaPorte.INGRESO) {
|
||||
if (cartaPorte.total === null || cartaPorte.total === undefined) {
|
||||
errors.push('Total del CFDI es requerido para tipo Ingreso');
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
async marcarValidada(id: string, tenantId: string): Promise<CartaPorte | null> {
|
||||
const cartaPorte = await this.findOne(id, tenantId);
|
||||
if (!cartaPorte) return null;
|
||||
|
||||
const validation = await this.validar(id, tenantId);
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Carta Porte no valida: ${validation.errors.join(', ')}`);
|
||||
}
|
||||
|
||||
cartaPorte.estado = EstadoCartaPorte.VALIDADA;
|
||||
return this.cartaPorteRepository.save(cartaPorte);
|
||||
}
|
||||
|
||||
async timbrar(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
dto: TimbrarCartaPorteDto
|
||||
): Promise<CartaPorte | null> {
|
||||
const cartaPorte = await this.findOne(id, tenantId);
|
||||
if (!cartaPorte) return null;
|
||||
|
||||
if (cartaPorte.estado !== EstadoCartaPorte.VALIDADA) {
|
||||
throw new Error('La Carta Porte debe estar validada antes de timbrar');
|
||||
}
|
||||
|
||||
Object.assign(cartaPorte, {
|
||||
...dto,
|
||||
estado: EstadoCartaPorte.TIMBRADA,
|
||||
});
|
||||
|
||||
return this.cartaPorteRepository.save(cartaPorte);
|
||||
}
|
||||
|
||||
async cancelar(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
dto: CancelarCartaPorteDto
|
||||
): Promise<CartaPorte | null> {
|
||||
const cartaPorte = await this.findOne(id, tenantId);
|
||||
if (!cartaPorte) return null;
|
||||
|
||||
if (cartaPorte.estado !== EstadoCartaPorte.TIMBRADA) {
|
||||
throw new Error('Solo se pueden cancelar Cartas Porte timbradas');
|
||||
}
|
||||
|
||||
Object.assign(cartaPorte, {
|
||||
estado: EstadoCartaPorte.CANCELADA,
|
||||
fechaCancelacion: new Date(),
|
||||
motivoCancelacion: dto.motivoCancelacion,
|
||||
uuidSustitucion: dto.uuidSustitucion,
|
||||
});
|
||||
|
||||
return this.cartaPorteRepository.save(cartaPorte);
|
||||
}
|
||||
|
||||
// Ubicaciones
|
||||
async addUbicacion(
|
||||
cartaPorteId: string,
|
||||
tenantId: string,
|
||||
ubicacionData: Partial<UbicacionCartaPorte>
|
||||
): Promise<UbicacionCartaPorte> {
|
||||
const cartaPorte = await this.findOne(cartaPorteId, tenantId);
|
||||
if (!cartaPorte) {
|
||||
throw new Error('Carta Porte no encontrada');
|
||||
}
|
||||
|
||||
if (cartaPorte.estado !== EstadoCartaPorte.BORRADOR) {
|
||||
throw new Error('Solo se pueden agregar ubicaciones en estado BORRADOR');
|
||||
}
|
||||
|
||||
const ubicacion = this.ubicacionRepository.create({
|
||||
...ubicacionData,
|
||||
cartaPorteId,
|
||||
});
|
||||
|
||||
return this.ubicacionRepository.save(ubicacion);
|
||||
}
|
||||
|
||||
// Mercancias
|
||||
async addMercancia(
|
||||
cartaPorteId: string,
|
||||
tenantId: string,
|
||||
mercanciaData: Partial<MercanciaCartaPorte>
|
||||
): Promise<MercanciaCartaPorte> {
|
||||
const cartaPorte = await this.findOne(cartaPorteId, tenantId);
|
||||
if (!cartaPorte) {
|
||||
throw new Error('Carta Porte no encontrada');
|
||||
}
|
||||
|
||||
if (cartaPorte.estado !== EstadoCartaPorte.BORRADOR) {
|
||||
throw new Error('Solo se pueden agregar mercancias en estado BORRADOR');
|
||||
}
|
||||
|
||||
const mercancia = this.mercanciaRepository.create({
|
||||
...mercanciaData,
|
||||
cartaPorteId,
|
||||
});
|
||||
|
||||
return this.mercanciaRepository.save(mercancia);
|
||||
}
|
||||
|
||||
// Figuras de transporte
|
||||
async addFigura(
|
||||
cartaPorteId: string,
|
||||
tenantId: string,
|
||||
figuraData: Partial<FiguraTransporte>
|
||||
): Promise<FiguraTransporte> {
|
||||
const cartaPorte = await this.findOne(cartaPorteId, tenantId);
|
||||
if (!cartaPorte) {
|
||||
throw new Error('Carta Porte no encontrada');
|
||||
}
|
||||
|
||||
if (cartaPorte.estado !== EstadoCartaPorte.BORRADOR) {
|
||||
throw new Error('Solo se pueden agregar figuras en estado BORRADOR');
|
||||
}
|
||||
|
||||
const figura = this.figuraRepository.create({
|
||||
...figuraData,
|
||||
cartaPorteId,
|
||||
});
|
||||
|
||||
return this.figuraRepository.save(figura);
|
||||
}
|
||||
|
||||
// Autotransporte
|
||||
async addAutotransporte(
|
||||
cartaPorteId: string,
|
||||
tenantId: string,
|
||||
autoData: Partial<AutotransporteCartaPorte>
|
||||
): Promise<AutotransporteCartaPorte> {
|
||||
const cartaPorte = await this.findOne(cartaPorteId, tenantId);
|
||||
if (!cartaPorte) {
|
||||
throw new Error('Carta Porte no encontrada');
|
||||
}
|
||||
|
||||
if (cartaPorte.estado !== EstadoCartaPorte.BORRADOR) {
|
||||
throw new Error('Solo se puede agregar autotransporte en estado BORRADOR');
|
||||
}
|
||||
|
||||
const auto = this.autotransporteRepository.create({
|
||||
...autoData,
|
||||
cartaPorteId,
|
||||
});
|
||||
|
||||
return this.autotransporteRepository.save(auto);
|
||||
}
|
||||
|
||||
// Cartas porte pendientes de timbrar
|
||||
async getPendientesTimbrar(tenantId: string): Promise<CartaPorte[]> {
|
||||
return this.cartaPorteRepository.find({
|
||||
where: {
|
||||
tenantId,
|
||||
estado: In([EstadoCartaPorte.BORRADOR, EstadoCartaPorte.VALIDADA]),
|
||||
},
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
// Cartas porte por viaje (para reportes)
|
||||
async getCartasPorteViaje(viajeId: string, tenantId: string): Promise<CartaPorte[]> {
|
||||
return this.cartaPorteRepository.find({
|
||||
where: { viajeId, tenantId },
|
||||
relations: ['ubicaciones', 'mercancias', 'figuras', 'autotransporte'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string, tenantId: string): Promise<boolean> {
|
||||
const cartaPorte = await this.findOne(id, tenantId);
|
||||
if (!cartaPorte) return false;
|
||||
|
||||
// Only allow deletion of BORRADOR
|
||||
if (cartaPorte.estado !== EstadoCartaPorte.BORRADOR) {
|
||||
throw new Error('Solo se pueden eliminar Cartas Porte en estado BORRADOR');
|
||||
}
|
||||
|
||||
const result = await this.cartaPorteRepository.delete(id);
|
||||
return (result.affected ?? 0) > 0;
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
/**
|
||||
* Carta Porte Services
|
||||
* CFDI con Complemento Carta Porte 3.1
|
||||
*/
|
||||
// TODO: Implement services
|
||||
// - carta-porte.service.ts
|
||||
// - carta-porte-validator.service.ts
|
||||
// - pac-integration.service.ts
|
||||
// - cfdi-generator.service.ts
|
||||
export * from './carta-porte.service';
|
||||
|
||||
// TODO: Implement additional services
|
||||
// - carta-porte-validator.service.ts (validacion SAT detallada)
|
||||
// - pac-integration.service.ts (integracion con PAC)
|
||||
// - cfdi-generator.service.ts (generacion XML CFDI)
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
/**
|
||||
* Tarifas Services
|
||||
* Tarifas de Transporte Services
|
||||
* Cotizacion, lanes, tarifas
|
||||
*/
|
||||
// TODO: Implement services
|
||||
// - tarifas.service.ts
|
||||
// - recargos.service.ts
|
||||
// - cotizador.service.ts
|
||||
export * from './tarifas.service';
|
||||
export * from './lanes.service';
|
||||
|
||||
// TODO: Implement additional services
|
||||
// - recargos.service.ts (RecargoCatalogo)
|
||||
// - fuel-surcharge.service.ts (FuelSurcharge)
|
||||
// - cotizador.service.ts (motor de cotizacion avanzado)
|
||||
|
||||
319
src/modules/tarifas-transporte/services/lanes.service.ts
Normal file
319
src/modules/tarifas-transporte/services/lanes.service.ts
Normal file
@ -0,0 +1,319 @@
|
||||
import { Repository, FindOptionsWhere, ILike } from 'typeorm';
|
||||
import { Lane } from '../entities';
|
||||
|
||||
export interface LaneSearchParams {
|
||||
tenantId: string;
|
||||
search?: string;
|
||||
origenCiudad?: string;
|
||||
origenEstado?: string;
|
||||
destinoCiudad?: string;
|
||||
destinoEstado?: string;
|
||||
activo?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface CreateLaneDto {
|
||||
codigo: string;
|
||||
nombre: string;
|
||||
origenCiudad: string;
|
||||
origenEstado: string;
|
||||
origenCodigoPostal?: string;
|
||||
destinoCiudad: string;
|
||||
destinoEstado: string;
|
||||
destinoCodigoPostal?: string;
|
||||
distanciaKm?: number;
|
||||
tiempoEstimadoHoras?: number;
|
||||
}
|
||||
|
||||
export interface UpdateLaneDto extends Partial<CreateLaneDto> {
|
||||
activo?: boolean;
|
||||
}
|
||||
|
||||
export class LanesService {
|
||||
constructor(private readonly laneRepository: Repository<Lane>) {}
|
||||
|
||||
async findAll(params: LaneSearchParams): Promise<{ data: Lane[]; total: number }> {
|
||||
const {
|
||||
tenantId,
|
||||
search,
|
||||
origenCiudad,
|
||||
origenEstado,
|
||||
destinoCiudad,
|
||||
destinoEstado,
|
||||
activo,
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
} = params;
|
||||
|
||||
const where: FindOptionsWhere<Lane>[] = [];
|
||||
const baseWhere: FindOptionsWhere<Lane> = { tenantId };
|
||||
|
||||
if (activo !== undefined) {
|
||||
baseWhere.activo = activo;
|
||||
}
|
||||
|
||||
if (origenCiudad) {
|
||||
baseWhere.origenCiudad = ILike(`%${origenCiudad}%`);
|
||||
}
|
||||
|
||||
if (origenEstado) {
|
||||
baseWhere.origenEstado = ILike(`%${origenEstado}%`);
|
||||
}
|
||||
|
||||
if (destinoCiudad) {
|
||||
baseWhere.destinoCiudad = ILike(`%${destinoCiudad}%`);
|
||||
}
|
||||
|
||||
if (destinoEstado) {
|
||||
baseWhere.destinoEstado = ILike(`%${destinoEstado}%`);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.push(
|
||||
{ ...baseWhere, codigo: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, nombre: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, origenCiudad: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, destinoCiudad: ILike(`%${search}%`) }
|
||||
);
|
||||
} else {
|
||||
where.push(baseWhere);
|
||||
}
|
||||
|
||||
const [data, total] = await this.laneRepository.findAndCount({
|
||||
where,
|
||||
take: limit,
|
||||
skip: offset,
|
||||
order: { activo: 'DESC', codigo: 'ASC' },
|
||||
});
|
||||
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
async findOne(id: string, tenantId: string): Promise<Lane | null> {
|
||||
return this.laneRepository.findOne({
|
||||
where: { id, tenantId },
|
||||
});
|
||||
}
|
||||
|
||||
async findByCodigo(codigo: string, tenantId: string): Promise<Lane | null> {
|
||||
return this.laneRepository.findOne({
|
||||
where: { codigo, tenantId },
|
||||
});
|
||||
}
|
||||
|
||||
async findByOrigenDestino(
|
||||
origenCiudad: string,
|
||||
destinoCiudad: string,
|
||||
tenantId: string
|
||||
): Promise<Lane | null> {
|
||||
return this.laneRepository.findOne({
|
||||
where: {
|
||||
origenCiudad: ILike(origenCiudad),
|
||||
destinoCiudad: ILike(destinoCiudad),
|
||||
tenantId,
|
||||
activo: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateLaneDto, createdBy: string): Promise<Lane> {
|
||||
// Check if codigo already exists
|
||||
const existingCodigo = await this.findByCodigo(dto.codigo, tenantId);
|
||||
if (existingCodigo) {
|
||||
throw new Error(`Ya existe una lane con el codigo "${dto.codigo}"`);
|
||||
}
|
||||
|
||||
const lane = this.laneRepository.create({
|
||||
...dto,
|
||||
tenantId,
|
||||
activo: true,
|
||||
createdById: createdBy,
|
||||
});
|
||||
|
||||
return this.laneRepository.save(lane);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
dto: UpdateLaneDto,
|
||||
updatedBy: string
|
||||
): Promise<Lane | null> {
|
||||
const lane = await this.findOne(id, tenantId);
|
||||
if (!lane) return null;
|
||||
|
||||
// Check for codigo uniqueness if changing codigo
|
||||
if (dto.codigo && dto.codigo !== lane.codigo) {
|
||||
const existingCodigo = await this.findByCodigo(dto.codigo, tenantId);
|
||||
if (existingCodigo && existingCodigo.id !== id) {
|
||||
throw new Error(`Ya existe una lane con el codigo "${dto.codigo}"`);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(lane, dto);
|
||||
return this.laneRepository.save(lane);
|
||||
}
|
||||
|
||||
async activar(id: string, tenantId: string): Promise<Lane | null> {
|
||||
const lane = await this.findOne(id, tenantId);
|
||||
if (!lane) return null;
|
||||
|
||||
lane.activo = true;
|
||||
return this.laneRepository.save(lane);
|
||||
}
|
||||
|
||||
async desactivar(id: string, tenantId: string): Promise<Lane | null> {
|
||||
const lane = await this.findOne(id, tenantId);
|
||||
if (!lane) return null;
|
||||
|
||||
lane.activo = false;
|
||||
return this.laneRepository.save(lane);
|
||||
}
|
||||
|
||||
// Get lanes by origin state
|
||||
async getLanesByOrigenEstado(origenEstado: string, tenantId: string): Promise<Lane[]> {
|
||||
return this.laneRepository.find({
|
||||
where: { origenEstado: ILike(`%${origenEstado}%`), tenantId, activo: true },
|
||||
order: { origenCiudad: 'ASC', destinoCiudad: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
// Get lanes by destination state
|
||||
async getLanesByDestinoEstado(destinoEstado: string, tenantId: string): Promise<Lane[]> {
|
||||
return this.laneRepository.find({
|
||||
where: { destinoEstado: ILike(`%${destinoEstado}%`), tenantId, activo: true },
|
||||
order: { origenCiudad: 'ASC', destinoCiudad: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
// Get all unique origin cities
|
||||
async getOrigenCiudades(tenantId: string): Promise<string[]> {
|
||||
const lanes = await this.laneRepository
|
||||
.createQueryBuilder('l')
|
||||
.select('DISTINCT l.origen_ciudad', 'ciudad')
|
||||
.where('l.tenant_id = :tenantId', { tenantId })
|
||||
.andWhere('l.activo = true')
|
||||
.orderBy('l.origen_ciudad', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
return lanes.map(l => l.ciudad);
|
||||
}
|
||||
|
||||
// Get all unique destination cities
|
||||
async getDestinoCiudades(tenantId: string): Promise<string[]> {
|
||||
const lanes = await this.laneRepository
|
||||
.createQueryBuilder('l')
|
||||
.select('DISTINCT l.destino_ciudad', 'ciudad')
|
||||
.where('l.tenant_id = :tenantId', { tenantId })
|
||||
.andWhere('l.activo = true')
|
||||
.orderBy('l.destino_ciudad', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
return lanes.map(l => l.ciudad);
|
||||
}
|
||||
|
||||
// Get all unique origin states
|
||||
async getOrigenEstados(tenantId: string): Promise<string[]> {
|
||||
const lanes = await this.laneRepository
|
||||
.createQueryBuilder('l')
|
||||
.select('DISTINCT l.origen_estado', 'estado')
|
||||
.where('l.tenant_id = :tenantId', { tenantId })
|
||||
.andWhere('l.activo = true')
|
||||
.orderBy('l.origen_estado', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
return lanes.map(l => l.estado);
|
||||
}
|
||||
|
||||
// Get all unique destination states
|
||||
async getDestinoEstados(tenantId: string): Promise<string[]> {
|
||||
const lanes = await this.laneRepository
|
||||
.createQueryBuilder('l')
|
||||
.select('DISTINCT l.destino_estado', 'estado')
|
||||
.where('l.tenant_id = :tenantId', { tenantId })
|
||||
.andWhere('l.activo = true')
|
||||
.orderBy('l.destino_estado', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
return lanes.map(l => l.estado);
|
||||
}
|
||||
|
||||
// Search for lane or create if not exists
|
||||
async findOrCreate(
|
||||
tenantId: string,
|
||||
dto: CreateLaneDto,
|
||||
createdBy: string
|
||||
): Promise<{ lane: Lane; created: boolean }> {
|
||||
// Try to find by origen-destino first
|
||||
let lane = await this.findByOrigenDestino(dto.origenCiudad, dto.destinoCiudad, tenantId);
|
||||
|
||||
if (lane) {
|
||||
return { lane, created: false };
|
||||
}
|
||||
|
||||
// Try to find by codigo
|
||||
lane = await this.findByCodigo(dto.codigo, tenantId);
|
||||
if (lane) {
|
||||
return { lane, created: false };
|
||||
}
|
||||
|
||||
// Create new lane
|
||||
const newLane = await this.create(tenantId, dto, createdBy);
|
||||
return { lane: newLane, created: true };
|
||||
}
|
||||
|
||||
// Calculate reverse lane (destino -> origen)
|
||||
async getLaneInversa(id: string, tenantId: string): Promise<Lane | null> {
|
||||
const lane = await this.findOne(id, tenantId);
|
||||
if (!lane) return null;
|
||||
|
||||
return this.findByOrigenDestino(lane.destinoCiudad, lane.origenCiudad, tenantId);
|
||||
}
|
||||
|
||||
// Create reverse lane automatically
|
||||
async crearLaneInversa(id: string, tenantId: string, createdBy: string): Promise<Lane | null> {
|
||||
const lane = await this.findOne(id, tenantId);
|
||||
if (!lane) return null;
|
||||
|
||||
// Check if reverse already exists
|
||||
const existing = await this.findByOrigenDestino(
|
||||
lane.destinoCiudad,
|
||||
lane.origenCiudad,
|
||||
tenantId
|
||||
);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Generate reverse codigo
|
||||
const reverseCodigo = `${lane.codigo}-REV`;
|
||||
const reverseNombre = `${lane.destinoCiudad} - ${lane.origenCiudad}`;
|
||||
|
||||
return this.create(
|
||||
tenantId,
|
||||
{
|
||||
codigo: reverseCodigo,
|
||||
nombre: reverseNombre,
|
||||
origenCiudad: lane.destinoCiudad,
|
||||
origenEstado: lane.destinoEstado,
|
||||
origenCodigoPostal: lane.destinoCodigoPostal ?? undefined,
|
||||
destinoCiudad: lane.origenCiudad,
|
||||
destinoEstado: lane.origenEstado,
|
||||
destinoCodigoPostal: lane.origenCodigoPostal ?? undefined,
|
||||
distanciaKm: lane.distanciaKm ?? undefined,
|
||||
tiempoEstimadoHoras: lane.tiempoEstimadoHoras ?? undefined,
|
||||
},
|
||||
createdBy
|
||||
);
|
||||
}
|
||||
|
||||
async delete(id: string, tenantId: string): Promise<boolean> {
|
||||
const lane = await this.findOne(id, tenantId);
|
||||
if (!lane) return false;
|
||||
|
||||
// Note: Should check if lane is used in any tarifas before deleting
|
||||
const result = await this.laneRepository.delete(id);
|
||||
return (result.affected ?? 0) > 0;
|
||||
}
|
||||
}
|
||||
442
src/modules/tarifas-transporte/services/tarifas.service.ts
Normal file
442
src/modules/tarifas-transporte/services/tarifas.service.ts
Normal file
@ -0,0 +1,442 @@
|
||||
import { Repository, FindOptionsWhere, ILike, In, LessThanOrEqual, MoreThanOrEqual, IsNull, Or } from 'typeorm';
|
||||
import { Tarifa, TipoTarifa, Lane } from '../entities';
|
||||
|
||||
export interface TarifaSearchParams {
|
||||
tenantId: string;
|
||||
search?: string;
|
||||
clienteId?: string;
|
||||
laneId?: string;
|
||||
tipoTarifa?: TipoTarifa;
|
||||
activa?: boolean;
|
||||
vigente?: boolean;
|
||||
moneda?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface CreateTarifaDto {
|
||||
codigo: string;
|
||||
nombre: string;
|
||||
descripcion?: string;
|
||||
clienteId?: string;
|
||||
laneId?: string;
|
||||
modalidadServicio?: string;
|
||||
tipoEquipo?: string;
|
||||
tipoTarifa: TipoTarifa;
|
||||
tarifaBase: number;
|
||||
tarifaKm?: number;
|
||||
tarifaTonelada?: number;
|
||||
tarifaM3?: number;
|
||||
tarifaPallet?: number;
|
||||
tarifaHora?: number;
|
||||
minimoFacturar?: number;
|
||||
pesoMinimoKg?: number;
|
||||
moneda?: string;
|
||||
fechaInicio: Date;
|
||||
fechaFin?: Date;
|
||||
}
|
||||
|
||||
export interface UpdateTarifaDto extends Partial<CreateTarifaDto> {
|
||||
activa?: boolean;
|
||||
}
|
||||
|
||||
export interface CotizarParams {
|
||||
km?: number;
|
||||
toneladas?: number;
|
||||
m3?: number;
|
||||
pallets?: number;
|
||||
horas?: number;
|
||||
}
|
||||
|
||||
export interface CotizacionResult {
|
||||
tarifa: Tarifa;
|
||||
montoCalculado: number;
|
||||
desglose: {
|
||||
tarifaBase: number;
|
||||
montoVariable: number;
|
||||
minimoAplicado: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export class TarifasService {
|
||||
constructor(
|
||||
private readonly tarifaRepository: Repository<Tarifa>,
|
||||
private readonly laneRepository: Repository<Lane>,
|
||||
) {}
|
||||
|
||||
async findAll(params: TarifaSearchParams): Promise<{ data: Tarifa[]; total: number }> {
|
||||
const {
|
||||
tenantId,
|
||||
search,
|
||||
clienteId,
|
||||
laneId,
|
||||
tipoTarifa,
|
||||
activa,
|
||||
vigente,
|
||||
moneda,
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
} = params;
|
||||
|
||||
const qb = this.tarifaRepository.createQueryBuilder('t');
|
||||
|
||||
qb.where('t.tenant_id = :tenantId', { tenantId });
|
||||
|
||||
if (clienteId) {
|
||||
qb.andWhere('t.cliente_id = :clienteId', { clienteId });
|
||||
}
|
||||
|
||||
if (laneId) {
|
||||
qb.andWhere('t.lane_id = :laneId', { laneId });
|
||||
}
|
||||
|
||||
if (tipoTarifa) {
|
||||
qb.andWhere('t.tipo_tarifa = :tipoTarifa', { tipoTarifa });
|
||||
}
|
||||
|
||||
if (activa !== undefined) {
|
||||
qb.andWhere('t.activa = :activa', { activa });
|
||||
}
|
||||
|
||||
if (vigente) {
|
||||
const hoy = new Date();
|
||||
qb.andWhere('t.activa = true');
|
||||
qb.andWhere('t.fecha_inicio <= :hoy', { hoy });
|
||||
qb.andWhere('(t.fecha_fin IS NULL OR t.fecha_fin >= :hoy)', { hoy });
|
||||
}
|
||||
|
||||
if (moneda) {
|
||||
qb.andWhere('t.moneda = :moneda', { moneda });
|
||||
}
|
||||
|
||||
if (search) {
|
||||
qb.andWhere(
|
||||
'(t.codigo ILIKE :search OR t.nombre ILIKE :search OR t.descripcion ILIKE :search)',
|
||||
{ search: `%${search}%` }
|
||||
);
|
||||
}
|
||||
|
||||
qb.leftJoinAndSelect('t.lane', 'lane');
|
||||
qb.orderBy('t.activa', 'DESC');
|
||||
qb.addOrderBy('t.fecha_inicio', 'DESC');
|
||||
qb.take(limit);
|
||||
qb.skip(offset);
|
||||
|
||||
const [data, total] = await qb.getManyAndCount();
|
||||
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
async findOne(id: string, tenantId: string): Promise<Tarifa | null> {
|
||||
return this.tarifaRepository.findOne({
|
||||
where: { id, tenantId },
|
||||
relations: ['lane'],
|
||||
});
|
||||
}
|
||||
|
||||
async findByCodigo(codigo: string, tenantId: string): Promise<Tarifa | null> {
|
||||
return this.tarifaRepository.findOne({
|
||||
where: { codigo, tenantId },
|
||||
relations: ['lane'],
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateTarifaDto, createdBy: string): Promise<Tarifa> {
|
||||
// Check if codigo already exists
|
||||
const existingCodigo = await this.findByCodigo(dto.codigo, tenantId);
|
||||
if (existingCodigo) {
|
||||
throw new Error(`Ya existe una tarifa con el codigo "${dto.codigo}"`);
|
||||
}
|
||||
|
||||
// Validate lane if provided
|
||||
if (dto.laneId) {
|
||||
const lane = await this.laneRepository.findOne({
|
||||
where: { id: dto.laneId, tenantId },
|
||||
});
|
||||
if (!lane) {
|
||||
throw new Error('Lane no encontrada');
|
||||
}
|
||||
}
|
||||
|
||||
const tarifa = this.tarifaRepository.create({
|
||||
...dto,
|
||||
tenantId,
|
||||
activa: true,
|
||||
createdById: createdBy,
|
||||
});
|
||||
|
||||
return this.tarifaRepository.save(tarifa);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
dto: UpdateTarifaDto,
|
||||
updatedBy: string
|
||||
): Promise<Tarifa | null> {
|
||||
const tarifa = await this.findOne(id, tenantId);
|
||||
if (!tarifa) return null;
|
||||
|
||||
// Check for codigo uniqueness if changing codigo
|
||||
if (dto.codigo && dto.codigo !== tarifa.codigo) {
|
||||
const existingCodigo = await this.findByCodigo(dto.codigo, tenantId);
|
||||
if (existingCodigo && existingCodigo.id !== id) {
|
||||
throw new Error(`Ya existe una tarifa con el codigo "${dto.codigo}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate lane if provided
|
||||
if (dto.laneId && dto.laneId !== tarifa.laneId) {
|
||||
const lane = await this.laneRepository.findOne({
|
||||
where: { id: dto.laneId, tenantId },
|
||||
});
|
||||
if (!lane) {
|
||||
throw new Error('Lane no encontrada');
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(tarifa, dto);
|
||||
return this.tarifaRepository.save(tarifa);
|
||||
}
|
||||
|
||||
async activar(id: string, tenantId: string): Promise<Tarifa | null> {
|
||||
const tarifa = await this.findOne(id, tenantId);
|
||||
if (!tarifa) return null;
|
||||
|
||||
tarifa.activa = true;
|
||||
return this.tarifaRepository.save(tarifa);
|
||||
}
|
||||
|
||||
async desactivar(id: string, tenantId: string): Promise<Tarifa | null> {
|
||||
const tarifa = await this.findOne(id, tenantId);
|
||||
if (!tarifa) return null;
|
||||
|
||||
tarifa.activa = false;
|
||||
return this.tarifaRepository.save(tarifa);
|
||||
}
|
||||
|
||||
// Get best tariff for a client/lane combination
|
||||
async getTarifaAplicable(
|
||||
tenantId: string,
|
||||
params: {
|
||||
clienteId?: string;
|
||||
laneId?: string;
|
||||
tipoEquipo?: string;
|
||||
modalidadServicio?: string;
|
||||
}
|
||||
): Promise<Tarifa | null> {
|
||||
const hoy = new Date();
|
||||
const qb = this.tarifaRepository.createQueryBuilder('t');
|
||||
|
||||
qb.where('t.tenant_id = :tenantId', { tenantId });
|
||||
qb.andWhere('t.activa = true');
|
||||
qb.andWhere('t.fecha_inicio <= :hoy', { hoy });
|
||||
qb.andWhere('(t.fecha_fin IS NULL OR t.fecha_fin >= :hoy)', { hoy });
|
||||
|
||||
// Priority: specific client tariff > lane tariff > general tariff
|
||||
if (params.clienteId && params.laneId) {
|
||||
// First try client + lane specific
|
||||
const specific = await qb
|
||||
.clone()
|
||||
.andWhere('t.cliente_id = :clienteId', { clienteId: params.clienteId })
|
||||
.andWhere('t.lane_id = :laneId', { laneId: params.laneId })
|
||||
.getOne();
|
||||
if (specific) return specific;
|
||||
}
|
||||
|
||||
if (params.clienteId) {
|
||||
// Try client specific (any lane)
|
||||
const clientTarifa = await qb
|
||||
.clone()
|
||||
.andWhere('t.cliente_id = :clienteId', { clienteId: params.clienteId })
|
||||
.andWhere('t.lane_id IS NULL')
|
||||
.getOne();
|
||||
if (clientTarifa) return clientTarifa;
|
||||
}
|
||||
|
||||
if (params.laneId) {
|
||||
// Try lane specific (any client)
|
||||
const laneTarifa = await qb
|
||||
.clone()
|
||||
.andWhere('t.cliente_id IS NULL')
|
||||
.andWhere('t.lane_id = :laneId', { laneId: params.laneId })
|
||||
.getOne();
|
||||
if (laneTarifa) return laneTarifa;
|
||||
}
|
||||
|
||||
// Try general tariff with matching equipment/modalidad
|
||||
if (params.tipoEquipo || params.modalidadServicio) {
|
||||
const generalQb = qb.clone()
|
||||
.andWhere('t.cliente_id IS NULL')
|
||||
.andWhere('t.lane_id IS NULL');
|
||||
|
||||
if (params.tipoEquipo) {
|
||||
generalQb.andWhere('t.tipo_equipo = :tipoEquipo', { tipoEquipo: params.tipoEquipo });
|
||||
}
|
||||
if (params.modalidadServicio) {
|
||||
generalQb.andWhere('t.modalidad_servicio = :modalidadServicio', {
|
||||
modalidadServicio: params.modalidadServicio,
|
||||
});
|
||||
}
|
||||
|
||||
const generalTarifa = await generalQb.getOne();
|
||||
if (generalTarifa) return generalTarifa;
|
||||
}
|
||||
|
||||
// Fallback to any general tariff
|
||||
return qb
|
||||
.clone()
|
||||
.andWhere('t.cliente_id IS NULL')
|
||||
.andWhere('t.lane_id IS NULL')
|
||||
.orderBy('t.created_at', 'DESC')
|
||||
.getOne();
|
||||
}
|
||||
|
||||
// Calculate quote using a tariff
|
||||
async cotizar(tarifaId: string, tenantId: string, params: CotizarParams): Promise<CotizacionResult> {
|
||||
const tarifa = await this.findOne(tarifaId, tenantId);
|
||||
if (!tarifa) {
|
||||
throw new Error('Tarifa no encontrada');
|
||||
}
|
||||
|
||||
if (!tarifa.vigente) {
|
||||
throw new Error('La tarifa no esta vigente');
|
||||
}
|
||||
|
||||
let montoVariable = 0;
|
||||
|
||||
switch (tarifa.tipoTarifa) {
|
||||
case TipoTarifa.POR_KM:
|
||||
montoVariable = (params.km || 0) * (tarifa.tarifaKm || 0);
|
||||
break;
|
||||
case TipoTarifa.POR_TONELADA:
|
||||
montoVariable = (params.toneladas || 0) * (tarifa.tarifaTonelada || 0);
|
||||
break;
|
||||
case TipoTarifa.POR_M3:
|
||||
montoVariable = (params.m3 || 0) * (tarifa.tarifaM3 || 0);
|
||||
break;
|
||||
case TipoTarifa.POR_PALLET:
|
||||
montoVariable = (params.pallets || 0) * (tarifa.tarifaPallet || 0);
|
||||
break;
|
||||
case TipoTarifa.POR_HORA:
|
||||
montoVariable = (params.horas || 0) * (tarifa.tarifaHora || 0);
|
||||
break;
|
||||
case TipoTarifa.MIXTA:
|
||||
montoVariable += (params.km || 0) * (tarifa.tarifaKm || 0);
|
||||
montoVariable += (params.toneladas || 0) * (tarifa.tarifaTonelada || 0);
|
||||
break;
|
||||
case TipoTarifa.POR_VIAJE:
|
||||
default:
|
||||
montoVariable = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
let montoCalculado = Number(tarifa.tarifaBase) + montoVariable;
|
||||
let minimoAplicado = false;
|
||||
|
||||
if (tarifa.minimoFacturar && montoCalculado < Number(tarifa.minimoFacturar)) {
|
||||
montoCalculado = Number(tarifa.minimoFacturar);
|
||||
minimoAplicado = true;
|
||||
}
|
||||
|
||||
return {
|
||||
tarifa,
|
||||
montoCalculado,
|
||||
desglose: {
|
||||
tarifaBase: Number(tarifa.tarifaBase),
|
||||
montoVariable,
|
||||
minimoAplicado,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Get tarifas by cliente
|
||||
async getTarifasCliente(clienteId: string, tenantId: string): Promise<Tarifa[]> {
|
||||
return this.tarifaRepository.find({
|
||||
where: { clienteId, tenantId, activa: true },
|
||||
relations: ['lane'],
|
||||
order: { fechaInicio: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
// Get tarifas by lane
|
||||
async getTarifasLane(laneId: string, tenantId: string): Promise<Tarifa[]> {
|
||||
return this.tarifaRepository.find({
|
||||
where: { laneId, tenantId, activa: true },
|
||||
relations: ['lane'],
|
||||
order: { fechaInicio: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
// Get tarifas expiring soon
|
||||
async getTarifasPorVencer(tenantId: string, diasAntelacion: number = 30): Promise<Tarifa[]> {
|
||||
const hoy = new Date();
|
||||
const fechaLimite = new Date();
|
||||
fechaLimite.setDate(fechaLimite.getDate() + diasAntelacion);
|
||||
|
||||
return this.tarifaRepository
|
||||
.createQueryBuilder('t')
|
||||
.where('t.tenant_id = :tenantId', { tenantId })
|
||||
.andWhere('t.activa = true')
|
||||
.andWhere('t.fecha_fin IS NOT NULL')
|
||||
.andWhere('t.fecha_fin > :hoy', { hoy })
|
||||
.andWhere('t.fecha_fin <= :fechaLimite', { fechaLimite })
|
||||
.leftJoinAndSelect('t.lane', 'lane')
|
||||
.orderBy('t.fecha_fin', 'ASC')
|
||||
.getMany();
|
||||
}
|
||||
|
||||
// Clone a tariff with new validity period
|
||||
async clonarTarifa(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
nuevoCodigo: string,
|
||||
nuevaFechaInicio: Date,
|
||||
nuevaFechaFin?: Date,
|
||||
createdBy?: string
|
||||
): Promise<Tarifa | null> {
|
||||
const sourceTarifa = await this.findOne(id, tenantId);
|
||||
if (!sourceTarifa) return null;
|
||||
|
||||
// Check if new codigo exists
|
||||
const existingCodigo = await this.findByCodigo(nuevoCodigo, tenantId);
|
||||
if (existingCodigo) {
|
||||
throw new Error(`Ya existe una tarifa con el codigo "${nuevoCodigo}"`);
|
||||
}
|
||||
|
||||
const newTarifa = this.tarifaRepository.create({
|
||||
tenantId,
|
||||
codigo: nuevoCodigo,
|
||||
nombre: sourceTarifa.nombre,
|
||||
descripcion: sourceTarifa.descripcion,
|
||||
clienteId: sourceTarifa.clienteId,
|
||||
laneId: sourceTarifa.laneId,
|
||||
modalidadServicio: sourceTarifa.modalidadServicio,
|
||||
tipoEquipo: sourceTarifa.tipoEquipo,
|
||||
tipoTarifa: sourceTarifa.tipoTarifa,
|
||||
tarifaBase: sourceTarifa.tarifaBase,
|
||||
tarifaKm: sourceTarifa.tarifaKm,
|
||||
tarifaTonelada: sourceTarifa.tarifaTonelada,
|
||||
tarifaM3: sourceTarifa.tarifaM3,
|
||||
tarifaPallet: sourceTarifa.tarifaPallet,
|
||||
tarifaHora: sourceTarifa.tarifaHora,
|
||||
minimoFacturar: sourceTarifa.minimoFacturar,
|
||||
pesoMinimoKg: sourceTarifa.pesoMinimoKg,
|
||||
moneda: sourceTarifa.moneda,
|
||||
fechaInicio: nuevaFechaInicio,
|
||||
fechaFin: nuevaFechaFin ?? null,
|
||||
activa: true,
|
||||
createdById: createdBy ?? sourceTarifa.createdById,
|
||||
});
|
||||
|
||||
return this.tarifaRepository.save(newTarifa);
|
||||
}
|
||||
|
||||
async delete(id: string, tenantId: string): Promise<boolean> {
|
||||
const tarifa = await this.findOne(id, tenantId);
|
||||
if (!tarifa) return false;
|
||||
|
||||
const result = await this.tarifaRepository.delete(id);
|
||||
return (result.affected ?? 0) > 0;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user