first commit

This commit is contained in:
O K
2025-09-13 12:09:42 +03:00
commit 72bfbef4a8
6 changed files with 816 additions and 0 deletions

447
checkprestabox.php Normal file
View File

@@ -0,0 +1,447 @@
<?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' => '9.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) * 100, 2);
$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' => $price,
],
'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($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' => $value,
'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');
}
}