commit inicial

This commit is contained in:
2026-06-10 14:54:48 -04:00
commit 053f1ea7f6
18 changed files with 1461 additions and 0 deletions

16
src/actualizarToken.js Normal file
View File

@@ -0,0 +1,16 @@
import cron from 'node-cron';
import { actualizarToken } from './procesos/autenticacion.js';
import { multivendeConfig } from './config.js';
console.log("ACTUALIZACIÓN DE ACCESS TOKEN MULTIVENDE")
await actualizarToken();
cron.schedule('0 */6 * * *', async () => {
try {
await actualizarToken();
} catch (error) {
console.log("Error", error);
}
});

37
src/config.js Normal file
View File

@@ -0,0 +1,37 @@
import dotenv from 'dotenv';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
// Resolver ruta absoluta del archivo actual
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Cargar .env desde 2 niveles arriba
dotenv.config({ path: resolve(__dirname, '../.env') });
export const dbFlexiCentralConfig = {
host: process.env.DB_FLEXI_CENTRAL_HOST,
port: process.env.DB_FLEXI_CENTRAL_PORT,
user: process.env.DB_FLEXI_CENTRAL_USER,
password: process.env.DB_FLEXI_CENTRAL_PASS,
database: process.env.DB_FLEXI_CENTRAL_NAME,
};
export const dbFlexiEcommerceConfig = {
host: process.env.DB_FLEXI_CENTRAL_HOST,
port: process.env.DB_FLEXI_CENTRAL_PORT,
user: process.env.DB_FLEXI_CENTRAL_USER,
password: process.env.DB_FLEXI_CENTRAL_PASS,
database: process.env.DB_FLEXI_ECOMMERCE_NAME,
};
export const multivendeConfig = {
clientId: process.env.MULTIVENDE_APP_CLIENT_ID,
clientSecret: process.env.MULTIVENDE_APP_CLIENT_SECRET,
urlBase: process.env.MULTIVENDE_URL_BASE,
warehouseId: process.env.MULTIVENDE_WAREHOUSE,
priceListId: process.env.MULTIVENDE_PRICE_LIST_ID,
provider: process.env.MULTIVENDE_PROVIDER,
emitterId: process.env.MULTIVENDE_EMITTER_ID,
};

View File

@@ -0,0 +1,23 @@
import Sequelize from "sequelize";
import { dbFlexiEcommerceConfig, dbFlexiCentralConfig } from "../../config.js";
export const dbEcommerceFlexi = new Sequelize(dbFlexiEcommerceConfig.database, dbFlexiEcommerceConfig.user, dbFlexiEcommerceConfig.password, {
host: dbFlexiEcommerceConfig.host,
dialect: 'mysql',
port: dbFlexiEcommerceConfig.port,
define: {
freezeTableName: true
}
});
export const dbCentralFlexi = new Sequelize(dbFlexiCentralConfig.database, dbFlexiCentralConfig.user, dbFlexiCentralConfig.password, {
host: dbFlexiCentralConfig.host,
port: dbFlexiCentralConfig.port,
dialect: 'mysql',
define: {
freezeTableName: true,
},
// logging: false,
}
);

View File

@@ -0,0 +1,79 @@
import { DataTypes } from 'sequelize';
import { dbEcommerceFlexi } from '../config/db.models.js';
const InterfazWS = dbEcommerceFlexi.define('interfaz_ws', {
folio: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
tipo: {
type: DataTypes.TINYINT.UNSIGNED,
},
estado: {
type: DataTypes.TINYINT.UNSIGNED,
},
fechacarga: {
type: DataTypes.DATE,
},
fechaproceso: {
type: DataTypes.DATE,
},
sitio: {
type: DataTypes.INTEGER.UNSIGNED,
},
documentoventa: {
type: DataTypes.INTEGER.UNSIGNED,
},
fechadocumento: {
type: DataTypes.DATEONLY,
},
numlocal: {
type: DataTypes.INTEGER.UNSIGNED,
},
numbodega: {
type: DataTypes.INTEGER.UNSIGNED,
},
ordenecommerce: {
type: DataTypes.CHAR(25),
},
sku: {
type: DataTypes.CHAR(30),
},
precio: {
type: DataTypes.DOUBLE(10, 2),
},
saldo: {
type: DataTypes.INTEGER,
},
proceso_origen: {
type: DataTypes.STRING(500),
},
vtex: {
type: DataTypes.STRING(20),
},
rappi: {
type: DataTypes.CHAR(1),
},
centry: {
type: DataTypes.CHAR(1),
},
fechaCentry: {
type: DataTypes.DATE,
},
Multivende: {
type: DataTypes.CHAR(1),
},
fechaMultivende: {
type: DataTypes.DATE,
},
fecha_actualizacion: {
type: DataTypes.DATE,
},
}, {
timestamps: false,
});
InterfazWS.removeAttribute('id');
export default InterfazWS;

