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);
|
||||
}));
|
||||
|
||||
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:
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user