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
15 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' => 'Hesab #',
'add_payment' => 'Ödəniş Əlavə Etmək',
'amount_due' => 'Qalıq',
'amount_due_not_covered' => 'Ödənişlər ödəniləcək məbləği əhatə etmir.',
'amount_tendered' => 'Ödənilən məbləğ',
'authorized_signature' => 'Səlahiyyətli İmza',
'cancel_sale' => 'İmtina',
'cash' => 'Nəğd pullar',
'cash_1' => '',
'cash_2' => '',
'cash_3' => '',
'cash_4' => '',
'cash_adjustment' => 'Nağd pulun tənzimlənməsi',
'cash_deposit' => 'Nağd depozit',
'cash_filter' => 'Nəğd Pul',
'change_due' => 'Qalıq',
'change_price' => 'Satış qiymətini dəyiş',
'check' => 'Çek',
'check_balance' => 'Çek Xatırladan',
'check_filter' => 'Çek',
'close' => '',
'comment' => 'Şərh',
'comments' => 'Şərhlər',
'company_name' => '',
'complete' => '',
'complete_sale' => 'Tamamlayın',
'confirm_cancel_sale' => 'Siz əminsiniz ki,satışı ləğv etmək istəyirsiniz? Bütün detallar silinəcək.',
'confirm_delete' => 'Siz əminsiniz ki seçilmiş satışı (lar) silmək istəyirsiz?',
'confirm_restore' => 'Seçilmiş Satışları bərpa etmək istədiyinizə əminsinizmi?',
'credit' => 'Kredit kartı',
'credit_deposit' => 'Kredit Depoziti',
'credit_filter' => 'Kredit kartı',
'current_table' => '',
'customer' => 'Ad',
'customer_address' => 'Ünvan',
'customer_discount' => 'Endirim',
'customer_email' => 'E-poçt',
'customer_location' => 'Yer',
'customer_mailchimp_status' => 'Mailchimp Statusu',
'customer_optional' => '(Ödənişlərdə tələb olunur)',
'customer_required' => '(Vacib)',
'customer_total' => 'Cəmi',
'customer_total_spent' => '',
'customers_available_points' => 'Mövcud ballar',
'daily_sales' => '',
'date' => 'Satış Tarixi',
'date_range' => 'Tarix Aralığı',
'date_required' => 'Düzgün tarix daxil edilməlidir.',
'date_type' => 'Boşluğa tarix əlavə edilməlidir.',
'debit' => 'Debit kart',
'debit_filter' => '',
'delete' => 'Silməyə izin ver',
'delete_confirmation' => 'Siz bu satışı silmək istəyinizə əminsiniz? Bu əməlliyat bərpa edilməyəcək.',
'delete_entire_sale' => 'Bütün Satışı Silmək',
'delete_successful' => 'Siz uğurla satışı sildiniz.',
'delete_unsuccessful' => 'Satışın silinməsi uğursuz oldu.',
'description_abbrv' => 'Təsvir.',
'discard' => 'İmtina',
'discard_quote' => '',
'discount' => 'Disk',
'discount_exceeds_item_total' => '',
'discount_included' => '% Endirim',
'discount_percent_exceeds_100' => '',
'discount_short' => '%',
'due' => 'Görə',
'due_filter' => 'Görə',
'edit' => 'Redaktə',
'edit_item' => 'Malın Redaktəsi',
'edit_sale' => 'Satışın Redaktəsi',
'email_receipt' => 'E-poçt Qəbz',
'employee' => 'Əməkdaş',
'entry' => 'Daxil',
'error_editing_item' => 'XƏTA Malın redaktəsində',
'find_or_scan_item' => 'Malın axtarışı',
'find_or_scan_item_or_receipt' => 'Tapmaq skan etmək və ya kvitansiya',
'giftcard' => 'Hədiyyə Kartı',
'giftcard_balance' => 'Hədiyyə Kartı Balansı',
'giftcard_filter' => '',
'giftcard_number' => 'Hədiyyə kartının nömrəsi',
'group_by_category' => 'Bölməyə görə grup',
'group_by_type' => 'Növə görə qrup',
'hsn' => 'HSN',
'id' => 'Satış İD',
'include_prices' => 'Qiymətlər daxildir?',
'insufficient_giftcard_balance' => 'Bu ödeme için yetersiz hediye kartı bakiyesi.',
'insufficient_reward_points' => 'Bu ödeme için yetersiz ödül puanları.',
'invoice' => 'Qaimə',
'invoice_confirm' => 'Bu qaimə göndəriləcək',
'invoice_enable' => 'Qaimə Yarat',
'invoice_filter' => 'Qaimələr',
'invoice_no_email' => 'Bu müştərinin elektron ünvanı düzgün deyil.',
'invoice_number' => 'Qaimə #',
'invoice_number_duplicate' => 'Qaimə nömrəsi unikal olmalıdır.',
'invoice_sent' => 'Qaimə göndərildi',
'invoice_total' => 'Qaimə Çəmi',
'invoice_type_custom_invoice' => 'Fərqli Qaimə (custom_invoice.php)',
'invoice_type_custom_tax_invoice' => 'Fərqli Vergi Qaiməsi (custom_tax_invoice.php)',
'invoice_type_invoice' => 'Qaimə (invoice.php)',
'invoice_type_tax_invoice' => 'Vergi Qaiməsi (tax_invoice.php)',
'invoice_unsent' => 'Qaimə göndərilə bilmədi',
'invoice_update' => 'Yeniden say',
'item_insufficient_of_stock' => 'Anbarda kifayət qədər mal yoxdur.',
'item_name' => 'Malın adı',
'item_number' => 'Mal #',
'item_out_of_stock' => 'Mal satılıb.',
'key_browser' => 'Faydalı Qısayollar',
'key_cancel' => 'Cari Kotirovka/Fatura/Satışı ləğv edir',
'key_customer_search' => 'Müştəri Axtarışı',
'key_finish_quote' => 'Ödəniş etmədən Kotirovka/Faturanı tamamlayın',
'key_finish_sale' => 'Ödəniş əlavə edib, faktura/satışı tamamlayın',
'key_full' => 'Tam Ekran rejimində açın',
'key_function' => 'Function',
'key_help' => 'Qısa yollar',
'key_help_modal' => 'Qısa yollar pəncərəsini açın',
'key_in' => 'Yaxınlaşdır',
'key_item_search' => 'Element Axtarışı',
'key_out' => 'Uzaqlaşdır',
'key_payment' => 'Ödəniş əlavə et',
'key_print' => 'Cari səhifəni çap et',
'key_restore' => 'Orjinal ekran ölçüsünə qayıt',
'key_search' => 'Axtarışın hesabat cədvəlləri',
'key_suspend' => 'Cari satışı dayandırın',
'key_suspended' => 'Dayandırılmış Satışları göstər',
'key_system' => 'Sistem Qısayolları',
'key_tendered' => 'Təklif olunan məbləği dəyişdirin',
'key_title' => 'Satış üçün klaviatura qısa yolları',
'mc' => '',
'mode' => 'Qeydiyyat Rejimi',
'must_enter_numeric' => 'Ödəniş məbləği rəqəm ilə olmalıdır.',
'must_enter_numeric_giftcard' => 'Hədiyyə Kartın nömrəsi rəqəmlə olmalıdır.',
'must_enter_reference_code' => 'İstinad/Axtarış nömrəsi daxil edilməlidir.',
'negative_amount_tendered' => 'Ödənilən məbləğ mənfi ola bilməz.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
'negative_total_invalid' => '',
'new_customer' => 'Yeni Müştəri',
'new_item' => 'Yeni Mal',
'no_description' => 'Heç Biri',
'no_filter' => 'Hamısı',
'no_items_in_cart' => 'Səbətdə heç bir mal yoxdur.',
'no_sales_to_display' => 'Satış yoxdur.',
'none_selected' => 'Silmək üçün hər hansı bir Satış (lar) seçmədiniz.',
'nontaxed_ind' => ' . ',
'not_authorized' => 'Bu hərəkət səlahiyyətli deyil.',
'one_or_multiple' => 'Satış (lar)',
'payment' => 'Ödəniş Növü',
'payment_amount' => 'Məbləğ',
'payment_not_cover_total' => 'Ödəmə məbləği Toplamdan çox və ya bərabər olmalıdır.',
'payment_type' => 'Növ',
'payments' => '',
'payments_total' => 'Cəmi Ödənilənlər',
'price' => 'Qiymət',
'print_after_sale' => 'Satışdan sonra Çap edin',
'quantity' => 'Miqdarı',
'quantity_less_than_reorder_level' => 'Diqqət: İstədiyinizin Miqdarı bu Öhdəlik üçün Yenidən Səviyyə səviyyəsindən aşağıdadır.',
'quantity_less_than_zero' => 'Xəbərdarlıq: İstədiyiniz miqdar kifayət deyil. Siz hələ də satışı edə bilərsiniz, lakin malın sayını yoxlayın.',
'quantity_of_items' => '{0} Məhsulların miqdarı',
'quote' => 'Qiymət ver',
'quote_number' => 'Sitat Nömrəsi',
'quote_number_duplicate' => 'Sitatın nömrəsi unikal olmalıdır.',
'quote_sent' => 'Sitat göndərildi',
'quote_unsent' => 'Sitat göndırilə bilmədi',
'receipt' => 'malın çeki',
'receipt_no_email' => 'Bu müştərinin etibarlı bir e-poçt ünvanı yoxdur.',
'receipt_number' => 'Satış №',
'receipt_sent' => 'Çek Göndərildi',
'receipt_unsent' => 'Çek göndərilə Bilmədi',
'reference_code' => 'Ödəniş İstinad Kodu',
'reference_code_invalid_characters' => 'İstinad kodu yalnız hərf və rəqəmlərdən ibarət olmalıdır.',
'reference_code_length_error' => 'İstinad kodunun uzunluğu etibarsızdır.',
'refund' => 'Qaytarılma növü',
'register' => 'jurnal satışı',
'remove_customer' => 'müştəriləri silmək',
'remove_discount' => '',
'return' => 'qaytarmaq',
'rewards' => 'Mükafat Balları',
'rewards_balance' => 'Mükafat Balları Balansı',
'rewards_package' => 'Mükafatlar',
'rewards_remaining_balance' => 'Mükafatın yerdə qalan bal dəyəri ',
'sale' => 'satış',
'sale_by_invoice' => 'Faktura ilə Satış',
'sale_for_customer' => 'Müştəri:',
'sale_time' => 'Vaxt',
'sales_tax' => 'Satış Vergisi',
'sales_total' => '',
'select_customer' => 'Müştəri seçmək',
'send_invoice' => 'Faktura Göndər',
'send_quote' => 'Sitat Göndər',
'send_receipt' => 'Çek Göndər',
'send_work_order' => 'İş Sifarişini Göndərin',
'serial' => 'seriya nömrəsi',
'service_charge' => '',
'show_due' => '',
'show_invoice' => 'Fakturanı Göstər',
'show_receipt' => 'Çek Göstər',
'start_typing_customer_name' => 'müştəri adını çap etməyə başlayın...',
'start_typing_item_name' => 'Malın adın başlayın çap etməyə yada skan edin şifrəni...',
'stock' => 'Anbar',
'stock_location' => 'Ehtiyyatın Yeri',
'sub_total' => 'yekun',
'successfully_deleted' => 'satış',
'successfully_restored' => 'Siz uğurla bərpa etdimiz',
'successfully_suspended_sale' => 'Sizin satışınız uğurla dayandırıldı.',
'successfully_updated' => 'Satış uğurla yeniləndi.',
'suspend_sale' => 'Dayandırmaq',
'suspended_doc_id' => 'Sənəd',
'suspended_sale_id' => 'Satış dayandırıldı İD',
'suspended_sales' => 'satış dayandırıldı',
'table' => 'Masa',
'takings' => 'Gündəlik Satışlar',
'tax' => 'vergi',
'tax_id' => 'Vergi İD',
'tax_invoice' => 'Vergi Qaimə',
'tax_percent' => 'vergi %',
'taxed_ind' => 'T',
'total' => 'cəm',
'total_tax_exclusive' => 'Vergi Xaric',
'transaction_failed' => 'Satış əməliyyatı uğursuz oldu.',
'unable_to_add_item' => 'Satışa əlavə edilən məhsul uğursuz oldu',
'unsuccessfully_deleted' => 'Satış (lar) silmək uğursuz oldu.',
'unsuccessfully_restored' => 'Satış (lar) bərpa olunmadı.',
'unsuccessfully_suspended_sale' => 'Satış dayandırıla bilmədi.',
'unsuccessfully_updated' => 'Satış yeniləməsi uğursuz oldu.',
'unsuspend' => 'blok etmək',
'unsuspend_and_delete' => 'blok etmək və silmək',
'update' => 'Yenilə',
'upi' => 'UPI',
'visa' => '',
'wholesale' => '',
'work_order' => 'İş Sifarişi',
'work_order_number' => 'Sifariş nömrəsi',
'work_order_number_duplicate' => 'İş sifarişinin nömrəsi unikal olmalıdır.',
'work_order_sent' => 'İş sifarişi göndərildi',
'work_order_unsent' => 'İş Sifarişi göndərilməmişdi',
];