feat: add Warehouses + Integrations modules, /admin/configuracion page

- Schema v0.3: Warehouse + Integration models (db push applied)
- WarehousesModule: CRUD, set-default, multi-warehouse support
- IntegrationsModule: 26 keys across 7 groups (payment, notifications,
  customs, courier, marketplace, compliance, warehouse)
- AuthService: suite address pulled from default Warehouse in DB (env fallback)
- AuthModule: imports WarehousesModule
- Seed: creates default warehouse from WAREHOUSE_ADDRESS_* env vars
- Web: /admin/configuracion page (3 tabs: Bodegas, Integraciones, Estado APIs)
- Web: admin layout adds Configuracion nav link
- api.ts: warehouses + integrations client methods
This commit is contained in:
Lizandro Guarnizo
2026-06-01 16:49:19 -05:00
parent 03b18e7a84
commit b2b292c50a
15 changed files with 822 additions and 18 deletions
@@ -0,0 +1,32 @@
import { IsString, IsOptional, IsBoolean, IsNumber, Min } from "class-validator";
import { Type } from "class-transformer";
export class CreateWarehouseDto {
@IsString() name!: string;
@IsString() street!: string;
@IsString() city!: string;
@IsString() state!: string;
@IsString() zip!: string;
@IsOptional() @IsString() country?: string;
@IsOptional() @IsString() phone?: string;
@IsOptional() @IsString() email?: string;
@IsOptional() @IsString() contactName?: string;
@IsOptional() @IsString() schedule?: string;
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) capacityM2?: number;
@IsOptional() @IsBoolean() isDefault?: boolean;
}
export class UpdateWarehouseDto {
@IsOptional() @IsString() name?: string;
@IsOptional() @IsString() street?: string;
@IsOptional() @IsString() city?: string;
@IsOptional() @IsString() state?: string;
@IsOptional() @IsString() zip?: string;
@IsOptional() @IsString() country?: string;
@IsOptional() @IsString() phone?: string;
@IsOptional() @IsString() email?: string;
@IsOptional() @IsString() contactName?: string;
@IsOptional() @IsString() schedule?: string;
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) capacityM2?: number;
@IsOptional() @IsBoolean() isActive?: boolean;
}
@@ -0,0 +1,53 @@
import { Controller, Get, Post, Patch, Delete, Body, Param, UseGuards } from "@nestjs/common";
import { WarehousesService } from "./warehouses.service";
import { CreateWarehouseDto, UpdateWarehouseDto } from "./dto/warehouse.dto";
import { JwtAuthGuard, RolesGuard, Roles } from "../auth/guards/auth.guard";
import { CurrentUser } from "../auth/decorators/current-user.decorator";
@Controller("warehouses")
@UseGuards(JwtAuthGuard, RolesGuard)
export class WarehousesController {
constructor(private svc: WarehousesService) {}
@Get()
@Roles("ADMIN_EMPRESA", "SUPER_ADMIN", "OPERADOR_BODEGA", "AGENTE_ADUANERO")
findAll(@CurrentUser() user: any) {
return this.svc.findAll(user.tenantId);
}
@Get("default")
@Roles("ADMIN_EMPRESA", "SUPER_ADMIN", "OPERADOR_BODEGA", "AGENTE_ADUANERO", "CLIENTE")
findDefault(@CurrentUser() user: any) {
return this.svc.findDefault(user.tenantId);
}
@Get(":id")
@Roles("ADMIN_EMPRESA", "SUPER_ADMIN", "OPERADOR_BODEGA")
findOne(@Param("id") id: string, @CurrentUser() user: any) {
return this.svc.findOne(id, user.tenantId);
}
@Post()
@Roles("ADMIN_EMPRESA", "SUPER_ADMIN")
create(@Body() dto: CreateWarehouseDto, @CurrentUser() user: any) {
return this.svc.create(dto, user.tenantId);
}
@Patch(":id")
@Roles("ADMIN_EMPRESA", "SUPER_ADMIN")
update(@Param("id") id: string, @Body() dto: UpdateWarehouseDto, @CurrentUser() user: any) {
return this.svc.update(id, dto, user.tenantId);
}
@Patch(":id/set-default")
@Roles("ADMIN_EMPRESA", "SUPER_ADMIN")
setDefault(@Param("id") id: string, @CurrentUser() user: any) {
return this.svc.setDefault(id, user.tenantId);
}
@Delete(":id")
@Roles("ADMIN_EMPRESA", "SUPER_ADMIN")
remove(@Param("id") id: string, @CurrentUser() user: any) {
return this.svc.remove(id, user.tenantId);
}
}
@@ -0,0 +1,12 @@
import { Module } from "@nestjs/common";
import { WarehousesService } from "./warehouses.service";
import { WarehousesController } from "./warehouses.controller";
import { PrismaModule } from "../prisma/prisma.module";
@Module({
imports: [PrismaModule],
providers: [WarehousesService],
controllers: [WarehousesController],
exports: [WarehousesService],
})
export class WarehousesModule {}
@@ -0,0 +1,90 @@
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { CreateWarehouseDto, UpdateWarehouseDto } from "./dto/warehouse.dto";
@Injectable()
export class WarehousesService {
constructor(private prisma: PrismaService) {}
async findAll(tenantId: string): Promise<any[]> {
return this.prisma.client.warehouse.findMany({
where: { tenantId },
orderBy: [{ isDefault: "desc" }, { createdAt: "asc" }],
});
}
async findDefault(tenantId: string): Promise<any | null> {
return this.prisma.client.warehouse.findFirst({
where: { tenantId, isDefault: true, isActive: true },
});
}
async findOne(id: string, tenantId: string): Promise<any> {
const wh = await this.prisma.client.warehouse.findFirst({ where: { id, tenantId } });
if (!wh) throw new NotFoundException("Bodega no encontrada.");
return wh;
}
async create(dto: CreateWarehouseDto, tenantId: string): Promise<any> {
// If this is the first warehouse or explicitly set as default, handle default logic
const existing = await this.prisma.client.warehouse.count({ where: { tenantId } });
const setAsDefault = dto.isDefault ?? existing === 0;
if (setAsDefault) {
await this.prisma.client.warehouse.updateMany({
where: { tenantId, isDefault: true },
data: { isDefault: false },
});
}
return this.prisma.client.warehouse.create({
data: {
tenantId,
name: dto.name,
street: dto.street,
city: dto.city,
state: dto.state,
zip: dto.zip,
country: dto.country ?? "US",
phone: dto.phone,
email: dto.email,
contactName: dto.contactName,
schedule: dto.schedule,
capacityM2: dto.capacityM2,
isDefault: setAsDefault,
},
});
}
async update(id: string, dto: UpdateWarehouseDto, tenantId: string): Promise<any> {
await this.findOne(id, tenantId);
return this.prisma.client.warehouse.update({
where: { id },
data: dto,
});
}
async setDefault(id: string, tenantId: string): Promise<any> {
await this.findOne(id, tenantId);
// Remove default from all, set on this one
await this.prisma.client.warehouse.updateMany({
where: { tenantId, isDefault: true },
data: { isDefault: false },
});
return this.prisma.client.warehouse.update({
where: { id },
data: { isDefault: true, isActive: true },
});
}
async remove(id: string, tenantId: string): Promise<void> {
const wh = await this.findOne(id, tenantId);
if (wh.isDefault) throw new BadRequestException("No puedes eliminar la bodega predeterminada.");
await this.prisma.client.warehouse.delete({ where: { id } });
}
/** Build the suite address string from the default warehouse */
buildSuiteAddress(warehouse: any, suiteCode: string): string {
return `${warehouse.street}, Suite ${suiteCode}, ${warehouse.city}, ${warehouse.state} ${warehouse.zip}, EE.UU.`;
}
}