Marketplace móvil para negocios locales mexicanos. Estructura inicial: - apps/backend (NestJS API) - apps/frontend (React Web) - apps/mobile (Expo/React Native) - apps/mcp-server (Claude MCP Server) - apps/whatsapp-service (WhatsApp Business API) - database/ (PostgreSQL DDL) - docs/ (Documentación) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
196 lines
8.1 KiB
JavaScript
196 lines
8.1 KiB
JavaScript
"use strict";
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
};
|
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.InventoryService = void 0;
|
|
const common_1 = require("@nestjs/common");
|
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
const typeorm_2 = require("typeorm");
|
|
const inventory_movement_entity_1 = require("./entities/inventory-movement.entity");
|
|
const stock_alert_entity_1 = require("./entities/stock-alert.entity");
|
|
const product_entity_1 = require("../products/entities/product.entity");
|
|
let InventoryService = class InventoryService {
|
|
constructor(movementRepo, alertRepo, productRepo) {
|
|
this.movementRepo = movementRepo;
|
|
this.alertRepo = alertRepo;
|
|
this.productRepo = productRepo;
|
|
}
|
|
async createMovement(tenantId, dto, userId) {
|
|
const product = await this.productRepo.findOne({
|
|
where: { id: dto.productId, tenantId },
|
|
});
|
|
if (!product) {
|
|
throw new common_1.NotFoundException('Producto no encontrado');
|
|
}
|
|
const quantityBefore = Number(product.stockQuantity);
|
|
const quantityAfter = quantityBefore + dto.quantity;
|
|
if (quantityAfter < 0) {
|
|
throw new common_1.BadRequestException(`Stock insuficiente. Disponible: ${quantityBefore}, Solicitado: ${Math.abs(dto.quantity)}`);
|
|
}
|
|
const movement = this.movementRepo.create({
|
|
tenantId,
|
|
productId: dto.productId,
|
|
movementType: dto.movementType,
|
|
quantity: dto.quantity,
|
|
quantityBefore,
|
|
quantityAfter,
|
|
unitCost: dto.unitCost,
|
|
referenceType: dto.referenceType,
|
|
referenceId: dto.referenceId,
|
|
notes: dto.notes,
|
|
createdBy: userId,
|
|
});
|
|
await this.movementRepo.save(movement);
|
|
product.stockQuantity = quantityAfter;
|
|
await this.productRepo.save(product);
|
|
await this.checkStockAlerts(tenantId, product);
|
|
return movement;
|
|
}
|
|
async adjustStock(tenantId, dto, userId) {
|
|
const product = await this.productRepo.findOne({
|
|
where: { id: dto.productId, tenantId },
|
|
});
|
|
if (!product) {
|
|
throw new common_1.NotFoundException('Producto no encontrado');
|
|
}
|
|
const quantityBefore = Number(product.stockQuantity);
|
|
const difference = dto.newQuantity - quantityBefore;
|
|
return this.createMovement(tenantId, {
|
|
productId: dto.productId,
|
|
movementType: inventory_movement_entity_1.MovementType.ADJUSTMENT,
|
|
quantity: difference,
|
|
notes: dto.reason || `Ajuste de inventario: ${quantityBefore} -> ${dto.newQuantity}`,
|
|
}, userId);
|
|
}
|
|
async getMovements(tenantId, productId, limit = 50) {
|
|
const where = { tenantId };
|
|
if (productId) {
|
|
where.productId = productId;
|
|
}
|
|
return this.movementRepo.find({
|
|
where,
|
|
relations: ['product'],
|
|
order: { createdAt: 'DESC' },
|
|
take: limit,
|
|
});
|
|
}
|
|
async getProductHistory(tenantId, productId) {
|
|
return this.movementRepo.find({
|
|
where: { tenantId, productId },
|
|
order: { createdAt: 'DESC' },
|
|
});
|
|
}
|
|
async checkStockAlerts(tenantId, product) {
|
|
const currentStock = Number(product.stockQuantity);
|
|
const threshold = Number(product.lowStockThreshold);
|
|
if (currentStock > threshold) {
|
|
await this.alertRepo.update({ productId: product.id, status: stock_alert_entity_1.AlertStatus.ACTIVE }, { status: stock_alert_entity_1.AlertStatus.RESOLVED, resolvedAt: new Date() });
|
|
return;
|
|
}
|
|
const existingAlert = await this.alertRepo.findOne({
|
|
where: { productId: product.id, status: stock_alert_entity_1.AlertStatus.ACTIVE },
|
|
});
|
|
if (existingAlert) {
|
|
existingAlert.currentStock = currentStock;
|
|
await this.alertRepo.save(existingAlert);
|
|
return;
|
|
}
|
|
const alertType = currentStock <= 0 ? stock_alert_entity_1.AlertType.OUT_OF_STOCK : stock_alert_entity_1.AlertType.LOW_STOCK;
|
|
const alert = this.alertRepo.create({
|
|
tenantId,
|
|
productId: product.id,
|
|
alertType,
|
|
currentStock,
|
|
threshold,
|
|
});
|
|
await this.alertRepo.save(alert);
|
|
}
|
|
async getActiveAlerts(tenantId) {
|
|
return this.alertRepo.find({
|
|
where: { tenantId, status: stock_alert_entity_1.AlertStatus.ACTIVE },
|
|
relations: ['product'],
|
|
order: { createdAt: 'DESC' },
|
|
});
|
|
}
|
|
async dismissAlert(tenantId, alertId) {
|
|
const alert = await this.alertRepo.findOne({
|
|
where: { id: alertId, tenantId },
|
|
});
|
|
if (!alert) {
|
|
throw new common_1.NotFoundException('Alerta no encontrada');
|
|
}
|
|
alert.status = stock_alert_entity_1.AlertStatus.DISMISSED;
|
|
return this.alertRepo.save(alert);
|
|
}
|
|
async getLowStockProducts(tenantId) {
|
|
return this.productRepo
|
|
.createQueryBuilder('product')
|
|
.where('product.tenant_id = :tenantId', { tenantId })
|
|
.andWhere('product.track_inventory = true')
|
|
.andWhere('product.stock_quantity <= product.low_stock_threshold')
|
|
.andWhere("product.status = 'active'")
|
|
.orderBy('product.stock_quantity', 'ASC')
|
|
.getMany();
|
|
}
|
|
async getOutOfStockProducts(tenantId) {
|
|
return this.productRepo.find({
|
|
where: {
|
|
tenantId,
|
|
trackInventory: true,
|
|
stockQuantity: (0, typeorm_2.LessThanOrEqual)(0),
|
|
status: 'active',
|
|
},
|
|
order: { name: 'ASC' },
|
|
});
|
|
}
|
|
async getInventoryStats(tenantId) {
|
|
const products = await this.productRepo.find({
|
|
where: { tenantId, trackInventory: true, status: 'active' },
|
|
});
|
|
let totalValue = 0;
|
|
let lowStockCount = 0;
|
|
let outOfStockCount = 0;
|
|
for (const product of products) {
|
|
const stock = Number(product.stockQuantity);
|
|
const cost = Number(product.costPrice) || 0;
|
|
totalValue += stock * cost;
|
|
if (stock <= 0) {
|
|
outOfStockCount++;
|
|
}
|
|
else if (stock <= Number(product.lowStockThreshold)) {
|
|
lowStockCount++;
|
|
}
|
|
}
|
|
const activeAlerts = await this.alertRepo.count({
|
|
where: { tenantId, status: stock_alert_entity_1.AlertStatus.ACTIVE },
|
|
});
|
|
return {
|
|
totalProducts: products.length,
|
|
totalValue,
|
|
lowStockCount,
|
|
outOfStockCount,
|
|
activeAlerts,
|
|
};
|
|
}
|
|
};
|
|
exports.InventoryService = InventoryService;
|
|
exports.InventoryService = InventoryService = __decorate([
|
|
(0, common_1.Injectable)(),
|
|
__param(0, (0, typeorm_1.InjectRepository)(inventory_movement_entity_1.InventoryMovement)),
|
|
__param(1, (0, typeorm_1.InjectRepository)(stock_alert_entity_1.StockAlert)),
|
|
__param(2, (0, typeorm_1.InjectRepository)(product_entity_1.Product)),
|
|
__metadata("design:paramtypes", [typeorm_2.Repository,
|
|
typeorm_2.Repository,
|
|
typeorm_2.Repository])
|
|
], InventoryService);
|
|
//# sourceMappingURL=inventory.service.js.map
|