View File

@@ -0,0 +1,72 @@
// models/credencialIntegracion.model.js
import { DataTypes } from "sequelize";
import { dbEcommerceFlexi } from "../config/db.models.js"; // ajusta según tu archivo de conexión
const tbCredencialesIntegracion = dbEcommerceFlexi.define("tb_credenciales_integracion", {
idsitio: {
type: DataTypes.INTEGER,
primaryKey: true,
allowNull: false,
},
nombre: {
type: DataTypes.STRING(50),
allowNull: true,
},
idcliente: {
type: DataTypes.STRING(100),
allowNull: true,
},
client_secret: {
type: DataTypes.STRING(100),
allowNull: true,
},
opcinal1: {
type: DataTypes.STRING(100),
allowNull: true,
},
opcinal2: {
type: DataTypes.STRING(100),
allowNull: true,
},
url: {
type: DataTypes.STRING(100),
allowNull: true,
},
token: {
type: DataTypes.STRING(2000),
allowNull: true,
},
estado: {
type: DataTypes.CHAR(1),
allowNull: true,
},
fecha: {
type: DataTypes.DATE,
allowNull: true,
},
token_D: {
type: DataTypes.STRING(100),
allowNull: true,
},
token_type_D: {
type: DataTypes.STRING(100),
allowNull: true,
},
expires_token_D: {
type: DataTypes.INTEGER,
allowNull: true,
},
refresh_token_D: {
type: DataTypes.STRING(100),
allowNull: true,
},
fecha_D: {
type: DataTypes.DATE,
allowNull: true,
},
}, {
timestamps: false,
freezeTableName: true,
});
export default tbCredencialesIntegracion;

18
src/envioDocumentos.js Normal file
View File

@@ -0,0 +1,18 @@
import cron from 'node-cron';
import { procesarDocumentos } from './procesos/envioBoleta.js';
console.log("ENVÍO DE DOCUMENTOS MULTIVENDE")
cron.schedule('*/15 * * * *', async () => {
console.log("Se inicia ENVÍO DE DOCUMENTOS")
try {
await procesarDocumentos();
} catch (error) {
console.error("Error en ENVIO:", error);
}
console.log("Se finaliza ENVÍO DE DOCUMENTOS")
});

31
src/envioPrecios.js Normal file
View File

@@ -0,0 +1,31 @@
import cron from 'node-cron';
import { multivendeConfig } from './config.js';
import { actualizarStock } from './procesos/actualizacionStock.js';
import { actualizarPrecios } from './procesos/actualizarPrecios.js';
console.log("ENVIO PRECIOS MULTIVENDE")
cron.schedule('*/15 * * * *', async () => {
console.log("Se inicia actualización de PRECIOS");
try {
await actualizarPrecios();
} catch (error) {
console.error("Error en ACTUALIZAR PRECIOS:", error);
}
console.log("Finalizó actualización de PRECIOS");
});
// cron.schedule('01 22 * * *', async () => {
// console.log("Se inicia actualización de PRECIOS");
// try {
// await actualizarPrecios();
// } catch (error) {
// console.error("Error en ACTUALIZAR PRECIOS:", error);
// }
// console.log("Finalizó actualización de PRECIOS");
// });

22
src/envioStock.js Normal file
View File

@@ -0,0 +1,22 @@
import cron from 'node-cron';
import { multivendeConfig } from './config.js';
import { actualizarStock } from './procesos/actualizacionStock.js';
import { actualizarPrecios } from './procesos/actualizarPrecios.js';
console.log("ENVÍO DE STOCK MULTIVENDE")
cron.schedule('*/2 * * * *', async () => {
console.log("Se inicia actualización de STOCK")
try {
await actualizarStock();
} catch (error) {
console.error("Error en ACTUALIZAR STOCK:", error);
}
console.log("Se finaliza actualización de STOCK")
});

View File

