Ejemplos

Ejemplos de código

Snippets de los flujos más comunes en tres lenguajes. Todos asumen:

export FE_API_KEY_ID="fak_a1b2c3d4e5f6"
export FE_API_SECRET="abcd1234ef56..."
export FE_API_BASE="https://digimart-api-v2.appworkcloud.com/api/v1/fe"

1. Cliente completo con caché de JWT

// fe-api-client.js
let cached = { token: null, expiry: 0 };
 
async function getToken() {
  if (cached.token && Date.now() < cached.expiry - 60_000) {
    return cached.token;
  }
  const resp = await fetch(`${process.env.FE_API_BASE}/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      apiKeyId: process.env.FE_API_KEY_ID,
      apiSecret: process.env.FE_API_SECRET,
    }),
  });
  if (!resp.ok) throw new Error(`Login failed: ${resp.status}`);
  const { accessToken, expiresIn } = await resp.json();
  cached = { token: accessToken, expiry: Date.now() + expiresIn * 1000 };
  return accessToken;
}
 
async function authedFetch(path, opts = {}) {
  const token = await getToken();
  return fetch(`${process.env.FE_API_BASE}${path}`, {
    ...opts,
    headers: {
      ...(opts.headers || {}),
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
  });
}
 
export { authedFetch };

2. Emitir un e-CF y esperar el resultado

import { authedFetch } from './fe-api-client.js';
 
async function emitInvoice(payload, clientRequestId) {
  const resp = await authedFetch('/invoices', {
    method: 'POST',
    body: JSON.stringify({
      format: 'digimart',
      clientRequestId,
      payload,
    }),
  });
  if (!resp.ok) {
    const err = await resp.json();
    throw new Error(`POST /invoices failed: ${err.message}`);
  }
  const { dgiiLogId } = await resp.json();
  return dgiiLogId;
}
 
async function waitForResult(dgiiLogId, opts = {}) {
  const { maxAttempts = 15, baseDelayMs = 1000 } = opts;
  for (let i = 0; i < maxAttempts; i++) {
    const resp = await authedFetch(`/invoices/${dgiiLogId}`);
    const data = await resp.json();
 
    if (data.status === 'Aceptado' || data.status === 'Aceptado Condicional') {
      return { success: true, ...data };
    }
    if (data.status === 'Rechazado') {
      return { success: false, ...data };
    }
    if (data.response?.status === 'error') {
      throw new Error(`Permanent error: ${data.response.error}`);
    }
    // queued / En Proceso → backoff exponential
    await new Promise(r => setTimeout(r, baseDelayMs * Math.pow(1.5, i)));
  }
  throw new Error('Timeout esperando estado final');
}
 
// Uso:
const dgiiLogId = await emitInvoice(myPayload, 'order-12345');
const result = await waitForResult(dgiiLogId);
console.log('Estado:', result.status, 'NCF:', result.response?.encf);

3. Dashboard de uso (últimos 30 días)

async function getUsageStats(days = 30) {
  const resp = await authedFetch(`/usage?days=${days}`);
  const { items } = await resp.json();
 
  const totals = items.reduce((acc, day) => ({
    emissionsAccepted: acc.emissionsAccepted + day.emissionsAccepted,
    emissionsRejected: acc.emissionsRejected + day.emissionsRejected,
    requestsTotal: acc.requestsTotal + day.requestsTotal,
  }), { emissionsAccepted: 0, emissionsRejected: 0, requestsTotal: 0 });
 
  const successRate = (totals.emissionsAccepted /
    (totals.emissionsAccepted + totals.emissionsRejected) * 100).toFixed(2);
 
  console.log(`Últimos ${days} días:`);
  console.log(`  ${totals.emissionsAccepted} aceptadas`);
  console.log(`  ${totals.emissionsRejected} rechazadas (${100 - successRate}%)`);
  console.log(`  ${totals.requestsTotal} requests totales`);
}

4. Monitor de presupuesto

const BUDGET_USD = 100; // alerta si proyección excede este monto
 
async function checkBudget() {
  const resp = await authedFetch('/billing/current-period');
  const { period, emissionsAccepted, subtotal, currency } = await resp.json();
 
  console.log(`Período ${period}:`);
  console.log(`  ${emissionsAccepted} emisiones`);
  console.log(`  Proyección: ${currency} ${subtotal.toFixed(3)}`);
 
  if (subtotal > BUDGET_USD) {
    await notifySlack({
      text: `⚠️ FE API consumo este mes ($${subtotal.toFixed(2)}) excedió budget ($${BUDGET_USD})`,
    });
  }
}
 
// Correr cada hora en un cron
setInterval(checkBudget, 60 * 60 * 1000);

5. Webhook handler de tu lado (post-emisión)

La FE API no emite webhooks todavía. El patrón es polling con GET /invoices/:dgiiLogId. Si necesitas webhooks, escríbenos.

Patrón recomendado: en tu DB local guarda el dgiiLogId junto a tu factura interna. Un cron cada 1-5 minutos itera sobre los que están en pending y actualiza estados:

async function syncPendingEmissions() {
  const pending = await db.query(
    `SELECT id, dgii_log_id FROM facturas
     WHERE estado_dgii IN ('pending', 'En Proceso')
     LIMIT 100`
  );
 
  for (const row of pending) {
    const resp = await authedFetch(`/invoices/${row.dgii_log_id}`);
    const data = await resp.json();
    if (data.status && data.status !== 'queued' && data.status !== 'En Proceso') {
      await db.query(
        `UPDATE facturas SET estado_dgii = $1, qr_code = $2, ncf_seguro = $3
         WHERE id = $4`,
        [data.status, data.qrCodeUrl, data.securityCode, row.id]
      );
    }
  }
}