bon(['items' => [...], 'payments' => [...]], 'comanda-10471'); * * * ### UNDE RULEAZA PHP-UL - CITESTE INAINTE SA ALEGI ARHITECTURA ### * * Poleo Fiscal asculta IMPLICIT doar pe calculatorul lui (127.0.0.1). Ascultarea in retea se * aprinde separat, din Setari -> API. Asta inseamna trei asezari posibile, si doar doua merg: * * 1. PHP pe ACELASI calculator cu Poleo Fiscal -> merge din start, fara nicio setare. * 2. PHP pe alt calculator din ACEEASI retea locala -> merge daca aprinzi "Ascultare in retea" * si pui adresa statiei in POLEO_URL. * 3. PHP pe un server din INTERNET (portal in cloud) -> NU ajunge la casa. Niciun token nu * rezolva asta: statia sta in reteaua * magazinului, in spatele routerului. * * Pentru cazul 3 nu deschide portul catre internet. Solutia este sa lasi BROWSERUL casierului sa * vorbeasca cu statia lui: pui adresa portalului in Setari -> API -> "Origini permise", iar * JavaScript-ul paginii cheama http://127.0.0.1:9111 direct. Serverul tau nu atinge casa deloc, * si nici nu are de ce: bonul se tipareste acolo unde sta clientul. * * * ### DE CITIT INAINTE DE PRIMA LINIE DE COD IN PRODUCTIE ### * * Bonul fiscal (inscrisul tiparit) este foarte complicat de anulat si comporta anumite * constrangeri si consecinte. De aceea, orice operatiune care lasa urma trebuie trimisa cu o * CHEIE DE OPERATIUNE (`Idempotency-Key`): daca reteaua cade dupa ce bonul a iesit si programul * tau reincearca, cheia opreste al doilea bon si iti intoarce rezultatul primului. * * Intr-un portal web asta conteaza mai mult decat intr-un program desktop: utilizatorul da F5, * da de doua ori pe buton, sau browserul reincearca singur cererea. Cheia trebuie sa fie a TA si * STABILA pentru aceeasi vanzare - numarul comenzii, id-ul bonului din gestiune - NU ceva generat * la fiecare incercare (uniqid(), time(), random_bytes()), fiindca atunci fiecare reincercare ar * fi alta operatiune si ar iesi al doilea bon. * * Nu trimite "probe" catre casa de marcat. Daca vrei sa te asiguri ca ai stabilit conexiunea cu * aceasta, fara ca asta sa fie o recomandare, poti cere un raport X (Atentie! NU Z). */ /** * Un refuz al statiei, cu tot cu explicatia ei. * * `cod` - statusul HTTP (400, 401, 422, 501, 503 ...); 0 daca n-am ajuns deloc la statie * `codEroare` - codul STABIL din raspuns ("bad_request", "device_error", "unsupported" ...); * pe ASTA te ramifici in cod, nu pe mesaj: mesajul este pentru om si se poate * schimba fara sa strice integrarea ta * `mesaj` - propozitia pentru om * `detalii` - ce a mai trimis statia (ex. ce terminal a refuzat) * `corp` - raspunsul intreg, asa cum a venit */ class EroareApi extends RuntimeException { /** @var int */ public $cod; /** @var string */ public $codEroare; /** @var string */ public $mesaj; /** @var array */ public $corp; /** @var array */ public $detalii; public function __construct($cod, $mesaj, array $corp = [], $codEroare = '', array $detalii = []) { parent::__construct(sprintf('HTTP %s: %s', $cod, $mesaj), (int) $cod); $this->cod = (int) $cod; $this->mesaj = (string) $mesaj; $this->corp = $corp; $this->codEroare = (string) $codEroare; $this->detalii = $detalii; } /** * 501 + `unsupported` inseamna "aparatul asta nu are operatiunea". Este un raspuns DEFINITIV: * nu se reincearca niciodata, nici acum nici maine. Solutia este alt terminal, alt model, sau * alt contract cu banca. Ramificatia se face pe COD, nu pe mesaj. */ public function esteNesuportat() { return $this->cod === 501 || $this->codEroare === 'unsupported'; } } final class PoleoFiscal { const VERSIUNE = 'v1'; /** * Casa poate tipari zeci de secunde la un raport din memoria fiscala. Un timeout de 5 secunde, * obisnuit in clientii HTTP, ar taia exact operatiunile lungi - si ai crede ca au esuat, desi * ele continua la casa. * * ATENTIE IN WEB: `max_execution_time` din php.ini este, de obicei, 30 de secunde. Un raport * periodic sau un export de jurnal depaseste asta si PHP-ul TAU moare inainte sa raspunda * statia - iar tu vezi o pagina alba, desi la casa totul a mers. Pentru operatiunile lungi * ridica limita (`set_time_limit(0)`) sau muta-le intr-o coada. */ const TIMEOUT_SCURT = 15; const TIMEOUT_LUNG = 180; /** @var string */ private $baza; /** @var string */ private $token; /** @var string */ private $casa; /** ultimul status HTTP primit; util cand ceri raspunsuri binare si nu ai un dict de citit */ public $ultimulStatus = 0; /** * @param string $baza adresa statiei, ex. http://127.0.0.1:9111 * @param string $token Setari -> API -> "Copiaza tokenul" * @param string $casa NUI-ul casei. Gol = statia alege casa ei obisnuita - ca good practice nu recomandam asta, ci recomandam sa identifici casa dupa NUI (id-ul unic al fiecarui aparat fiscal; similar cu seria de sasiu -VIN- a unei masini), acesta fiind singurul element ce nu se va schimba indiferent de orice alta variabila. */ public function __construct($baza = 'http://127.0.0.1:9111', $token = '', $casa = '') { $this->baza = rtrim((string) $baza, '/'); $this->token = (string) $token; $this->casa = (string) $casa; } /** Citeste configurarea din variabile de mediu - la fel ca `poleo_demo.py`. */ public static function dinMediu() { return new self( getenv('POLEO_URL') ?: 'http://127.0.0.1:9111', getenv('POLEO_TOKEN') ?: 'PUNE-TOKENUL-AICI', getenv('POLEO_CASA') ?: '' ); } public function casa() { return $this->casa; } public function cuCasa($nui) { $c = clone $this; $c->casa = (string) $nui; return $c; } // == transportul ====== /** * O cerere catre Poleo Fiscal -> array (sau octeti bruti, cu $brut = true). $cheie devine antetul `Idempotency-Key`; pune-o la TOT ce lasa urma: bonuri, rapoarte, numerar. Foloseste un identificator al TAU - numarul comenzii, al bonului din gestiune. * @return array|string * @throws EroareApi */ public function cere($metoda, $cale, array $corp = null, $cheie = null, $timeout = self::TIMEOUT_SCURT, $brut = false) { $url = sprintf('%s/api/%s/%s', $this->baza, self::VERSIUNE, ltrim($cale, '/')); $date = $corp === null ? null : json_encode($corp, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $antete = [ 'Authorization: Bearer ' . $this->token, 'Accept: application/json', ]; if ($date !== null) { $antete[] = 'Content-Type: application/json; charset=utf-8'; } if ($cheie !== null && $cheie !== '') { $antete[] = 'Idempotency-Key: ' . $cheie; } list($status, $octeti, $eroareRetea) = function_exists('curl_init') ? $this->prinCurl($metoda, $url, $antete, $date, $timeout) : $this->prinFluxuri($metoda, $url, $antete, $date, $timeout); $this->ultimulStatus = $status; if ($eroareRetea !== null) { throw new EroareApi(0, sprintf( 'nu am ajuns la statie (%s). Ruleaza Poleo Fiscal? E pornit API-ul din Setari -> API? Adresa %s este buna?', $eroareRetea, $this->baza )); } if ($status >= 400) { $d = json_decode((string) $octeti, true); $d = is_array($d) ? $d : []; $er = isset($d['error']) ? $d['error'] : null; if (is_array($er)) { // forma normala a API-ului throw new EroareApi( $status, isset($er['message']) ? $er['message'] : 'eroare', $d, isset($er['code']) ? $er['code'] : '', isset($er['details']) && is_array($er['details']) ? $er['details'] : [] ); } $mesaj = isset($d['message']) ? $d['message'] : (is_string($er) ? $er : 'eroare'); throw new EroareApi($status, $mesaj, $d); } if ($brut) { return (string) $octeti; } $d = json_decode((string) $octeti, true); return is_array($d) ? $d : []; } /** @return array{0:int,1:string,2:?string} status, corp, eroare de retea */ private function prinCurl($metoda, $url, array $antete, $date, $timeout) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $metoda, CURLOPT_HTTPHEADER => $antete, CURLOPT_RETURNTRANSFER => true, // raspunsul poate fi BINAR (PDF-uri, fisiere de jurnal) - nu-l atingem CURLOPT_BINARYTRANSFER => true, CURLOPT_TIMEOUT => (int) $timeout, CURLOPT_CONNECTTIMEOUT => 5, // statia raspunde si cu 4xx/5xx purtand explicatia in corp; o vrem, nu o pierdem CURLOPT_FAILONERROR => false, ]); if ($date !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $date); } $corp = curl_exec($ch); $eroare = $corp === false ? curl_error($ch) : null; $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); return [$status, $corp === false ? '' : $corp, $eroare]; } /** varianta fara cURL, pe fluxuri; acelasi rezultat, doar ceva mai lenta */ private function prinFluxuri($metoda, $url, array $antete, $date, $timeout) { $ctx = stream_context_create(['http' => [ 'method' => $metoda, 'header' => implode("\r\n", $antete), 'content' => $date === null ? '' : $date, 'timeout' => (int) $timeout, // fara asta, PHP arunca un warning la 4xx si NU-ti da corpul cu explicatia 'ignore_errors' => true, ]]); $corp = @file_get_contents($url, false, $ctx); $status = 0; if (isset($http_response_header) && is_array($http_response_header)) { foreach ($http_response_header as $linie) { if (preg_match('#^HTTP/\S+\s+(\d{3})#', $linie, $m)) { $status = (int) $m[1]; } } } if ($corp === false) { $ultima = error_get_last(); return [0, '', $ultima ? $ultima['message'] : 'conexiune esuata']; } return [$status, $corp, null]; } /* Filtrul de casa pentru listele de evidenta. Pe o statie cu mai multe case, listele intorc salvarile TUTUROR caselor; daca apoi ceri detaliile uneia care nu este a ta, primesti 404 - nu fiindca este stricat ceva, ci fiindca ai intrebat de casa altcuiva. Deci filtram de la inceput. */ private function peCasa() { return $this->casa !== '' ? '&case=' . rawurlencode($this->casa) : ''; } private function nui($nui = null) { $t = $nui !== null && $nui !== '' ? $nui : $this->casa; return rawurlencode($t !== '' ? $t : '-'); } // == 1-4: legatura si ce stie statia ====== /** singura cale care nu cere token */ public function sanatate() { return $this->cere('GET', 'health'); } /* Metodele pe care le stie ACEASTA statie. Cere-le la pornirea integrarii tale, nu presupune: o statie mai veche poate sa nu aiba ultimele metode, iar asa afli din cod, nu dintr-o eroare la client. */ public function metode() { return $this->cere('GET', 'methods'); } /* Ce case sunt configurate - si de unde iei NUI-ul. Identificarea dupa denumire functioneaza si ea, dar daca un utilizator decide intr-o zi sa ii schimbe denumirea, vei avea probleme. Similar si cu id-ul inregistrarii din baza: un utilizator poate edita si inversa conexiunile. */ public function casele() { return $this->cere('GET', 'cases'); } /* Starea casei, intrebata ACUM: daca aparatul raspunde (nu daca este configurat), plus ce a raspuns el despre sine - model, serie, hartie, capac. Fiecare apel vorbeste chiar cu aparatul, deci intreaba la intervale rezonabile, nu in bucla si nu la fiecare incarcare de pagina. Intr-un portal, pune-l pe un buton sau pe un interval de zeci de secunde. Ce NU iti da: starea monitorizarii si ziua fiscala. Pe alea le afli din `probleme()`. */ public function stare($nui = null) { return $this->cere('GET', 'cases/' . $this->nui($nui) . '/status', null, null, self::TIMEOUT_LUNG); } /* Cotele de TVA si sloturile de incasare, citite din casa. Nu sunt doua liste separate in doua metode: amandoua vin din configuratia aparatului, fiindca amandoua sunt programate de tehnicianul care a fiscalizat casa. Astea sunt adevarul - nu presupune ca 21% este mereu grupa A sau ca 0 = NUMERAR, pentru ca vei altera datele fiscal-contabile. */ public function config($nui = null) { return $this->cere('GET', 'cases/' . $this->nui($nui) . '/config', null, null, self::TIMEOUT_LUNG); } // == bonuri ====== /* Trimite un bon fiscal. $receipt este structura cu `items` si `payments`. Raspunsul vine DUPA ce bonul a iesit; doua-trei secunde este normal. `receipt_no` din raspuns este numarul bonului tiparit de casa. Pune-l in evidenta ta - este legatura dintre vanzarea din programul tau si hartia pe care o are clientul. */ public function bon(array $receipt, $cheie, $case = null) { $corp = ['receipt' => $receipt]; $tinta = $case !== null && $case !== '' ? $case : $this->casa; if ($tinta !== '') { $corp['case'] = $tinta; } return $this->cere('POST', 'receipts', $corp, $cheie, self::TIMEOUT_LUNG); } /* Lista bonurilor din evidenta. Raspunsul imediat al lui `bon()` iti da numarul; lista asta este pentru verificare ulterioara: reconciliere de sfarsit de zi, sau aflarea a ce s-a intamplat cu un bon care a intrat in coada cat timp casa era deconectata. */ public function bonuri($limit = 20) { return $this->cere('GET', 'receipts?limit=' . (int) $limit . $this->peCasa()); } /* un bon anume, cu liniile lui */ public function bonul($id) { return $this->cere('GET', 'receipts/' . rawurlencode($id)); } /* Ce s-a incasat cu cardul, inclusiv platile ramase neclarificate. Pastreaza `auth` si `rrn`: cu ele reconciliezi cu extrasul bancii si cu ele se lamureste o disputa. Plata INCERTA este cazul care conteaza: terminalul a primit comanda, dar raspunsul nu a ajuns inapoi, deci cardul poate sa fi fost debitat. Nu relua plata automat - se verifica pe terminal si se confirma de un om. */ public function platiCard($limit = 5) { return $this->cere('GET', 'card-payments?limit=' . (int) $limit . $this->peCasa()); } /** copia unui bon deja tiparit */ public function copie($cheie) { return $this->cere('POST', 'copy', [], $cheie, self::TIMEOUT_LUNG); } /* Anularea bonului DESCHIS (nefinalizat) de pe casa. Ca si deschiderea sertarului, functioneaza si cu licenta expirata - sunt cele doua supape ale programului. */ public function anuleazaBonDeschis($cheie) { return $this->cere('POST', 'void', [], $cheie, self::TIMEOUT_LUNG); } /* Ce nu e in regula ACUM: monitorizare oprita, ziua fiscala deschisa de prea multe ore, hartie, legatura. Iti spune inclusiv de cate ore este deschisa ziua si daca urmeaza blocarea la 24 de ore. Intr-un portal, asta e ce pui in bara de stare, nu starea casei. */ public function probleme() { return $this->cere('GET', 'problems'); } // == rapoarte ====== /* Un raport tiparit pe casa. $fel: "X", "Z", "periodic", "nomenclator" (vezi `metode()` pentru ce stie chiar statia ta). $optiuni completeaza corpul: `from`/`to`/`detailed` la periodic. ATENTIE LA Z: pune toate valorile pe 0 si inchide ziua fiscala. Nu-l pune pe un buton la indemana si nu-l chema din cron fara sa stii ce faci. Cheia de operatiune pentru Z se face, de obicei, din ziua respectiva ("z-2026-08-17") - asa, doua apasari nu dau doua rapoarte. */ public function raport($fel, array $optiuni = [], $cheie = null) { return $this->cere('POST', 'reports', array_merge(['kind' => $fel], $optiuni), $cheie, self::TIMEOUT_LUNG); } /* rapoartele tiparite, din evidenta */ public function rapoarte($limit = 3) { return $this->cere('GET', 'reports?limit=' . (int) $limit . $this->peCasa()); } // == numerar si sertar ====== /* Miscare de numerar. $fel: "in" (depunere, fond de rulaj) sau "out" (retragere, ridicare incasari). Se tipareste pe casa, deci merge cu cheie de operatiune. */ public function numerar($fel, $suma, $motiv, $cheie) { return $this->cere('POST', 'cash', [ 'kind' => $fel, 'amount' => round((float) $suma, 2), 'reason' => (string) $motiv, ], $cheie, self::TIMEOUT_LUNG); } public function miscariNumerar($limit = 3) { return $this->cere('GET', 'cash?limit=' . (int) $limit . $this->peCasa()); } /** soldul de sertar, citit din evidenta */ public function sertar() { return $this->cere('GET', 'drawer'); } /* Deschide sertarul, fara sa tipareasca nimic. Necesita un sertar legat electric la casa. Unele case nu pot gestiona sertarele de bani (desi au port dedicat), iar altele au functia dezactivata din soft. */ public function deschideSertar($cheie) { return $this->cere('POST', 'drawer', [], $cheie, self::TIMEOUT_LUNG); } // == totaluri, TVA, ANAF, jurnal electronic ====== public function totaluri() { return $this->cere('GET', 'totals'); } public function cote() { return $this->cere('GET', 'vat'); } /** starea raportarii catre ANAF */ public function anaf() { return $this->cere('GET', 'anaf'); } /* Exportul jurnalului electronic pentru ANAF, pe un interval. Datele se dau in forma zi-luna-an ("01-08-2026"), asa cum le asteapta statia - NU in ISO. */ public function exportJe($de_la, $pana_la, $cheie) { return $this->cere('POST', 'anaf', ['from' => $de_la, 'to' => $pana_la], $cheie, self::TIMEOUT_LUNG); } /* salvarile de jurnal electronic */ public function je($limit = 3) { return $this->cere('GET', 'je?limit=' . (int) $limit . $this->peCasa()); } /** fisierele unei salvari */ public function jeSalvare($id) { return $this->cere('GET', 'je/' . rawurlencode($id)); } /* Continutul unui fisier de jurnal. Raspuns BINAR, nu JSON: se scrie pe disc asa cum vine. Daca il servesti mai departe catre browser, trimite-l cu `Content-Type` binar si NU-l trece prin nicio functie de text. */ public function jeFisier($id, $nume) { return $this->cere('GET', 'je/' . rawurlencode($id) . '/files/' . rawurlencode($nume), null, null, self::TIMEOUT_LUNG, true); } // == statia, licenta, terminalele ====== public function licenta() { return $this->cere('GET', 'license'); } /* Cartea de vizita a statiei. AMPRENTA este numele sub care te cunosc partenerii. Nu are legatura cu licenta si nu se schimba singura - nici la actualizare, nici daca schimbi portul, IP-ul sau casa. */ public function statie() { return $this->cere('GET', 'station'); } /* Ce terminale de card sunt configurate si CE STIE fiecare. `can` spune ce operatiuni ARE fiecare terminal: retur, anulare din borderoul curent, inchidere de zi. Citeste-l INAINTE sa promiti ceva clientului - nu costa nimic, se citeste din driver, nu de pe fir. Ce este `false` acolo nu se reincearca niciodata. */ public function terminale() { return $this->cere('GET', 'terminals'); } // == terminalul de card (POS bancar) ====== /* CERE BANII de pe card; nu tipareste bonul. Fluxul obisnuit este: `pos()` intai, si doar daca terminalul a aprobat, `bon()` cu plata pe card. Daca raspunsul terminalului nu ajunge inapoi, plata ramane INCERTA. Nu o relua automat: cardul poate fi deja debitat. Programul nu vede niciodata numarul cardului, CVV-ul sau PIN-ul: trimite o suma si primeste un rezultat cu RRN, cod de autorizare si STAN. Datele cardului raman intre terminal si banca. */ public function pos($suma, $cheie, array $optiuni = []) { return $this->cere('POST', 'pos', array_merge(['amount' => round((float) $suma, 2)], $optiuni), $cheie, self::TIMEOUT_LUNG); } /* Retur pe card: scoate bani REALI din contul comerciantului, deci nu se face automat - il ceri cu o decizie in spate. E altceva decat anularea: returul merge oricand, chiar si dupa ce borderoul s-a inchis. Verifica `can.refund` la `terminale()` INAINTE. */ public function posRetur($suma, $cheie, array $optiuni = []) { return $this->cere('POST', 'pos/refund', array_merge(['amount' => round((float) $suma, 2)], $optiuni), $cheie, self::TIMEOUT_LUNG); } /* Anularea unei plati din borderoul CURENT. Are nevoie de STAN (numarul tranzactiei din terminal, primit in raspunsul platii) SI de suma exacta. Terminalele cer amandoua tocmai ca sa nu anulezi alta tranzactie. Pastreaza STAN-ul la fiecare plata, altfel nu mai ai cu ce anula. */ public function posAnulare($stan, $suma, $cheie, array $optiuni = []) { return $this->cere('POST', 'pos/void', array_merge([ 'stan' => (string) $stan, 'amount' => round((float) $suma, 2), ], $optiuni), $cheie, self::TIMEOUT_LUNG); } /* Inchiderea de zi a terminalului (settlement): trimite borderoul la banca si porneste decontarea. NU are legatura cu raportul Z al casei de marcat - sunt doua zile diferite, a terminalului si a aparatului fiscal. */ public function posInchidereZi($cheie) { return $this->cere('POST', 'pos/settlement', [], $cheie, self::TIMEOUT_LUNG); } // == returul pe bon emis, si documentele lui ====== /* Restituirea banilor pe un bon deja emis. Aici NU se tipareste bon de storno: bonul fiscal initial ramane valabil, iar ce iese este retragerea de numerar si HARTIA care justifica banii scosi din sertar (procesul-verbal). `acknowledged` trebuie sa fie true si inseamna ca OMUL a decis returul. Nu-l pune pe true din oficiu intr-un portal: e o operatiune cu bani reali si cu urmari fiscale, deci trebuie sa existe o apasare constienta in spatele lui, nu un camp ascuns in formular. */ public function retur($bon, $motiv, $cerut_de, array $optiuni = [], $cheie = null) { return $this->cere('POST', 'returns', array_merge([ 'receipt' => $bon, 'reason' => (string) $motiv, 'requested_by' => (string) $cerut_de, 'acknowledged' => true, ], $optiuni), $cheie, self::TIMEOUT_LUNG); } public function retururi($limit = 3) { return $this->cere('GET', 'returns?limit=' . (int) $limit . $this->peCasa()); } // == declaratia de bon gresit ====== /* motivele pe care le accepta statia; nu le inventa, ia-le de aici */ public function motiveDeclaratie() { return $this->cere('GET', 'declaration-reasons'); } public function declaratie($bon, array $motive, $operator, array $optiuni = [], $cheie = null) { return $this->cere('POST', 'declarations', array_merge([ 'receipt' => $bon, 'reasons' => $motive, 'operator' => (string) $operator, 'cash_out' => false, 'acknowledged' => true, ], $optiuni), $cheie, self::TIMEOUT_LUNG); } public function declaratii($limit = 3) { return $this->cere('GET', 'declarations?limit=' . (int) $limit . $this->peCasa()); } /** PDF-ul unei declaratii. Raspuns BINAR, ca si fisierele de jurnal. */ public function declaratieFisier($id) { return $this->cere('GET', 'declarations/' . rawurlencode($id) . '/file', null, null, self::TIMEOUT_LUNG, true); } // == posta, firma, setari ====== public function posta($limit = 3) { return $this->cere('GET', 'emails?limit=' . (int) $limit); } public function firma() { return $this->cere('GET', 'company'); } /* Setarile statiei. La citire, PAROLELE NU IES NICIODATA: campurile secrete vin cu `value: null` si `secret: true`, plus un semn ca sunt completate. Deci nu incerca sa faci un formular care "reincarca" parola existenta - n-o vei primi. */ public function setari() { return $this->cere('GET', 'settings'); } /* Scrie setari. Se accepta doar cele din lista ALBA a statiei; restul sunt refuzate cu motiv, una cate una, si nimic nu se aplica pe jumatate. Tabul API in intregime este blocat DINADINS: API-ul nu isi poate schimba propriile conditii de existenta - trebuie ca omul sa-l porneasca si sa-l accepte din aplicatie inainte, si abia dupa aceea este autorizat. */ public function scrieSetari(array $setari) { return $this->cere('POST', 'settings', ['settings' => $setari]); } }