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>
220 lines
9.4 KiB
JavaScript
220 lines
9.4 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.SalesService = void 0;
|
|
const common_1 = require("@nestjs/common");
|
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
const typeorm_2 = require("typeorm");
|
|
const sale_entity_1 = require("./entities/sale.entity");
|
|
const sale_item_entity_1 = require("./entities/sale-item.entity");
|
|
const product_entity_1 = require("../products/entities/product.entity");
|
|
const tenant_entity_1 = require("../auth/entities/tenant.entity");
|
|
let SalesService = class SalesService {
|
|
constructor(saleRepository, saleItemRepository, productRepository, tenantRepository) {
|
|
this.saleRepository = saleRepository;
|
|
this.saleItemRepository = saleItemRepository;
|
|
this.productRepository = productRepository;
|
|
this.tenantRepository = tenantRepository;
|
|
}
|
|
async findAll(tenantId, filters) {
|
|
const query = this.saleRepository
|
|
.createQueryBuilder('sale')
|
|
.leftJoinAndSelect('sale.items', 'items')
|
|
.leftJoinAndSelect('sale.paymentMethod', 'paymentMethod')
|
|
.where('sale.tenantId = :tenantId', { tenantId });
|
|
if (filters.startDate && filters.endDate) {
|
|
query.andWhere('DATE(sale.createdAt) BETWEEN :startDate AND :endDate', {
|
|
startDate: filters.startDate,
|
|
endDate: filters.endDate,
|
|
});
|
|
}
|
|
else if (filters.startDate) {
|
|
query.andWhere('DATE(sale.createdAt) >= :startDate', {
|
|
startDate: filters.startDate,
|
|
});
|
|
}
|
|
else if (filters.endDate) {
|
|
query.andWhere('DATE(sale.createdAt) <= :endDate', {
|
|
endDate: filters.endDate,
|
|
});
|
|
}
|
|
if (filters.status) {
|
|
query.andWhere('sale.status = :status', { status: filters.status });
|
|
}
|
|
if (filters.ticketNumber) {
|
|
query.andWhere('sale.ticketNumber ILIKE :ticketNumber', {
|
|
ticketNumber: `%${filters.ticketNumber}%`,
|
|
});
|
|
}
|
|
query.orderBy('sale.createdAt', 'DESC');
|
|
if (filters.limit) {
|
|
query.limit(filters.limit);
|
|
}
|
|
return query.getMany();
|
|
}
|
|
async findOne(tenantId, id) {
|
|
const sale = await this.saleRepository.findOne({
|
|
where: { id, tenantId },
|
|
relations: ['items', 'paymentMethod'],
|
|
});
|
|
if (!sale) {
|
|
throw new common_1.NotFoundException('Venta no encontrada');
|
|
}
|
|
return sale;
|
|
}
|
|
async findByTicketNumber(tenantId, ticketNumber) {
|
|
const sale = await this.saleRepository.findOne({
|
|
where: { ticketNumber, tenantId },
|
|
relations: ['items', 'paymentMethod'],
|
|
});
|
|
if (!sale) {
|
|
throw new common_1.NotFoundException('Venta no encontrada');
|
|
}
|
|
return sale;
|
|
}
|
|
async create(tenantId, dto) {
|
|
const tenant = await this.tenantRepository.findOne({
|
|
where: { id: tenantId },
|
|
});
|
|
if (!tenant) {
|
|
throw new common_1.BadRequestException('Tenant no encontrado');
|
|
}
|
|
let subtotal = 0;
|
|
const saleItems = [];
|
|
for (const item of dto.items) {
|
|
const product = await this.productRepository.findOne({
|
|
where: { id: item.productId, tenantId, status: 'active' },
|
|
});
|
|
if (!product) {
|
|
throw new common_1.BadRequestException(`Producto ${item.productId} no encontrado`);
|
|
}
|
|
if (product.trackInventory && Number(product.stockQuantity) < item.quantity) {
|
|
throw new common_1.BadRequestException(`Stock insuficiente para ${product.name}. Disponible: ${product.stockQuantity}`);
|
|
}
|
|
const itemSubtotal = Number(product.price) * item.quantity * (1 - (item.discountPercent || 0) / 100);
|
|
saleItems.push({
|
|
productId: product.id,
|
|
productName: product.name,
|
|
productSku: product.sku,
|
|
quantity: item.quantity,
|
|
unitPrice: Number(product.price),
|
|
discountPercent: item.discountPercent || 0,
|
|
subtotal: itemSubtotal,
|
|
});
|
|
subtotal += itemSubtotal;
|
|
}
|
|
const taxRate = Number(tenant.taxRate) / 100;
|
|
const taxAmount = subtotal - subtotal / (1 + taxRate);
|
|
const total = subtotal;
|
|
if (dto.amountReceived < total) {
|
|
throw new common_1.BadRequestException(`Monto recibido ($${dto.amountReceived}) es menor al total ($${total.toFixed(2)})`);
|
|
}
|
|
const changeAmount = dto.amountReceived - total;
|
|
const sale = this.saleRepository.create({
|
|
tenantId,
|
|
subtotal,
|
|
taxAmount,
|
|
discountAmount: 0,
|
|
total,
|
|
paymentMethodId: dto.paymentMethodId,
|
|
amountReceived: dto.amountReceived,
|
|
changeAmount,
|
|
customerName: dto.customerName,
|
|
customerPhone: dto.customerPhone,
|
|
notes: dto.notes,
|
|
deviceInfo: dto.deviceInfo,
|
|
status: sale_entity_1.SaleStatus.COMPLETED,
|
|
});
|
|
const savedSale = await this.saleRepository.save(sale);
|
|
for (const item of saleItems) {
|
|
const saleItem = this.saleItemRepository.create({
|
|
...item,
|
|
saleId: savedSale.id,
|
|
});
|
|
await this.saleItemRepository.save(saleItem);
|
|
}
|
|
return this.findOne(tenantId, savedSale.id);
|
|
}
|
|
async cancel(tenantId, id, dto) {
|
|
const sale = await this.findOne(tenantId, id);
|
|
if (sale.status !== sale_entity_1.SaleStatus.COMPLETED) {
|
|
throw new common_1.BadRequestException('Solo se pueden cancelar ventas completadas');
|
|
}
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const saleDate = new Date(sale.createdAt);
|
|
saleDate.setHours(0, 0, 0, 0);
|
|
if (saleDate.getTime() !== today.getTime()) {
|
|
throw new common_1.BadRequestException('Solo se pueden cancelar ventas del día actual');
|
|
}
|
|
sale.status = sale_entity_1.SaleStatus.CANCELLED;
|
|
sale.cancelledAt = new Date();
|
|
sale.cancelReason = dto.reason;
|
|
for (const item of sale.items) {
|
|
if (item.productId) {
|
|
const product = await this.productRepository.findOne({
|
|
where: { id: item.productId },
|
|
});
|
|
if (product?.trackInventory) {
|
|
product.stockQuantity = Number(product.stockQuantity) + Number(item.quantity);
|
|
await this.productRepository.save(product);
|
|
}
|
|
}
|
|
}
|
|
return this.saleRepository.save(sale);
|
|
}
|
|
async getTodaySummary(tenantId) {
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const result = await this.saleRepository
|
|
.createQueryBuilder('sale')
|
|
.select([
|
|
'COUNT(sale.id) as totalSales',
|
|
'COALESCE(SUM(sale.total), 0) as totalRevenue',
|
|
'COALESCE(SUM(sale.taxAmount), 0) as totalTax',
|
|
'COALESCE(AVG(sale.total), 0) as avgTicket',
|
|
])
|
|
.where('sale.tenantId = :tenantId', { tenantId })
|
|
.andWhere('DATE(sale.createdAt) = CURRENT_DATE')
|
|
.andWhere('sale.status = :status', { status: sale_entity_1.SaleStatus.COMPLETED })
|
|
.getRawOne();
|
|
return {
|
|
totalSales: parseInt(result.totalsales, 10) || 0,
|
|
totalRevenue: parseFloat(result.totalrevenue) || 0,
|
|
totalTax: parseFloat(result.totaltax) || 0,
|
|
avgTicket: parseFloat(result.avgticket) || 0,
|
|
};
|
|
}
|
|
async getRecentSales(tenantId, limit = 10) {
|
|
return this.saleRepository.find({
|
|
where: { tenantId },
|
|
relations: ['items', 'paymentMethod'],
|
|
order: { createdAt: 'DESC' },
|
|
take: limit,
|
|
});
|
|
}
|
|
};
|
|
exports.SalesService = SalesService;
|
|
exports.SalesService = SalesService = __decorate([
|
|
(0, common_1.Injectable)(),
|
|
__param(0, (0, typeorm_1.InjectRepository)(sale_entity_1.Sale)),
|
|
__param(1, (0, typeorm_1.InjectRepository)(sale_item_entity_1.SaleItem)),
|
|
__param(2, (0, typeorm_1.InjectRepository)(product_entity_1.Product)),
|
|
__param(3, (0, typeorm_1.InjectRepository)(tenant_entity_1.Tenant)),
|
|
__metadata("design:paramtypes", [typeorm_2.Repository,
|
|
typeorm_2.Repository,
|
|
typeorm_2.Repository,
|
|
typeorm_2.Repository])
|
|
], SalesService);
|
|
//# sourceMappingURL=sales.service.js.map
|