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