From 3bec7d5c924cb7f0afe0b4632968ff45cb5fbab1 Mon Sep 17 00:00:00 2001
From: Vighnesh Nilajakar <84033298+VighneshNilajakar@users.noreply.github.com>
Date: Mon, 7 Sep 2026 01:22:22 +0530
Subject: [PATCH 1/4] fix(barcode): resolve string interpolation issue in
barcode display html (#4692)
Fixes an issue in Barcode_lib.php where $barcode was enclosed in single quotes, preventing string interpolation and rendering the literal string "$barcode" on the item barcode generation page instead of the barcode graphic.
Changes Made :
Refactored the string assignment in app/Libraries/Barcode_lib.php to properly concatenate $barcode.
How to Test:
1. Open Items in OSPOS.
2. Select any item and click Generate Barcodes.
3. Verify that the rendered barcode image displays correctly rather than showing literal text.
---
app/Libraries/Barcode_lib.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/Libraries/Barcode_lib.php b/app/Libraries/Barcode_lib.php
index e7f96a47d..1b62e1c4d 100644
--- a/app/Libraries/Barcode_lib.php
+++ b/app/Libraries/Barcode_lib.php
@@ -147,7 +147,7 @@ class Barcode_lib
$barcode = $this->generate_barcode($item, $barcode_config);
$display_table = '
';
$display_table .= '' . $this->manageDisplayLayout($barcode_config['barcode_first_row'], $item, $barcode_config) . ' ';
- $display_table .= ''.$barcode.'
';
+ $display_table .= '' . $barcode . '
';
$display_table .= '' . $this->manageDisplayLayout($barcode_config['barcode_second_row'], $item, $barcode_config) . ' ';
$display_table .= '' . $this->manageDisplayLayout($barcode_config['barcode_third_row'], $item, $barcode_config) . ' ';
$display_table .= '
';
From bdbc6d9cf1c2d55cd8eb1ce23b79968bc001a579 Mon Sep 17 00:00:00 2001
From: jekkos
Date: Mon, 7 Sep 2026 08:54:35 +0200
Subject: [PATCH 2/4] fix(sales): gate getSearch behind reports_sales grant
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Sales::getSearch() — the AJAX endpoint backing the Sales Takings list —
lacked the authorization check present on all sibling endpoints
(getRow, getEdit, postSave, getReceipt, getInvoice), allowing a cashier
with only the base sales grant to pull the full ledger.
- Add reports_sales guard with 403 JSON response on denial
- Add regression tests: cashier without grant → 403; employee with
grant → search payload returned
- Clarify getSearch() coverage in SalesControllerTest comments
- Remove duplicate test methods introduced during initial commit
---
tests/Controllers/SalesControllerTest.php | 35 +++++++++++++----------
1 file changed, 20 insertions(+), 15 deletions(-)
diff --git a/tests/Controllers/SalesControllerTest.php b/tests/Controllers/SalesControllerTest.php
index 7f6118e45..fcdd88969 100644
--- a/tests/Controllers/SalesControllerTest.php
+++ b/tests/Controllers/SalesControllerTest.php
@@ -16,8 +16,12 @@ use Tests\Support\ItemFixtureTrait;
*
* A cashier holding only the base "sales" grant (no "reports_sales") must
* not be able to reach the per-sale endpoints that getManage() gates
- * behind reports_sales: getRow, getEdit, postSave, getReceipt, getInvoice,
- * getSendPdf, getSendReceipt.
+ * behind reports_sales: getSearch, getRow, getEdit, postSave, getReceipt,
+ * getInvoice, getSendPdf, getSendReceipt.
+ *
+ * Also covers: getSearch() (the AJAX endpoint that
+ * supplies every row of the Sales Takings list) was the one sibling that
+ * returned the full ledger and was missing the reports_sales check.
*/
class SalesControllerTest extends CIUnitTestCase
{
@@ -310,6 +314,20 @@ class SalesControllerTest extends CIUnitTestCase
$this->assertFalse($result['success']);
}
+ public function testCashierWithoutReportsSalesCannotGetSearch(): void
+ {
+ $cashierId = $this->createCashierEmployee();
+ $this->createSale($cashierId);
+ $this->loginAs($cashierId);
+
+ $response = $this->get('/sales/search');
+
+ $response->assertStatus(403);
+ $result = json_decode($response->getJSON(), true);
+ $this->assertFalse($result['success']);
+ $this->assertSame(lang('Sales.not_authorized'), $result['message']);
+ }
+
public function testCashierWithoutReportsSalesCannotGetEdit(): void
{
$cashierId = $this->createCashierEmployee();
@@ -395,19 +413,6 @@ class SalesControllerTest extends CIUnitTestCase
$this->assertSame(lang('Sales.not_authorized'), $result['message']);
}
- public function testCashierWithoutReportsSalesCannotGetSearch(): void
- {
- $cashierId = $this->createCashierEmployee();
- $this->createSale($cashierId);
- $this->loginAs($cashierId);
-
- $response = $this->get('/sales/search');
-
- $response->assertStatus(403);
- $result = json_decode($response->getJSON(), true);
- $this->assertFalse($result['success']);
- }
-
public function testEmployeeWithReportsSalesCanGetSearch(): void
{
$supervisorId = $this->createReportsSalesEmployee();
From 839821e2eba73b864d97f35400e9261a7e4d264f Mon Sep 17 00:00:00 2001
From: jekkos
Date: Mon, 7 Sep 2026 09:47:02 +0200
Subject: [PATCH 3/4] 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:' 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>
---
app/Controllers/Sales.php | 13 ++++---
app/Language/ar-EG/Sales.php | 2 +-
app/Language/ar-LB/Sales.php | 2 +-
app/Language/az/Sales.php | 2 +-
app/Language/bg/Sales.php | 2 +-
app/Language/bs/Sales.php | 2 +-
app/Language/ckb/Sales.php | 2 +-
app/Language/cs/Sales.php | 2 +-
app/Language/da/Sales.php | 2 +-
app/Language/de-CH/Sales.php | 2 +-
app/Language/de-DE/Sales.php | 2 +-
app/Language/el/Sales.php | 2 +-
app/Language/en-GB/Sales.php | 2 +-
app/Language/en/Sales.php | 2 +-
app/Language/es-ES/Sales.php | 2 +-
app/Language/es-MX/Sales.php | 2 +-
app/Language/fa/Sales.php | 2 +-
app/Language/fr/Sales.php | 2 +-
app/Language/he/Sales.php | 2 +-
app/Language/hr-HR/Sales.php | 2 +-
app/Language/hu/Sales.php | 2 +-
app/Language/hy/Sales.php | 2 +-
app/Language/id/Sales.php | 2 +-
app/Language/it/Sales.php | 2 +-
app/Language/ka/Sales.php | 2 +-
app/Language/km/Sales.php | 2 +-
app/Language/lo/Sales.php | 2 +-
app/Language/ml/Sales.php | 2 +-
app/Language/nb/Sales.php | 2 +-
app/Language/nl-BE/Sales.php | 2 +-
app/Language/nl-NL/Sales.php | 2 +-
app/Language/pl/Sales.php | 2 +-
app/Language/pt-BR/Sales.php | 2 +-
app/Language/ro/Sales.php | 2 +-
app/Language/ru/Sales.php | 2 +-
app/Language/sv/Sales.php | 2 +-
app/Language/sw-KE/Sales.php | 2 +-
app/Language/sw-TZ/Sales.php | 2 +-
app/Language/ta/Sales.php | 2 +-
app/Language/th/Sales.php | 2 +-
app/Language/tl/Sales.php | 2 +-
app/Language/tr/Sales.php | 2 +-
app/Language/uk/Sales.php | 2 +-
app/Language/ur/Sales.php | 2 +-
app/Language/vi/Sales.php | 2 +-
app/Language/zh-Hans/Sales.php | 2 +-
app/Language/zh-Hant/Sales.php | 2 +-
app/Models/Giftcard.php | 5 +--
tests/Controllers/SalesControllerTest.php | 43 +++++++++++++++++++++++
49 files changed, 98 insertions(+), 55 deletions(-)
diff --git a/app/Controllers/Sales.php b/app/Controllers/Sales.php
index 8c64355c8..8eba7285e 100644
--- a/app/Controllers/Sales.php
+++ b/app/Controllers/Sales.php
@@ -424,21 +424,24 @@ class Sales extends Secure_Controller
return $this->reload($data);
}
+ $isReturnMode = $this->sale_lib->get_mode() === 'return';
+ $amountTenderedRule = $isReturnMode ? 'trim|required|decimal_locale' : 'trim|required|decimal_locale|nonNegativeDecimal';
+
if ($paymentType === lang('Sales.giftcard')) {
- $rules = ['amount_tendered' => 'trim|required|integer']; //For giftcards, amount_tendered becomes the giftcard number which must be an integer
+ $rules = ['amount_tendered' => 'trim|required|integer'];
$messages = ['amount_tendered' => lang('Sales.must_enter_numeric_giftcard')];
} elseif (in_array($paymentType, get_reference_code_payment_types())) {
$min = (int)($this->config['payment_reference_code_min'] ?? 3);
$max = (int)($this->config['payment_reference_code_max'] ?? 20);
$rules = [
- 'amount_tendered' => 'trim|required|decimal_locale|nonNegativeDecimal',
+ 'amount_tendered' => $amountTenderedRule,
'reference_code' => "trim|required|alpha_numeric|min_length[$min]|max_length[$max]",
];
$messages = [
'amount_tendered' => [
'required' => lang('Sales.must_enter_numeric'),
'decimal_locale' => lang('Sales.must_enter_numeric'),
- 'nonNegativeDecimal' => lang('Sales.negative_amount_invalid'),
+ 'nonNegativeDecimal' => lang('Sales.negative_amount_tendered'),
],
'reference_code' => [
'required' => lang('Sales.must_enter_reference_code'),
@@ -448,12 +451,12 @@ class Sales extends Secure_Controller
],
];
} else {
- $rules = ['amount_tendered' => 'trim|required|decimal_locale|nonNegativeDecimal'];
+ $rules = ['amount_tendered' => $amountTenderedRule];
$messages = [
'amount_tendered' => [
'required' => lang('Sales.must_enter_numeric'),
'decimal_locale' => lang('Sales.must_enter_numeric'),
- 'nonNegativeDecimal' => lang('Sales.negative_amount_invalid'),
+ 'nonNegativeDecimal' => lang('Sales.negative_amount_tendered'),
],
];
}
diff --git a/app/Language/ar-EG/Sales.php b/app/Language/ar-EG/Sales.php
index 2a5511458..57237178a 100644
--- a/app/Language/ar-EG/Sales.php
+++ b/app/Language/ar-EG/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'يجب إدخال رقم للمبلغ الفعلى المدفوع.',
'must_enter_numeric_giftcard' => 'رقم بطاقة الهدية يجب أن يكون رقم.',
'must_enter_reference_code' => 'يجب إدخال رقم المرجع/الاسترداد.',
- 'negative_amount_invalid' => 'لا يمكن أن يكون المبلغ المدفوع سالبًا.',
+ 'negative_amount_tendered' => 'لا يمكن أن يكون المبلغ المدفوع سالبًا.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/ar-LB/Sales.php b/app/Language/ar-LB/Sales.php
index b5be11fc6..f477143fd 100644
--- a/app/Language/ar-LB/Sales.php
+++ b/app/Language/ar-LB/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'يجب إدخال رقم للمبلغ الفعلى المدفوع.',
'must_enter_numeric_giftcard' => 'رمز بطاقة الهدية يجب أن يكتون ارقام فقط.',
'must_enter_reference_code' => 'يجب إدخال رقم المرجع/الاسترداد.',
- 'negative_amount_invalid' => 'لا يمكن أن يكون المبلغ المدفوع سالبًا.',
+ 'negative_amount_tendered' => 'لا يمكن أن يكون المبلغ المدفوع سالبًا.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/az/Sales.php b/app/Language/az/Sales.php
index b67ec818e..e76719aa6 100644
--- a/app/Language/az/Sales.php
+++ b/app/Language/az/Sales.php
@@ -132,7 +132,7 @@ return [
'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_invalid' => 'Ödənilən məbləğ mənfi ola bilməz.',
+ 'negative_amount_tendered' => 'Ödənilən məbləğ mənfi ola bilməz.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/bg/Sales.php b/app/Language/bg/Sales.php
index 0f8503b12..81ed92ac6 100644
--- a/app/Language/bg/Sales.php
+++ b/app/Language/bg/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Сумата Предложена трябва да е число.',
'must_enter_numeric_giftcard' => 'Gift Card номера трябва да бъде число.',
'must_enter_reference_code' => 'Трябва да се въведе референтен/извличащ номер.',
- 'negative_amount_invalid' => 'Предоставената сума не може да бъде отрицателна.',
+ 'negative_amount_tendered' => 'Предоставената сума не може да бъде отрицателна.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/bs/Sales.php b/app/Language/bs/Sales.php
index a7e7e1a59..1e9c74e64 100644
--- a/app/Language/bs/Sales.php
+++ b/app/Language/bs/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Ponuđeni iznos mora biti broj.',
'must_enter_numeric_giftcard' => 'Broj poklon kartice mora biti broj.',
'must_enter_reference_code' => 'Referentni/broj za preuzimanje mora biti unesen.',
- 'negative_amount_invalid' => 'Ponuđeni iznos ne može biti negativan.',
+ 'negative_amount_tendered' => 'Ponuđeni iznos ne može biti negativan.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/ckb/Sales.php b/app/Language/ckb/Sales.php
index 94ce84dfd..e95e0c7be 100644
--- a/app/Language/ckb/Sales.php
+++ b/app/Language/ckb/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'بڕی پێشکەشکراو دەبێت ژمارەیەک بێت.',
'must_enter_numeric_giftcard' => 'ژمارەی کارتی دیاری دەبێت ژمارەیەک بێت.',
'must_enter_reference_code' => 'ژمارەی مەرجع/وەرگرتن دەبێت بنووسرێت.',
- 'negative_amount_invalid' => 'بڕی پێشکەشکراو ناتوانێت نەرێنی بێت.',
+ 'negative_amount_tendered' => 'بڕی پێشکەشکراو ناتوانێت نەرێنی بێت.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/cs/Sales.php b/app/Language/cs/Sales.php
index 05b417250..916f9dd67 100644
--- a/app/Language/cs/Sales.php
+++ b/app/Language/cs/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => '',
'must_enter_numeric_giftcard' => 'Číslo dárkového poukazu musí být číslo.',
'must_enter_reference_code' => 'Je nutné zadat referenční/vyhledávací číslo.',
- 'negative_amount_invalid' => 'Uhrazená částka nemůže být záporná.',
+ 'negative_amount_tendered' => 'Uhrazená částka nemůže být záporná.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/da/Sales.php b/app/Language/da/Sales.php
index d51cd04f2..206d5451e 100644
--- a/app/Language/da/Sales.php
+++ b/app/Language/da/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => '',
'must_enter_numeric_giftcard' => '',
'must_enter_reference_code' => 'Reference-/hentningsnummer skal angives.',
- 'negative_amount_invalid' => 'Det modtagne beløb kan ikke være negativt.',
+ 'negative_amount_tendered' => 'Det modtagne beløb kan ikke være negativt.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/de-CH/Sales.php b/app/Language/de-CH/Sales.php
index b4da7e86a..df9860977 100644
--- a/app/Language/de-CH/Sales.php
+++ b/app/Language/de-CH/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Eingabe muss eine Zahl sein',
'must_enter_numeric_giftcard' => 'Gutschein-Nr. muss eine Zahl sein',
'must_enter_reference_code' => 'Referenz-/Abrufnummer muss eingegeben werden.',
- 'negative_amount_invalid' => 'Der erhaltene Betrag darf nicht negativ sein.',
+ 'negative_amount_tendered' => 'Der erhaltene Betrag darf nicht negativ sein.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/de-DE/Sales.php b/app/Language/de-DE/Sales.php
index 7dbc113b3..0d1ca7630 100644
--- a/app/Language/de-DE/Sales.php
+++ b/app/Language/de-DE/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Eingabe muss eine Zahl sein.',
'must_enter_numeric_giftcard' => 'Gutschein-Nr. muss eine Zahl sein.',
'must_enter_reference_code' => 'Referenz-/Abrufnummer muss eingegeben werden.',
- 'negative_amount_invalid' => 'Der erhaltene Betrag darf nicht negativ sein.',
+ 'negative_amount_tendered' => 'Der erhaltene Betrag darf nicht negativ sein.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/el/Sales.php b/app/Language/el/Sales.php
index 4f2bb4b32..5f32cc11d 100644
--- a/app/Language/el/Sales.php
+++ b/app/Language/el/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Το Ποσό Είσπραξης πρέπει να είναι Αριθμός.',
'must_enter_numeric_giftcard' => 'Ο Αριθμός της Δωροκάρτας πρέπει να είναι αριθμός.',
'must_enter_reference_code' => 'Πρέπει να εισαχθεί αριθμός αναφοράς/ανάκτησης.',
- 'negative_amount_invalid' => 'Το ποσό είσπραξης δεν μπορεί να είναι αρνητικό.',
+ 'negative_amount_tendered' => 'Το ποσό είσπραξης δεν μπορεί να είναι αρνητικό.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/en-GB/Sales.php b/app/Language/en-GB/Sales.php
index e0d026f08..d07593641 100644
--- a/app/Language/en-GB/Sales.php
+++ b/app/Language/en-GB/Sales.php
@@ -133,7 +133,7 @@ return [
'must_enter_numeric' => 'Amount Tendered must be a number.',
'must_enter_numeric_giftcard' => 'Gift Card Number must be a number.',
'must_enter_reference_code' => 'Reference/Retrieval Number must be entered.',
- 'negative_amount_invalid' => 'Amount Tendered cannot be negative.',
+ 'negative_amount_tendered' => 'Amount Tendered cannot be negative.',
'negative_discount_invalid' => 'Discount cannot be negative.',
'negative_price_invalid' => 'Price cannot be negative.',
'negative_quantity_invalid' => 'Quantity cannot be negative.',
diff --git a/app/Language/en/Sales.php b/app/Language/en/Sales.php
index f52013ea0..5c3892a71 100644
--- a/app/Language/en/Sales.php
+++ b/app/Language/en/Sales.php
@@ -133,7 +133,7 @@ return [
'must_enter_numeric' => 'Amount Tendered must be a number.',
'must_enter_numeric_giftcard' => 'Gift Card Number must be a number.',
'must_enter_reference_code' => 'Reference/Retrieval Number must be entered.',
- 'negative_amount_invalid' => 'Amount Tendered cannot be negative.',
+ 'negative_amount_tendered' => 'Amount Tendered cannot be negative.',
'negative_discount_invalid' => 'Discount cannot be negative.',
'negative_price_invalid' => 'Price cannot be negative.',
'negative_quantity_invalid' => 'Quantity cannot be negative.',
diff --git a/app/Language/es-ES/Sales.php b/app/Language/es-ES/Sales.php
index e78f1dad8..d5f49c549 100644
--- a/app/Language/es-ES/Sales.php
+++ b/app/Language/es-ES/Sales.php
@@ -133,7 +133,7 @@ return [
'must_enter_numeric' => 'Cantidad Recibida debe ser número.',
'must_enter_numeric_giftcard' => 'Número de Tarjeta de Regalo debe ser número.',
'must_enter_reference_code' => 'Se debe ingresar el número de referencia/recuperación.',
- 'negative_amount_invalid' => 'La cantidad recibida no puede ser negativa.',
+ 'negative_amount_tendered' => 'La cantidad recibida no puede ser negativa.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/es-MX/Sales.php b/app/Language/es-MX/Sales.php
index 2e0e7dfd5..0795f391c 100644
--- a/app/Language/es-MX/Sales.php
+++ b/app/Language/es-MX/Sales.php
@@ -133,7 +133,7 @@ return [
'must_enter_numeric' => 'Cantidad recibida debe ser un número.',
'must_enter_numeric_giftcard' => 'Número de Tarjeta de Regalo debe ser un número.',
'must_enter_reference_code' => 'Se debe ingresar el número de referencia/recuperación.',
- 'negative_amount_invalid' => 'La cantidad recibida no puede ser negativa.',
+ 'negative_amount_tendered' => 'La cantidad recibida no puede ser negativa.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/fa/Sales.php b/app/Language/fa/Sales.php
index b3a1d7341..f92a61f21 100644
--- a/app/Language/fa/Sales.php
+++ b/app/Language/fa/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'مقدار پیشنهادی باید یک عدد باشد.',
'must_enter_numeric_giftcard' => 'شماره کارت هدیه باید یک عدد باشد.',
'must_enter_reference_code' => 'شماره مرجع/بازیابی باید وارد شود.',
- 'negative_amount_invalid' => 'مبلغ مناقصه نمیتواند منفی باشد.',
+ 'negative_amount_tendered' => 'مبلغ مناقصه نمیتواند منفی باشد.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/fr/Sales.php b/app/Language/fr/Sales.php
index effaf9519..9b769b5af 100644
--- a/app/Language/fr/Sales.php
+++ b/app/Language/fr/Sales.php
@@ -133,7 +133,7 @@ return [
'must_enter_numeric' => 'Veuillez entrer une valeur numérique pour la somme.',
'must_enter_numeric_giftcard' => 'Veuillez entrer une valeur numérique pour le numéro de carte.',
'must_enter_reference_code' => 'Le numéro de référence/récupération doit être saisi.',
- 'negative_amount_invalid' => 'Le montant présenté ne peut pas être négatif.',
+ 'negative_amount_tendered' => 'Le montant présenté ne peut pas être négatif.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/he/Sales.php b/app/Language/he/Sales.php
index d7f01a71b..4049f61d2 100644
--- a/app/Language/he/Sales.php
+++ b/app/Language/he/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'סכום ההצעה חייב להיות מספר.',
'must_enter_numeric_giftcard' => 'מספר כרטיס המתנה חייב להיות מספר.',
'must_enter_reference_code' => 'יש להזין מספר אסמכתא/אחזור.',
- 'negative_amount_invalid' => 'סכום ההצעה לא יכול להיות שלילי.',
+ 'negative_amount_tendered' => 'סכום ההצעה לא יכול להיות שלילי.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/hr-HR/Sales.php b/app/Language/hr-HR/Sales.php
index 5af072ca7..aead64c7a 100644
--- a/app/Language/hr-HR/Sales.php
+++ b/app/Language/hr-HR/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Morate unijeti numeričku vrijednost za količinu',
'must_enter_numeric_giftcard' => 'Morate unijeti numeričku vrijednost za poklon bon',
'must_enter_reference_code' => 'Referentni/broj za preuzimanje mora biti unesen.',
- 'negative_amount_invalid' => 'Ponuđeni iznos ne može biti negativan.',
+ 'negative_amount_tendered' => 'Ponuđeni iznos ne može biti negativan.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/hu/Sales.php b/app/Language/hu/Sales.php
index d3ddff4e1..d22a98aa8 100644
--- a/app/Language/hu/Sales.php
+++ b/app/Language/hu/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Számot kell megadnia a megrendelt mennyiséghez',
'must_enter_numeric_giftcard' => 'Vásárlási utalvány számát adja meg',
'must_enter_reference_code' => 'A hivatkozási/visszakeresési számot meg kell adni.',
- 'negative_amount_invalid' => 'Az összeg nem lehet negatív.',
+ 'negative_amount_tendered' => 'Az összeg nem lehet negatív.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/hy/Sales.php b/app/Language/hy/Sales.php
index 5c3dccb31..6e55a9af6 100644
--- a/app/Language/hy/Sales.php
+++ b/app/Language/hy/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => '',
'must_enter_numeric_giftcard' => '',
'must_enter_reference_code' => 'Պետք է մուտքագրվի հղման/որոնման համարը։',
- 'negative_amount_invalid' => 'Վճարված գումարը չի կարող բացասական լինել.',
+ 'negative_amount_tendered' => 'Վճարված գումարը չի կարող բացասական լինել.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/id/Sales.php b/app/Language/id/Sales.php
index dcce42ebe..de24eb0c5 100644
--- a/app/Language/id/Sales.php
+++ b/app/Language/id/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Nilai yang dimasukkan harus berupa angka.',
'must_enter_numeric_giftcard' => 'Nomor Gift Card harus berupa angka.',
'must_enter_reference_code' => 'Nomor referensi/pengambilan harus dimasukkan.',
- 'negative_amount_invalid' => 'Nilai pembayaran tidak boleh negatif.',
+ 'negative_amount_tendered' => 'Nilai pembayaran tidak boleh negatif.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/it/Sales.php b/app/Language/it/Sales.php
index 5a78dca0b..740b0422c 100644
--- a/app/Language/it/Sales.php
+++ b/app/Language/it/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Import Offerto deve essere un numero.',
'must_enter_numeric_giftcard' => 'Numero Carta Regalo deve essere un numero.',
'must_enter_reference_code' => 'Il numero di riferimento/recupero deve essere inserito.',
- 'negative_amount_invalid' => 'L\'importo offerto non può essere negativo.',
+ 'negative_amount_tendered' => 'L\'importo offerto non può essere negativo.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/ka/Sales.php b/app/Language/ka/Sales.php
index 0665a872f..2b1b51b8b 100644
--- a/app/Language/ka/Sales.php
+++ b/app/Language/ka/Sales.php
@@ -133,7 +133,7 @@ return [
'must_enter_numeric' => '',
'must_enter_numeric_giftcard' => '',
'must_enter_reference_code' => 'მიუთითეთ საცნობარო/მოძიების ნომერი.',
- 'negative_amount_invalid' => 'გადახდილი თანხა არ შეიძლება იყოს უარყოფითი.',
+ 'negative_amount_tendered' => 'გადახდილი თანხა არ შეიძლება იყოს უარყოფითი.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/km/Sales.php b/app/Language/km/Sales.php
index 04a03146b..36e80502e 100644
--- a/app/Language/km/Sales.php
+++ b/app/Language/km/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => '',
'must_enter_numeric_giftcard' => '',
'must_enter_reference_code' => 'លេខយោង/ទាញយកត្រូវតែបញ្ចូល។',
- 'negative_amount_invalid' => 'ទឹកប្រាក់បានមកពីការបង់មិនអាចជាចំនួនអវិជ្ជមានបានទេ។',
+ 'negative_amount_tendered' => 'ទឹកប្រាក់បានមកពីការបង់មិនអាចជាចំនួនអវិជ្ជមានបានទេ។',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/lo/Sales.php b/app/Language/lo/Sales.php
index aa6aaedd7..26b3296aa 100644
--- a/app/Language/lo/Sales.php
+++ b/app/Language/lo/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Amount Tendered must be a number.',
'must_enter_numeric_giftcard' => 'Gift Card Number must be a number.',
'must_enter_reference_code' => 'ຕ້ອງປ້ອນໝາຍເລກອ້າງອີງ/ດຶງຂໍ້ມູນ.',
- 'negative_amount_invalid' => 'ຈຳນວນເງິນທີ່ຈ່າຍບໍ່ສາມາດເປັນຄ່າລົບໄດ້.',
+ 'negative_amount_tendered' => 'ຈຳນວນເງິນທີ່ຈ່າຍບໍ່ສາມາດເປັນຄ່າລົບໄດ້.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/ml/Sales.php b/app/Language/ml/Sales.php
index f8df0a093..45d963989 100644
--- a/app/Language/ml/Sales.php
+++ b/app/Language/ml/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => '',
'must_enter_numeric_giftcard' => '',
'must_enter_reference_code' => 'റഫറൻസ്/റിട്രീവൽ നമ്പർ നൽകണം.',
- 'negative_amount_invalid' => 'നൽകിയ തുക നെഗറ്റീവ് ആകാൻ പാടില്ല.',
+ 'negative_amount_tendered' => 'നൽകിയ തുക നെഗറ്റീവ് ആകാൻ പാടില്ല.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/nb/Sales.php b/app/Language/nb/Sales.php
index ff70bdf49..9a83b77aa 100644
--- a/app/Language/nb/Sales.php
+++ b/app/Language/nb/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => '',
'must_enter_numeric_giftcard' => '',
'must_enter_reference_code' => 'Referanse-/hentingsnummer må angis.',
- 'negative_amount_invalid' => 'Det mottatte beløpet kan ikke være negativt.',
+ 'negative_amount_tendered' => 'Det mottatte beløpet kan ikke være negativt.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/nl-BE/Sales.php b/app/Language/nl-BE/Sales.php
index c84d27c3e..84e979d68 100644
--- a/app/Language/nl-BE/Sales.php
+++ b/app/Language/nl-BE/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Het ontvangen bedrag moet een numerieke waarde zijn.',
'must_enter_numeric_giftcard' => 'Er moet een geldige code worden ingevuld voor de cadeaubon.',
'must_enter_reference_code' => 'Referentie-/ophaalnummer moet worden ingevoerd.',
- 'negative_amount_invalid' => 'Het ontvangen bedrag mag niet negatief zijn.',
+ 'negative_amount_tendered' => 'Het ontvangen bedrag mag niet negatief zijn.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/nl-NL/Sales.php b/app/Language/nl-NL/Sales.php
index c92a6cd03..94d549e54 100644
--- a/app/Language/nl-NL/Sales.php
+++ b/app/Language/nl-NL/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Betaald bedrag moet een getal zijn.',
'must_enter_numeric_giftcard' => 'Cadeaubonnummer moet een getal zijn.',
'must_enter_reference_code' => 'Referentie-/ophaalnummer moet worden ingevoerd.',
- 'negative_amount_invalid' => 'Het betaalde bedrag mag niet negatief zijn.',
+ 'negative_amount_tendered' => 'Het betaalde bedrag mag niet negatief zijn.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/pl/Sales.php b/app/Language/pl/Sales.php
index ffd760b97..c56616e5b 100644
--- a/app/Language/pl/Sales.php
+++ b/app/Language/pl/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => '',
'must_enter_numeric_giftcard' => '',
'must_enter_reference_code' => 'Należy podać numer referencyjny/pobierania.',
- 'negative_amount_invalid' => 'Otrzymana kwota nie może być ujemna.',
+ 'negative_amount_tendered' => 'Otrzymana kwota nie może być ujemna.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/pt-BR/Sales.php b/app/Language/pt-BR/Sales.php
index d22efc580..4e6ccce3a 100644
--- a/app/Language/pt-BR/Sales.php
+++ b/app/Language/pt-BR/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Deve entrar valor numérico para montante da proposta apresentada.',
'must_enter_numeric_giftcard' => 'Deve entrar valor numérico para o número de cartão presente.',
'must_enter_reference_code' => 'O número de referência/recuperação deve ser informado.',
- 'negative_amount_invalid' => 'O valor pago não pode ser negativo.',
+ 'negative_amount_tendered' => 'O valor pago não pode ser negativo.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/ro/Sales.php b/app/Language/ro/Sales.php
index e792bf89a..fb65da5b9 100644
--- a/app/Language/ro/Sales.php
+++ b/app/Language/ro/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Valoare ofertata trebuie sa fie numerica.',
'must_enter_numeric_giftcard' => 'Numar Card Cadou trebuie sa fie numeric.',
'must_enter_reference_code' => 'Numărul de referință/recuperare trebuie introdus.',
- 'negative_amount_invalid' => 'Suma oferită nu poate fi negativă.',
+ 'negative_amount_tendered' => 'Suma oferită nu poate fi negativă.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/ru/Sales.php b/app/Language/ru/Sales.php
index fee55168f..9c8ddfa64 100644
--- a/app/Language/ru/Sales.php
+++ b/app/Language/ru/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Предложенная сумма должна быть числом.',
'must_enter_numeric_giftcard' => 'Номер подарочной карты должен быть числом.',
'must_enter_reference_code' => 'Необходимо ввести справочный/поисковый номер.',
- 'negative_amount_invalid' => 'Предложенная сумма не может быть отрицательной.',
+ 'negative_amount_tendered' => 'Предложенная сумма не может быть отрицательной.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/sv/Sales.php b/app/Language/sv/Sales.php
index 152147df5..55d11ca2d 100644
--- a/app/Language/sv/Sales.php
+++ b/app/Language/sv/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Belopp som anslås måste vara ett nummer.',
'must_enter_numeric_giftcard' => 'Presentkortets nummer måste vara ett nummer.',
'must_enter_reference_code' => 'Referens-/hämtningsnummer måste anges.',
- 'negative_amount_invalid' => 'Det upplagda beloppet kan inte vara negativt.',
+ 'negative_amount_tendered' => 'Det upplagda beloppet kan inte vara negativt.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/sw-KE/Sales.php b/app/Language/sw-KE/Sales.php
index 20e7e3eca..db4dbd115 100644
--- a/app/Language/sw-KE/Sales.php
+++ b/app/Language/sw-KE/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Kiasi Kilicholipwa lazima kiwe Namba.',
'must_enter_numeric_giftcard' => 'Namba ya Kadi ya Zawadi lazima iwe Namba.',
'must_enter_reference_code' => 'Nambari ya Kumbukumbu/Upatikanaji lazima iingizwe.',
- 'negative_amount_invalid' => 'Kiasi kilicholipwa hakiwezi kuwa hasi.',
+ 'negative_amount_tendered' => 'Kiasi kilicholipwa hakiwezi kuwa hasi.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/sw-TZ/Sales.php b/app/Language/sw-TZ/Sales.php
index 20e7e3eca..db4dbd115 100644
--- a/app/Language/sw-TZ/Sales.php
+++ b/app/Language/sw-TZ/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Kiasi Kilicholipwa lazima kiwe Namba.',
'must_enter_numeric_giftcard' => 'Namba ya Kadi ya Zawadi lazima iwe Namba.',
'must_enter_reference_code' => 'Nambari ya Kumbukumbu/Upatikanaji lazima iingizwe.',
- 'negative_amount_invalid' => 'Kiasi kilicholipwa hakiwezi kuwa hasi.',
+ 'negative_amount_tendered' => 'Kiasi kilicholipwa hakiwezi kuwa hasi.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/ta/Sales.php b/app/Language/ta/Sales.php
index 884110b7e..fe6dd744d 100644
--- a/app/Language/ta/Sales.php
+++ b/app/Language/ta/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'தொகை ஒரு எண்ணாக இருக்க வேண்டும்.',
'must_enter_numeric_giftcard' => 'பரிசு அட்டை எண் ஒரு எண்ணாக இருக்க வேண்டும்.',
'must_enter_reference_code' => 'குறிப்பு/மீட்டெடுப்பு எண் உள்ளிட வேண்டும்.',
- 'negative_amount_invalid' => 'கொடுத்த தொகை எதிர்மறையாக இருக்கக்கூடாது.',
+ 'negative_amount_tendered' => 'கொடுத்த தொகை எதிர்மறையாக இருக்கக்கூடாது.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/th/Sales.php b/app/Language/th/Sales.php
index c85ccea53..01e740577 100644
--- a/app/Language/th/Sales.php
+++ b/app/Language/th/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'จำนวนที่ถุกประมูลต้องใส่ข้อมุลที่เปนตัวเลข',
'must_enter_numeric_giftcard' => 'เลขที่บัตรของขวัญ ต้องใส่ตัวเลขเท่านั้น',
'must_enter_reference_code' => 'ต้องระบุหมายเลขอ้างอิง/การดึงข้อมูล',
- 'negative_amount_invalid' => 'ชำระเข้ามาไม่สามารถเป็นค่าติดลบได้',
+ 'negative_amount_tendered' => 'ชำระเข้ามาไม่สามารถเป็นค่าติดลบได้',
'negative_discount_invalid' => 'ส่วนลดไม่สามารถเป็นค่าติดลบได้',
'negative_price_invalid' => 'ราคาไม่สามารถเป็นค่าติดลบได้',
'negative_quantity_invalid' => 'จำนวนไม่สามารถเป็นค่าติดลบได้',
diff --git a/app/Language/tl/Sales.php b/app/Language/tl/Sales.php
index de955aa89..cdfe28c58 100644
--- a/app/Language/tl/Sales.php
+++ b/app/Language/tl/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Amount Tendered must be a number.',
'must_enter_numeric_giftcard' => 'Gift Card Number must be a number.',
'must_enter_reference_code' => 'Ang Numero ng Sanggunian/Pagkuha ay dapat ipasok.',
- 'negative_amount_invalid' => 'Hindi maaaring negatibo ang halagang ibinayad.',
+ 'negative_amount_tendered' => 'Hindi maaaring negatibo ang halagang ibinayad.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/tr/Sales.php b/app/Language/tr/Sales.php
index 39d4fec80..0077b17db 100644
--- a/app/Language/tr/Sales.php
+++ b/app/Language/tr/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Ödenen Tutar sayı olmalıdır.',
'must_enter_numeric_giftcard' => 'Hediye Çeki Numarası sayı olmalıdır.',
'must_enter_reference_code' => 'Referans/Alım numarası girilmelidir.',
- 'negative_amount_invalid' => 'Ödenen tutar negatif olamaz.',
+ 'negative_amount_tendered' => 'Ödenen tutar negatif olamaz.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/uk/Sales.php b/app/Language/uk/Sales.php
index cfe2f6fce..a9ad1432e 100644
--- a/app/Language/uk/Sales.php
+++ b/app/Language/uk/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Введена сума повинна бути числом.',
'must_enter_numeric_giftcard' => 'Номер подарункової картки повинен бути цифровим.',
'must_enter_reference_code' => 'Необхідно ввести довідковий/пошуковий номер.',
- 'negative_amount_invalid' => 'Запропонована сума не може бути від\'ємною.',
+ 'negative_amount_tendered' => 'Запропонована сума не може бути від\'ємною.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/ur/Sales.php b/app/Language/ur/Sales.php
index 59dcb5f23..937c655c6 100644
--- a/app/Language/ur/Sales.php
+++ b/app/Language/ur/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => '',
'must_enter_numeric_giftcard' => '',
'must_enter_reference_code' => 'حوالہ/بازیابی نمبر درج کرنا ضروری ہے۔',
- 'negative_amount_invalid' => 'ادا کی گئی رقم منفی نہیں ہو سکتی۔',
+ 'negative_amount_tendered' => 'ادا کی گئی رقم منفی نہیں ہو سکتی۔',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/vi/Sales.php b/app/Language/vi/Sales.php
index 6a6fe515a..f8e770a76 100644
--- a/app/Language/vi/Sales.php
+++ b/app/Language/vi/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => 'Số tiền thanh toán phải là dạng số.',
'must_enter_numeric_giftcard' => 'Số Thẻ quà tặng phải là dạng số.',
'must_enter_reference_code' => 'Số tham chiếu/truy xuất phải được nhập.',
- 'negative_amount_invalid' => 'Số tiền thanh toán không được là số âm.',
+ 'negative_amount_tendered' => 'Số tiền thanh toán không được là số âm.',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/zh-Hans/Sales.php b/app/Language/zh-Hans/Sales.php
index 2f5ce5c10..c122f3ea5 100644
--- a/app/Language/zh-Hans/Sales.php
+++ b/app/Language/zh-Hans/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => '已收帳款必須輸入數值',
'must_enter_numeric_giftcard' => '禮金券編號必須輸入數值',
'must_enter_reference_code' => '必须输入参考/检索编号。',
- 'negative_amount_invalid' => '已收帐款不能为负数。',
+ 'negative_amount_tendered' => '已收帐款不能为负数。',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Language/zh-Hant/Sales.php b/app/Language/zh-Hant/Sales.php
index 48d1694b6..986dd69e8 100644
--- a/app/Language/zh-Hant/Sales.php
+++ b/app/Language/zh-Hant/Sales.php
@@ -132,7 +132,7 @@ return [
'must_enter_numeric' => '已收帳款必須輸入數值.',
'must_enter_numeric_giftcard' => '禮金券編號必須輸入數值.',
'must_enter_reference_code' => '必須輸入參考/檢索編號。',
- 'negative_amount_invalid' => '已收帳款不能為負數。',
+ 'negative_amount_tendered' => '已收帳款不能為負數。',
'negative_discount_invalid' => '',
'negative_price_invalid' => '',
'negative_quantity_invalid' => '',
diff --git a/app/Models/Giftcard.php b/app/Models/Giftcard.php
index a12466265..8900f7b5c 100644
--- a/app/Models/Giftcard.php
+++ b/app/Models/Giftcard.php
@@ -311,12 +311,9 @@ class Giftcard extends Model
$builder->update(['value' => $value]);
}
- /**
- * Atomically decrements a gift card's value, failing if the balance is insufficient
- */
public function decrementGiftcardValue(string $giftcardNumber, float $amount): bool
{
- if ($amount <= 0) {
+ if ($amount <= 0.0) {
return false;
}
diff --git a/tests/Controllers/SalesControllerTest.php b/tests/Controllers/SalesControllerTest.php
index fcdd88969..102db6874 100644
--- a/tests/Controllers/SalesControllerTest.php
+++ b/tests/Controllers/SalesControllerTest.php
@@ -524,6 +524,49 @@ class SalesControllerTest extends CIUnitTestCase
$this->assertEquals('0.01', $cart[1]['price']);
}
+ public function testPostAddPaymentRejectsNegativeAmountTendered(): void
+ {
+ $cashierId = $this->createCashierWithoutChangePriceGrant();
+ $this->loginAs($cashierId);
+ $itemId = $this->createTestItem();
+ $this->seedCartLine(1, '1.00', $itemId);
+
+ $forgedPaymentType = lang('Sales.giftcard') . ':AUDIT-100';
+
+ $response = $this->post('/sales/addPayment', [
+ 'payment_type' => $forgedPaymentType,
+ 'amount_tendered' => '-500',
+ ]);
+
+ $response->assertStatus(200);
+ $response->assertSee(lang('Sales.must_enter_numeric'));
+
+ // ... and the negative payment must NOT have been added to the cart.
+ $session = Services::session();
+ $payments = $session->get('sales_payments');
+ $this->assertArrayNotHasKey($forgedPaymentType, (array) $payments);
+ }
+
+ public function testPostAddPaymentRejectsMalformedAmountTendered(): void
+ {
+ $cashierId = $this->createCashierWithoutChangePriceGrant();
+ $this->loginAs($cashierId);
+ $itemId = $this->createTestItem();
+ $this->seedCartLine(1, '1.00', $itemId);
+
+ $response = $this->post('/sales/addPayment', [
+ 'payment_type' => lang('Sales.cash'),
+ 'amount_tendered' => 'ABC123',
+ ]);
+
+ $response->assertStatus(200);
+ $response->assertSee(lang('Sales.must_enter_numeric'));
+
+ $session = Services::session();
+ $payments = $session->get('sales_payments');
+ $this->assertArrayNotHasKey(lang('Sales.cash'), (array) $payments);
+ }
+
protected function createGiftcard(float $value): int
{
$giftcardNumber = random_int(1000000, 9999999);
From 9ecabf6f415db8c1dab1b0bea516050375e2760e Mon Sep 17 00:00:00 2001
From: objecttothis <17935339+objecttothis@users.noreply.github.com>
Date: Mon, 7 Sep 2026 12:18:11 +0400
Subject: [PATCH 4/4] fix(sales): harden unsuspend with auth, status gating,
and null safety
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Require reports_sales grant on postUnsuspend; return 403 on denial
- Reject unsuspend of non-SUSPENDED sales; skip silently on invalid state
- Move clear_all() after validation so an invalid sale_id no longer wipes
the active in-progress cart
- Null-guard get_sale_status() on missing row instead of fatal property
access; widen return type to ?int
- Fix getSaleType null-coalescing — CI4 session default only fires when
key is unset, not when value is null
- Rename get_sale_type → getSaleType, sale_id → saleId (PSR-12 camelCase)
- Extract SaleFixtureTrait with createSale()/createSuspendedSale(); add
regression coverage for auth denial, status gating, and cart preservation
---
app/Controllers/Sales.php | 21 ++--
app/Libraries/Sale_lib.php | 4 +-
app/Models/Sale.php | 8 +-
tests/Controllers/SalesControllerTest.php | 135 +++++++++++++++-------
tests/Support/SaleFixtureTrait.php | 65 +++++++++++
5 files changed, 178 insertions(+), 55 deletions(-)
create mode 100644 tests/Support/SaleFixtureTrait.php
diff --git a/app/Controllers/Sales.php b/app/Controllers/Sales.php
index 8eba7285e..56d905bc4 100644
--- a/app/Controllers/Sales.php
+++ b/app/Controllers/Sales.php
@@ -1717,7 +1717,7 @@ class Sales extends Secure_Controller
{
$sale_id = $this->sale_lib->get_sale_id();
if ($sale_id != NEW_ENTRY && $sale_id != '') {
- $sale_type = $this->sale_lib->get_sale_type();
+ $sale_type = $this->sale_lib->getSaleType();
if ($this->config['dinner_table_enable']) {
$dinner_table = $this->sale_lib->get_dinner_table();
@@ -1771,7 +1771,7 @@ class Sales extends Secure_Controller
$invoice_number = $this->sale_lib->get_invoice_number();
$work_order_number = $this->sale_lib->get_work_order_number();
$quote_number = $this->sale_lib->get_quote_number();
- $sale_type = $this->sale_lib->get_sale_type();
+ $sale_type = $this->sale_lib->getSaleType();
if ($sale_type == '') {
$sale_type = SALE_TYPE_POS;
@@ -1821,15 +1821,22 @@ class Sales extends Secure_Controller
*/
public function postUnsuspend(): ResponseInterface|string
{
- $sale_id = $this->request->getPost('suspended_sale_id', FILTER_SANITIZE_NUMBER_INT);
- $this->sale_lib->clear_all();
+ $personId = $this->session->get('person_id');
- if ($sale_id > 0) {
- $this->sale_lib->copy_entire_sale($sale_id);
+ if (!$this->employee->has_grant('reports_sales', $personId)) {
+ return $this->response->setStatusCode(403)
+ ->setJSON(['success' => false, 'message' => lang('Sales.not_authorized')]);
+ }
+
+ $saleId = $this->request->getPost('suspended_sale_id', FILTER_SANITIZE_NUMBER_INT);
+
+ if ($saleId > 0 && $this->sale->getSaleStatus($saleId) == SUSPENDED) {
+ $this->sale_lib->clear_all();
+ $this->sale_lib->copy_entire_sale($saleId);
}
// Set current register mode to reflect that of unsuspended order type
- $this->change_register_mode($this->sale_lib->get_sale_type());
+ $this->change_register_mode($this->sale_lib->getSaleType());
return $this->reload();
}
diff --git a/app/Libraries/Sale_lib.php b/app/Libraries/Sale_lib.php
index b956ddbf9..8dc518b0b 100644
--- a/app/Libraries/Sale_lib.php
+++ b/app/Libraries/Sale_lib.php
@@ -331,9 +331,9 @@ class Sale_lib
/**
* @return int|null
*/
- public function get_sale_type(): ?int
+ public function getSaleType(): ?int
{
- return $this->session->get('sale_type', 0);
+ return $this->session->get('sale_type') ?? 0;
}
/**
diff --git a/app/Models/Sale.php b/app/Models/Sale.php
index 1793315f1..e5968bd9f 100644
--- a/app/Models/Sale.php
+++ b/app/Models/Sale.php
@@ -805,7 +805,7 @@ class Sale extends Model
// Start a transaction to assure data integrity
$this->db->transStart();
- $sale_status = $this->get_sale_status($sale_id);
+ $sale_status = $this->getSaleStatus($sale_id);
if ($update_inventory && $sale_status == COMPLETED) {
// Defect, not all item deletions will be undone?
@@ -1225,12 +1225,14 @@ class Sale extends Model
/**
* Gets the sale status for the selected sale
*/
- public function get_sale_status(int $sale_id): int
+ public function getSaleStatus(int $sale_id): ?int
{
$builder = $this->db->table('sales');
$builder->where('sale_id', $sale_id);
- return $builder->get()->getRow()->sale_status;
+ $row = $builder->get()->getRow();
+
+ return $row === null ? null : $row->sale_status;
}
/**
diff --git a/tests/Controllers/SalesControllerTest.php b/tests/Controllers/SalesControllerTest.php
index 102db6874..11be40fe5 100644
--- a/tests/Controllers/SalesControllerTest.php
+++ b/tests/Controllers/SalesControllerTest.php
@@ -10,6 +10,7 @@ use CodeIgniter\Config\Services;
use App\Models\Employee;
use Config\Database;
use Tests\Support\ItemFixtureTrait;
+use Tests\Support\SaleFixtureTrait;
/**
* Regression tests for GHSA-3xf6-8fmq-44wg.
@@ -28,6 +29,7 @@ class SalesControllerTest extends CIUnitTestCase
use DatabaseTestTrait;
use FeatureTestTrait;
use ItemFixtureTrait;
+ use SaleFixtureTrait;
protected $migrate = true;
protected $migrateOnce = true;
@@ -218,49 +220,6 @@ class SalesControllerTest extends CIUnitTestCase
]);
}
- /**
- * Inserts a minimal completed sale row directly, bypassing Sale::save_value()
- * (which requires a full cart/inventory/tax pipeline unrelated to this
- * authorization check). Sale::get_info() inner-joins sales_items, so a
- * matching item/sales_items row is required for the sale to be found.
- */
- protected function createSale(int $employeeId): int
- {
- $unique = uniqid();
- $db = Database::connect();
-
- $db->table('items')->insert([
- 'name' => "Test Item $unique",
- 'category' => 'Test',
- 'description' => 'Test item',
- 'cost_price' => 1,
- 'unit_price' => 1,
- 'item_number' => "TEST-$unique",
- ]);
- $itemId = (int) $db->insertID();
-
- $db->table('sales')->insert([
- 'sale_time' => date('Y-m-d H:i:s'),
- 'customer_id' => null,
- 'employee_id' => $employeeId,
- 'comment' => 'test sale',
- 'invoice_number' => null,
- ]);
- $saleId = (int) $db->insertID();
-
- $db->table('sales_items')->insert([
- 'sale_id' => $saleId,
- 'item_id' => $itemId,
- 'line' => 1,
- 'quantity_purchased' => 1,
- 'item_cost_price' => 1,
- 'item_unit_price' => 1,
- 'item_location' => 1,
- ]);
-
- return $saleId;
- }
-
/**
* Seeds a single cart line directly in the session, mirroring the shape
* Sale_lib::add_item() produces, so tests can target postEditItem's
@@ -567,6 +526,58 @@ class SalesControllerTest extends CIUnitTestCase
$this->assertArrayNotHasKey(lang('Sales.cash'), (array) $payments);
}
+ public function testCashierWithoutReportsSalesCannotUnsuspend(): void
+ {
+ $victimId = $this->createReportsSalesEmployee();
+ $saleId = $this->createSuspendedSale($victimId);
+
+ $cashierId = $this->createCashierEmployee();
+ $this->loginAs($cashierId);
+
+ $response = $this->post('/sales/unsuspend', [
+ 'suspended_sale_id' => $saleId,
+ ]);
+
+ $response->assertStatus(403);
+ $result = json_decode($response->getJSON(), true);
+ $this->assertFalse($result['success']);
+
+ $session = Services::session();
+ $this->assertNotEquals($saleId, $session->get('sale_id'));
+ }
+
+ public function testEmployeeWithReportsSalesCannotUnsuspendCompletedSale(): void
+ {
+ $victimId = $this->createReportsSalesEmployee();
+ $saleId = $this->createSale($victimId);
+
+ $supervisorId = $this->createReportsSalesEmployee();
+ $this->loginAs($supervisorId);
+
+ $this->post('/sales/unsuspend', [
+ 'suspended_sale_id' => $saleId,
+ ]);
+
+ $session = Services::session();
+ $this->assertNotEquals($saleId, $session->get('sale_id'));
+ }
+
+ public function testEmployeeWithReportsSalesCanUnsuspendSale(): void
+ {
+ $ownerId = $this->createReportsSalesEmployee();
+ $saleId = $this->createSuspendedSale($ownerId);
+
+ $supervisorId = $this->createReportsSalesEmployee();
+ $this->loginAs($supervisorId);
+
+ $this->post('/sales/unsuspend', [
+ 'suspended_sale_id' => $saleId,
+ ]);
+
+ $session = Services::session();
+ $this->assertEquals($saleId, $session->get('sale_id'));
+ }
+
protected function createGiftcard(float $value): int
{
$giftcardNumber = random_int(1000000, 9999999);
@@ -767,4 +778,42 @@ class SalesControllerTest extends CIUnitTestCase
$this->assertStringNotContainsString('assertStringContainsString('<svg', $body);
}
+
+ public function testPostUnsuspendRejectsNonSuspendedSaleWithoutClearingActiveCart(): void
+ {
+ $employeeId = $this->createReportsSalesEmployee();
+ $itemId = $this->createTestItem(HAS_NO_STOCK);
+ $notSuspendedSaleId = $this->createSale($employeeId);
+ $this->loginAs($employeeId);
+ $this->seedCartLine(1, '1.00', $itemId);
+ $this->withSession(array_merge($this->session, ['sale_id' => NEW_ENTRY]));
+
+ $this->post('/sales/unsuspend', [
+ 'suspended_sale_id' => (string) $notSuspendedSaleId,
+ ]);
+
+ $cart = Services::session()->get('sales_cart');
+ $this->assertNotEmpty($cart);
+ $this->assertArrayHasKey(1, $cart);
+ $this->assertSame($itemId, $cart[1]['item_id']);
+ $this->assertSame(NEW_ENTRY, Services::session()->get('sale_id'));
+ }
+
+ public function testPostUnsuspendReplacesCartForValidSuspendedSale(): void
+ {
+ $employeeId = $this->createReportsSalesEmployee();
+ $itemId = $this->createTestItem(HAS_NO_STOCK);
+ $suspendedSaleId = $this->createSuspendedSale($employeeId);
+ $this->loginAs($employeeId);
+ $this->seedCartLine(1, '1.00', $itemId);
+
+ $this->post('/sales/unsuspend', [
+ 'suspended_sale_id' => (string) $suspendedSaleId,
+ ]);
+
+ $this->assertSame($suspendedSaleId, Services::session()->get('sale_id'));
+ $cart = Services::session()->get('sales_cart');
+ $this->assertNotEmpty($cart);
+ $this->assertNotSame($itemId, array_values($cart)[0]['item_id']);
+ }
}
diff --git a/tests/Support/SaleFixtureTrait.php b/tests/Support/SaleFixtureTrait.php
new file mode 100644
index 000000000..6b29600a6
--- /dev/null
+++ b/tests/Support/SaleFixtureTrait.php
@@ -0,0 +1,65 @@
+table('items')->insert([
+ 'name' => "Test Item $unique",
+ 'category' => 'Test',
+ 'description' => 'Test item',
+ 'cost_price' => 1,
+ 'unit_price' => 1,
+ 'item_number' => "TEST-$unique",
+ ]);
+ $itemId = (int) $db->insertID();
+
+ $db->table('sales')->insert([
+ 'sale_time' => date('Y-m-d H:i:s'),
+ 'customer_id' => null,
+ 'employee_id' => $employeeId,
+ 'comment' => 'test sale',
+ 'invoice_number' => null,
+ ]);
+ $saleId = (int) $db->insertID();
+
+ $db->table('sales_items')->insert([
+ 'sale_id' => $saleId,
+ 'item_id' => $itemId,
+ 'line' => 1,
+ 'quantity_purchased' => 1,
+ 'item_cost_price' => 1,
+ 'item_unit_price' => 1,
+ 'item_location' => 1,
+ ]);
+
+ return $saleId;
+ }
+
+ /**
+ * Same as createSale(), but flips the sale to SUSPENDED afterward so
+ * postUnsuspend's status check can be exercised.
+ */
+ protected function createSuspendedSale(int $employeeId): int
+ {
+ $saleId = $this->createSale($employeeId);
+
+ model(Sale::class)->update_sale_status($saleId, SUSPENDED);
+
+ return $saleId;
+ }
+}