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:
@@ -14,6 +14,17 @@ export class CreatePackageDto {
|
||||
@IsOptional() @Type(() => Number) @IsNumber() @IsPositive() heightCm?: number;
|
||||
}
|
||||
|
||||
/** Cliente registra su propia compra — el userId viene del JWT (doc §09 paso 5) */
|
||||
export class RegisterPackageDto {
|
||||
@IsString() description!: string;
|
||||
@IsOptional() @IsString() store?: string;
|
||||
@IsOptional() @IsString() vendorTracking?: string;
|
||||
@IsOptional() @IsString() productUrl?: string;
|
||||
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) declaredValue?: number;
|
||||
@IsOptional() @Type(() => Number) @IsNumber() @IsPositive() declaredWeightLb?: number;
|
||||
@IsOptional() @IsEnum(["REGIMEN_4X4","CATEGORIA_B","CATEGORIA_C","CATEGORIA_D"]) senaeCategory?: string;
|
||||
}
|
||||
|
||||
export class UpdateStatusDto {
|
||||
@IsEnum([
|
||||
"REGISTRADO","EN_TRANSITO_BODEGA","RECIBIDO_BODEGA","EN_VERIFICACION",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { memoryStorage } from "multer";
|
||||
import { extname } from "path";
|
||||
import { PackagesService } from "./packages.service";
|
||||
import { StorageService } from "../storage/storage.service";
|
||||
import { CreatePackageDto, UpdateStatusDto, VerifyPackageDto, SenaeDeclarationDto } from "./dto/package.dto";
|
||||
import { CreatePackageDto, RegisterPackageDto, UpdateStatusDto, VerifyPackageDto, SenaeDeclarationDto } from "./dto/package.dto";
|
||||
import { JwtAuthGuard, RolesGuard, Roles } from "../auth/guards/auth.guard";
|
||||
import { CurrentUser } from "../auth/decorators/current-user.decorator";
|
||||
|
||||
@@ -45,6 +45,13 @@ export class PackagesController {
|
||||
return this.svc.create(dto, user.id, user.tenantId);
|
||||
}
|
||||
|
||||
/** Cliente registra su propia compra (doc §09 paso 5) */
|
||||
@Post("register")
|
||||
@Roles("CLIENTE")
|
||||
selfRegister(@Body() dto: RegisterPackageDto, @CurrentUser() user: any): Promise<any> {
|
||||
return this.svc.selfRegister(dto, user.id, user.tenantId);
|
||||
}
|
||||
|
||||
@Patch(":id/status")
|
||||
@Roles("OPERADOR_BODEGA", "AGENTE_ADUANERO", "ADMIN_EMPRESA", "SUPER_ADMIN")
|
||||
updateStatus(
|
||||
|
||||
@@ -5,9 +5,10 @@ import { PrismaModule } from "../prisma/prisma.module";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { NotificationsModule } from "../notifications/notifications.module";
|
||||
import { StorageModule } from "../storage/storage.module";
|
||||
import { SenaeModule } from "../senae/senae.module";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, ConfigModule, NotificationsModule, StorageModule],
|
||||
imports: [PrismaModule, ConfigModule, NotificationsModule, StorageModule, SenaeModule],
|
||||
providers: [PackagesService],
|
||||
controllers: [PackagesController],
|
||||
exports: [PackagesService],
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user