// EnvíoClick (ADR 0009 §2, ya anticipado desde el modelo de direcciones // del checkout): cotización y generación de guía real por peso/volumen, // reemplazando la tarifa fija de $15.000 COP. Contrato REST confirmado en // vivo contra la cuenta real del usuario (apiDoc real, // apidoc.envioclickpro.com.co/api_data.json -- la SPA de la doc no // renderiza sin JS, pero ese archivo generado por apidoc trae la spec // completa) y una llamada de prueba real que devolvió 200 con tarifas // reales de PIBOX/COORDINADORA/ENVIA/RAPPI/TCC. const BASE_URL = "https://api.envioclickpro.com.co/api/v2"; export interface EnvioClickCredentials { apiKey: string; } export interface EnvioClickPackage { weight: number; height: number; length: number; // "widht" es un typo real de la API (confirmado en el body de // /shipment de la doc oficial), no un error nuestro -- solo se usa acá, // en el request de /quotation el campo si se llama "width" (confirmado // tambien contra la doc real, son dos endpoints con esa inconsistencia). width: number; } export interface EnvioClickRate { idRate: number; idProduct: number; product: string; idCarrier: number; carrier: string; flete: number; minimumInsurance: number; extraInsurance: number; deliveryDays: number; cod: boolean; } export interface EnvioClickAddress { daneCode: string; address: string; } async function envioclickRequest( credentials: EnvioClickCredentials, path: string, body: Record, ): Promise> { const response = await fetch(`${BASE_URL}${path}`, { method: "POST", headers: { "Content-Type": "application/json", // Header real confirmado contra la doc oficial y una llamada en // vivo -- "Authorization" a secas (sin "Bearer"), NO // "AuthorizationKey" como usaba el plugin de WordPress de // referencia (ese era para la API v1, desactualizada). Authorization: credentials.apiKey, }, body: JSON.stringify(body), }); const text = await response.text(); let json: Record; try { json = JSON.parse(text); } catch { throw new Error(`Respuesta no-JSON de EnvíoClick (HTTP ${response.status}): ${text.slice(0, 300)}`); } if (json.status !== "OK") { throw new Error(`EnvíoClick respondió con error: ${JSON.stringify(json).slice(0, 400)}`); } return json; } // Solo 1 paquete por cotizacion (limite real documentado y confirmado: // "min 1, max 1" -- cada vez menos transportadoras ofrecen multi-paquete). export async function quoteShipment( credentials: EnvioClickCredentials, params: { pkg: EnvioClickPackage; description: string; contentValue: number; origin: EnvioClickAddress; destination: EnvioClickAddress; }, ): Promise { const json = await envioclickRequest(credentials, "/quotation", { packages: [params.pkg], description: params.description, contentValue: params.contentValue, origin: params.origin, destination: params.destination, }); const data = json.data as Record; return (data.rates as EnvioClickRate[]) ?? []; } export interface EnvioClickPerson { company: string; firstName: string; lastName: string; email: string; phone: string; address: string; suburb: string; crossStreet: string; reference: string; daneCode: string; } export interface CreateGuideResult { guide: string; url: string; tracker: string; idOrder: number; } export async function createGuide( credentials: EnvioClickCredentials, sandbox: boolean, params: { idRate: number; myShipmentReference: string; externalOrderId: string; pkg: EnvioClickPackage; description: string; contentValue: number; origin: EnvioClickPerson; destination: EnvioClickPerson; }, ): Promise { // /shipment_sandbox es el UNICO endpoint con modo de pruebas real (la // cotizacion siempre usa tarifas reales de las transportadoras, no // tiene sandbox) -- confirmado en la doc oficial ("Deberás cambiar a // la URL de producción para que puedas generar guías reales"). const path = sandbox ? "/shipment_sandbox" : "/shipment"; const json = await envioclickRequest(credentials, path, { idRate: params.idRate, myShipmentReference: params.myShipmentReference, external_order_id: params.externalOrderId, requestPickup: false, pickupDate: new Date().toISOString().slice(0, 10), insurance: false, description: params.description, contentValue: params.contentValue, packages: [ { weight: params.pkg.weight, height: params.pkg.height, length: params.pkg.length, widht: params.pkg.width, }, ], origin: params.origin, destination: params.destination, }); const data = json.data as Record; return { guide: String(data.guide ?? ""), url: String(data.url ?? ""), tracker: String(data.tracker ?? ""), idOrder: Number(data.idOrder ?? 0), }; } // Hasta 50 ordenes por llamada (limite real documentado). export async function cancelGuides(credentials: EnvioClickCredentials, idOrders: number[]): Promise { await envioclickRequest(credentials, "cancellation/batch/order", { idOrders }); } // código DANE estandar (5 digitos: 2 de departamento + 3 de municipio, // ej. 11001 = Bogota, 5001 = Medellin -- mismo id que ya usa // lib/data/colombia.ts del storefront, confirmado contra su propio // comentario) con el padding real que espera EnvioClick (8 caracteres, // confirmado contra su propio ejemplo real: "11001000" para Bogota = // "11001" + "000"). export function toEnvioClickDaneCode(divipolaCityId: number): string { return `${String(divipolaCityId).padStart(5, "0")}000`; }