feat(pacifictrans): enable dual SuperAdmin numbers and connect DIAN billing hub to production URL
This commit is contained in:
parent
3dc430e4e3
commit
cd1f7190be
@ -1494,6 +1494,9 @@
|
||||
<button type="button" id="btn-billing-run-cut" class="btn-primary" style="padding: 0.6rem 1.25rem; font-size: 0.85rem; width: auto; background: linear-gradient(135deg, #10b981, #059669);">
|
||||
⚡ Ejecutar Corte Mensual Ahora
|
||||
</button>
|
||||
<a href="https://facturacion.almeira.com.co" target="_blank" rel="noreferrer" class="btn-primary" style="padding: 0.6rem 1.25rem; font-size: 0.85rem; width: auto; background: rgba(16, 185, 129, 0.15); border: 1px solid #10b981; color: #34d399; text-decoration: none; display: inline-flex; align-items: center; gap: 0.4rem;">
|
||||
🏛️ Facturación Electrónica Oficial
|
||||
</a>
|
||||
<button type="button" id="btn-billing-download-csv" class="btn-toggle-payment" style="padding: 0.6rem 1.25rem; font-size: 0.85rem; width: auto; background: rgba(56, 189, 248, 0.15); border-color: #38bdf8; color: #38bdf8;">
|
||||
📥 Descargar CSV para DIAN
|
||||
</button>
|
||||
@ -1591,11 +1594,12 @@
|
||||
<th>Cantidad Viajes</th>
|
||||
<th>Estado Facturación</th>
|
||||
<th>Fecha Cierre</th>
|
||||
<th>Acción DIAN</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-billing-cierres-body">
|
||||
<tr>
|
||||
<td colspan="9" style="text-align: center; padding: 2rem; color: #94a3b8;">
|
||||
<td colspan="10" style="text-align: center; padding: 2rem; color: #94a3b8;">
|
||||
Cargando información fiscal...
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -5218,11 +5218,63 @@ function renderMonthlyBillingTable(cierres, mes, anio) {
|
||||
<td style="font-size: 0.8rem; color: #94a3b8;">
|
||||
${fechaCierreFmt}
|
||||
</td>
|
||||
<td>
|
||||
${isApi || c.numero_factura ? `
|
||||
<div style="display: flex; flex-direction: column; gap: 3px;">
|
||||
<span style="font-family: monospace; font-size: 0.75rem; color: #34d399; font-weight: 700;">
|
||||
${c.numero_factura || 'Factura DIAN'}
|
||||
</span>
|
||||
<a href="${c.pdf_url || ('https://facturacion.almeira.com.co/api/v1/invoices/' + (c.numero_factura || '') + '/pdf')}" target="_blank" rel="noreferrer" style="font-size: 0.72rem; color: #38bdf8; text-decoration: underline; cursor: pointer;">
|
||||
📄 Ver PDF
|
||||
</a>
|
||||
</div>
|
||||
` : `
|
||||
<button type="button" class="btn-emit-invoice-dian" data-id="${id}" style="padding: 4px 8px; font-size: 0.75rem; background: linear-gradient(135deg, #10b981, #059669); color: #fff; border: none; border-radius: 6px; font-weight: 700; cursor: pointer; display: flex; align-items: center; gap: 3px;">
|
||||
⚡ Facturar
|
||||
</button>
|
||||
`}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
attachBillingItemCheckboxListeners();
|
||||
attachBillingActionListeners();
|
||||
}
|
||||
|
||||
function attachBillingActionListeners() {
|
||||
const emitBtns = document.querySelectorAll('.btn-emit-invoice-dian');
|
||||
emitBtns.forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const id = btn.getAttribute('data-id');
|
||||
if (!confirm('¿Deseas emitir la Factura Electrónica oficial ante la DIAN para este cierre?')) {
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Emitiendo...';
|
||||
try {
|
||||
const res = await fetch('/api/admin/billing/emitir-factura', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ idCierre: id })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.success) {
|
||||
alert(`✅ Factura ${data.factura.fullNumber} emitida con éxito ante la DIAN.`);
|
||||
window.fetchMonthlyBillingReport();
|
||||
} else {
|
||||
alert(`Error al emitir factura: ${data.error || 'Fallo de emisión'}`);
|
||||
btn.disabled = false;
|
||||
btn.textContent = '⚡ Facturar';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error de conexión con el servidor fiscal.');
|
||||
btn.disabled = false;
|
||||
btn.textContent = '⚡ Facturar';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function attachBillingItemCheckboxListeners() {
|
||||
|
||||
@ -1494,6 +1494,9 @@
|
||||
<button type="button" id="btn-billing-run-cut" class="btn-primary" style="padding: 0.6rem 1.25rem; font-size: 0.85rem; width: auto; background: linear-gradient(135deg, #10b981, #059669);">
|
||||
⚡ Ejecutar Corte Mensual Ahora
|
||||
</button>
|
||||
<a href="https://facturacion.almeira.com.co" target="_blank" rel="noreferrer" class="btn-primary" style="padding: 0.6rem 1.25rem; font-size: 0.85rem; width: auto; background: rgba(16, 185, 129, 0.15); border: 1px solid #10b981; color: #34d399; text-decoration: none; display: inline-flex; align-items: center; gap: 0.4rem;">
|
||||
🏛️ Facturación Electrónica Oficial
|
||||
</a>
|
||||
<button type="button" id="btn-billing-download-csv" class="btn-toggle-payment" style="padding: 0.6rem 1.25rem; font-size: 0.85rem; width: auto; background: rgba(56, 189, 248, 0.15); border-color: #38bdf8; color: #38bdf8;">
|
||||
📥 Descargar CSV para DIAN
|
||||
</button>
|
||||
@ -1591,11 +1594,12 @@
|
||||
<th>Cantidad Viajes</th>
|
||||
<th>Estado Facturación</th>
|
||||
<th>Fecha Cierre</th>
|
||||
<th>Acción DIAN</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-billing-cierres-body">
|
||||
<tr>
|
||||
<td colspan="9" style="text-align: center; padding: 2rem; color: #94a3b8;">
|
||||
<td colspan="10" style="text-align: center; padding: 2rem; color: #94a3b8;">
|
||||
Cargando información fiscal...
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -5218,11 +5218,63 @@ function renderMonthlyBillingTable(cierres, mes, anio) {
|
||||
<td style="font-size: 0.8rem; color: #94a3b8;">
|
||||
${fechaCierreFmt}
|
||||
</td>
|
||||
<td>
|
||||
${isApi || c.numero_factura ? `
|
||||
<div style="display: flex; flex-direction: column; gap: 3px;">
|
||||
<span style="font-family: monospace; font-size: 0.75rem; color: #34d399; font-weight: 700;">
|
||||
${c.numero_factura || 'Factura DIAN'}
|
||||
</span>
|
||||
<a href="${c.pdf_url || ('https://facturacion.almeira.com.co/api/v1/invoices/' + (c.numero_factura || '') + '/pdf')}" target="_blank" rel="noreferrer" style="font-size: 0.72rem; color: #38bdf8; text-decoration: underline; cursor: pointer;">
|
||||
📄 Ver PDF
|
||||
</a>
|
||||
</div>
|
||||
` : `
|
||||
<button type="button" class="btn-emit-invoice-dian" data-id="${id}" style="padding: 4px 8px; font-size: 0.75rem; background: linear-gradient(135deg, #10b981, #059669); color: #fff; border: none; border-radius: 6px; font-weight: 700; cursor: pointer; display: flex; align-items: center; gap: 3px;">
|
||||
⚡ Facturar
|
||||
</button>
|
||||
`}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
attachBillingItemCheckboxListeners();
|
||||
attachBillingActionListeners();
|
||||
}
|
||||
|
||||
function attachBillingActionListeners() {
|
||||
const emitBtns = document.querySelectorAll('.btn-emit-invoice-dian');
|
||||
emitBtns.forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const id = btn.getAttribute('data-id');
|
||||
if (!confirm('¿Deseas emitir la Factura Electrónica oficial ante la DIAN para este cierre?')) {
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Emitiendo...';
|
||||
try {
|
||||
const res = await fetch('/api/admin/billing/emitir-factura', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ idCierre: id })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.success) {
|
||||
alert(`✅ Factura ${data.factura.fullNumber} emitida con éxito ante la DIAN.`);
|
||||
window.fetchMonthlyBillingReport();
|
||||
} else {
|
||||
alert(`Error al emitir factura: ${data.error || 'Fallo de emisión'}`);
|
||||
btn.disabled = false;
|
||||
btn.textContent = '⚡ Facturar';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error de conexión con el servidor fiscal.');
|
||||
btn.disabled = false;
|
||||
btn.textContent = '⚡ Facturar';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function attachBillingItemCheckboxListeners() {
|
||||
|
||||
@ -660,6 +660,22 @@ const server = http.createServer(async (req, res) => {
|
||||
return sendJson(200, resultado);
|
||||
}
|
||||
|
||||
// 13.5.4. Emitir Factura Electrónica oficial DIAN para un cierre
|
||||
if (pathname === '/api/admin/billing/emitir-factura' && req.method === 'POST') {
|
||||
const body = await getJsonBody();
|
||||
const idCierre = body.idCierre || body.id;
|
||||
if (!idCierre) {
|
||||
return sendJson(400, { success: false, error: 'idCierre es requerido' });
|
||||
}
|
||||
|
||||
try {
|
||||
const resultado = await billingConsolidationService.emitirFacturaDian(idCierre);
|
||||
return sendJson(200, resultado);
|
||||
} catch (err: any) {
|
||||
return sendJson(500, { success: false, error: err.message || 'Error al emitir factura DIAN' });
|
||||
}
|
||||
}
|
||||
|
||||
// 14. Servidor de Archivos Estáticos para la Plataforma Web & APK
|
||||
const serveStaticFile = (filePath: string, contentType: string): boolean => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
|
||||
@ -171,8 +171,9 @@ export class AuthService {
|
||||
user = (await Promise.race([dbQuery, timeoutPromise])) as any;
|
||||
|
||||
if (!user) {
|
||||
const defaultSuperId = cleanPhone === SUPER_ADMIN_PHONE ? 'usr-superadmin-platform' : `usr-superadmin-${cleanPhone}`;
|
||||
const createPromise = User.create({
|
||||
appwriteId: isSuper ? 'usr-superadmin-platform' : `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
|
||||
appwriteId: isSuper ? defaultSuperId : `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
|
||||
nombre: isSuper ? (params.nombre || 'Superadministrador Principal') : (params.nombre || `Usuario ${cleanPhone.slice(-4)}`),
|
||||
telefono: cleanPhone,
|
||||
email: userEmail || (isSuper ? SUPER_ADMIN_EMAIL : undefined),
|
||||
|
||||
@ -425,6 +425,116 @@ export class BillingConsolidationService {
|
||||
ids: uniqueIds,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Emite la Factura Electrónica oficial ante la DIAN comunicándose con el Hub Central de Facturación.
|
||||
*/
|
||||
public async emitirFacturaDian(idCierre: string): Promise<any> {
|
||||
const HUB_URL = process.env.FACTURACION_HUB_URL || 'https://facturacion.almeira.com.co';
|
||||
const TENANT_ID = process.env.PACIFICTRANS_TENANT_ID || 'd0000002-0000-0000-0000-000000000002';
|
||||
const API_KEY = process.env.PACIFICTRANS_FISCAL_API_KEY || 'pt_live_key_33c9a184ef10';
|
||||
|
||||
let cierre: any = null;
|
||||
if (isDatabaseConnected()) {
|
||||
try {
|
||||
cierre = await CierreFacturacionMensual.findOne({ id: idCierre });
|
||||
} catch (err) {}
|
||||
}
|
||||
if (!cierre) {
|
||||
cierre = localBillingStore.findById(idCierre);
|
||||
}
|
||||
|
||||
if (!cierre) {
|
||||
throw new Error(`Cierre no encontrado con ID ${idCierre}`);
|
||||
}
|
||||
|
||||
const total = Number(cierre.total_comisiones_acumuladas || 0);
|
||||
const subtotal = Math.round(total / 1.19);
|
||||
|
||||
const payload = {
|
||||
tenantId: TENANT_ID,
|
||||
prefix: 'PACT',
|
||||
customer: {
|
||||
identificationType: '13',
|
||||
identificationNumber: String(cierre.nit_o_cedula || '222222222222').replace(/\D/g, '').slice(-10) || '2222222222',
|
||||
legalOrFullName: cierre.razon_social || 'Conductor Pacific Trans',
|
||||
email: cierre.correo_electronico || 'facturacion@pacifictrans.com',
|
||||
phone: '3167455877',
|
||||
address: 'Terminal de Transportes Cali / Buenaventura',
|
||||
cityName: 'Buenaventura',
|
||||
departmentCode: '76',
|
||||
cityCode: '76109',
|
||||
taxRegime: '49',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
sku: 'SERV-COM-TRANS',
|
||||
description: `Comisión de intermediación de software y despacho de viajes - Período ${cierre.mes_calendario || cierre.mes}/${cierre.anio_calendario || cierre.anio} (${cierre.total_viajes_realizados} viajes)`,
|
||||
quantity: 1,
|
||||
unitPrice: subtotal,
|
||||
taxPercentage: 19,
|
||||
taxTypeCode: '01',
|
||||
discountAmount: 0,
|
||||
},
|
||||
],
|
||||
paymentMethod: {
|
||||
paymentMeansCode: '48',
|
||||
paymentDueDate: new Date().toISOString().split('T')[0],
|
||||
},
|
||||
notes: `Liquidación mensual de comisiones ID: ${cierre.id}`,
|
||||
};
|
||||
|
||||
const res = await fetch(`${HUB_URL}/api/v1/invoices`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': API_KEY,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.success) {
|
||||
throw new Error(data.message || 'Error al emitir factura en el Hub Fiscal');
|
||||
}
|
||||
|
||||
const factura = data.data;
|
||||
|
||||
localBillingStore.actualizarFacturaDian(idCierre, {
|
||||
numero_factura: factura.fullNumber,
|
||||
cufe: factura.cufe,
|
||||
pdf_url: `${HUB_URL}/api/v1/invoices/${factura.id}/pdf`,
|
||||
});
|
||||
|
||||
if (isDatabaseConnected()) {
|
||||
try {
|
||||
await CierreFacturacionMensual.updateOne(
|
||||
{ id: idCierre },
|
||||
{
|
||||
$set: {
|
||||
estado_facturacion: 'enviado_api',
|
||||
numero_factura: factura.fullNumber,
|
||||
cufe: factura.cufe,
|
||||
pdf_url: `${HUB_URL}/api/v1/invoices/${factura.id}/pdf`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
cierreId: idCierre,
|
||||
factura: {
|
||||
id: factura.id,
|
||||
fullNumber: factura.fullNumber,
|
||||
cufe: factura.cufe,
|
||||
pdfUrl: `${HUB_URL}/api/v1/invoices/${factura.id}/pdf`,
|
||||
xmlUrl: `${HUB_URL}/api/v1/invoices/${factura.id}/xml`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const billingConsolidationService = BillingConsolidationService.getInstance();
|
||||
|
||||
|
||||
@ -15,6 +15,9 @@ export interface LocalCierreRecord {
|
||||
total_viajes_realizados: number;
|
||||
estado_facturacion: EstadoFacturacion;
|
||||
fecha_cierre: string; // ISO string
|
||||
numero_factura?: string;
|
||||
cufe?: string;
|
||||
pdf_url?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@ -158,6 +161,29 @@ export class LocalBillingStore {
|
||||
return { updatedCount: updated.length, records: updated };
|
||||
}
|
||||
|
||||
public actualizarFacturaDian(
|
||||
id: string,
|
||||
info: { numero_factura: string; cufe: string; pdf_url: string }
|
||||
): LocalCierreRecord | null {
|
||||
this.ensureInitialized();
|
||||
for (const [key, r] of this.recordsMap.entries()) {
|
||||
if (r.id === id) {
|
||||
const mod: LocalCierreRecord = {
|
||||
...r,
|
||||
estado_facturacion: 'enviado_api' as any,
|
||||
numero_factura: info.numero_factura,
|
||||
cufe: info.cufe,
|
||||
pdf_url: info.pdf_url,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
this.recordsMap.set(key, mod);
|
||||
this.persist();
|
||||
return mod;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getAll(): LocalCierreRecord[] {
|
||||
this.ensureInitialized();
|
||||
return Array.from(this.recordsMap.values());
|
||||
|
||||
@ -4,15 +4,22 @@ import { UserRole } from '../models/User';
|
||||
|
||||
export const SUPER_ADMIN_PHONE = '573167455877';
|
||||
export const SUPER_ADMIN_LOCAL_PHONE = '3167455877';
|
||||
export const SUPER_ADMIN_ALT_PHONE = '573106390296';
|
||||
export const SUPER_ADMIN_ALT_LOCAL_PHONE = '3106390296';
|
||||
export const SUPER_ADMIN_EMAIL = 'admin@pacifictrans.almeira.com.co';
|
||||
|
||||
export function isPlatformSuperAdmin(identifier?: string): boolean {
|
||||
if (!identifier) return false;
|
||||
const clean = identifier.replace(/\D/g, '');
|
||||
const lower = identifier.toLowerCase().trim();
|
||||
return (
|
||||
clean === SUPER_ADMIN_PHONE ||
|
||||
clean === SUPER_ADMIN_LOCAL_PHONE ||
|
||||
identifier.toLowerCase().trim() === SUPER_ADMIN_EMAIL
|
||||
clean === SUPER_ADMIN_ALT_PHONE ||
|
||||
clean === SUPER_ADMIN_ALT_LOCAL_PHONE ||
|
||||
lower === SUPER_ADMIN_EMAIL ||
|
||||
lower === 'carlos@almeira.com.co' ||
|
||||
lower === 'innovacion4desarrollo@gmail.com'
|
||||
);
|
||||
}
|
||||
|
||||
@ -162,7 +169,8 @@ class LocalUserStore {
|
||||
const isSuper = isPlatformSuperAdmin(cleanPhone) || (user.email && isPlatformSuperAdmin(user.email));
|
||||
|
||||
const existing = this.findByPhone(cleanPhone) || (user.id ? this.findById(user.id) : null);
|
||||
const userId = existing?.id || user.id || (isSuper ? 'usr-superadmin-platform' : `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`);
|
||||
const defaultSuperId = (cleanPhone === SUPER_ADMIN_PHONE || cleanPhone === SUPER_ADMIN_LOCAL_PHONE) ? 'usr-superadmin-platform' : `usr-superadmin-${cleanPhone}`;
|
||||
const userId = existing?.id || user.id || (isSuper ? defaultSuperId : `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`);
|
||||
|
||||
const nowIso = new Date().toISOString();
|
||||
const record: LocalUserRecord = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user