This commit is contained in:
Emmanuel Rojas committed 2026-09-30 14:45:10 -05:00
1 parent 90125aede9
commit 2d6c29a1f3
404 files changed
+126983 -85

No files matched your search

@@ -0,0 +1,15 @@
import type {
InventoryMovement,
InventoryMovementKey,
} from "../../../entities/inventory-movement.entity.js";
import type { CreateInventoryMovementInput } from "../../../dtos/inventory-movement.dto.js";
export const INVENTORY_MOVEMENT_REPOSITORY = Symbol(
"INVENTORY_MOVEMENT_REPOSITORY",
);
export interface IInventoryMovementRepository {
findAll(): Promise<InventoryMovement[]>;
findById(where: InventoryMovementKey): Promise<InventoryMovement | null>;
create(data: CreateInventoryMovementInput): Promise<InventoryMovement>;
}
@@ -0,0 +1,13 @@
import type {
InventoryMovement,
InventoryMovementKey,
} from "../../../entities/inventory-movement.entity.js";
import type { CreateInventoryMovementInput } from "../../../dtos/inventory-movement.dto.js";
export const INVENTORY_MOVEMENT_SERVICE = Symbol("INVENTORY_MOVEMENT_SERVICE");
export interface IInventoryMovementService {
findAll(): Promise<InventoryMovement[]>;
findById(where: InventoryMovementKey): Promise<InventoryMovement>;
create(data: CreateInventoryMovementInput): Promise<InventoryMovement>;
}
@@ -0,0 +1,29 @@
import { Body, Controller, Get, Inject, Param, ParseIntPipe, Post } from "@nestjs/common";
import type { CreateInventoryMovementInput } from "../../dtos/inventory-movement.dto.js";
import {
INVENTORY_MOVEMENT_SERVICE,
type IInventoryMovementService,
} from "./interfaces/inventory-movement-service.interface.js";
@Controller("inventory-movements")
export class InventoryMovementController {
constructor(
@Inject(INVENTORY_MOVEMENT_SERVICE)
private readonly inventoryMovementService: IInventoryMovementService,
) {}
@Get()
findAll() {
return this.inventoryMovementService.findAll();
}
@Get(":id")
findOne(@Param("id", ParseIntPipe) id: number) {
return this.inventoryMovementService.findById({ id });
}
@Post()
create(@Body() body: CreateInventoryMovementInput) {
return this.inventoryMovementService.create(body);
}
}
@@ -0,0 +1,20 @@
import { Module } from "@nestjs/common";
import { ProductModule } from "../product/product.module.js";
import { SupplierModule } from "../supplier/supplier.module.js";
import { WarehouseModule } from "../warehouse/warehouse.module.js";
import { INVENTORY_MOVEMENT_REPOSITORY } from "./interfaces/inventory-movement-repository.interface.js";
import { INVENTORY_MOVEMENT_SERVICE } from "./interfaces/inventory-movement-service.interface.js";
import { InventoryMovementController } from "./inventory-movement.controller.js";
import { InventoryMovementRepository } from "./inventory-movement.repository.js";
import { InventoryMovementService } from "./inventory-movement.service.js";
@Module({
imports: [WarehouseModule, ProductModule, SupplierModule],
controllers: [InventoryMovementController],
providers: [
{ provide: INVENTORY_MOVEMENT_REPOSITORY, useClass: InventoryMovementRepository },
{ provide: INVENTORY_MOVEMENT_SERVICE, useClass: InventoryMovementService },
],
exports: [INVENTORY_MOVEMENT_SERVICE],
})
export class InventoryMovementModule {}
@@ -0,0 +1,43 @@
import { Inject, Injectable } from "@nestjs/common";
import { DB } from "../../database/database.tokens.js";
import type { Database } from "../../prisma/db.js";
import type {
InventoryMovement,
InventoryMovementKey,
} from "../../entities/inventory-movement.entity.js";
import type { CreateInventoryMovementInput } from "../../dtos/inventory-movement.dto.js";
import type { IInventoryMovementRepository } from "./interfaces/inventory-movement-repository.interface.js";
const COLUMNS = [
"id",
"productId",
"inventoryId",
"type",
"quantity",
"reason",
"userId",
"createdAt",
] as const;
@Injectable()
export class InventoryMovementRepository
implements IInventoryMovementRepository
{
constructor(@Inject(DB) private readonly db: Database) {}
private get model() {
return this.db.orm.public.InventoryMovement;
}
findAll(): Promise<InventoryMovement[]> {
return this.model.select(...COLUMNS).all().toArray();
}
findById(where: InventoryMovementKey): Promise<InventoryMovement | null> {
return this.model.select(...COLUMNS).where(where).first();
}
create(data: CreateInventoryMovementInput): Promise<InventoryMovement> {
return this.model.select(...COLUMNS).create(data);
}
}
@@ -0,0 +1,90 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from "@nestjs/common";
import {
PRODUCT_SERVICE,
type IProductService,
} from "../product/interfaces/product-service.interface.js";
import {
SUPPLIER_SERVICE,
type ISupplierService,
} from "../supplier/interfaces/supplier-service.interface.js";
import {
WAREHOUSE_SERVICE,
type IWarehouseService,
} from "../warehouse/interfaces/warehouse-service.interface.js";
import type {
InventoryMovement,
InventoryMovementKey,
} from "../../entities/inventory-movement.entity.js";
import type { CreateInventoryMovementInput } from "../../dtos/inventory-movement.dto.js";
import {
INVENTORY_MOVEMENT_REPOSITORY,
type IInventoryMovementRepository,
} from "./interfaces/inventory-movement-repository.interface.js";
import type { IInventoryMovementService } from "./interfaces/inventory-movement-service.interface.js";
/**
* El stock de un almacén nunca se edita directamente: cada cambio queda
* registrado como un movimiento (entrada/salida), que es quien ajusta
* `Warehouse.stock`. Una entrada además actualiza `Supplier.lastSupply`
* cuando el producto tiene proveedor asociado.
*/
@Injectable()
export class InventoryMovementService implements IInventoryMovementService {
constructor(
@Inject(INVENTORY_MOVEMENT_REPOSITORY)
private readonly repository: IInventoryMovementRepository,
@Inject(WAREHOUSE_SERVICE) private readonly warehouseService: IWarehouseService,
@Inject(PRODUCT_SERVICE) private readonly productService: IProductService,
@Inject(SUPPLIER_SERVICE) private readonly supplierService: ISupplierService,
) {}
findAll(): Promise<InventoryMovement[]> {
return this.repository.findAll();
}
async findById(where: InventoryMovementKey): Promise<InventoryMovement> {
const movement = await this.repository.findById(where);
if (!movement) {
throw new NotFoundException(`InventoryMovement ${where.id} not found`);
}
return movement;
}
async create(data: CreateInventoryMovementInput): Promise<InventoryMovement> {
if (data.quantity <= 0) {
throw new BadRequestException("La cantidad debe ser mayor a cero");
}
if (data.type !== "in" && data.type !== "out") {
throw new BadRequestException('El tipo debe ser "in" o "out"');
}
const key = { inventoryId: data.inventoryId, productId: data.productId };
const existing = await this.warehouseService
.findById(key)
.catch(() => null);
if (data.type === "out") {
const currentStock = existing?.stock ?? 0;
if (currentStock < data.quantity) {
throw new BadRequestException("Stock insuficiente para registrar la salida");
}
await this.warehouseService.update(key, { stock: currentStock - data.quantity });
} else {
if (existing) {
await this.warehouseService.update(key, { stock: existing.stock + data.quantity });
} else {
await this.warehouseService.create({ ...key, stock: data.quantity });
}
const product = await this.productService.findById({ id: data.productId }).catch(() => null);
if (product?.supplierId) {
await this.supplierService.update(
{ id: product.supplierId },
{ lastSupply: new Date().toISOString() },
);
}
}
return this.repository.create(data);
}
}