feat: Fase 2 — SENAE DSI, verificación bodega, fotos, notificaciones, auditoría, tarifas CRUD, páginas faltantes
API:
- PackagesService: verifyPackage (discrepancia >10%), addPhotos (multer disk), generateSenaeDeclaration (DSI stub)
- PATCH /packages/:id/verify — peso real + dimensiones + detección automática de discrepancia
- POST /packages/:id/photos — upload multipart fotos (hasta 10, 10 MB c/u)
- POST /packages/:id/senae/declare — genera DSI, N° autorización SENAE, cambia estado DECLARACION_ADUANERA
- GET /packages/pending-declaration — cola de paquetes VERIFICADO para agente aduanero
- AuditLogModule: GET /audit-logs con filtros (acción, recurso, fechas, paginación)
- NotificationsModule: notifyStatusChange → crea registros EMAIL/WHATSAPP/PUSH en DB (stub dispatch)
- TariffsModule: GET/PUT /tariffs — configuración de tarifas por tenant (pricePerLb, IVA, FODINFA…)
- ProductsModule: POST /products/scan — stub extracción de producto desde URL Amazon/eBay/Walmart
- StorageModule: saveFile/deleteFile con disco local (S3-ready)
- main.ts: NestExpressApplication + useStaticAssets('/uploads')
- Dirección bodega: de hardcoded a env vars (WAREHOUSE_ADDRESS_*)
Web:
- /bodega/verificacion — formulario real: peso real, dims, fotos, detección discrepancia, notificación
- /bodega/despacho — lista paquetes DECLARACION_ADUANERA → despacha a EN_TRANSITO_ECUADOR
- /bodega/declaraciones — cola SENAE: auto-detecta categoría 4×4, genera DSI por paquete
- /bodega/layout — agrega enlace Declaraciones SENAE, corrige ALLOWED roles
- /admin/tarifas — CRUD real: edita pricePerLb, IVA, FODINFA, límites 4×4; preview fórmulas
- /admin/auditoria — tabla paginada con filtros desde GET /audit-logs
- /portal/pre-alerta — tabs URL-scan / Manual, upload de factura, campos correctos
- /casillero/como-usar — timeline 9 pasos con dirección NJ y CTA
- /carga-pesada/como-funciona — flujo B2B 5 pasos, servicios incluidos
- /carga-pesada/inen — 6 categorías reguladas, callout de advertencia, proceso de asistencia
- api.ts: verify, uploadPhotos, senaeDeclare, pendingDeclaration, tariffs, auditLogs, products.scan
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { Controller, Get, Query, UseGuards } from "@nestjs/common";
|
||||
import { AuditLogService } from "./audit-log.service";
|
||||
import { JwtAuthGuard, RolesGuard, Roles } from "../auth/guards/auth.guard";
|
||||
import { CurrentUser } from "../auth/decorators/current-user.decorator";
|
||||
|
||||
@Controller("audit-logs")
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles("SUPER_ADMIN", "ADMIN_EMPRESA")
|
||||
export class AuditLogController {
|
||||
constructor(private svc: AuditLogService) {}
|
||||
|
||||
@Get()
|
||||
findAll(
|
||||
@CurrentUser() user: any,
|
||||
@Query("userId") userId?: string,
|
||||
@Query("action") action?: string,
|
||||
@Query("resource") resource?: string,
|
||||
@Query("from") from?: string,
|
||||
@Query("to") to?: string,
|
||||
@Query("page") page?: string,
|
||||
@Query("limit") limit?: string,
|
||||
) {
|
||||
return this.svc.findAll({
|
||||
tenantId: user.role === "SUPER_ADMIN" ? undefined : user.tenantId,
|
||||
userId,
|
||||
action,
|
||||
resource,
|
||||
from,
|
||||
to,
|
||||
page: page ? parseInt(page) : 1,
|
||||
limit: limit ? parseInt(limit) : 50,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AuditLogService } from "./audit-log.service";
|
||||
import { AuditLogController } from "./audit-log.controller";
|
||||
import { PrismaModule } from "../prisma/prisma.module";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [AuditLogService],
|
||||
controllers: [AuditLogController],
|
||||
exports: [AuditLogService],
|
||||
})
|
||||
export class AuditLogModule {}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
|
||||
export interface AuditLogEntry {
|
||||
tenantId?: string;
|
||||
userId?: string;
|
||||
action: string;
|
||||
resource?: string;
|
||||
resourceId?: string;
|
||||
metadata?: Record<string, any>;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditLogService {
|
||||
private readonly logger = new Logger(AuditLogService.name);
|
||||
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async log(entry: AuditLogEntry): Promise<void> {
|
||||
try {
|
||||
await this.prisma.client.auditLog.create({ data: entry });
|
||||
} catch (e: unknown) {
|
||||
// Never let audit log failure break main flow
|
||||
this.logger.error("AuditLog write failed", (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(filters: {
|
||||
tenantId?: string;
|
||||
userId?: string;
|
||||
action?: string;
|
||||
resource?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<{ data: any[]; total: number }> {
|
||||
const page = filters.page ?? 1;
|
||||
const limit = filters.limit ?? 50;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = {};
|
||||
if (filters.tenantId) where.tenantId = filters.tenantId;
|
||||
if (filters.userId) where.userId = filters.userId;
|
||||
if (filters.action) where.action = { contains: filters.action, mode: "insensitive" };
|
||||
if (filters.resource) where.resource = { contains: filters.resource, mode: "insensitive" };
|
||||
if (filters.from || filters.to) {
|
||||
where.createdAt = {};
|
||||
if (filters.from) where.createdAt.gte = new Date(filters.from);
|
||||
if (filters.to) where.createdAt.lte = new Date(filters.to);
|
||||
}
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
this.prisma.client.auditLog.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: "desc" },
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
this.prisma.client.auditLog.count({ where }),
|
||||
]);
|
||||
|
||||
return { data, total };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user