Files
checkprestabox/checkprestabox.php
2025-09-23 08:54:02 +03:00

445 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
if (!defined('_PS_VERSION_')) {
exit;
}
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class CheckPrestaBox extends Module
{
public const API_URL = 'https://api.checkbox.in.ua';
public function __construct()
{
$this->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');
$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')) {
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() wont 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');
}
}