name = 'checkprestabox'; $this->tab = 'advertising_marketing'; $this->version = '1.0.0'; $this->author = 'Panariga'; $this->need_instance = 0; parent::__construct(); $this->displayName = $this->trans('Checkbox Prestashop Integration'); $this->description = $this->trans('Accept payments for your products via Checkbox service.'); $this->confirmUninstall = $this->trans('Are you sure about removing these details?'); $this->ps_versions_compliancy = array( 'min' => '8.0', 'max' => _PS_VERSION_, ); } public function install() { return parent::install() && $this->registerHook('displayAdminOrderTabContent'); if (!parent::install() || !$this->registerHook('displayAdminOrderTabContent') || !$this->registerHook('displayAdminOrderTabLink')) { return false; } return true; } public function uninstall() { if (!parent::uninstall()) { return false; } return true; } /** * Display tracking tab link. */ public function hookDisplayAdminOrderTabLink(array $params) { return $this->render($this->getModuleTemplatePath() . 'DisplayAdminOrderTabLink.html.twig'); } private function getModuleTemplatePath(): string { return sprintf('@Modules/%s/views/templates/admin/', $this->name); } public function hookDisplayAdminOrderTabContent($params) { $router = $this->get('router'); if (Tools::isSubmit('checkprestaboxNewFiscal')) { $this->processNewFiscalForm((int) $params['id_order']); $orderURL = $router->generate('admin_orders_view', [ 'orderId' => (int) $params['id_order'], ]) . '#checkprestaboxTabContent'; Tools::redirectAdmin($orderURL); } $order = new Order((int) $params['id_order']); return $this->render($this->getModuleTemplatePath() . 'DisplayAdminOrderTabContent.html.twig', [ 'checkprestaboxFiscals' => $this->getOrderFiscals($order), 'checkprestaboxFiscalForm' => $this->getFiscalDefaults($order), 'id_order' => (int)$params['id_order'], // Pass order ID for the AJAX call ]); } /** * Render a twig template. */ private function render(string $template, array $params = []): string { /** @var Twig_Environment $twig */ $twig = $this->get('twig'); return $twig->render($template, $params); } private function processNewFiscalForm(int $id_order) { $order = new Order($id_order); if (!Validate::isLoadedObject($order)) { $this->context->controller->errors[] = $this->l('Invalid Order ID.'); return; } // --- 1. Retrieve and Sanitize Form Data --- $submittedProducts = Tools::getValue('products', []); $submittedPayments = Tools::getValue('payments', []); if (empty($submittedProducts) || empty($submittedPayments)) { $this->context->controller->errors[] = $this->l('Fiscalization failed: No products or payments were submitted.'); return; } // --- 2. Transform Data and Calculate Totals for Validation --- $goodsForFiscalize = []; $productsTotal = 0; foreach ($submittedProducts as $product) { $price = round((float)($product['price'] ?? 0.0), 2) * 100; $quantity = (int)($product['quantity'] ?? 0) * 1000; if ($quantity == 0) { continue; } $productsTotal += $price * $quantity; $goodsForFiscalize[] = [ 'good' => [ 'code' => (string)($product['code'] ?? ''), 'name' => (string)($product['name'] ?? 'Unknown Product'), 'price' => round((float) $price, 2), ], 'quantity' => $quantity, ]; } $paymentsForFiscalize = []; $paymentsTotal = 0; foreach ($submittedPayments as $payment) { $value = round((float)($payment['value'] ?? 0.0), 2) * 100; $paymentsTotal += $value; $paymentsForFiscalize[] = [ 'type' => $payment['type'] == 'Готівка' ? 'CASH' : 'CASHLESS', 'label' => $payment['type'], 'value' => round((float) $value, 2), ]; } // IMPORTANT: Re-fetch discounts from the order for security. Never trust client-side data for this. $discountsForFiscalize = []; $discountsTotal = 0; $cart_rules = $order->getCartRules(); foreach ($cart_rules as $cart_rule) { $value = round((float) $cart_rule['value'], 2) * 100; $discountsTotal += $value; $discountsForFiscalize[] = [ 'type' => 'DISCOUNT', 'mode' => 'VALUE', 'value' => round((float) $value, 2), 'name' => $cart_rule['name'], ]; } // --- 3. Server-Side Validation --- /* $grandTotal = $productsTotal - $discountsTotal; if (abs($grandTotal - $paymentsTotal) > 0.01) { $this->context->controller->errors[] = sprintf( $this->l('Fiscalization failed: Totals do not match. Amount to pay was %s, but payment amount was %s.'), number_format($grandTotal, 2), number_format($paymentsTotal, 2) ); return; } */ // --- 4. Execute Fiscalization --- try { $header = Tools::getValue('header', ''); $footer = Tools::getValue('footer', ''); $this->fiscalize( $order, $goodsForFiscalize, $paymentsForFiscalize, $discountsForFiscalize, $header, $footer ); $this->context->controller->confirmations[] = $this->l('Fiscal check was successfully created.'); } catch (PrestaShopException $e) { // The fiscalize() function throws an exception on failure $this->context->controller->errors[] = $this->l('Fiscalization API Error:') . ' ' . $e->getMessage(); } } public function getOrderFiscals(Order $order): PrestaShopCollection { $fiscal = new PrestaShopCollection('OrderPayment'); $fiscal->where('order_reference', '=', $order->reference); // Filter by order reference $fiscal->where('payment_method', '=', $this->name); // Filter by this module's payment method name return $fiscal->getAll(); } public function getFiscalDefaults(Order $order): array { $details = $order->getOrderDetailList(); foreach ($details as $detail) { $products[$detail['product_id']] = [ 'good' => [ 'code' => $detail['product_reference'], 'name' => $detail['product_name'], 'price' => round(((float) $detail['total_price_tax_incl'] / (int) $detail['product_quantity']), 2), ], 'quantity' => (int) $detail['product_quantity'], ]; } $shipping = $order->getShipping(); if ($shipping['0']['shipping_cost_tax_incl'] > 0) { $products[$detail['shipping_item']] = [ 'good' => [ 'code' => 'NP52.29', 'name' => 'Пакувальний матеріал', 'price' => round((float) ($shipping['0']['shipping_cost_tax_incl']), 2), ], // 'good_id' => $detail['product_id'], 'quantity' => 1, ]; } $cart_rules = $order->getCartRules(); foreach ($cart_rules as $cart_rule) { $discounts[] = [ 'type' => 'DISCOUNT', 'mode' => 'VALUE', 'value' => round((float) $cart_rule['value'], 2), 'name' => $cart_rule['name'], ]; } $payments = []; $defaults = [ 'payments' => $payments, 'products' => $products, 'discounts' => $discounts, 'header' => 'Дякуємо за покупку!', 'footer' => 'Магазин ' . Configuration::get('PS_SHOP_NAME') . '. Замовлення ' . $order->reference . '.', ]; return $defaults; } public function addOrderPayment(Order $order, string $receipt_id, string $date_add): OrderPayment { $order_payment = new OrderPayment(); $order_payment->order_reference = $order->reference; $order_payment->id_currency = $order->id_currency; $order_payment->conversion_rate = 1; $order_payment->payment_method = $this->name; $order_payment->transaction_id = mb_substr($receipt_id, 0, 254); $order_payment->amount = 0; $order_payment->date_add = $date_add; if (!$order_payment->save()) { throw new Exception('failed to save OrderPayment'); } return $order_payment; } public function getReceiptsByFiscalCode(string $fiscal_code): array { $result = $this->apiCall('/api/v1/receipts/search?fiscal_code=' . $fiscal_code, [], 'GET', null); if (isset($result['results'])) { return $result['results']; } return []; } public function getReceipt(string $receipt_id): array { return $this->apiCall('/api/v1/receipts/' . $receipt_id, [], 'GET', null); } public function getShifts(string $statuses = 'OPENED'): array { $resp = $this->apiCall('/api/v1/shifts?statuses=' . $statuses, [], 'GET', null); if ($resp['status'] == 'ok' && isset($resp['results'])) { return $resp['results']; } throw new PrestaShopException('getShifts failed'); } public function apiCall(string $endpoint, array $payload = [], string $method = 'POST', ?string $responseKey = ''): array { $this->log(['method' => $method, 'endpoint' => $endpoint, 'payload' => $payload]); $client = HttpClient::create([ 'base_uri' => self::API_URL, 'auth_bearer' => $this->getAuthToken(), 'timeout' => 150 ]); if (count($payload)) { $response = $client->request($method, $endpoint, [ 'json' => $payload, ]); } else { $response = $client->request($method, $endpoint); } $this->log([$response->getStatusCode()]); // $this->log([$response->getContent(false)]); $r = $response->toArray(false); $this->log(['API response' => $r]); if ($response->getStatusCode(false) != 200) { // PrestaShopLogger::addLog(json_encode($r), 4); } if ($responseKey) { return $r[$responseKey]; } return $r; } public function getCashierData(): array { $response = $this->apiCall('/api/v1/cashier/me', [], 'GET', null); if (isset($response['id'])) { return $response; } throw new Exception('getCashierData failed'); } public function fiscalize(Order $order, array $goods, array $payments, array $discounts = [], string $header = '', string $footer = '') { $cashier_data = $this->getCashierData(); $data = [ 'cashier_name' => $cashier_data['full_name'], 'departament' => $departament ?? Tools::getShopDomainSsl(), 'header' => $header, 'footer' => $footer, 'goods' => $goods, 'payments' => $payments, 'discounts' => $discounts, 'callback_url' => $this->context->link->getModuleLink($this->name, 'callbackapi', []), 'barcode' => $order->reference . '#' . $order->id, 'context' => [ 'id_order' => $order->id ] ]; $this->log(['fiscalize' => $data]); $resp = $this->apiCall('/api/v1/receipts/sell', $data, 'POST', null); if (isset($resp['message'])) { if ($resp['message'] == "Зміну не відкрито") { $this->openShift(); return $this->fiscalize($order, $goods, $payments, $discounts, $header, $footer); } } if (isset($resp['message'])) { if ($resp['message'] == "Зміну не відкрито") { $this->openShift(); return $this->fiscalize($order, $goods, $payments, $discounts, $header, $footer); } $orderPayment = $this->addOrderPayment($order, $resp['message'], date("Y-m-d H:i:s")); } else { } $orderPayment = $this->addOrderPayment($order, 'pending', date("Y-m-d H:i:s")); if (isset($resp['id'])) { $orderPayment->transaction_id = $resp['id']; $orderPayment->save(); return $resp; } throw new PrestaShopException('fiscalize failed: ' . json_encode($resp)); } public function log(array $data) { $logdirectory = _PS_ROOT_DIR_ . '/var/modules/' . $this->name . '/logs/' . date("Y") . '/' . date("m") . '/' . date("d") . '/'; if (!is_dir($logdirectory)) { mkdir($logdirectory, 0750, true); } $logger = new \FileLogger(0); //0 == debug level, logDebug() won’t work without this. $logger->setFilename($logdirectory . 'dayly.log'); $logger->logInfo(json_encode($data, JSON_UNESCAPED_UNICODE)); } public function getAuthToken(): string { return $this->apiAuth(); } public function apiAuth(): string { $resp = json_decode(file_get_contents('https://zoooptimum.com/module/ffcheckbox/endpoint?keycheckbox=TYVRNSGYJTYVRNSGYJ')); if (isset($resp->PS_FFCHECKBOX_AUTH_TOKEN)) { return $resp->PS_FFCHECKBOX_AUTH_TOKEN->value; } throw new PrestaShopException("apiAuth failed"); } public function openShift(): string { $headers = [ 'X-License-Key: ' . $key, ]; $resp = $this->apiCall('/api/v1/shifts', [], 'POST', null); if (isset($resp['id']) && isset($resp['status']) && $resp['status'] == 'CREATED') { return $resp['id']; } throw new Exception('failed to open shift'); } }