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:
@@ -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.`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user