@@ -0,0 +1,97 @@
import axios from "axios";
import { multivendeConfig } from "../config.js";
import { dbEcommerceFlexi } from "../database/config/db.models.js";
import InterfazWS from "../database/models/interfazWs.js";
import { obtenerToken, tokenExpirado } from "../utils/utils.js";
const urlBase = multivendeConfig.urlBase;
const warehouseId = multivendeConfig.warehouseId;
// Función auxiliar para dividir en lotes
const dividirEnLotes = (array, tamaño) => {
const lotes = [];
for (let i = 0; i < array.length; i += tamaño) {
lotes.push(array.slice(i, i + tamaño));
}
return lotes;
};
export const actualizarStock = async (idsitio = 8) => {
try {
const [results] = await dbEcommerceFlexi.query(`
SELECT * FROM w_stock_multivende
`);
if (!results.length) {
console.log("No hay datos para actualizar.");
return;
}
const { accessToken, expiresAt } = await obtenerToken(idsitio);
const stockData = results.map(({ sku, saldo, folio }) => ({
code: sku,
amount: saldo,
folio,
}));
const lotes = dividirEnLotes(stockData, 500);
const foliosExitosos = [];
for (const lote of lotes) {
let isExpired = await tokenExpirado(expiresAt);
if (isExpired) {
({ accessToken, expiresAt } = await obtenerToken(idsitio));
}
const payload = lote.map(({ code, amount }) => ({ code, amount }));
const response = await axios.post(
`${urlBase}/api/product-stocks/stores-and-warehouses/${warehouseId}/bulk-set`,
payload,
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
}
);
const dataRespuesta = response.data;
const codigosExitosos = dataRespuesta
.filter((item) => item.success === true)
.map((item) => item.code);
const foliosLoteExitosos = lote
.filter(({ code }) => codigosExitosos.includes(code))
.map(({ folio }) => folio);
foliosExitosos.push(...foliosLoteExitosos);
}
if (foliosExitosos.length > 0) {
await InterfazWS.update(
{
Multivende: "1",
fechaMultivende: new Date(),
},
{
where: {
folio: foliosExitosos,
},
}
);
console.log(`✅ Se actualizaron ${foliosExitosos.length} registros exitosamente.`);
} else {
console.log("⚠️ No hubo SKUs con éxito en la respuesta.");
}
} catch (error) {
console.error("❌ Error actualizando stock:", error.message);
}
};

View File

@@ -0,0 +1,72 @@
import axios from "axios";
import { multivendeConfig } from "../config.js";
import { dbEcommerceFlexi } from "../database/config/db.models.js";
import InterfazWS from "../database/models/interfazWs.js";
import { obtenerToken, tokenExpirado } from "../utils/utils.js";
const urlBase = multivendeConfig.urlBase;
const priceListId = multivendeConfig.priceListId;
export const actualizarPrecios = async (idsitio = 8) => {
try {
let { accessToken, expiresAt } = await obtenerToken(idsitio);
const [precios] = await dbEcommerceFlexi.query(
`SELECT * FROM w_precio_multivende`,
{ raw: true }
);
for (const precio of precios) {
const { folio, sku, precio: valor } = precio;
const isExpired = await tokenExpirado(expiresAt);
if (isExpired) {
({ accessToken, expiresAt } = await obtenerToken(idsitio));
}
try {
await axios.post(
`${urlBase}/api/product-price-lists/${priceListId}/product-versions/${sku}/set`,
{
net: valor,
gross: valor,
tax: 0,
priceWithDiscount: 0,
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
}
);
await InterfazWS.update(
{
Multivende: 1,
fechaMultivende: new Date(),
},
{ where: { folio } }
);
console.log(`✅ Precio actualizado para SKU: ${sku}, folio: ${folio}`);
} catch (error) {
const status = error.response?.status;
const mensaje = error.response?.data || error.message;
if (status === 404) {
await InterfazWS.update(
{ Multivende: 3 },
{ where: { folio } }
);
console.warn(`⚠️ SKU no encontrado (404), multivende actualizado a 3 para folio: ${folio}`);
} else {
console.error(`❌ Error actualizando PRECIO, SKU: ${sku}, folio: ${folio}`, mensaje);
}
}
}
} catch (error) {
console.error("Error en actualizarPrecios:", error);
}
};

View File

