econt.ts raw

   1  import { getDb } from './db';
   2  
   3  const DEMO_URL = 'https://demo.econt.com/ee/services';
   4  const PROD_URL = 'https://ee.econt.com/services';
   5  
   6  interface EcontConfig {
   7    username: string;
   8    password: string;
   9    senderCity: string;
  10    senderPostCode: string;
  11    testMode: boolean;
  12  }
  13  
  14  function getEcontConfig(): EcontConfig & { senderStreet: string; senderStreetNum: string; senderQuarter: string } | null {
  15    const db = getDb();
  16    const rows = db.query("SELECT key, value FROM content WHERE key LIKE 'econt_%'").all() as { key: string; value: string }[];
  17    const cfg: Record<string, string> = {};
  18    for (const r of rows) cfg[r.key] = r.value;
  19    if (!cfg.econt_username || !cfg.econt_password) return null;
  20    return {
  21      username: cfg.econt_username,
  22      password: cfg.econt_password,
  23      senderCity: cfg.econt_sender_city || 'София',
  24      senderPostCode: cfg.econt_sender_postcode || '',
  25      testMode: cfg.econt_test_mode === '1',
  26      senderStreet: cfg.econt_sender_street || '',
  27      senderStreetNum: cfg.econt_sender_street_num || '',
  28      senderQuarter: cfg.econt_sender_quarter || '',
  29    };
  30  }
  31  
  32  function authHeader(cfg: EcontConfig): string {
  33    return 'Basic ' + Buffer.from(`${cfg.username}:${cfg.password}`).toString('base64');
  34  }
  35  
  36  function apiUrl(cfg: EcontConfig): string {
  37    return cfg.testMode ? DEMO_URL : PROD_URL;
  38  }
  39  
  40  function cityObj(name: string, postCode: string = '') {
  41    const c: any = { country: { code3: 'BGR' }, name };
  42    if (postCode) c.postCode = postCode;
  43    return c;
  44  }
  45  
  46  function senderAddress(cfg: EcontConfig) {
  47    const addr: any = { city: cityObj(cfg.senderCity, cfg.senderPostCode) };
  48    const street = (cfg as any).senderStreet;
  49    const num = (cfg as any).senderStreetNum;
  50    const quarter = (cfg as any).senderQuarter;
  51    if (street) addr.street = street;
  52    if (num) addr.num = num;
  53    if (quarter) addr.quarter = quarter;
  54    return addr;
  55  }
  56  
  57  export interface EcontCalculateResult {
  58    totalPrice: number;
  59    currency: string;
  60    priceBreakdown: { type: string; description: string; price: number; currency: string }[];
  61    error?: string;
  62  }
  63  
  64  // Econt pricing via LabelService.createLabel with mode=calculate.
  65  // Uses city names directly (no ID resolution needed).
  66  // codAmountCents is the COD amount in euro cents (converted to EUR for the API).
  67  export async function econtCalculate(
  68    receiverCity: string,
  69    weightKg: number,
  70    codAmountCents: number = 0,
  71    officeDelivery: boolean = false,
  72  ): Promise<EcontCalculateResult> {
  73    const cfg = getEcontConfig();
  74    if (!cfg) return { totalPrice: 0, currency: 'EUR', priceBreakdown: [], error: 'econt not configured' };
  75  
  76    const label: any = {
  77      senderAddress: { city: cityObj(cfg.senderCity) },
  78      shipmentType: 'PACK',
  79      weight: Math.max(weightKg, 0.1),
  80      packCount: 1,
  81    };
  82  
  83    if (officeDelivery) {
  84      label.receiverOfficeCode = receiverCity;
  85    } else {
  86      label.receiverAddress = { city: cityObj(receiverCity) };
  87    }
  88  
  89    const cdEur = codAmountCents > 0 ? parseFloat((codAmountCents / 100).toFixed(2)) : 0;
  90    if (cdEur > 0) {
  91      label.services = { cdAmount: cdEur, cdType: 'CASH', cdCurrency: 'EUR' };
  92    }
  93  
  94    try {
  95      const resp = await fetch(`${apiUrl(cfg)}/Shipments/LabelService.createLabel.json`, {
  96        method: 'POST',
  97        headers: {
  98          'Content-Type': 'application/json',
  99          'Authorization': authHeader(cfg),
 100        },
 101        body: JSON.stringify({ mode: 'calculate', label }),
 102      });
 103      if (!resp.ok) return { totalPrice: 0, currency: 'EUR', priceBreakdown: [], error: `econt API error: ${resp.status}` };
 104      const data = await resp.json() as any;
 105      const result = data?.label;
 106      if (!result) return { totalPrice: 0, currency: 'EUR', priceBreakdown: [], error: 'unexpected econt response' };
 107      if (result.error) return { totalPrice: 0, currency: 'EUR', priceBreakdown: [], error: result.error };
 108  
 109      return {
 110        totalPrice: Number(result.totalPrice) || 0,
 111        currency: result.currency || 'EUR',
 112        priceBreakdown: (result.services || []).map((s: any) => ({
 113          type: s.type,
 114          description: s.description,
 115          price: Number(s.price) || 0,
 116          currency: s.currency || 'EUR',
 117        })),
 118      };
 119    } catch (e: any) {
 120      return { totalPrice: 0, currency: 'EUR', priceBreakdown: [], error: e.message || 'econt request failed' };
 121    }
 122  }
 123  
 124  export interface EcontLabelResult {
 125    shipmentNumber?: string;
 126    pdfURL?: string;
 127    totalPrice?: number;
 128    currency?: string;
 129    error?: string;
 130  }
 131  
 132  export async function econtCreateLabel(
 133    receiverName: string,
 134    receiverPhone: string,
 135    receiverCity: string,
 136    receiverAddress: string,
 137    receiverPostCode: string,
 138    receiverQuarter: string,
 139    weightKg: number,
 140    officeDelivery: boolean,
 141    codAmountCents: number,
 142  ): Promise<EcontLabelResult> {
 143    const cfg = getEcontConfig();
 144    if (!cfg) return { error: 'econt not configured' };
 145  
 146    const label: any = {
 147      senderAddress: senderAddress(cfg),
 148      shipmentType: 'PACK',
 149      weight: Math.max(weightKg, 0.1),
 150      packCount: 1,
 151      shipmentDescription: 'Чай',
 152    };
 153  
 154    if (officeDelivery) {
 155      label.receiverOfficeCode = receiverAddress;
 156    } else {
 157      label.receiverAddress = {
 158        city: cityObj(receiverCity, receiverPostCode),
 159      };
 160      if (receiverAddress) label.receiverAddress.street = receiverAddress;
 161      if (receiverQuarter) label.receiverAddress.quarter = receiverQuarter;
 162    }
 163    label.receiverClient = { name: receiverName, phones: receiverPhone ? [receiverPhone] : [] };
 164  
 165    const cdEur = codAmountCents > 0 ? parseFloat((codAmountCents / 100).toFixed(2)) : 0;
 166    if (cdEur > 0) {
 167      label.services = { cdAmount: cdEur, cdType: 'CASH', cdCurrency: 'EUR' };
 168    }
 169  
 170    try {
 171      const resp = await fetch(`${apiUrl(cfg)}/Shipments/LabelService.createLabel.json`, {
 172        method: 'POST',
 173        headers: {
 174          'Content-Type': 'application/json',
 175          'Authorization': authHeader(cfg),
 176        },
 177        body: JSON.stringify({ mode: 'create', label }),
 178      });
 179      if (!resp.ok) {
 180        const err = await resp.json().catch(() => ({}));
 181        return { error: (err as any)?.message || `econt API error: ${resp.status}` };
 182      }
 183      const data = await resp.json() as any;
 184      const result = data?.label;
 185      if (!result) return { error: 'unexpected econt response' };
 186      if (result.error) return { error: result.error };
 187  
 188      return {
 189        shipmentNumber: result.shipmentNumber,
 190        pdfURL: result.pdfURL,
 191        totalPrice: Number(result.totalPrice) || 0,
 192        currency: result.currency || 'EUR',
 193      };
 194    } catch (e: any) {
 195      return { error: e.message || 'econt request failed' };
 196    }
 197  }
 198