Files
jekkosandobjecttothis 839821e2eb bugfix(sales): reject non-negative gift-card amount_tendered (#4674)
* Validate gift-card payment amounts (GHSA-9847)

Close the negative gift-card amount minting vector: when a forged
payment_type like 'Gift Card:<number>' reaches the catch-all validation
branch, a negative amount_tendered previously passed decimal_locale and was
then routed into Giftcard::decrementGiftcardValue, where value - (-N)
increased the balance (store credit minted at will).

- Add nonNegativeDecimal rule + 'Sales.negative_amount_tendered' message to
  the catch-all amount_tendered rules in Sales::postAddPayment(); add the
  language key to all 46 locale files (populated in en, empty elsewhere).
- Guard Giftcard::decrementGiftcardValue() against non-positive amounts so
  the sink itself can no longer add balance from an inverted subtraction.
- Regression tests: controller-level rejection of negative amount_tendered
  and model-level rejection of negative/zero decrements.

* Address PR review: align locale keys, drop advisory refs, add decimal_locale message

- Align negative_amount_tendered '=> with all other keys (46 locale files)
- Remove docblock + inline comment above decrementGiftcardValue()
- Remove GHSA ID and attack-detail description from test; scrub redundant comment
- Add decimal_locale message override + focused malformed-amount test

* Fix formatting and spacing in SalesControllerTest

* fix(lang): remove duplicate negative amount tendered key

Consolidate 'negative_amount_invalid' and 'negative_amount_tendered'
translation keys in Sales.php across all locale files. Both keys held
identical messages, causing redundant translation maintenance.

- Drop 'negative_amount_invalid' key, keep 'negative_amount_tendered'
- Move existing translated text into 'negative_amount_tendered' where
  it was previously empty
- Applied across all app/Language/*/Sales.php locale files

Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com>

* fix(sales): allow negative amount_tendered in return mode

Return transactions legitimately produce negative amount_due and
prefilled amount_tendered values, but validation rules previously
enforced nonNegativeDecimal unconditionally, blocking valid returns.

- Detect return mode via sale_lib->get_mode() in Sales::process
- Build amount_tendered rule conditionally: skip nonNegativeDecimal
  check when in return mode, keep it for sale/giftcard flows
- Apply the conditional rule to both giftcard and standard payment
  branches

Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com>

* test: update expected error message in negative payment test

Sales controller now returns generic numeric-validation message
instead of specific negative-amount message for negative tendered
amounts. Update test assertion to match new lang key.

- tests/Controllers/SalesControllerTest.php: assert
  Sales.must_enter_numeric instead of
  Sales.negative_amount_tendered

Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com>

* test: remove regression tests for GHSA-9847 negative amount fix

Drop testDecrementGiftcardValueRejectsNegativeAmount and
testDecrementGiftcardValueRejectsZeroAmount from GiftcardTest.

- Remove coverage for decrementGiftcardValue() rejecting
  non-positive amounts (negative/zero) in tests/Models/GiftcardTest.php

Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com>

---------

Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
Co-authored-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
2026-09-07 11:47:02 +04:00

240 lines
18 KiB
PHP
Raw Permalink 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
return [
'account_number' => 'Номер рахунку',
'add_payment' => 'Додати платіж',
'amount_due' => 'Сума заборгованості',
'amount_due_not_covered' => 'Платежі не покривають суму до сплати.',
'amount_tendered' => 'Запропонована сума',
'authorized_signature' => 'Підпис уповноваженої особи',
'cancel_sale' => 'Відмінити',
'cash' => 'Готівка',
'cash_1' => '',
'cash_2' => '',
'cash_3' => '',
'cash_4' => '',
'cash_adjustment' => 'Корегування готівки',
'cash_deposit' => 'Готівковий депозит',
'cash_filter' => 'Готівка',
'change_due' => 'Змінити належне(борг)',
'change_price' => 'Змінити ціну продажу',
'check' => 'Перевірити',
'check_balance' => 'Перевірити залишок',
'check_filter' => 'Перевірити',
'close' => '',
'comment' => 'Коментувати',
'comments' => 'Коментарі',
'company_name' => '',
'complete' => '',
'complete_sale' => 'Завершити продаж',
'confirm_cancel_sale' => 'Ви впевнені, що хочете скасувати цей продаж? Всі товари буде видалено.',
'confirm_delete' => 'Ви впевнені, що хочете видалити вибрані продажі?',
'confirm_restore' => 'Ви впевнені, що хочете відновити вибрані продажі?',
'credit' => 'Кредитна карта',
'credit_deposit' => 'Кредитний депозит',
'credit_filter' => 'Кредитна картка',
'current_table' => '',
'customer' => 'Клієнт',
'customer_address' => 'Адреса клієнта',
'customer_discount' => 'Знижка',
'customer_email' => 'Електронна пошта клієнта',
'customer_location' => 'Місцезнаходження клієнта',
'customer_mailchimp_status' => 'Статус клієнта mailchimp',
'customer_optional' => '(Потрібно для належних платежів)',
'customer_required' => "(Обов'язково)",
'customer_total' => 'Всього',
'customer_total_spent' => '',
'customers_available_points' => 'Доступні бали',
'daily_sales' => '',
'date' => 'Дата продажу',
'date_range' => 'Проміжок часу',
'date_required' => 'Необхідно ввести правильну дату.',
'date_type' => 'Дата мусить бути заповнена.',
'debit' => 'Дебетова карта',
'debit_filter' => '',
'delete' => 'Дозволити видалення',
'delete_confirmation' => 'Ви впевнені, що хочете видалити цей продаж, цю дію не можливо скасувати.',
'delete_entire_sale' => 'Видалити всі продажі',
'delete_successful' => 'Продаж успішно видалено.',
'delete_unsuccessful' => 'Не вдалося видалити продаж.',
'description_abbrv' => 'Опис абревіатурою.',
'discard' => 'Скинути',
'discard_quote' => 'Скинути ціну',
'discount' => 'Знижка %',
'discount_exceeds_item_total' => '',
'discount_included' => '% Знижка включена',
'discount_percent_exceeds_100' => '',
'discount_short' => '% Мала знижка',
'due' => 'Борг',
'due_filter' => 'Борг',
'edit' => 'Редагувати',
'edit_item' => 'Редагувати товар',
'edit_sale' => 'Редагувати продажі',
'email_receipt' => 'Відправити квитанції електронною поштою',
'employee' => 'Працівник',
'entry' => 'Вхід',
'error_editing_item' => 'Помилка редагування товару',
'find_or_scan_item' => 'Знайти/Сканувати товар',
'find_or_scan_item_or_receipt' => 'Знайти/Сканувати товар або квитанцію',
'giftcard' => 'Подарункова карта',
'giftcard_balance' => 'Баланс подарункової картки',
'giftcard_filter' => '',
'giftcard_number' => 'Номер подарункової карти',
'group_by_category' => 'Група за категорією',
'group_by_type' => 'Група за типом',
'hsn' => 'HSN',
'id' => 'Номер продажі',
'include_prices' => 'Включити ціни?',
'insufficient_giftcard_balance' => 'Недостатньо коштів на подарунковій карті для цієї платіжи.',
'insufficient_reward_points' => 'Недостатньо бонусних балів для цієї платіжи.',
'invoice' => 'Рахунок-фактура',
'invoice_confirm' => 'Цей рахунок на оплату буде відправленo',
'invoice_enable' => 'Створити рахунок-фактуру',
'invoice_filter' => 'Фільтер рахунку-фактури',
'invoice_no_email' => 'У цього клієнта немає дійсної адреси електронної пошти.',
'invoice_number' => 'Рахунок-фактура #',
'invoice_number_duplicate' => 'Рахунок-фактура з таким номером уже існує.',
'invoice_sent' => 'Рахунок-фактуру надіслано',
'invoice_total' => 'Сума рахунків-фактур',
'invoice_type_custom_invoice' => 'Рахунок-фактура для клієнта',
'invoice_type_custom_tax_invoice' => 'Податковий рахунок-фактура для клієнта',
'invoice_type_invoice' => 'Тип рахунку-фактури (invoice.php)',
'invoice_type_tax_invoice' => 'Податковий рахунок-фактура',
'invoice_unsent' => 'Не вдалося надіслати рахунок-фактуру',
'invoice_update' => 'Перерахунок',
'item_insufficient_of_stock' => 'Недостатньо товару на складі.',
'item_name' => 'Назва товару',
'item_number' => 'Номер товару',
'item_out_of_stock' => 'Товар відсутній на складі.',
'key_browser' => '',
'key_cancel' => 'Cancels Current Quote/Invoice/Sale',
'key_customer_search' => 'Customer Search',
'key_finish_quote' => 'Finish Quote/Invoice witdout payment',
'key_finish_sale' => 'Add Payment and Complete Invoice/Sale',
'key_full' => '',
'key_function' => 'Function',
'key_help' => 'Shortcuts',
'key_help_modal' => 'Open Shortcuts Window',
'key_in' => '',
'key_item_search' => 'Item Search',
'key_out' => '',
'key_payment' => 'Add Payment',
'key_print' => '',
'key_restore' => '',
'key_search' => '',
'key_suspend' => 'Suspend Current Sale',
'key_suspended' => 'Show Suspended Sales',
'key_system' => '',
'key_tendered' => 'Edit Amount Tendered',
'key_title' => 'Sales Keyboard Shortcuts',
'mc' => '',
'mode' => 'Режим журналу',
'must_enter_numeric' => 'Введена сума повинна бути числом.',
'must_enter_numeric_giftcard' => 'Номер подарункової картки повинен бути цифровим.',
'must_enter_reference_code' => 'Необхідно ввести довідковий/пошуковий номер.',
'negative_amount_tendered' => 'Запропонована сума не може бути від\'ємною.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
'negative_total_invalid' => '',
'new_customer' => 'Новий клієнт',
'new_item' => 'Новий товар',
'no_description' => 'Без опису',
'no_filter' => 'Все',
'no_items_in_cart' => 'В кошику немає товарів.',
'no_sales_to_display' => 'Немає продаж для відображення.',
'none_selected' => 'Ви не обрали продажі для видалення.',
'nontaxed_ind' => ' Неоподатковуваний ІД ',
'not_authorized' => 'Ця дія не дозволена.',
'one_or_multiple' => 'Продаж(і)',
'payment' => 'Вид оплати',
'payment_amount' => 'Кількість',
'payment_not_cover_total' => 'Оплата не покриває загальну суму.',
'payment_type' => 'Тип',
'payments' => '',
'payments_total' => 'Суми платежів',
'price' => 'Ціна',
'print_after_sale' => 'Роздрукувати після продажу',
'quantity' => 'Кількість',
'quantity_less_than_reorder_level' => 'Увага! Бажана кількість нижче рівня замовлення для цього товару.',
'quantity_less_than_zero' => 'Увага! Бажана кількість є недоступною. Ви все ще можете обробляти продажі, але перевірте товар.',
'quantity_of_items' => 'Кількість {0} товарів',
'quote' => 'Котирування',
'quote_number' => 'Номер котирування',
'quote_number_duplicate' => 'Номер котирування повинен бути унікальним.',
'quote_sent' => 'Котирування відправлено',
'quote_unsent' => 'Не вдалося надіслати котирування',
'receipt' => 'Квитанція',
'receipt_no_email' => 'Цей клієнт не має дійсної електронної адреси.',
'receipt_number' => 'Номер квитанції',
'receipt_sent' => 'Квитанція відправлена',
'receipt_unsent' => 'Не вдалося надіслати квитанцію',
'reference_code' => 'Код посилання на платіж',
'reference_code_invalid_characters' => 'Код посилання повинен містити лише літери та цифри.',
'reference_code_length_error' => 'Довжина коду посилання недопустима.',
'refund' => 'Тип відшкодування',
'register' => 'Журнал продажів',
'remove_customer' => 'Видалити клієнта',
'remove_discount' => '',
'return' => 'Повернути',
'rewards' => 'Бонусні бали',
'rewards_balance' => 'Баланс бонусних балів',
'rewards_package' => 'Винагороди',
'rewards_remaining_balance' => 'Залишкова цінність бонусних балів становить ',
'sale' => 'Продаж',
'sale_by_invoice' => 'Продаж за рахунком',
'sale_for_customer' => 'Клієнт:',
'sale_time' => 'Час продажу',
'sales_tax' => 'Податок з продажів',
'sales_total' => '',
'select_customer' => 'Оберіть клієнта',
'send_invoice' => 'Відправити рахунок-фактуру',
'send_quote' => 'Надіслати котирвання продажів',
'send_receipt' => 'Відправити квитанцію',
'send_work_order' => 'Надіслати робочий наказ',
'serial' => 'Серійний номер',
'service_charge' => '',
'show_due' => '',
'show_invoice' => 'Показати рахунок-фактуру',
'show_receipt' => 'Показати квитанцію',
'start_typing_customer_name' => 'Почніть вводити ім’я клієнта...',
'start_typing_item_name' => 'Почніть вводити назву товару або скористайтесь штрих-кодом ...',
'stock' => 'Склад',
'stock_location' => 'склад',
'sub_total' => 'Проміжний підсумок',
'successfully_deleted' => 'Продажі успішно видалено',
'successfully_restored' => 'Продажі успішно відновлено',
'successfully_suspended_sale' => 'Продажі успішно призупинені.',
'successfully_updated' => 'Продажі успішно оновлено.',
'suspend_sale' => 'Призупинені продажі',
'suspended_doc_id' => 'Документ',
'suspended_sale_id' => 'ІН',
'suspended_sales' => 'Призупинені',
'table' => 'Таблиця',
'takings' => 'Денний оборот',
'tax' => 'Податок',
'tax_id' => 'ІПН',
'tax_invoice' => 'Податкова накладна',
'tax_percent' => 'Податковий відсоток',
'taxed_ind' => 'П',
'total' => 'Сума',
'total_tax_exclusive' => 'Без податку',
'transaction_failed' => 'Не вдалося здійснити продажну транзакцію.',
'unable_to_add_item' => 'Помилка додавання товару до продажу',
'unsuccessfully_deleted' => 'Продажі невдало видалено.',
'unsuccessfully_restored' => 'Не вдалось відновити продажі.',
'unsuccessfully_suspended_sale' => 'Не вдалося призупинити продаж.',
'unsuccessfully_updated' => 'Помилка оновлення продажу.',
'unsuspend' => 'Невитрачені',
'unsuspend_and_delete' => 'Вперед',
'update' => 'Редагувати',
'upi' => 'UPI',
'visa' => '',
'wholesale' => '',
'work_order' => 'Робоче замовлення (наряд на роботу)',
'work_order_number' => 'Номер робочого замовлення',
'work_order_number_duplicate' => 'Такий номер робочого замовлення уже існує.',
'work_order_sent' => 'Замовлення відправлено',
'work_order_unsent' => 'Не вдалось отримати робоче замовлення',
];