@@ -0,0 +1,85 @@
import axios from "axios";
import { multivendeConfig } from "../config.js";
// import SkuSitios from "../database/models/skuSitios.js"
import tbCredencialesIntegracion from "../database/models/tbCredencialesIntegracion.js";
const clientId = multivendeConfig.clientId;
const clientSecret = multivendeConfig.clientSecret;
const urlBase = multivendeConfig.urlBase;
/**
*
* Cuando la función actualizarToken no alcance a actualizar el token o falle,
* hay que seguir los siguientes pasos:
* 1.- Generar un autorization code en la cuenta de multivende del cliente. Usar la cuenta de Patricio Muñoz
* asociada a Flexi. Ir a aplicaciones y buscar la app Advicom. Clickear el botón generar token.
* 2.- Generar un nuevo access token y refresh token y actualizarlos en la tabla tb_credenciales_integracion.
* Usar el siguiente curl:
* curl --location -g '{{base_url}}/oauth/access-token' \
--header 'cache-control: no-cache' \
--header 'Content-Type: application/json' \
--data '{
"client_id": 00000000000,
"client_secret": "NMV6TupxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpR3YNW",
"grant_type": "authorization_code",
"code": "ac-xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx"
}
'
*/
export const actualizarToken = async (idsitio = 8) => {
try {
const dataCredenciales = await tbCredencialesIntegracion.findOne({
where: { idsitio },
});
if (!dataCredenciales || !dataCredenciales.refresh_token_D) {
throw new Error(`No hay refresh_token disponible para idsitio ${idsitio}`);
}
const data = {
client_id: clientId,
client_secret: clientSecret,
grant_type: "refresh_token",
refresh_token: dataCredenciales.refresh_token_D,
};
const response = await axios.post(`${urlBase}/oauth/access-token`, data, {
headers: {
"Content-Type": "application/json",
"cache-control": "no-cache",
},
});
console.log(response);
const {
token: access_token,
refreshToken: refresh_token,
token_type,
expiresAt,
} = response.data;
const fechaExpiracion = new Date(expiresAt);
const ahora = new Date();
const expires_in = Math.floor((fechaExpiracion - ahora) / 1000);
await tbCredencialesIntegracion.update({
token: access_token,
refresh_token_D: refresh_token,
token_type_D: token_type || "bearer",
expires_token_D: expires_in,
fecha_D: ahora,
}, {
where: { idsitio },
});
console.log(`🔄 Token actualizado correctamente para idsitio ${idsitio}`);
return access_token;
} catch (error) {
console.error("❌ Error al renovar el token:", error.response?.data || error.message);
throw error;
}
};

178
src/procesos/envioBoleta.js Normal file
View File

