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
19 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' => '',
'id' => '№ продажи',
'include_prices' => 'Включить цены?',
'insufficient_giftcard_balance' => 'Недостаточный баланс подарочной карты для этого платежа.',
'insufficient_reward_points' => 'Недостаточно баллов вознаграждения для этого платежа.',
'invoice' => 'Накладная',
'invoice_confirm' => 'Эта накладная будет отправлена',
'invoice_enable' => 'Номер накладной',
'invoice_filter' => 'Накладные',
'invoice_no_email' => 'У этого клиента нет действующего адреса электронной почты.',
'invoice_number' => 'Накладная #',
'invoice_number_duplicate' => 'Номер накладной {0} должен быть уникальным.',
'invoice_sent' => 'Накладная отправлена',
'invoice_total' => 'Общая сумма накладной',
'invoice_type_custom_invoice' => 'Пользовательская накладная (custom_invoice.php)',
'invoice_type_custom_tax_invoice' => 'Пользовательская налоговая накладная (custom_tax_invoice.php)',
'invoice_type_invoice' => 'Накладная (invoice.php)',
'invoice_type_tax_invoice' => 'Налоговая накладная (tax_invoice.php)',
'invoice_unsent' => 'Не удалось отправить накладную',
'invoice_update' => 'Пересчет',
'item_insufficient_of_stock' => 'Недостаточно товара в наличии.',
'item_name' => 'Название товара',
'item_number' => '№ товара',
'item_out_of_stock' => 'Этот товар отсутствует в наличии.',
'key_browser' => 'Сочетания клавиш',
'key_cancel' => 'Отмена текущих предложений/накладных/продаж',
'key_customer_search' => 'Поиск клиента',
'key_finish_quote' => 'Закрыть предложение/накладную без оплаты',
'key_finish_sale' => 'Добавить платеж и заполнить накладную/продажу',
'key_full' => 'Открыть в полноэкранном режиме',
'key_function' => 'Назначение',
'key_help' => 'Сочетания клавиш',
'key_help_modal' => 'Показать сочетания клавиш',
'key_in' => 'Увеличить',
'key_item_search' => 'Поиск товара',
'key_out' => 'Отдалить',
'key_payment' => 'Добавить оплату',
'key_print' => 'Распечатать текущую страницу',
'key_restore' => 'Восстановить исходное отображение/увеличение',
'key_search' => 'Поиск таблиц отчетов',
'key_suspend' => 'Приостановить текущую продажу',
'key_suspended' => 'Показать приостановленные продажи',
'key_system' => 'Системные горячие клавиши',
'key_tendered' => 'Изменить предложенную сумму',
'key_title' => 'Сочетания клавиш для продаж',
'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' => '',
'visa' => '',
'wholesale' => '',
'work_order' => 'Рабочий заказ',
'work_order_number' => 'Номер рабочего заказа',
'work_order_number_duplicate' => 'Номер рабочего заказа должен быть уникальным.',
'work_order_sent' => 'Рабочий заказ отправлен',
'work_order_unsent' => 'Не удалось отправить рабочий заказ',
];