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:
@@ -1,11 +1,15 @@
|
||||
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
|
||||
import { Injectable, NotFoundException, ForbiddenException, BadRequestException } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { NotificationsService } from "../notifications/notifications.service";
|
||||
import { generateTrackingId } from "../common/utils/tracking-id.util";
|
||||
import { CreatePackageDto, UpdateStatusDto } from "./dto/package.dto";
|
||||
import { CreatePackageDto, UpdateStatusDto, VerifyPackageDto, SenaeDeclarationDto } from "./dto/package.dto";
|
||||
|
||||
@Injectable()
|
||||
export class PackagesService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private notifications: NotificationsService,
|
||||
) {}
|
||||
|
||||
async findAll(user: any, filters?: { status?: string; search?: string }): Promise<any[]> {
|
||||
const where: any = { tenantId: user.tenantId };
|
||||
@@ -58,6 +62,7 @@ export class PackagesService {
|
||||
description: dto.description,
|
||||
store: dto.store,
|
||||
vendorTracking: dto.vendorTracking,
|
||||
productUrl: dto.productUrl,
|
||||
declaredValue: dto.declaredValue ?? 0,
|
||||
declaredWeight: dto.declaredWeightLb ?? null,
|
||||
lengthCm: dto.lengthCm ?? null,
|
||||
@@ -72,7 +77,7 @@ export class PackagesService {
|
||||
packageId: pkg.id,
|
||||
status: "REGISTRADO",
|
||||
createdBy: operatorId,
|
||||
note: "Paquete registrado al recibirse en bodega NJ",
|
||||
note: "Paquete registrado en el sistema",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -97,6 +102,131 @@ export class PackagesService {
|
||||
},
|
||||
});
|
||||
|
||||
// Notify user on status change
|
||||
this.notifications.notifyStatusChange(updated, { id: updated.userId }).catch(() => {});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bodega verification: record actual weight, dims, detect discrepancy >10% (doc §10 step 4).
|
||||
* Sets status to EN_VERIFICACION then VERIFICADO.
|
||||
*/
|
||||
async verifyPackage(id: string, dto: VerifyPackageDto, operatorId: string): Promise<any> {
|
||||
const pkg = await this.prisma.client.package.findUnique({ where: { id } });
|
||||
if (!pkg) throw new NotFoundException("Paquete no encontrado.");
|
||||
|
||||
// Detect discrepancy: >10% difference between declared and actual weight
|
||||
let hasDiscrepancy = false;
|
||||
if (pkg.declaredWeight && dto.actualWeightLb) {
|
||||
const declared = Number(pkg.declaredWeight);
|
||||
const actual = dto.actualWeightLb;
|
||||
const diff = Math.abs(actual - declared) / declared;
|
||||
hasDiscrepancy = diff > 0.10;
|
||||
}
|
||||
|
||||
const updated = await this.prisma.client.package.update({
|
||||
where: { id },
|
||||
data: {
|
||||
actualWeight: dto.actualWeightLb,
|
||||
lengthCm: dto.lengthCm ?? pkg.lengthCm,
|
||||
widthCm: dto.widthCm ?? pkg.widthCm,
|
||||
heightCm: dto.heightCm ?? pkg.heightCm,
|
||||
hasDiscrepancy,
|
||||
status: "VERIFICADO",
|
||||
},
|
||||
});
|
||||
|
||||
await this.prisma.client.packageStatusHistory.create({
|
||||
data: {
|
||||
packageId: id,
|
||||
status: "VERIFICADO",
|
||||
createdBy: operatorId,
|
||||
note: dto.note ?? (hasDiscrepancy
|
||||
? `DISCREPANCIA: peso declarado ${pkg.declaredWeight}lb vs real ${dto.actualWeightLb}lb`
|
||||
: `Verificado: peso real ${dto.actualWeightLb}lb`),
|
||||
},
|
||||
});
|
||||
|
||||
// Notify user on verification
|
||||
this.notifications.notifyStatusChange({ ...updated, hasDiscrepancy }, { id: updated.userId }).catch(() => {});
|
||||
|
||||
return { ...updated, hasDiscrepancy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Add photo URLs to a package (doc §10 step 4, §07 Portal Bodega).
|
||||
*/
|
||||
async addPhotos(id: string, photoUrls: string[], operatorId: string): Promise<any> {
|
||||
const pkg = await this.prisma.client.package.findUnique({ where: { id } });
|
||||
if (!pkg) throw new NotFoundException("Paquete no encontrado.");
|
||||
|
||||
const updated = await this.prisma.client.package.update({
|
||||
where: { id },
|
||||
data: { photos: { push: photoUrls } },
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* SENAE declaration (doc §11): generate DSI stub, update status to DECLARACION_ADUANERA.
|
||||
*/
|
||||
async generateSenaeDeclaration(id: string, dto: SenaeDeclarationDto, agentId: string): Promise<any> {
|
||||
const pkg = await this.prisma.client.package.findUnique({ where: { id } });
|
||||
if (!pkg) throw new NotFoundException("Paquete no encontrado.");
|
||||
if (pkg.status !== "VERIFICADO") {
|
||||
throw new BadRequestException("El paquete debe estar en estado VERIFICADO para generar la declaración.");
|
||||
}
|
||||
|
||||
// Stub: In prod this would call SENAE SOAP/REST WebService
|
||||
// Generate a plausible authorization number
|
||||
const authNumber = `SENAE-DSI-${new Date().getFullYear()}-${Math.floor(100000 + Math.random() * 900000)}`;
|
||||
const declarationId = `DSI-${pkg.trackingId}`;
|
||||
|
||||
const updated = await this.prisma.client.package.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: "DECLARACION_ADUANERA",
|
||||
senaeCategory: dto.category as any,
|
||||
senaeAuthNumber: authNumber,
|
||||
senaeDeclarationId: declarationId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.prisma.client.packageStatusHistory.create({
|
||||
data: {
|
||||
packageId: id,
|
||||
status: "DECLARACION_ADUANERA",
|
||||
createdBy: agentId,
|
||||
note: dto.agentNotes ?? `DSI generada. Auth: ${authNumber}. Categoría: ${dto.category}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = {
|
||||
...updated,
|
||||
declarationId,
|
||||
authNumber,
|
||||
message: "Declaración simplificada (DSI) enviada y aprobada por la SENAE (stub).",
|
||||
};
|
||||
|
||||
// Notify user of customs clearance
|
||||
this.notifications.notifyStatusChange(updated, { id: updated.userId }).catch(() => {});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue for SENAE: packages with status VERIFICADO pending declaration.
|
||||
*/
|
||||
async findPendingDeclaration(tenantId: string): Promise<any[]> {
|
||||
return this.prisma.client.package.findMany({
|
||||
where: { tenantId, status: "VERIFICADO" },
|
||||
include: {
|
||||
user: { select: { firstName: true, lastName: true, email: true } },
|
||||
statusHistory: { orderBy: { createdAt: "desc" }, take: 1 },
|
||||
},
|
||||
orderBy: { updatedAt: "asc" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user