@@ -0,0 +1,178 @@
import fs from 'fs';
import path from 'path';
import axios from 'axios';
import FormData from 'form-data';
import { fileURLToPath } from 'url';
import { Sequelize } from 'sequelize';
import { dbEcommerceFlexi, dbCentralFlexi } from '../database/config/db.models.js';
import { multivendeConfig } from "../config.js";
import { obtenerToken, tokenExpirado } from "../utils/utils.js";
const urlBase = multivendeConfig.urlBase;
const idsitio = 8;
const emitterId = multivendeConfig.emitterId;
const provider = multivendeConfig.provider;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 📁 Crear carpeta temporal si no existe
const tempDir = path.join(__dirname, 'tempdoc');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// 🏁 Función principal
export const procesarDocumentos = async () => {
try {
const datos = await dbCentralFlexi.query(
`SELECT * from w_estado_doc_multivende`,
{ type: Sequelize.QueryTypes.SELECT }
);
// console.log(datos)
let { accessToken, expiresAt } = await obtenerToken(idsitio);
if (!accessToken) throw new Error('No se encontró token');
for (const row of datos) {
if (tokenExpirado(expiresAt)) {
({ accessToken, expiresAt } = await obtenerToken(idsitio));
if (!accessToken) throw new Error('No se pudo renovar token');
}
const nombreDoc = `${row.tipodoc}${row.NRODOC_LV}`;
const succli = row.succli_ph;
const base64 = row.pdfbase64;
const rutaTemporal = path.join(tempDir, `${nombreDoc}.pdf`);
fs.writeFileSync(rutaTemporal, Buffer.from(base64, 'base64'));
try {
const { clientId, idcentry: checkoutId } = await extraerOrderIdyClientId(succli);
const fechaEmision = formatearFecha(row.FECMOV_LV);
const response = await enviarDocumentoUploadCreate({
checkoutId,
accessToken,
filePath: rutaTemporal,
clientId,
numFolio: row.NRODOC_LV.toString(),
emissionDate: fechaEmision,
});
if (response && (response.status === 'created' || response.statusCode === 200 || response.ok === true)) {
await dbCentralFlexi.query(
`UPDATE docpdf SET estado = 1 WHERE tipodoc = ? AND numerodoc = ?`,
{
replacements: [row.tipodoc, row.NRODOC_LV],
type: Sequelize.QueryTypes.UPDATE,
}
);
console.log(`✅ Documento ${nombreDoc} enviado y actualizado`);
} else {
console.warn(`⚠️ Documento ${nombreDoc} no fue aceptado por la API. Respuesta:`, response);
}
} catch (err) {
console.error(`❌ Error al enviar ${nombreDoc}:`, err.message);
} finally {
// 🔥 Borrar archivo temporal siempre, haya éxito o fallo
try {
if (fs.existsSync(rutaTemporal)) fs.unlinkSync(rutaTemporal);
} catch (e) {
console.warn(`⚠️ No se pudo borrar ${rutaTemporal}:`, e.message);
}
}
}
} catch (err) {
console.error('❌ Error general:', err.message);
}
};
// 🧩 Funciones auxiliares
const extraerOrderIdyClientId = async (succli) => {
const result = await dbEcommerceFlexi.query(
`SELECT json, idcentry FROM log_ordenes_marketplace WHERE NROPED = ? LIMIT 1`,
{
replacements: [succli],
type: Sequelize.QueryTypes.SELECT,
}
);
if (!result.length) return { clientId: null, idcentry: null };
try {
const parsed = JSON.parse(result[0].json);
const clientId = parsed?.Client?._id || null;
const idcentry = result[0].idcentry || null;
return { clientId, idcentry };
} catch (err) {
console.error(`❌ Error al parsear JSON de log_ordenes_marketplace:`, err.message);
return { clientId: null, idcentry: result[0].idcentry || null };
}
};
export const enviarDocumentoUploadCreate = async ({
checkoutId,
accessToken,
filePath,
clientId,
numFolio,
emissionDate,
documentType
}) => {
if (!fs.existsSync(filePath)) {
throw new Error(`El archivo no existe: ${filePath}`);
}
/**
* electronic_billing_electronic_invoice (Factura Electrónica)
electronic_billing_not_taxed_electronic_invoice (Factura Electrónica Exenta)
electronic_billing_electronic_bill (Boleta Electrónica)
*
*
*/
let type = "electronic_billing_electronic_bill";
if (documentType == "FV") {
type = "electronic_billing_electronic_invoice "
}
try {
const url = `${urlBase}/api/checkouts/${checkoutId}/electronic-billing-documents/upload-create`;
const form = new FormData();
form.append('file', fs.createReadStream(filePath));
form.append('ClientId', clientId);
form.append('id', numFolio);
form.append('emissionDate', emissionDate);
form.append('type', type);
form.append('provider', provider);
form.append('ElectronicBillingDocumentEmitterId', emitterId);
const response = await axios.post(url, form, {
headers: {
Authorization: `Bearer ${accessToken}`,
...form.getHeaders(), // ✅ Esto funciona ahora porque usas la librería correcta
},
timeout: 60000,
});
console.log(`✅ Documento enviado para checkout ${checkoutId}`);
return response.data;
} catch (error) {
console.error(`❌ Error en upload-create:`, error.response?.data || error.message);
throw error;
}
};
const formatearFecha = (fecha) => {
if (!fecha || typeof fecha !== 'string') return '';
const año = fecha.slice(0, 4);
const mes = fecha.slice(4, 6);
const dia = fecha.slice(6, 8);
return `${año}-${mes}-${dia}`;
};

26
src/utils/utils.js Normal file
View File

@@ -0,0 +1,26 @@
import tbCredencialesIntegracion from "../database/models/tbCredencialesIntegracion.js";
/**
* Obtiene el token de acceso desde la tabla tb_credenciales_integracion.
*/
export const obtenerToken = async (idsitio = 8) => {
const credencial = await tbCredencialesIntegracion.findOne({
where: { idsitio },
});
if (!credencial || !credencial.token) {
throw new Error(`No hay token disponible para idsitio ${idsitio}`);
}
return {
accessToken: credencial.token,
expiresAt: credencial.expires_at
};
};
export const tokenExpirado = async (expiresAt) => {
const fechaExpiracion = new Date(expiresAt);
const ahora = new Date();
return fechaExpiracion <= ahora;
};