Files
e20fb2a792 feat(db): add MySQL 8.4 support (#4048)
Add MySQL 8.4 compatibility, including a utf8mb4 conversion migration,
hardened foreign key handling, and expanded CI database coverage.

Migrations:
- Add Mysql84Utf8mb4Conversion migration using
  sqlscripts/3.4.3_mysql84_utf8mb4_conversion.sql via executeScript()
- Drop ospos_people composite index before conversion, since its
  prefix length exceeds the 3072-byte key limit under utf8mb4
- Add assertNoCollationCollisions() pre-flight check to fail fast on
  unique values that collide under utf8mb4_unicode_520_ci, instead of
  hitting "Duplicate entry" partway through ALTER TABLE
- Wrap the conversion in try/catch so foreign keys are recreated on
  failure; log restore errors separately so they don't mask the
  original exception
- Throw RuntimeException if re-adding the people index fails
- Remove the re-add of the sales_items_taxes FK in down(), which
  MySQL 8.4 rejects without a unique key
- Revert edits to historical migrations; fix FK handling and table
  names

migration_helper:
- Add shared dropAllForeignKeyConstraints() and
  recreateForeignKeyConstraints(); table/column now optional, dropping
  all FKs in the schema when omitted
- Match CONSTRAINT_SCHEMA when joining KEY_COLUMN_USAGE to
  REFERENTIAL_CONSTRAINTS to avoid cross-schema name collisions
- Rebuild FK lookup with the query builder and bound values instead
  of interpolated SQL

CI/tests:
- Add mysql:5.7, mariadb:10.6 and mariadb:10.11 to PHPUnit matrix
- Use master instead of latest image tag; improve CI robustness
- Mock table() in FailedQueryConnectionTrait so query builder chains
  work against the failing connection

Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
Co-authored-by: aliworksx08 <57456290+aliworksx08@users.noreply.github.com>
Co-authored-by: jekkos <jeroen.peelaerts@gmail.com>
Co-authored-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-19 14:24:51 +04:00

192 lines
8.7 KiB
PHP

<?php
namespace App\Models\Reports;
use Config\OSPOS;
class Summary_payments extends Summary_report
{
/**
* @return array[]
*/
protected function _get_data_columns(): array // TODO: Hungarian notation
{
return [
['trans_group' => lang('Reports.trans_group')],
['trans_type' => lang('Reports.trans_type')],
['trans_sales' => lang('Reports.sales')],
['trans_amount' => lang('Reports.trans_amount')],
['trans_payments' => lang('Reports.trans_payments')],
['trans_refunded' => lang('Reports.trans_refunded')],
['trans_due' => lang('Reports.trans_due')]
];
}
/**
* @param array $inputs
* @return array
*/
public function getData(array $inputs): array
{
$cash_payment = lang('Sales.cash'); // TODO: This is never used. Should it be?
$config = config(OSPOS::class)->settings;
$separator[] = [
'trans_group' => '<HR>',
'trans_type' => '',
'trans_sales' => '',
'trans_amount' => '',
'trans_payments' => '',
'trans_refunded' => '',
'trans_due' => ''
];
$where = ''; // TODO: Duplicated code
// TODO: this needs to be converted to ternary notation
if (empty($config['date_or_time_format'])) {
$where .= 'DATE(sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']);
} else {
$where .= 'sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']));
}
$this->create_summary_payments_temp_tables($where);
$select = '\'' . lang('Reports.trans_sales') . '\' AS trans_group, ';
$select .= '(CASE sale_type WHEN ' . SALE_TYPE_POS . ' THEN \'' . lang('Reports.code_pos')
. '\' WHEN ' . SALE_TYPE_INVOICE . ' THEN \'' . lang('Sales.invoice')
. '\' WHEN ' . SALE_TYPE_RETURN . ' THEN \'' . lang('Sales.return')
. '\' END) AS trans_type, ';
$select .= 'COUNT(sales.sale_id) AS trans_sales, ';
$select .= 'SUM(sumpay_items.trans_amount) AS trans_amount, ';
$select .= 'IFNULL(SUM(sumpay_payments.total_payments),0) AS trans_payments, ';
$select .= 'IFNULL(SUM(sumpay_payments.total_cash_refund),0) AS trans_refunded, ';
$select .= 'SUM(CASE WHEN sumpay_items.trans_amount - IFNULL(sumpay_payments.total_payments,0) > 0 THEN sumpay_items.trans_amount - IFNULL(sumpay_payments.total_payments,0) ELSE 0 END) as trans_due ';
$builder = $this->db->table('ospos_sales AS sales');
$builder->select($select);
$builder->join('sumpay_items_temp AS sumpay_items', 'sales.sale_id = sumpay_items.sale_id', 'left outer');
$builder->join('sumpay_payments_temp AS sumpay_payments', 'sales.sale_id = sumpay_payments.sale_id', 'left outer');
$builder->where('sales.sale_status', COMPLETED);
$this->_where($inputs, $builder);
$builder->groupBy('trans_type');
$sales = $builder->get()->getResultArray();
// At this point in time refunds are assumed to be cash refunds.
$total_cash_refund = 0;
foreach ($sales as $key => $sale_summary) {
if ($sale_summary['trans_refunded'] <> 0) {
$total_cash_refund += $sale_summary['trans_refunded'];
}
}
$select = '\'' . lang('Reports.trans_payments') . '\' AS trans_group, ';
$select .= 'sales_payments.payment_type as trans_type, ';
$select .= 'COUNT(sales.sale_id) AS trans_sales, ';
$select .= 'SUM(payment_amount - cash_refund) AS trans_amount,';
$select .= 'SUM(payment_amount) AS trans_payments,';
$select .= 'SUM(cash_refund) AS trans_refunded, ';
$select .= '0 AS trans_due ';
$builder = $this->db->table('sales AS sales');
$builder->select($select);
$builder->join('sales_payments AS sales_payments', 'sales.sale_id = sales_payments.sale_id', 'left outer');
$builder->where('sales.sale_status', COMPLETED);
$this->_where($inputs, $builder);
$builder->groupBy('sales_payments.payment_type');
$payments = $builder->get()->getResultArray();
// Consider Gift Card as only one type of payment and do not show "Gift Card: 1, Gift Card: 2, etc." in the total
$gift_card_count = 0;
$gift_card_amount = 0;
foreach ($payments as $key => $payment) {
if (str_contains($payment['trans_type'], lang('Sales.giftcard'))) {
$gift_card_count += $payment['trans_sales'];
$gift_card_amount += $payment['trans_amount'];
// Remove the "Gift Card: 1", "Gift Card: 2", etc. payment string
unset($payments[$key]);
}
}
if ($gift_card_count > 0) {
$payments[] = [
'trans_group' => lang('Reports.trans_payments'),
'trans_type' => lang('Sales.giftcard'),
'trans_sales' => $gift_card_count,
'trans_amount' => $gift_card_amount,
'trans_payments' => $gift_card_amount,
'trans_refunded' => 0,
'trans_due' => 0
];
}
return array_merge($sales, $separator, $payments);
}
/**
* @param string $where
* @return void
*/
protected function create_summary_payments_temp_tables(string $where): void
{
// TODO: convert to using QueryBuilder. Use App/Models/Reports/Summary_taxes.php getData() as a reference template
$decimals = totals_decimals();
$trans_amount = 'SUM(CASE WHEN sales_items.discount_type = ' . PERCENT
. " THEN sales_items.quantity_purchased * sales_items.item_unit_price - ROUND(sales_items.quantity_purchased * sales_items.item_unit_price * sales_items.discount / 100, $decimals) "
. ' ELSE sales_items.quantity_purchased * (sales_items.item_unit_price - sales_items.discount) END) AS trans_amount';
$this->db->query(
'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $this->db->prefixTable('sumpay_taxes_temp') .
' (INDEX(sale_id)) ENGINE=MEMORY
(
SELECT sales.sale_id, SUM(sales_taxes.sale_tax_amount) AS total_taxes
FROM ' . $this->db->prefixTable('sales') . ' AS sales
LEFT OUTER JOIN ' . $this->db->prefixTable('sales_taxes') . ' AS sales_taxes
ON sales.sale_id = sales_taxes.sale_id
WHERE ' . $where . ' AND sales_taxes.tax_type = \'1\'
GROUP BY sales.sale_id
)'
);
$this->db->query(
'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $this->db->prefixTable('sumpay_items_temp') .
' (INDEX(sale_id)) ENGINE=MEMORY
(
SELECT sales.sale_id, ' . $trans_amount
. ' FROM ' . $this->db->prefixTable('sales') . ' AS sales '
. 'LEFT OUTER JOIN ' . $this->db->prefixTable('sales_items') . ' AS sales_items '
. 'ON sales.sale_id = sales_items.sale_id '
. 'LEFT OUTER JOIN ' . $this->db->prefixTable('sumpay_taxes_temp') . ' AS sumpay_taxes '
. 'ON sales.sale_id = sumpay_taxes.sale_id '
. 'WHERE ' . $where . ' GROUP BY sales.sale_id
)'
);
$this->db->query('UPDATE ' . $this->db->prefixTable('sumpay_items_temp') . ' AS sumpay_items '
. 'SET trans_amount = trans_amount + IFNULL((SELECT total_taxes FROM ' . $this->db->prefixTable('sumpay_taxes_temp')
. ' AS sumpay_taxes WHERE sumpay_items.sale_id = sumpay_taxes.sale_id),0)');
$this->db->query(
'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $this->db->prefixTable('sumpay_payments_temp') .
' (INDEX(sale_id)) ENGINE=MEMORY
(
SELECT sales.sale_id, COUNT(sales.sale_id) AS number_payments,
SUM(CASE WHEN sales_payments.cash_adjustment = 0 THEN sales_payments.payment_amount ELSE 0 END) AS total_payments,
SUM(CASE WHEN sales_payments.cash_adjustment = 1 THEN sales_payments.payment_amount ELSE 0 END) AS total_cash_adjustment,
SUM(sales_payments.cash_refund) AS total_cash_refund
FROM ' . $this->db->prefixTable('sales') . ' AS sales
LEFT OUTER JOIN ' . $this->db->prefixTable('sales_payments') . ' AS sales_payments
ON sales.sale_id = sales_payments.sale_id
WHERE ' . $where . '
GROUP BY sales.sale_id
)'
);
}
}