This commit is contained in:
Flatlogic Bot 2026-05-04 20:50:22 +00:00
parent 75caf737e4
commit 937482724e
2 changed files with 244 additions and 25 deletions

View File

@ -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:

View File

@ -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;
}
}
};