From 937482724e2bbc0c3c4689f8287ccccdc67e179c Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 4 May 2026 20:50:22 +0000 Subject: [PATCH] CMG1.0 --- backend/src/routes/payments.js | 5 + backend/src/services/payments.js | 264 ++++++++++++++++++++++++++++--- 2 files changed, 244 insertions(+), 25 deletions(-) diff --git a/backend/src/routes/payments.js b/backend/src/routes/payments.js index 3d0fda3..2190636 100644 --- a/backend/src/routes/payments.js +++ b/backend/src/routes/payments.js @@ -263,6 +263,11 @@ router.post('/deleteByIds', wrapAsync(async (req, res) => { res.status(200).send(payload); })); +router.post('/register-batch', wrapAsync(async (req, res) => { + const payload = await PaymentsService.registerBatch(req.body.data, req.currentUser); + res.status(200).send(payload); +})); + /** * @swagger * /api/payments: diff --git a/backend/src/services/payments.js b/backend/src/services/payments.js index ef655fe..7c94837 100644 --- a/backend/src/services/payments.js +++ b/backend/src/services/payments.js @@ -1,15 +1,19 @@ const db = require('../db/models'); const PaymentsDBApi = require('../db/api/payments'); -const processFile = require("../middlewares/upload"); +const Monthly_chargesDBApi = require('../db/api/monthly_charges'); +const ReceiptsDBApi = require('../db/api/receipts'); +const processFile = require('../middlewares/upload'); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); const stream = require('stream'); +const Op = db.Sequelize.Op; - - +const createBadRequestError = (message) => { + const error = new Error(message); + error.code = 400; + return error; +}; module.exports = class PaymentsService { static async create(data, currentUser) { @@ -28,7 +32,224 @@ module.exports = class PaymentsService { await transaction.rollback(); throw error; } - }; + } + + static async registerBatch(data, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + const apartmentId = data?.apartmentId; + const chargeIds = Array.isArray(data?.chargeIds) + ? data.chargeIds.filter(Boolean) + : []; + const accountId = data?.accountId; + const paymentMethodId = data?.paymentMethodId; + const paidAt = data?.paidAt || new Date().toISOString(); + const referenceCode = data?.referenceCode ? String(data.referenceCode).trim() : ''; + const notes = data?.notes ? String(data.notes).trim() : null; + + if (!apartmentId) { + throw createBadRequestError('Debes seleccionar un apartamento.'); + } + + if (!chargeIds.length) { + throw createBadRequestError('Debes seleccionar al menos una cuota pendiente.'); + } + + if (!accountId) { + throw createBadRequestError('Debes seleccionar una cuenta de destino.'); + } + + if (!paymentMethodId) { + throw createBadRequestError('Debes seleccionar un método de pago.'); + } + + const apartment = await db.apartments.findByPk(apartmentId, { transaction }); + const account = await db.accounts.findByPk(accountId, { transaction }); + const paymentMethod = await db.payment_methods.findByPk(paymentMethodId, { + transaction, + }); + + if (!apartment) { + throw createBadRequestError('El apartamento seleccionado no existe.'); + } + + if (!account) { + throw createBadRequestError('La cuenta seleccionada no existe.'); + } + + if (!paymentMethod) { + throw createBadRequestError('El método de pago seleccionado no existe.'); + } + + if (!account.is_active) { + throw createBadRequestError('La cuenta seleccionada está inactiva.'); + } + + if (!paymentMethod.is_active) { + throw createBadRequestError('El método de pago seleccionado está inactivo.'); + } + + if (paymentMethod.method_type !== 'cash' && !referenceCode) { + throw createBadRequestError( + 'La referencia es obligatoria para métodos de pago distintos a efectivo.', + ); + } + + const charges = await db.monthly_charges.findAll({ + where: { + id: { + [Op.in]: chargeIds, + }, + apartmentId, + }, + order: [['due_date', 'ASC']], + transaction, + }); + + if (charges.length !== chargeIds.length) { + throw createBadRequestError( + 'Una o más cuotas seleccionadas no pertenecen al apartamento indicado.', + ); + } + + const nonPendingCharge = charges.find((charge) => charge.status !== 'pending'); + + if (nonPendingCharge) { + throw createBadRequestError( + 'Solo puedes registrar en lote cuotas con estado pendiente en esta primera entrega.', + ); + } + + const currencies = Array.from( + new Set(charges.map((charge) => charge.currency).filter(Boolean)), + ); + + if (currencies.length !== 1) { + throw createBadRequestError( + 'No puedes mezclar cuotas en bolívares y dólares en el mismo registro.', + ); + } + + const [currency] = currencies; + + if (account.currency !== currency) { + throw createBadRequestError( + 'La cuenta seleccionada no coincide con la moneda de las cuotas elegidas.', + ); + } + + const createdPayments = []; + const createdReceipts = []; + let totalAmount = 0; + + for (const charge of charges) { + const payment = await PaymentsDBApi.create( + { + paid_at: paidAt, + amount: charge.amount, + currency: charge.currency, + reference_code: referenceCode || null, + status: 'confirmed', + notes: + notes || + `Pago registrado desde el Centro de cobranza${ + charge.notes ? ` · ${charge.notes}` : '' + }`, + apartment: apartment.id, + charge: charge.id, + account: account.id, + payment_method: paymentMethod.id, + }, + { + currentUser, + transaction, + }, + ); + + const receipt = await ReceiptsDBApi.create( + { + receipt_number: this.buildReceiptNumber( + apartment.apartment_code, + charge.period_start, + payment.id, + ), + issued_at: paidAt, + delivery_status: 'not_sent', + notes: + notes || + `Recibo generado automáticamente para ${apartment.apartment_code || apartment.id}`, + payment: payment.id, + apartment: apartment.id, + }, + { + currentUser, + transaction, + }, + ); + + await Monthly_chargesDBApi.update( + charge.id, + { + status: 'paid', + }, + { + currentUser, + transaction, + }, + ); + + totalAmount += Number(charge.amount || 0); + + createdPayments.push({ + id: payment.id, + chargeId: charge.id, + amount: Number(charge.amount || 0), + currency: charge.currency, + reference_code: payment.reference_code, + }); + + createdReceipts.push({ + id: receipt.id, + receipt_number: receipt.receipt_number, + delivery_status: receipt.delivery_status, + paymentId: payment.id, + }); + } + + await transaction.commit(); + + return { + apartment: { + id: apartment.id, + apartment_code: apartment.apartment_code, + }, + currency, + totalAmount, + payments: createdPayments, + receipts: createdReceipts, + }; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static buildReceiptNumber(apartmentCode, periodStart, paymentId) { + const normalizedApartmentCode = (apartmentCode || 'APT') + .toString() + .replace(/[^A-Za-z0-9]/g, '') + .toUpperCase(); + const chargeDate = periodStart ? new Date(periodStart) : new Date(); + const safeDate = Number.isNaN(chargeDate.getTime()) ? new Date() : chargeDate; + const period = `${safeDate.getUTCFullYear()}${String( + safeDate.getUTCMonth() + 1, + ).padStart(2, '0')}`; + + return `RCPT-${normalizedApartmentCode}-${period}-${paymentId + .slice(0, 8) + .toUpperCase()}`; + } static async bulkImport(req, res, sendInvitationEmails = true, host) { const transaction = await db.sequelize.transaction(); @@ -38,24 +259,24 @@ module.exports = class PaymentsService { const bufferStream = new stream.PassThrough(); const results = []; - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); await new Promise((resolve, reject) => { bufferStream .pipe(csv()) - .on('data', (data) => results.push(data)) + .on('data', (row) => results.push(row)) .on('end', async () => { console.log('CSV results', results); resolve(); }) .on('error', (error) => reject(error)); - }) + }); await PaymentsDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, }); await transaction.commit(); @@ -68,15 +289,13 @@ module.exports = class PaymentsService { static async update(data, id, currentUser) { const transaction = await db.sequelize.transaction(); try { - let payments = await PaymentsDBApi.findBy( - {id}, - {transaction}, + const payments = await PaymentsDBApi.findBy( + { id }, + { transaction }, ); if (!payments) { - throw new ValidationError( - 'paymentsNotFound', - ); + throw new ValidationError('paymentsNotFound'); } const updatedPayments = await PaymentsDBApi.update( @@ -90,12 +309,11 @@ module.exports = class PaymentsService { await transaction.commit(); return updatedPayments; - } catch (error) { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); @@ -131,8 +349,4 @@ module.exports = class PaymentsService { throw error; } } - - }; - -