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,10 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { NotificationsService } from "./notifications.service";
|
||||
import { PrismaModule } from "../prisma/prisma.module";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [NotificationsService],
|
||||
exports: [NotificationsService],
|
||||
})
|
||||
export class NotificationsModule {}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class NotificationsService {
|
||||
private readonly logger = new Logger(NotificationsService.name);
|
||||
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
/** Called whenever a package status changes. Creates Notification records and stubs dispatch. */
|
||||
async notifyStatusChange(pkg: any, user: any): Promise<void> {
|
||||
const statusLabels: Record<string, string> = {
|
||||
REGISTRADO: "fue registrado en el sistema",
|
||||
EN_TRANSITO_BODEGA: "está en tránsito hacia la bodega NJ",
|
||||
RECIBIDO_BODEGA: "fue recibido en la bodega de NJ",
|
||||
EN_VERIFICACION: "está siendo verificado en bodega",
|
||||
VERIFICADO: "fue verificado. El cobro final fue aplicado.",
|
||||
DECLARACION_ADUANERA: "tiene su declaración aduanera aprobada (SENAE)",
|
||||
EN_TRANSITO_ECUADOR: "está en tránsito hacia Ecuador",
|
||||
EN_ADUANA_ECUADOR: "está en inspección aduanera en Ecuador",
|
||||
LISTO_ENTREGA: "está listo para entrega",
|
||||
ENTREGADO: "fue entregado exitosamente",
|
||||
INCIDENCIA: "tiene una incidencia reportada",
|
||||
};
|
||||
|
||||
const label = statusLabels[pkg.status] ?? `cambió a estado ${pkg.status}`;
|
||||
const body = `Tu paquete ${pkg.trackingId} ${label}.`;
|
||||
const subject = `Estado de tu paquete: ${pkg.trackingId}`;
|
||||
|
||||
const channels: Array<"EMAIL" | "WHATSAPP" | "SMS" | "PUSH"> = ["EMAIL", "WHATSAPP", "PUSH"];
|
||||
for (const channel of channels) {
|
||||
try {
|
||||
await this.prisma.client.notification.create({
|
||||
data: {
|
||||
packageId: pkg.id,
|
||||
userId: pkg.userId,
|
||||
channel,
|
||||
status: "PENDIENTE",
|
||||
subject,
|
||||
body,
|
||||
},
|
||||
});
|
||||
// STUB: In production, dispatch via SendGrid (EMAIL), WhatsApp Business API (WHATSAPP), etc.
|
||||
this.logger.log(`[NOTIF STUB] ${channel} → userId=${pkg.userId} | ${body}`);
|
||||
// Mark as sent (stub — in prod this would be async)
|
||||
await this.prisma.client.notification.updateMany({
|
||||
where: { packageId: pkg.id, userId: pkg.userId, channel, status: "PENDIENTE" },
|
||||
data: { status: "ENVIADO", sentAt: new Date() },
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
this.logger.error(`Notification ${channel} failed: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async findByUser(userId: string, limit = 20): Promise<any[]> {
|
||||
return this.prisma.client.notification.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
async findByPackage(packageId: string): Promise<any[]> {
|
||||
return this.prisma.client.notification.findMany({
|
||||
where: { packageId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user