CMG1.0
This commit is contained in:
parent
75caf737e4
commit
937482724e
@ -263,6 +263,11 @@ router.post('/deleteByIds', wrapAsync(async (req, res) => {
|
|||||||
res.status(200).send(payload);
|
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
|
* @swagger
|
||||||
* /api/payments:
|
* /api/payments:
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
const db = require('../db/models');
|
const db = require('../db/models');
|
||||||
const PaymentsDBApi = require('../db/api/payments');
|
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 ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
|
||||||
const config = require('../config');
|
|
||||||
const stream = require('stream');
|
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 {
|
module.exports = class PaymentsService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
@ -28,7 +32,224 @@ module.exports = class PaymentsService {
|
|||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
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) {
|
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
@ -38,24 +259,24 @@ module.exports = class PaymentsService {
|
|||||||
const bufferStream = new stream.PassThrough();
|
const bufferStream = new stream.PassThrough();
|
||||||
const results = [];
|
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) => {
|
await new Promise((resolve, reject) => {
|
||||||
bufferStream
|
bufferStream
|
||||||
.pipe(csv())
|
.pipe(csv())
|
||||||
.on('data', (data) => results.push(data))
|
.on('data', (row) => results.push(row))
|
||||||
.on('end', async () => {
|
.on('end', async () => {
|
||||||
console.log('CSV results', results);
|
console.log('CSV results', results);
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
})
|
});
|
||||||
|
|
||||||
await PaymentsDBApi.bulkImport(results, {
|
await PaymentsDBApi.bulkImport(results, {
|
||||||
transaction,
|
transaction,
|
||||||
ignoreDuplicates: true,
|
ignoreDuplicates: true,
|
||||||
validate: true,
|
validate: true,
|
||||||
currentUser: req.currentUser
|
currentUser: req.currentUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
@ -68,15 +289,13 @@ module.exports = class PaymentsService {
|
|||||||
static async update(data, id, currentUser) {
|
static async update(data, id, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
let payments = await PaymentsDBApi.findBy(
|
const payments = await PaymentsDBApi.findBy(
|
||||||
{id},
|
{ id },
|
||||||
{transaction},
|
{ transaction },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!payments) {
|
if (!payments) {
|
||||||
throw new ValidationError(
|
throw new ValidationError('paymentsNotFound');
|
||||||
'paymentsNotFound',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedPayments = await PaymentsDBApi.update(
|
const updatedPayments = await PaymentsDBApi.update(
|
||||||
@ -90,12 +309,11 @@ module.exports = class PaymentsService {
|
|||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
return updatedPayments;
|
return updatedPayments;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
static async deleteByIds(ids, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
@ -131,8 +349,4 @@ module.exports = class PaymentsService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user