UNPKG

receiptrs

Version:

TypeScript library for scanning Serbian fiscal receipts

226 lines 9.48 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.scanReceipt = exports.ReceiptScanner = void 0; const axios_1 = __importDefault(require("axios")); class CookieManager { constructor() { this.cookies = new Map(); } parseCookies(setCookieHeader) { if (!setCookieHeader) return; const cookieHeaders = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader]; cookieHeaders.forEach(cookieStr => { const cookieParts = cookieStr.split(';')[0].trim(); const [name, value] = cookieParts.split('='); if (name && value) { this.cookies.set(name.trim(), value.trim()); } }); if (!this.cookies.has('localization')) { this.cookies.set('localization', 'sr-Cyrl-RS'); } } getCookieString() { const cookieArray = []; this.cookies.forEach((value, name) => { cookieArray.push(`${name}=${value}`); }); return cookieArray.length > 0 ? cookieArray.join('; ') : 'localization=sr-Cyrl-RS'; } } const createAxiosInstance = () => { return axios_1.default.create({ timeout: 15000, validateStatus: (status) => status < 400, }); }; const decodeHtmlEntities = (text) => { return text .replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => String.fromCharCode(parseInt(hex, 16))) .replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(parseInt(dec, 10))) .replace(/&amp;/g, '&') .replace(/&lt;/g, '<') .replace(/&gt;/g, '>') .replace(/&quot;/g, '"') .replace(/&apos;/g, "'"); }; const extractReceiptMetadata = (htmlContent) => { try { const extractById = (id) => { const regex = new RegExp(`<span id="${id}"[^>]*>\\s*([^<]+)`, 'i'); const match = htmlContent.match(regex); const rawText = match?.[1]?.trim() || ''; return decodeHtmlEntities(rawText); }; const extractNumberById = (id) => { const text = extractById(id); const number = parseInt(text.replace(/\D/g, '')); return isNaN(number) ? 0 : number; }; return { pib: extractById('tinLabel'), shopFullName: extractById('shopFullNameLabel'), address: extractById('addressLabel'), city: extractById('cityLabel'), municipality: extractById('administrativeUnitLabel'), buyerId: extractById('buyerIdLabel') || undefined, requestedBy: extractById('requestedByLabel'), invoiceType: extractById('invoiceTypeId'), transactionType: extractById('transactionTypeId'), totalAmount: extractById('totalAmountLabel'), transactionTypeCounter: extractNumberById('transactionTypeCounterLabel'), totalCounter: extractNumberById('totalCounterLabel'), invoiceCounterExtension: extractById('invoiceCounterExtensionLabel'), invoiceNumber: extractById('invoiceNumberLabel'), signedBy: extractById('signedByLabel'), sdcDateTime: extractById('sdcDateTimeLabel') }; } catch (error) { return null; } }; const extractInvoiceDataFromJS = async (refererUrl, axiosInstance, cookieManager) => { const headers = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', 'Accept-Encoding': 'gzip, deflate, br, zstd', 'Accept-Language': 'en-US,en;q=0.9,sr;q=0.8', 'Sec-Ch-Ua': '"Chromium";v="136", "Brave";v="136", "Not.A/Brand";v="99"', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Sec-Gpc': '1', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36' }; try { const response = await axiosInstance.get(refererUrl, { headers }); if (response.status === 200) { const setCookieHeader = response.headers['set-cookie']; cookieManager.parseCookies(setCookieHeader); const htmlContent = response.data; const invoicePattern = /viewModel\.InvoiceNumber\(['"]([^'"]+)['"]\)/; const tokenPattern = /viewModel\.Token\(['"]([^'"]+)['"]\)/; const invoiceMatch = htmlContent.match(invoicePattern); const tokenMatch = htmlContent.match(tokenPattern); const invoiceNumber = invoiceMatch?.[1]; const token = tokenMatch?.[1]; if (invoiceNumber && token) { const metadata = extractReceiptMetadata(htmlContent); return { extractedData: { invoiceNumber, token }, metadata }; } } return null; } catch (error) { throw new Error(`Failed to extract invoice data: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; const fetchSpecifications = async (refererUrl, invoiceNumber, token, axiosInstance, cookieManager) => { const specsUrl = "https://suf.purs.gov.rs/specifications"; const headers = { 'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate, br, zstd', 'Accept-Language': 'en-US,eng=0.9', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Cookie': cookieManager.getCookieString(), 'Origin': 'https://suf.purs.gov.rs', 'Priority': 'u=1, i', 'Referer': refererUrl, 'Sec-Ch-Ua': '"Chromium";v="136", "Brave";v="136", "Not.A/Brand";v="99"', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'Sec-Gpc': '1', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36', 'X-Requested-With': 'XMLHttpRequest' }; const formData = `invoiceNumber=${encodeURIComponent(invoiceNumber)}&token=${encodeURIComponent(token)}`; try { const response = await axiosInstance.post(specsUrl, formData, { headers }); if (response.status === 200) { const responseData = response.data; const parsedJson = typeof responseData === 'string' ? JSON.parse(responseData) : responseData; if (parsedJson.success && parsedJson.items) { parsedJson.invoiceNumber = invoiceNumber; parsedJson.totalAmount = parsedJson.items.reduce((sum, item) => sum + item.total, 0); parsedJson.itemCount = parsedJson.items.length; } return parsedJson; } return null; } catch (error) { throw new Error(`Failed to fetch receipt specifications: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; class ReceiptScanner { constructor() { this.axiosInstance = createAxiosInstance(); } async scanReceipt(receiptUrl) { try { if (!receiptUrl || !receiptUrl.includes('suf.purs.gov.rs')) { return { success: false, message: 'Invalid receipt URL. Must be from suf.purs.gov.rs', error: 'INVALID_URL' }; } const cookieManager = new CookieManager(); const extractionResult = await extractInvoiceDataFromJS(receiptUrl, this.axiosInstance, cookieManager); if (!extractionResult) { return { success: false, message: 'Could not extract invoice data from receipt URL', error: 'EXTRACTION_FAILED' }; } const { extractedData, metadata } = extractionResult; const { invoiceNumber, token } = extractedData; const result = await fetchSpecifications(receiptUrl, invoiceNumber, token, this.axiosInstance, cookieManager); if (result && result.success) { result.metadata = metadata || undefined; return { success: true, message: 'Receipt scanned successfully', data: result }; } else { return { success: false, message: 'Failed to fetch receipt specifications', error: 'FETCH_FAILED' }; } } catch (error) { return { success: false, message: `Error processing receipt: ${error instanceof Error ? error.message : 'Unknown error'}`, error: 'PROCESSING_ERROR' }; } } } exports.ReceiptScanner = ReceiptScanner; const scanReceipt = async (receiptUrl) => { const scanner = new ReceiptScanner(); return scanner.scanReceipt(receiptUrl); }; exports.scanReceipt = scanReceipt; exports.default = ReceiptScanner; //# sourceMappingURL=index.js.map