feat: C-8/C-1/C-2/C-3/C-4/C-6/M-1/M-5/M-6 — WebSocket gateway, SENAE real, SP-API, Twilio SMS, WhatsApp Business, soporte portal, HMAC audit, reportes CSV

This commit is contained in:
Lizandro Guarnizo
2026-06-01 21:28:42 -05:00
parent a5842278fb
commit 84c1fdec54
27 changed files with 1743 additions and 122 deletions
+61 -28
View File
@@ -1,14 +1,16 @@
import { Injectable, NotFoundException, ForbiddenException, BadRequestException } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { NotificationsService } from "../notifications/notifications.service";
import { SenaeService } from "../senae/senae.service";
import { generateTrackingId } from "../common/utils/tracking-id.util";
import { CreatePackageDto, UpdateStatusDto, VerifyPackageDto, SenaeDeclarationDto } from "./dto/package.dto";
import { CreatePackageDto, RegisterPackageDto, UpdateStatusDto, VerifyPackageDto, SenaeDeclarationDto } from "./dto/package.dto";
@Injectable()
export class PackagesService {
constructor(
private prisma: PrismaService,
private notifications: NotificationsService,
private senae: SenaeService,
) {}
async findAll(user: any, filters?: { status?: string; search?: string }): Promise<any[]> {
@@ -87,6 +89,29 @@ export class PackagesService {
return pkg;
}
async updateStatus(id: string, dto: UpdateStatusDto, 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: { status: dto.status as any },
});
await this.prisma.client.packageStatusHistory.create({
data: {
packageId: id,
status: dto.status as any,
createdBy: operatorId,
note: dto.note,
},
});
this.notifications.notifyStatusChange(updated, { id: updated.userId }).catch(() => {});
return updated;
}
/** Busca una pre-alerta PENDIENTE del mismo usuario que coincida por vendorTracking
* y la vincula automáticamente al paquete (status → VINCULADA, packageId set). */
private async tryLinkPreAlert(
@@ -115,28 +140,41 @@ export class PackagesService {
}
}
async updateStatus(id: string, dto: UpdateStatusDto, operatorId: string): Promise<any> {
const pkg = await this.prisma.client.package.findUnique({ where: { id } });
if (!pkg) throw new NotFoundException("Paquete no encontrado.");
/** Cliente registra su propia compra — doc §09 pasos 5-6 */
async selfRegister(dto: RegisterPackageDto, userId: string, tenantId: string): Promise<any> {
const trackingId = generateTrackingId();
const updated = await this.prisma.client.package.update({
where: { id },
data: { status: dto.status as any },
const pkg = await this.prisma.client.package.create({
data: {
trackingId,
tenantId,
userId,
description: dto.description,
store: dto.store,
vendorTracking: dto.vendorTracking,
productUrl: dto.productUrl,
declaredValue: dto.declaredValue ?? 0,
declaredWeight: dto.declaredWeightLb ?? null,
senaeCategory: dto.senaeCategory as any ?? null,
status: "REGISTRADO",
},
});
await this.prisma.client.packageStatusHistory.create({
data: {
packageId: id,
status: dto.status as any,
createdBy: operatorId,
note: dto.note,
packageId: pkg.id,
status: "REGISTRADO",
createdBy: userId,
note: "Compra registrada por el cliente",
},
});
// Notify user on status change
this.notifications.notifyStatusChange(updated, { id: updated.userId }).catch(() => {});
await this.tryLinkPreAlert(pkg.id, userId, tenantId, dto.vendorTracking);
return updated;
// Notify the user of registration
this.notifications.notifyStatusChange(pkg, { id: userId }).catch(() => {});
return pkg;
}
/**
@@ -201,7 +239,7 @@ export class PackagesService {
}
/**
* SENAE declaration (doc §11): generate DSI stub, update status to DECLARACION_ADUANERA.
* SENAE declaration (doc §11): call SenaeService (real or 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 } });
@@ -210,10 +248,13 @@ export class PackagesService {
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}`;
// Call real SENAE service (falls back to stub if credentials not set — C-1)
const { authNumber, declarationId, message } = await this.senae.submitDSI(
pkg,
pkg.tenantId,
dto.category,
dto.agentNotes,
);
const updated = await this.prisma.client.package.update({
where: { id },
@@ -234,17 +275,9 @@ export class PackagesService {
},
});
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;
return { ...updated, declarationId, authNumber, message };
}
/**