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' => 'Gift Карта',
'giftcard_balance' => 'Gift Card Баланс',
'giftcard_filter' => '',
'giftcard_number' => 'Gift Card Номер',
'group_by_category' => 'Групирайте по категория',
'group_by_type' => 'Групиране по тип',
'hsn' => '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' => 'Номерът на фактурите трябва да е уникален.',
'invoice_sent' => 'Фактура, изпратена до',
'invoice_total' => 'Фактура общо',
'invoice_type_custom_invoice' => 'Ръчна фактура',
'invoice_type_custom_tax_invoice' => 'Фактура по избор(custom_tax_invoice.php)',
'invoice_type_invoice' => 'Фактура',
'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' => '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' => 'Gift Card номера трябва да бъде число.',
'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' => ' sales 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' => 'Reward Points Баланс',
'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' => 'Action',
'update' => 'Актуализация',
'upi' => '',
'visa' => '',
'wholesale' => '',
'work_order' => 'Работна поръчка',
'work_order_number' => 'Номер работна поръчка',
'work_order_number_duplicate' => 'Номерът на работната поръчка трябва да е уникален.',
'work_order_sent' => 'Работната поръчка е изпратена до',
'work_order_unsent' => 'Работната поръчка не бе изпратена до',
];