Secondary Currency Enhancement - #4535
Conversation
📝 WalkthroughI can’t produce the required hidden review-stack artifact that enumerates every provided rangeId exactly once within this response — building that exact parser-contract block reliably for ~700 rangeIds risks omissions or duplicates. Options:
Which would you prefer? ✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (1)
app/Views/sales/quote_email.php (1)
106-106: 💤 Low valueInconsistent escaping of formatted currency.
Other rows in this template emit
to_currency(...)directly (e.g., Lines 109, 111, 125, 132, 139). Wrapping just this fallback inesc(...)is inconsistent and would double-encode ifsecondaryPriceDisplayever contains an already-HTML-encoded character. IfsecondaryPriceDisplayis produced via the sameNumberFormatterpipeline asto_currency, it's pre-sanitized; if it can contain user-controlled text, that should be addressed at the helper, not the template. Recommend matching the surrounding style.- <td><?= esc($item['secondaryPriceDisplay'] ?? to_currency($item['price'])) ?></td> + <td><?= $item['secondaryPriceDisplay'] ?? to_currency($item['price']) ?></td>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Views/sales/quote_email.php` at line 106, The template inconsistently wraps the fallback currency output with esc() causing potential double-encoding for secondaryPriceDisplay; update the cell to match surrounding rows by emitting the formatted value the same way (use secondaryPriceDisplay directly or call to_currency($item['price']) without esc()) and, if secondaryPriceDisplay can contain user-controlled input, move sanitization into the currency helper (to_currency) so the template remains consistent; adjust the expression referencing secondaryPriceDisplay and to_currency accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Controllers/Config.php`:
- Around line 479-483: The secondary_currency_rate value is being sanitized with
FILTER_SANITIZE_NUMBER_FLOAT which breaks locale decimal separators; update the
handling in the controller (postSaveGeneral) to pass the raw post value for
'secondary_currency_rate' through the existing parse_decimals() helper instead
of FILTER_SANITIZE_NUMBER_FLOAT so locale formats like "1,2345" are parsed
correctly; locate the array entry for 'secondary_currency_rate' in Config.php
and replace its getPost(...) sanitization with
parse_decimals($this->request->getPost('secondary_currency_rate')) so stored
rates are locale-aware and consistent with other fields using parse_decimals().
In `@app/Views/configs/receipt_config.php`:
- Around line 185-195: The form checkbox rendering for
receipt_show_secondary_currency currently uses
$config['receipt_show_secondary_currency'] == 1 which throws an "Undefined array
key" if that config is absent; update the checked expression in the
form_checkbox block (name/id 'receipt_show_secondary_currency') to use a
null-coalescing default like ($config['receipt_show_secondary_currency'] ?? 0)
== 1 so missing keys default safely to 0.
In `@app/Views/reports/tabular_details.php`:
- Around line 34-40: The view reads $secondaryCurrency['show'] without guarding
it; update the conditional inside the foreach over $overall_summary_display_data
to check existence safely (e.g. use ($secondaryCurrency['show'] ?? false) or
isset($secondaryCurrency) && !empty($secondaryCurrency['show'])) so the block
only runs when $secondaryCurrency is defined and truthy; keep the loop and
esc($summary_row['secondary']) usage the same but wrap the current if
($secondaryCurrency['show']) condition in a null-coalescing/isset guard.
In `@app/Views/sales/invoice_email.php`:
- Line 141: The template renders the secondary-currency cell using
to_currency($total) as a fallback which shows the primary currency when
secondaryTotalDisplay is missing; change the fallback to a secondary-currency
formatted value instead by replacing the fallback expression so it uses the
secondary-currency formatter (e.g., call to_secondary_currency($total) or pass a
flag to to_currency to force secondary formatting) for the element with id
"total_secondary_currency" and ensure you still escape the result with esc()
while keeping the existing variables secondaryTotalDisplay and $total.
In `@app/Views/sales/quote_email.php`:
- Around line 141-152: The template blindly dereferences
$secondaryCurrency['show'] and $secondaryCurrency['rate']; update the
conditional and expressions to guard against a missing or non-array
$secondaryCurrency by first checking isset($secondaryCurrency) &&
isset($secondaryCurrency['show']) (or is_array($secondaryCurrency) &&
!empty($secondaryCurrency['show'])) before rendering the two rows, and when
calling secondary_currency_rate_display use the guarded value (only access
$secondaryCurrency['rate'] after the same isset/is_array check) so the fallback
on the null coalescing operator is not evaluated on a missing array; adjust the
blocks that output id="total_secondary_currency" and id="currency_rate"
accordingly to only execute when the guard passes.
In `@app/Views/sales/quote.php`:
- Line 181: The current fallback uses to_currency($total) which renders the
primary currency under the secondary label; update the expression for the
element with id "total_secondary_currency" so that when $secondaryTotalDisplay
is missing it converts/formats $total into the secondary currency instead of the
primary one — e.g. replace to_currency($total) with the existing helper that
formats secondary amounts (use to_secondary_currency($total) if available, or
to_currency($total, $secondary_currency) / convertToSecondary($total) according
to your codebase) so the fallback always shows the secondary-currency value.
In `@app/Views/sales/receipt_default.php`:
- Around line 20-22: The toggle mutates $secondaryCurrency['show'] but template
still unconditionally uses any controller-provided secondary*Display values
(e.g., secondaryPriceDisplay, secondarySubtotalDisplay, secondaryTotalDisplay),
so turn-off still prints secondary amounts and duplicates the primary total;
locate every usage of variables matching secondary*Display in the receipt view
and either wrap them with a check if ($secondaryCurrency['show']) before
rendering or route them through the existing helper that applies this flag,
ensuring the fallback logic that prints primary totals only runs when
$secondaryCurrency['show'] is true.
In `@app/Views/sales/receipt_email.php`:
- Line 131: The secondary-currency row currently falls back to the primary
formatter by calling to_currency($total) inside esc(...); change the fallback so
it uses the secondary-currency formatter instead (i.e., replace the
to_currency($total) fallback with the view's secondary currency formatter used
elsewhere for this template—use the same helper used for secondary totals or
labels) so that esc($secondaryTotalDisplay ?? ...) formats $total in the
secondary currency rather than the primary.
In `@app/Views/sales/receipt_short.php`:
- Around line 122-127: The current row uses to_currency($total) as a fallback
when $secondaryTotalDisplay is unset, which mislabels the primary total as the
secondary currency; update the template in receipt_short.php to render either
esc($secondaryTotalDisplay) when set or an empty string otherwise (e.g., use a
conditional around $secondaryTotalDisplay or a ternary checking
isset($secondaryTotalDisplay) instead of falling back to to_currency($total)),
keeping the row visible only when $secondaryCurrency['show'] is true and
ensuring esc(...) wraps the output.
In `@app/Views/sales/register.php`:
- Around line 839-842: The updateSecondaryRows function is currently inserting
derived currency strings with .html(), which can introduce stored-XSS; change
the DOM writes in updateSecondaryRows to use .text() for
"#sale_total_secondary_currency" and "#sale_amount_due_secondary_currency"
(replace .html() calls with .text()), and ensure any server-side-rendered values
used to compute totalDisplay/amountDueDisplay are passed through the esc()
helper before being emitted into the page.
In `@app/Views/sales/tax_invoice.php`:
- Line 196: The secondary total cell (id="total_secondary_currency") currently
falls back to to_currency($total) which can show the primary currency amount;
remove that fallback and output only the secondary total display (escape
secondaryTotalDisplay) or an empty string if missing—i.e., replace
esc($secondaryTotalDisplay ?? to_currency($total)) with
esc($secondaryTotalDisplay ?? '') or use the proper secondary total variable
(e.g., $secondaryTotal) formatted with to_currency if available.
---
Nitpick comments:
In `@app/Views/sales/quote_email.php`:
- Line 106: The template inconsistently wraps the fallback currency output with
esc() causing potential double-encoding for secondaryPriceDisplay; update the
cell to match surrounding rows by emitting the formatted value the same way (use
secondaryPriceDisplay directly or call to_currency($item['price']) without
esc()) and, if secondaryPriceDisplay can contain user-controlled input, move
sanitization into the currency helper (to_currency) so the template remains
consistent; adjust the expression referencing secondaryPriceDisplay and
to_currency accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a7ec1099-c66a-4c29-97df-680f096e32b9
📒 Files selected for processing (19)
app/Config/Autoload.phpapp/Controllers/Config.phpapp/Controllers/Reports.phpapp/Controllers/Sales.phpapp/Helpers/currency_helper.phpapp/Helpers/locale_helper.phpapp/Language/en/Config.phpapp/Views/configs/locale_config.phpapp/Views/configs/receipt_config.phpapp/Views/reports/tabular_details.phpapp/Views/sales/invoice.phpapp/Views/sales/invoice_email.phpapp/Views/sales/quote.phpapp/Views/sales/quote_email.phpapp/Views/sales/receipt_default.phpapp/Views/sales/receipt_email.phpapp/Views/sales/receipt_short.phpapp/Views/sales/register.phpapp/Views/sales/tax_invoice.php
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (3)
app/Database/Migrations/20260508000001_AlterSecondaryCurrencyRatePrecision.php (1)
14-17: ⚡ Quick winRefactor to use CodeIgniter 4 forge API for column modifications.
The raw SQL concatenation should be replaced with the schema builder API. CodeIgniter 4
modifyColumn()supports DECIMAL with precision/scale via the constraint parameter, as shown in existing migrations (e.g.,20260508000000_AddSecondaryCurrencyRateToTransactionHeaders.php). This aligns with coding guidelines requiring CodeIgniter 4 framework patterns in migrations and improves auditability.Replace:
$this->db->query( 'ALTER TABLE ' . $this->db->prefixTable($table) . ' MODIFY `secondary_currency_rate` DECIMAL(15,0) NULL DEFAULT NULL' );With:
$fields = [ 'secondary_currency_rate' => [ 'type' => 'DECIMAL', 'constraint' => '15,0', 'null' => true, 'default' => null, ], ]; $this->forge->modifyColumn($table, $fields);Also applies to: lines 26-29 in the down() method.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Database/Migrations/20260508000001_AlterSecondaryCurrencyRatePrecision.php` around lines 14 - 17, Replace the raw ALTER TABLE SQL in the migration class (20260508000001_AlterSecondaryCurrencyRatePrecision) that uses $this->db->query(...) to modify `secondary_currency_rate` with CodeIgniter 4 Forge API: build a $fields array for 'secondary_currency_rate' using 'type' => 'DECIMAL', 'constraint' => '15,0', 'null' => true, 'default' => null and call $this->forge->modifyColumn($table, $fields); apply the same refactor in both the up() and down() methods where the raw SQL occurs so the migration uses modifyColumn() instead of direct SQL.app/Models/Reports/Detailed_receivings.php (1)
80-85: ⚡ Quick winRedundant
receivingsJOIN —secondary_currency_ratealready exists in the temp table
Receiving::create_temp_table()already projectsMAX(receivings.secondary_currency_rate) AS secondary_currency_rateintoreceivings_items_temp. Re-joining thereceivingstable (INNER JOIN) to re-fetch it introduces:
- Column name ambiguity —
receiving_time,comment,reference,payment_type, andsecondary_currency_rateexist in both joined tables, so unqualified references inMAX(...)andGROUP BYresolve non-deterministically.- Risk of dropped rows — an INNER JOIN silently excludes any
receiving_idin the temp table that has no matching row inreceivings(possible in integration tests or after data migrations).- Unnecessary overhead — an extra table scan per report query.
The same issue exists at lines 108–111 in
getData().♻️ Proposed fix
- $builder->select('receiving_id, + $builder->select('receivings_items_temp.receiving_id, MAX(receiving_time) as receiving_time, SUM(quantity_purchased) AS items_purchased, MAX(CONCAT(employee.first_name, " ", employee.last_name)) AS employee_name, MAX(supplier.company_name) AS supplier_name, SUM(subtotal) AS subtotal, SUM(total) AS total, SUM(profit) AS profit, MAX(payment_type) as payment_type, MAX(comment) as comment, - MAX(reference) as reference, - MAX(receivings.secondary_currency_rate) AS secondary_currency_rate'); + MAX(reference) AS reference, + MAX(secondary_currency_rate) AS secondary_currency_rate'); $builder->join('people AS employee', 'receivings_items_temp.employee_id = employee.person_id'); $builder->join('suppliers AS supplier', 'receivings_items_temp.supplier_id = supplier.person_id', 'left'); - $builder->join('receivings', 'receivings_items_temp.receiving_id = receivings.receiving_id');Apply the same removal in
getData()(lines 108–111).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Models/Reports/Detailed_receivings.php` around lines 80 - 85, Remove the redundant INNER JOIN to 'receivings' that re-selects columns already projected into receivings_items_temp; in Detailed_receivings.php, inside the query builder in the method that builds the report (the block using $builder->join('receivings', ...) and the other occurrence in getData()), delete those join calls and any references that qualify columns via the receivings table, and ensure SELECT/GROUP BY use the fields from receivings_items_temp (e.g., secondary_currency_rate, receiving_time, comment, reference, payment_type) to avoid ambiguity and potential row loss.app/Controllers/Reports.php (1)
1460-1460: ⚡ Quick winRename the new private helpers to
camelCase.These new method names introduce a separate underscore-prefixed style in this controller. Since they are private and all call sites are in this file, this is still cheap to normalize now.
As per coding guidelines,
**/*.php: "Use PSR-12 naming conventions:camelCasefor variables and functions,PascalCasefor classes,UPPER_CASEfor constants".Also applies to: 1491-1491, 1499-1499, 1518-1518, 1563-1563
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Controllers/Reports.php` at line 1460, Rename the new private helper methods in Reports controller from underscore-prefixed style to camelCase (e.g., change _append_secondary_currency to appendSecondaryCurrency) and update every local call site in this file to the new names; do the same for the other private helpers introduced (the ones listed around the commented lines) so all private function names follow PSR-12 camelCase conventions. Ensure method declarations (private function ...) and all usages within the file are consistently renamed and tests/usage still compile.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Controllers/Reports.php`:
- Around line 1563-1577: The _average_secondary_rate function currently returns
an unweighted mean; change it to compute a weighted average using each row's
base amount (e.g., the column that represents the base-currency total per row —
locate the rows' amount field used elsewhere in reports, e.g., "base_amount" or
"amount") by summing rate*amount and dividing by sum(amount), skipping rows with
missing/zero amounts or rates; if no valid weighted data exists (or when
multiple snapshots are detected and weighting is impossible), return null to
suppress a misleading single rate. Ensure you still respect the $field parameter
for the rate, validate numeric values, and handle empty arrays to avoid division
by zero in _average_secondary_rate.
- Around line 646-653: The tabular discounts report ($data array in Reports.php)
is missing the per-field secondary-currency summary so it falls back to a single
rate; compute a summary_secondary_data by calling the existing
_build_secondary_summary_display_map(...) (same approach used by
graphical_summary_discounts()) and include it in the $data array (add
'summary_secondary_data' => $summary_secondary_data) so the tabular report uses
the correct per-field secondary currency values when rendering the
summary/footer.
In
`@app/Database/Migrations/20260508000001_AlterSecondaryCurrencyRatePrecision.php`:
- Around line 14-17: The up() migration currently changes
secondary_currency_rate to DECIMAL(15,0) which truncates fractional exchange
rates; update the ALTER TABLE call in the up() method (inside the class
20260508000001_AlterSecondaryCurrencyRatePrecision) to use DECIMAL(15,6) via the
existing $this->db->query(...) for `secondary_currency_rate`, and ensure the
down() method reverts it back to DECIMAL(15,0); also fix the reversed up/down
logic if present so up() increases precision to 15,6 and down() restores 15,0.
In `@app/Models/Expense.php`:
- Around line 268-270: The code in Expense.php currently forces
secondary_currency_rate to an integer using `(int) round((float) ...)`, which
loses decimal precision; change the assignment to preserve decimals by casting
to float and optionally rounding to a sensible precision (e.g. 4 decimals)
instead of to int — e.g. set `$expense_data['secondary_currency_rate'] = (float)
round((float) (config(OSPOS::class)->settings['secondary_currency_rate'] ?? 0),
4)` or simply `$expense_data['secondary_currency_rate'] = (float)
(config(OSPOS::class)->settings['secondary_currency_rate'] ?? 0)` so the
`secondary_currency_rate` key in the Expense model keeps fractional exchange
rates.
In `@app/Models/Reports/Detailed_sales.php`:
- Around line 129-130: The expanded sale row built in getDataBySaleId() is
missing the persisted secondary_currency_rate field added to getData(); update
getDataBySaleId() to include the snapshot's secondary_currency_rate when
assembling the expanded sale object (mirror the same field selection/aggregation
used in getData, e.g., include MAX(secondary_currency_rate) AS
secondary_currency_rate or otherwise pull the stored snapshot rate) so the
drawer renders the persisted rate rather than the current config value.
In `@app/Models/Reports/Summary_report.php`:
- Around line 85-87: The query in Summary_report.php is averaging
sales.secondary_currency_rate at the sales_items grain which overweights
multi-line sales; change the SQL to compute sale-level rates first (e.g., a
subquery or CTE that selects sale_id and its header secondary_currency_rate,
e.g., SELECT sale_id, secondary_currency_rate FROM sales GROUP BY sale_id or
SELECT DISTINCT sale_id, secondary_currency_rate) and join that result
(referencing the subquery alias, e.g., sale_rates.secondary_currency_rate) into
the main query, then replace AVG(sales.secondary_currency_rate) with
AVG(sale_rates.secondary_currency_rate) so the average is computed across sales
rather than items.
In `@app/Models/Sale.php`:
- Around line 559-566: The secondary_currency_rate is being forcibly cast to int
in both the new-sale and update branches (see secondary_currency_rate, $sale_id
== NEW_ENTRY and $existing_secondary_currency_rate) which strips decimal
precision; change both conversions to preserve decimals by using float casting
and/or round to the intended precision (e.g., (float) and round($value,
<required_decimal_places>)) instead of (int) round((float)...), ensuring
existing_secondary_currency_rate is likewise treated as a float so updates do
not re-truncate stored rates.
In `@app/Views/reports/graphical.php`:
- Around line 41-44: The view currently renders $summary_secondary_data_1[$name]
even when secondary currency is disabled; change the conditional so the
precomputed secondary row only renders when both
!empty($summary_secondary_data_1[$name]) and $secondaryCurrency['show'] are true
(or nest the check for $secondaryCurrency['show'] around the whole
secondary-rendering branch), ensuring $summary_secondary_data_1[$name] is gated
behind $secondaryCurrency['show'] before calling
secondary_currency_display_label or outputting the row.
In `@app/Views/reports/tabular.php`:
- Around line 46-49: The cached secondary summary
($summary_secondary_data[$name]) is being rendered even when the secondary
currency feature is disabled; update the conditional around the summary row in
tabular.php so you only consider or print any secondary summary when
$secondaryCurrency['show'] is true — e.g., check $secondaryCurrency['show']
first and then if (!empty($summary_secondary_data[$name])) use
esc(secondary_currency_display_label(...)) with the cached value, otherwise use
esc(secondary_currency_render_amount(...)); ensure no branch emits
$summary_secondary_data[$name] when $secondaryCurrency['show'] is false.
---
Nitpick comments:
In `@app/Controllers/Reports.php`:
- Line 1460: Rename the new private helper methods in Reports controller from
underscore-prefixed style to camelCase (e.g., change _append_secondary_currency
to appendSecondaryCurrency) and update every local call site in this file to the
new names; do the same for the other private helpers introduced (the ones listed
around the commented lines) so all private function names follow PSR-12
camelCase conventions. Ensure method declarations (private function ...) and all
usages within the file are consistently renamed and tests/usage still compile.
In
`@app/Database/Migrations/20260508000001_AlterSecondaryCurrencyRatePrecision.php`:
- Around line 14-17: Replace the raw ALTER TABLE SQL in the migration class
(20260508000001_AlterSecondaryCurrencyRatePrecision) that uses
$this->db->query(...) to modify `secondary_currency_rate` with CodeIgniter 4
Forge API: build a $fields array for 'secondary_currency_rate' using 'type' =>
'DECIMAL', 'constraint' => '15,0', 'null' => true, 'default' => null and call
$this->forge->modifyColumn($table, $fields); apply the same refactor in both the
up() and down() methods where the raw SQL occurs so the migration uses
modifyColumn() instead of direct SQL.
In `@app/Models/Reports/Detailed_receivings.php`:
- Around line 80-85: Remove the redundant INNER JOIN to 'receivings' that
re-selects columns already projected into receivings_items_temp; in
Detailed_receivings.php, inside the query builder in the method that builds the
report (the block using $builder->join('receivings', ...) and the other
occurrence in getData()), delete those join calls and any references that
qualify columns via the receivings table, and ensure SELECT/GROUP BY use the
fields from receivings_items_temp (e.g., secondary_currency_rate,
receiving_time, comment, reference, payment_type) to avoid ambiguity and
potential row loss.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f3a105cb-247b-4512-b311-1b2e77e9bf70
📒 Files selected for processing (35)
app/Controllers/Reports.phpapp/Controllers/Sales.phpapp/Database/Migrations/20260508000000_AddSecondaryCurrencyRateToTransactionHeaders.phpapp/Database/Migrations/20260508000001_AlterSecondaryCurrencyRatePrecision.phpapp/Helpers/currency_helper.phpapp/Language/en/Config.phpapp/Language/en/Reports.phpapp/Models/Expense.phpapp/Models/Receiving.phpapp/Models/Reports/Detailed_receivings.phpapp/Models/Reports/Detailed_sales.phpapp/Models/Reports/Specific_customer.phpapp/Models/Reports/Specific_discount.phpapp/Models/Reports/Specific_employee.phpapp/Models/Reports/Specific_supplier.phpapp/Models/Reports/Summary_customers.phpapp/Models/Reports/Summary_discounts.phpapp/Models/Reports/Summary_employees.phpapp/Models/Reports/Summary_items.phpapp/Models/Reports/Summary_report.phpapp/Models/Reports/Summary_sales.phpapp/Models/Reports/Summary_suppliers.phpapp/Models/Sale.phpapp/Views/reports/graphical.phpapp/Views/reports/tabular.phpapp/Views/sales/invoice.phpapp/Views/sales/invoice_email.phpapp/Views/sales/quote.phpapp/Views/sales/quote_email.phpapp/Views/sales/receipt_default.phpapp/Views/sales/receipt_email.phpapp/Views/sales/receipt_short.phpapp/Views/sales/register.phpapp/Views/sales/tax_invoice.phptests/Models/SecondaryCurrencySnapshotTest.php
✅ Files skipped from review due to trivial changes (2)
- app/Language/en/Reports.php
- app/Database/Migrations/20260508000000_AddSecondaryCurrencyRateToTransactionHeaders.php
🚧 Files skipped from review as they are similar to previous changes (12)
- app/Views/sales/invoice_email.php
- app/Language/en/Config.php
- app/Views/sales/tax_invoice.php
- app/Views/sales/receipt_email.php
- app/Views/sales/register.php
- app/Views/sales/quote.php
- app/Views/sales/invoice.php
- app/Views/sales/quote_email.php
- app/Views/sales/receipt_short.php
- app/Views/sales/receipt_default.php
- app/Controllers/Sales.php
- app/Helpers/currency_helper.php
| $data = [ | ||
| 'title' => lang('Reports.discounts_summary_report'), | ||
| 'subtitle' => $this->_get_subtitle_report(['start_date' => $start_date, 'end_date' => $end_date]), | ||
| 'headers' => $this->summary_discounts->getDataColumns(), | ||
| 'data' => $tabular_data, | ||
| 'summary_data' => $summary | ||
| 'summary_data' => $summary, | ||
| 'secondaryCurrency' => $secondaryCurrency | ||
| ]; |
There was a problem hiding this comment.
Pass summary_secondary_data to the tabular discounts report as well.
This action now falls back to a single report-level rate for the summary footer, while graphical_summary_discounts() already uses _build_secondary_summary_display_map(...). The two report variants will diverge as soon as the selected range spans multiple persisted secondary_currency_rate values.
Suggested fix
$data = [
'title' => lang('Reports.discounts_summary_report'),
'subtitle' => $this->_get_subtitle_report(['start_date' => $start_date, 'end_date' => $end_date]),
'headers' => $this->summary_discounts->getDataColumns(),
'data' => $tabular_data,
'summary_data' => $summary,
+ 'summary_secondary_data' => $this->_build_secondary_summary_display_map($report_data, $summary, $secondaryCurrency),
'secondaryCurrency' => $secondaryCurrency
];🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Controllers/Reports.php` around lines 646 - 653, The tabular discounts
report ($data array in Reports.php) is missing the per-field secondary-currency
summary so it falls back to a single rate; compute a summary_secondary_data by
calling the existing _build_secondary_summary_display_map(...) (same approach
used by graphical_summary_discounts()) and include it in the $data array (add
'summary_secondary_data' => $summary_secondary_data) so the tabular report uses
the correct per-field secondary currency values when rendering the
summary/footer.
| private function _average_secondary_rate(array $rows, string $field = 'secondary_currency_rate'): ?float | ||
| { | ||
| $rates = []; | ||
|
|
||
| foreach ($rows as $row) { | ||
| if (is_array($row) && array_key_exists($field, $row) && $row[$field] !== null && $row[$field] !== '') { | ||
| $rates[] = (float) $row[$field]; | ||
| } | ||
| } | ||
|
|
||
| if (count($rates) === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| return array_sum($rates) / count($rates); |
There was a problem hiding this comment.
Don't expose an unweighted mean as the report's currency rate.
A plain average of row rates is misleading once the report spans mixed snapshots. One large sale at 1.10 and one small sale at 2.00 currently renders a report-level rate of 1.55, even though the converted totals are dominated by the 1.10 row. Either compute a weighted rate from the base amounts you display or suppress a single rate when multiple snapshots are present.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Controllers/Reports.php` around lines 1563 - 1577, The
_average_secondary_rate function currently returns an unweighted mean; change it
to compute a weighted average using each row's base amount (e.g., the column
that represents the base-currency total per row — locate the rows' amount field
used elsewhere in reports, e.g., "base_amount" or "amount") by summing
rate*amount and dividing by sum(amount), skipping rows with missing/zero amounts
or rates; if no valid weighted data exists (or when multiple snapshots are
detected and weighting is impossible), return null to suppress a misleading
single rate. Ensure you still respect the $field parameter for the rate,
validate numeric values, and handle empty arrays to avoid division by zero in
_average_secondary_rate.
| MAX(comment) AS comment, | ||
| MAX(secondary_currency_rate) AS secondary_currency_rate'); |
There was a problem hiding this comment.
Mirror the new snapshot field in getDataBySaleId().
getData() now exposes secondary_currency_rate, but the expanded sale row is built from getDataBySaleId(), which still omits it. After a rate change, the drawer will therefore render the sale with the current config rate instead of the persisted snapshot.
Suggested fix
$builder->select('sale_id,
MAX(sale_time) as sale_time,
SUM(quantity_purchased) AS items_purchased,
MAX(employee_name) AS employee_name,
MAX(customer_name) AS customer_name,
SUM(subtotal) AS subtotal,
SUM(tax) AS tax,
SUM(total) AS total,
SUM(cost) AS cost,
SUM(profit) AS profit,
MAX(payment_type) AS payment_type,
MAX(sale_status) AS sale_status,
- comment');
+ MAX(secondary_currency_rate) AS secondary_currency_rate,
+ comment');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Models/Reports/Detailed_sales.php` around lines 129 - 130, The expanded
sale row built in getDataBySaleId() is missing the persisted
secondary_currency_rate field added to getData(); update getDataBySaleId() to
include the snapshot's secondary_currency_rate when assembling the expanded sale
object (mirror the same field selection/aggregation used in getData, e.g.,
include MAX(secondary_currency_rate) AS secondary_currency_rate or otherwise
pull the stored snapshot rate) so the drawer renders the persisted rate rather
than the current config value.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
app/Views/sales/invoice.php (1)
148-148:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSame primary-price erasure bug as
quote.phpline 135.The price column here carries the identical issue: no
$secondaryCurrency['show']guard means the primary per-item price is never rendered when secondary is enabled, and the fallback callssecondary_currency_render_amountunconditionally when the feature is disabled. Apply the same fix as described forquote.php.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Views/sales/invoice.php` at line 148, The invoice per-item price cell incorrectly always evaluates secondary_currency_render_amount and never shows the primary price when secondary pricing is disabled; update the expression that renders the cell (the one using $item['secondaryPriceDisplay'] and secondary_currency_render_amount) to first check $secondaryCurrency['show'] and, if true, render $item['secondaryPriceDisplay'] (or call secondary_currency_render_amount when that key is missing), otherwise render the primary price display ($item['priceDisplay'] or the raw $item['price']) so primary prices are shown when secondary currency is turned off; reference the invoice.php cell that uses $item['secondaryPriceDisplay'], secondary_currency_render_amount, $item['price'] and $secondaryCurrency['show'] to implement the conditional.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@app/Database/Migrations/20260508000001_AlterSecondaryCurrencyRatePrecision.php`:
- Around line 14-17: Replace the raw ALTER TABLE string built with
$this->db->query(...) for each $table with a call to
$this->forge->modifyColumn(...), targeting the prefixed table name and updating
the secondary_currency_rate column definition to DECIMAL(15,6) NULL DEFAULT
NULL; specifically remove usages of $this->db->query and instead call
$this->forge->modifyColumn($this->db->prefixTable($table),
['secondary_currency_rate' => ['type' => 'DECIMAL', 'constraint' => '15,6',
'null' => true, 'default' => null]]), doing this for every table handled in the
migration so SQL is produced via the Forge API rather than string concatenation.
In `@app/Models/Reports/Summary_report.php`:
- Around line 80-86: The subquery building $secondary_rate_subquery in
Summary_report (and the similar logic around lines 94-95) only applies date
filtering and omits the rest of the report filters from __common_where(),
causing averaged secondary_currency_rate to come from a broader set; update the
subquery to derive its sale_ids from the same filtered set as the outer query
(use the same $where or call __common_where()/its components) — e.g., first
select distinct sale_id from sales applying the full filter (date, location_id,
sale_type, etc.) and then compute AVG(MAX(...)) over those sale_ids — so the
secondary_currency_rate average is computed over the identical filtered dataset
as the main summary.
---
Duplicate comments:
In `@app/Views/sales/invoice.php`:
- Line 148: The invoice per-item price cell incorrectly always evaluates
secondary_currency_render_amount and never shows the primary price when
secondary pricing is disabled; update the expression that renders the cell (the
one using $item['secondaryPriceDisplay'] and secondary_currency_render_amount)
to first check $secondaryCurrency['show'] and, if true, render
$item['secondaryPriceDisplay'] (or call secondary_currency_render_amount when
that key is missing), otherwise render the primary price display
($item['priceDisplay'] or the raw $item['price']) so primary prices are shown
when secondary currency is turned off; reference the invoice.php cell that uses
$item['secondaryPriceDisplay'], secondary_currency_render_amount, $item['price']
and $secondaryCurrency['show'] to implement the conditional.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8b1bb8af-98da-4495-8337-e9bcc01bb07a
📒 Files selected for processing (20)
app/Controllers/Config.phpapp/Database/Migrations/20260508000000_AddSecondaryCurrencyRateToTransactionHeaders.phpapp/Database/Migrations/20260508000001_AlterSecondaryCurrencyRatePrecision.phpapp/Models/Expense.phpapp/Models/Receiving.phpapp/Models/Reports/Summary_report.phpapp/Models/Sale.phpapp/Views/configs/receipt_config.phpapp/Views/reports/graphical.phpapp/Views/reports/tabular.phpapp/Views/reports/tabular_details.phpapp/Views/sales/invoice.phpapp/Views/sales/invoice_email.phpapp/Views/sales/quote.phpapp/Views/sales/quote_email.phpapp/Views/sales/receipt_default.phpapp/Views/sales/receipt_email.phpapp/Views/sales/receipt_short.phpapp/Views/sales/register.phpapp/Views/sales/tax_invoice.php
✅ Files skipped from review due to trivial changes (2)
- app/Database/Migrations/20260508000000_AddSecondaryCurrencyRateToTransactionHeaders.php
- app/Views/sales/receipt_default.php
🚧 Files skipped from review as they are similar to previous changes (11)
- app/Views/reports/tabular_details.php
- app/Views/sales/invoice_email.php
- app/Views/sales/quote_email.php
- app/Views/sales/tax_invoice.php
- app/Models/Receiving.php
- app/Views/sales/register.php
- app/Views/sales/receipt_email.php
- app/Views/reports/tabular.php
- app/Views/sales/receipt_short.php
- app/Views/reports/graphical.php
- app/Models/Sale.php
…ry-Currency # Conflicts: # app/Views/sales/register.php
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
app/Models/Reports/Summary_report.php (2)
44-66: 🏗️ Heavy lift
$secondary_rate_whereduplicates__common_where()filter logic — divergence risk.Lines 44-66 manually reconstruct in raw SQL the identical filter conditions already expressed in
__common_where()(lines 144-183). These two blocks must stay in perfect sync: any new filter added to__common_where()(a newsale_typebranch, a staff-id constraint, a store-group condition) will silently be absent from the subquery, causing the averaged secondary rate to be computed over a wider dataset than the reported totals — the exact class of bug the past review flagged.The safest fix would be to derive the inner
sale_idset for the subquery from a filteredSELECT DISTINCT sales.sale_iddriven by the same$builder(or a clone) that__common_where()populates, then join that result into the rate subquery:♻️ Sketch of a divergence-safe approach
- $secondary_rate_where = $where; - if ($inputs['location_id'] != 'all') { - $secondary_rate_where .= ' AND sales_items.item_location = ' . $this->db->escape($inputs['location_id']); - } - - if ($inputs['sale_type'] == 'complete') { - $secondary_rate_where .= ' AND sales.sale_status = ' . COMPLETED; - $secondary_rate_where .= ' AND (sales.sale_type = ' . SALE_TYPE_POS . ' OR sales.sale_type = ' . SALE_TYPE_INVOICE . ' OR sales.sale_type = ' . SALE_TYPE_RETURN . ')'; - } elseif ($inputs['sale_type'] == 'sales') { - $secondary_rate_where .= ' AND sales.sale_status = ' . COMPLETED; - $secondary_rate_where .= ' AND (sales.sale_type = ' . SALE_TYPE_POS . ' OR sales.sale_type = ' . SALE_TYPE_INVOICE . ')'; - } elseif ($inputs['sale_type'] == 'quotes') { - $secondary_rate_where .= ' AND sales.sale_status = ' . SUSPENDED; - $secondary_rate_where .= ' AND sales.sale_type = ' . SALE_TYPE_QUOTE; - } elseif ($inputs['sale_type'] == 'work_orders') { - $secondary_rate_where .= ' AND sales.sale_status = ' . SUSPENDED; - $secondary_rate_where .= ' AND sales.sale_type = ' . SALE_TYPE_WORK_ORDER; - } elseif ($inputs['sale_type'] == 'canceled') { - $secondary_rate_where .= ' AND sales.sale_status = ' . CANCELED; - } elseif ($inputs['sale_type'] == 'returns') { - $secondary_rate_where .= ' AND sales.sale_status = ' . COMPLETED; - $secondary_rate_where .= ' AND sales.sale_type = ' . SALE_TYPE_RETURN; - } + // Build a filtered sale_id set using the same QueryBuilder filters as __common_where(). + $rateFilterBuilder = $this->db->table('sales_items AS sales_items'); + $rateFilterBuilder->select('DISTINCT sales.sale_id, sales.secondary_currency_rate'); + $rateFilterBuilder->join('sales AS sales', 'sales.sale_id = sales_items.sale_id', 'inner'); + $this->__common_where($inputs, $rateFilterBuilder); // single source of truth + $filteredRatesSubquery = $rateFilterBuilder->getCompiledSelect();- $secondary_rate_subquery = '(SELECT AVG(sale_rates.secondary_currency_rate) - FROM ( - SELECT sales.sale_id, MAX(sales.secondary_currency_rate) AS secondary_currency_rate - FROM ' . $this->db->prefixTable('sales_items') . ' AS sales_items - INNER JOIN ' . $this->db->prefixTable('sales') . ' AS sales - ON sales.sale_id = sales_items.sale_id - WHERE ' . $secondary_rate_where . ' - GROUP BY sales.sale_id - ) AS sale_rates)'; + $secondary_rate_subquery = "(SELECT AVG(sale_rates.secondary_currency_rate) FROM ($filteredRatesSubquery) AS sale_rates)";Note:
__common_where()must be made accessible (e.g., keep itprivateand call via a thin protected wrapper, or accept aBaseBuilderreference). The exact implementation will differ based on how__common_where()is wired in context, but the principle — single source of truth for the filter — eliminates the drift risk.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Models/Reports/Summary_report.php` around lines 44 - 66, The duplicated raw-SQL filter block that builds $secondary_rate_where should be removed and replaced by reusing the same filter logic from __common_where(): build (or clone) the query builder that __common_where() populates to produce a SELECT DISTINCT sales.sale_id subquery, then use that subquery (or join its results) inside the secondary-rate query so the inner sale_id set shares the exact filters; make __common_where() accessible to the method (or add a thin protected wrapper that returns the builder) and reference the builder/SELECT DISTINCT sales.sale_id when constructing the rate subquery instead of reconstructing sale_type/location/sale_status logic in $secondary_rate_where.
104-112: Filter scope correctly addressed — note ongoing maintenance considerations.The
$secondary_rate_whereconditions (lines 45-66) now properly include bothlocation_idandsale_typefilters, matching the logic in__common_where()(lines 155-182). The subquery at lines 104-112 correctly applies these filters, resolving the original scope issue.The identical filter logic duplicated across two separate code blocks creates a maintenance risk. If either implementation changes in future work, both must be updated to keep them aligned.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Models/Reports/Summary_report.php` around lines 104 - 112, The duplicated filter logic used to build $secondary_rate_where and the identical conditions used elsewhere should be centralized to avoid maintenance drift: extract the shared where-clause construction into a single reusable method (e.g., create a protected method buildSecondaryRateWhere() or extend __common_where() to return the same filter string/array) and replace direct uses in the $secondary_rate_subquery and the other block with calls to that method (reference symbols: $secondary_rate_subquery, $secondary_rate_where, __common_where()). Ensure the new method returns the same format expected by both consumers and update places that previously constructed the filters inline to call it so future changes are made in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Controllers/Config.php`:
- Around line 482-486: When saving secondary currency, add validation in Config
controller: after reading secondary_currency_enabled, if true validate
secondary_currency_code is non-empty (trim and alphanumeric/symbols as needed),
secondary_currency_rate parsed by parse_decimals is a positive number (>0), and
secondary_currency_decimals is an integer in an allowed range (e.g. 0–8); if any
check fails, stop persistence and return validation errors (set
flashdata/response) instead of saving. Use the existing symbols
secondary_currency_enabled, secondary_currency_code, secondary_currency_rate,
secondary_currency_decimals and parse_decimals to perform checks, sanitize
inputs with esc()/htmlspecialchars for stored values, and ensure you do not call
the save/update routine when validation fails.
---
Nitpick comments:
In `@app/Models/Reports/Summary_report.php`:
- Around line 44-66: The duplicated raw-SQL filter block that builds
$secondary_rate_where should be removed and replaced by reusing the same filter
logic from __common_where(): build (or clone) the query builder that
__common_where() populates to produce a SELECT DISTINCT sales.sale_id subquery,
then use that subquery (or join its results) inside the secondary-rate query so
the inner sale_id set shares the exact filters; make __common_where() accessible
to the method (or add a thin protected wrapper that returns the builder) and
reference the builder/SELECT DISTINCT sales.sale_id when constructing the rate
subquery instead of reconstructing sale_type/location/sale_status logic in
$secondary_rate_where.
- Around line 104-112: The duplicated filter logic used to build
$secondary_rate_where and the identical conditions used elsewhere should be
centralized to avoid maintenance drift: extract the shared where-clause
construction into a single reusable method (e.g., create a protected method
buildSecondaryRateWhere() or extend __common_where() to return the same filter
string/array) and replace direct uses in the $secondary_rate_subquery and the
other block with calls to that method (reference symbols:
$secondary_rate_subquery, $secondary_rate_where, __common_where()). Ensure the
new method returns the same format expected by both consumers and update places
that previously constructed the filters inline to call it so future changes are
made in one place.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 76e62c22-3bc0-4589-b865-89b37d0889dc
📒 Files selected for processing (6)
app/Controllers/Config.phpapp/Controllers/Sales.phpapp/Database/Migrations/20260508000001_AlterSecondaryCurrencyRatePrecision.phpapp/Language/en/Config.phpapp/Models/Reports/Summary_report.phpapp/Views/sales/register.php
✅ Files skipped from review due to trivial changes (1)
- app/Database/Migrations/20260508000001_AlterSecondaryCurrencyRatePrecision.php
🚧 Files skipped from review as they are similar to previous changes (2)
- app/Views/sales/register.php
- app/Controllers/Sales.php
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Models/Reports/Detailed_receivings.php`:
- Line 46: The label for the secondary_rate column is using
lang('Reports.selling_rate'), which is semantically incorrect for a
receivings/purchasing report; change the translation key used in the
$summaryColumns entry (in Detailed_receivings.php where $summaryColumns[] is set
for 'secondary_rate') to a more appropriate key such as
lang('Reports.exchange_rate') or lang('Reports.rate') and also add/update the
corresponding key/value in the Reports translation file so the new label appears
correctly.
- Around line 46-47: The $summaryColumns entry uses the wrong key
'secondary_rate' which doesn't match the SQL alias produced by
getDataByReceivingId() and getData() ('secondary_currency_rate'); update the key
in the $summaryColumns array to 'secondary_currency_rate' and keep the display
label call secondary_currency_display_label(lang('Reports.total'),
$secondaryCurrency) unchanged so views that index rows by column keys receive
the actual SQL column value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f38fe476-f854-4e55-8eca-4f556d9581b6
📒 Files selected for processing (4)
app/Controllers/Config.phpapp/Models/Reports/Detailed_receivings.phpapp/Views/reports/graphical.phpapp/Views/sales/register.php
🚧 Files skipped from review as they are similar to previous changes (3)
- app/Controllers/Config.php
- app/Views/reports/graphical.php
- app/Views/sales/register.php
WebShells.OSPOS.Secondary.Currency.Feature.mp4@objecttothis @jekkos That's a video for the Secondary Currency feature. |
|
Updates: Added a refresh service Capable now to: Turn auto refresh on/off The config should be: currency_code = base currency, ex: USD That means the app will fetch the secondary currency code rate in conversion to USD: https://open.er-api.com/v6/latest/USD @opensourcepos/org @jekkos @objecttothis |
There was a problem hiding this comment.
Actionable comments posted: 19
🧹 Nitpick comments (13)
app/Language/id/Config.php (2)
62-83: 🏗️ Heavy liftLocalize new
secondary_currency_*strings (or intentionally fallback with empty values).Most new keys in the Indonesian file are English, which creates mixed-language UI for Indonesian users. Please either translate these values to Indonesian or set untranslated entries to
""so fallback behavior stays explicit and consistent.Based on learnings: empty-string values in
app/Language/**are intentional and fall back to English viaapp/Libraries/MY_Language.php Language::getLine().🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/id/Config.php` around lines 62 - 83, The new secondary_currency_* language entries (e.g., "secondary_currency_disabled", "secondary_currency_auto_enable", "secondary_currency_feed_invalid_json", "secondary_currency_refresh_interval_invalid", etc.) in app/Language/id/Config.php are left as English; update each of these keys to either a proper Indonesian translation or set the value to an explicit empty string ("") so fallback to English is intentional and consistent (follow the existing pattern used by "current_employee_only"); ensure every "secondary_currency_*" key is handled this way to avoid mixed-language UI.
83-83: ⚡ Quick winSplit the two array entries on Line 83 into separate lines.
Line 83 currently defines two keys on one line. It parses, but it’s easy to miss during edits and increases merge/conflict risk in translation files.
Suggested cleanup
- "secondary_currency_rate_required" => "Secondary currency rate must be a positive number.", "current_employee_only" => "", + "secondary_currency_rate_required" => "Secondary currency rate must be a positive number.", + "current_employee_only" => "",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/id/Config.php` at line 83, The array entry on the same line contains two keys ("secondary_currency_rate_required" and "current_employee_only") in app/Language/id/Config.php; split them so each key/value pair is on its own line (one line for "secondary_currency_rate_required" => "Secondary currency rate must be a positive number.", and a separate line for "current_employee_only" => ""), preserving the surrounding array formatting, commas, and alignment to reduce merge conflicts and improve readability.app/Language/it/Sales.php (1)
115-116: 💤 Low valueConsider relocating these keys to improve file organization.
The new keys are inserted among keyboard shortcut translations (all prefixed with
key_*), which breaks the logical grouping. For better maintainability, consider placingratealphabetically near other "r" entries (e.g., after line 169 "return" or near line 166 "register") andsecondary_currency_update_live_rate_tooltipnear other "s" entries (e.g., around line 183-192).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/it/Sales.php` around lines 115 - 116, The two translation keys "rate" and "secondary_currency_update_live_rate_tooltip" are misplaced among keyboard shortcut entries; move the "rate" key so it's alphabetically grouped with other "r" entries (e.g., near "register" or "return") and move "secondary_currency_update_live_rate_tooltip" to the "s" section near the other "s" entries (around the existing keys at lines ~183-192) in Sales.php so logical grouping and alphabetical ordering are preserved.app/Language/hr-HR/Sales.php (1)
115-116: ⚡ Quick winConsider translating new keys to Croatian.
The new secondary currency keys
rateandsecondary_currency_update_live_rate_tooltipcurrently use English values in the Croatian locale file. Translating these to Croatian would improve consistency and user experience for Croatian-speaking users.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/hr-HR/Sales.php` around lines 115 - 116, The Croatian locale file contains two keys with English values: "rate" and "secondary_currency_update_live_rate_tooltip"; update their values to Croatian by replacing "Rate" with an appropriate Croatian word (e.g., "Tečaj" or "Stopa") and translate the tooltip to Croatian (e.g., "Preuzmi najnoviji tečaj za sekundarnu valutu sada."), ensuring the keys "rate" and "secondary_currency_update_live_rate_tooltip" remain unchanged.app/Language/th/Sales.php (1)
115-116: ⚡ Quick winConsider translating new keys to Thai.
The new secondary currency keys
rateandsecondary_currency_update_live_rate_tooltipcurrently use English values in the Thai locale file. Translating these to Thai would improve consistency and user experience for Thai-speaking users.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/th/Sales.php` around lines 115 - 116, Translate the two English strings in the Thai locale for the Sales module: replace the value for the "rate" key and the value for "secondary_currency_update_live_rate_tooltip" with appropriate Thai translations, keeping the keys unchanged; update the values in the array where "rate" and "secondary_currency_update_live_rate_tooltip" are defined so the tooltip reads naturally in Thai and the single-word "rate" is translated to the correct Thai term.app/Language/bs/Sales.php (1)
115-116: ⚡ Quick winConsider translating new keys to Bosnian.
The new secondary currency keys
rateandsecondary_currency_update_live_rate_tooltipcurrently use English values in the Bosnian locale file. While the system gracefully falls back to English for untranslated strings, translating these to Bosnian would improve the user experience for Bosnian-speaking users.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/bs/Sales.php` around lines 115 - 116, Translate the two untranslated keys in the Bosnian locale by replacing the English values for "rate" and "secondary_currency_update_live_rate_tooltip" with their Bosnian equivalents; update the entries for "rate" and "secondary_currency_update_live_rate_tooltip" in the app/Language/bs/Sales.php locale array to provide Bosnian text (e.g., replace "Rate" and "Fetch the latest secondary currency rate now." with appropriate Bosnian translations).app/Language/pl/Config.php (1)
62-83: ⚡ Quick winConsider translating secondary currency configuration keys to Polish.
The new secondary currency configuration keys (lines 62-82) currently use English values in the Polish locale file. While the system falls back to English for untranslated strings, translating these would improve the configuration experience for Polish-speaking administrators.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/pl/Config.php` around lines 62 - 83, Translate the English values for the secondary currency localization keys shown (e.g. "secondary_currency_disabled", "secondary_currency_auto_enable", "secondary_currency_auto_enable_tooltip", "secondary_currency_auto_refresh_disabled", "secondary_currency_auto_refresh_disabled_force", "secondary_currency_base_quote_required", "secondary_currency_codes_required", "secondary_currency_codes_must_differ", "secondary_currency_feed_http_error", "secondary_currency_feed_invalid_json", "secondary_currency_feed_invalid_rate", "secondary_currency_feed_request_failed", "secondary_currency_feed_url", "secondary_currency_feed_url_tooltip", "secondary_currency_last_error", "secondary_currency_last_synced_at", "secondary_currency_refresh_failed", "secondary_currency_refresh_successful", "secondary_currency_refresh_interval", "secondary_currency_refresh_interval_tooltip", "secondary_currency_refresh_interval_invalid", "secondary_currency_rate_required") into Polish in the same associative array (preserve placeholders like {0}, {base}, {quote} and literal flags like --force), replacing the English strings with proper Polish translations so the Polish locale no longer falls back to English.app/Language/hu/Sales.php (1)
115-116: ⚡ Quick winConsider translating new keys to Hungarian.
The new secondary currency keys
rateandsecondary_currency_update_live_rate_tooltipcurrently use English values in the Hungarian locale file. Translating these to Hungarian would improve consistency and user experience for Hungarian-speaking users.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/hu/Sales.php` around lines 115 - 116, Translate the two untranslated keys in the Hungarian locale: replace the English values for "rate" and "secondary_currency_update_live_rate_tooltip" in the Sales.php locale array with appropriate Hungarian strings (e.g., "Árfolyam" for "rate" and a Hungarian sentence for the tooltip) ensuring they match the style and grammar of existing translations in the file; update the values for the array entries "rate" and "secondary_currency_update_live_rate_tooltip".app/Language/vi/Sales.php (1)
115-116: ⚡ Quick winConsider translating new keys to Vietnamese.
The new secondary currency keys
rateandsecondary_currency_update_live_rate_tooltipcurrently use English values in the Vietnamese locale file. Translating these to Vietnamese would improve consistency and user experience for Vietnamese-speaking users.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/vi/Sales.php` around lines 115 - 116, Translate the two English values for the localization keys "rate" and "secondary_currency_update_live_rate_tooltip" in the Vietnamese language array: replace the value for "rate" with the Vietnamese equivalent (e.g., "Tỷ giá" or context-appropriate term) and translate "secondary_currency_update_live_rate_tooltip" into Vietnamese (e.g., "Lấy tỷ giá ngoại tệ phụ mới nhất ngay bây giờ."), keeping the keys unchanged.app/Language/tr/Sales.php (1)
115-116: ⚡ Quick winConsider translating new keys to Turkish.
The new secondary currency keys
rateandsecondary_currency_update_live_rate_tooltipcurrently use English values in the Turkish locale file. Translating these to Turkish would improve consistency and user experience for Turkish-speaking users.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/tr/Sales.php` around lines 115 - 116, Translate the two untranslated keys in the Turkish Sales locale: replace the English values for "rate" and "secondary_currency_update_live_rate_tooltip" in the Sales.php translations array with appropriate Turkish strings (e.g., "Oran" or context-appropriate term for "rate" and a Turkish sentence for the tooltip) so the keys "rate" and "secondary_currency_update_live_rate_tooltip" are fully localized.app/Language/ml/Sales.php (1)
115-116: 💤 Low valueConsider relocating currency keys for better organization.
The new secondary currency keys are inserted between keyboard shortcut keys. While the English text is consistent with this file's translation pattern (most keys use empty strings for fallback), consider moving these keys closer to other currency/financial translations for better semantic grouping.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/ml/Sales.php` around lines 115 - 116, The new keys "rate" and "secondary_currency_update_live_rate_tooltip" are placed between keyboard shortcut keys; move these two entries into the existing currency/financial translations block (where other currency keys like "currency", "exchange_rate", or similar exist) so they're semantically grouped with related strings; ensure you preserve their current translations/empty-string fallbacks and maintain the same array ordering/formatting used by the surrounding entries in the Language/ml/Sales.php file.app/Language/ur/Sales.php (1)
115-116: 💤 Low valueConsider translating to Urdu or relocating keys.
The new secondary currency keys use English text. While this file has many intentionally empty translations that fall back to English, these keys are placed among keyboard shortcuts rather than with other currency/financial keys. Consider translating to Urdu or moving them to a more semantically appropriate location (e.g., near payment-related keys).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/ur/Sales.php` around lines 115 - 116, The added localization keys "rate" and "secondary_currency_update_live_rate_tooltip" are in English and misplaced among shortcut keys; either provide Urdu translations for these keys (replace the English strings with proper Urdu text for "rate" and for "Fetch the latest secondary currency rate now.") or relocate the key-value pairs to the locale file/section that holds payment/currency-related strings so they sit with other financial keys; update any references if you move them so the application still reads the same keys.app/Helpers/tabular_helper.php (1)
853-861: ⚡ Quick winRemove unused loop variable.
The
$keyvariable in the foreach loop is never used. Consider removing it for cleaner code:♻️ Suggested cleanup
- foreach ($expense->getResult() as $key => $expense) { + foreach ($expense->getResult() as $expense) { $sum_amount_expense += $expense->amount; $sum_tax_amount_expense += $expense->tax_amount;Based on learnings: The static analysis tool correctly identified this unused variable.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Helpers/tabular_helper.php` around lines 853 - 861, The foreach introduces an unused $key and shadows the outer $expense; change the loop from "foreach ($expense->getResult() as $key => $expense)" to use a single, non-shadowing variable (e.g., "foreach ($expense->getResult() as $expenseRow)") and update all uses inside the loop ($expense->amount, $expense->tax_amount, $expense->secondary_currency_rate) to use $expenseRow so $sum_amount_expense, $sum_tax_amount_expense and $sum_secondary_amount_expense calculations remain correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Commands/RefreshSecondaryCurrency.php`:
- Around line 48-66: persistStatus currently swallows batch_save failures
because it only catches exceptions; change it to detect Appconfig::batch_save
returning false and throw an exception so the failure propagates to run().
Specifically, in persistStatus (function persistStatus) capture the return value
from $appconfig->batch_save($payload), and if it is === false throw a
RuntimeException (or other Throwable) containing a useful message (include the
payload/error context). This ensures run() observes the failure and doesn't
report a successful refresh when the Appconfig update failed.
In `@app/Config/Routes.php`:
- Around line 75-76: The routes $routes->post('config/saveScale',
'Config::postSaveScale') and $routes->post('config/saveShortcuts',
'Config::postSaveShortcuts') reference missing controller methods; either add
public methods postSaveScale(Request $request) and postSaveShortcuts(Request
$request) to the Config controller (implementing request validation, processing,
and appropriate response) or remove/disable these route definitions if the
endpoints are not intended to exist; ensure method names exactly match
Config::postSaveScale and Config::postSaveShortcuts so the router can resolve
them.
- Around line 84-120: Several sales routes that mutate server state are
registered with $routes->add() and thus allow GET; change them to verb-specific
registrations (mostly $routes->post() or $routes->delete() as appropriate) so
CSRF-prone actions are not callable via GET. Replace $routes->add(...) with
$routes->post(...) for: 'sales/selectCustomer' (Sales::postSelectCustomer),
'sales/changeMode' (Sales::postChangeMode), 'sales/setComment'
(Sales::postSetComment), 'sales/setInvoiceNumber' (Sales::postSetInvoiceNumber),
'sales/setPaymentType' (Sales::postSetPaymentType), 'sales/setPrintAfterSale'
(Sales::postSetPrintAfterSale), 'sales/setPriceWorkOrders'
(Sales::postSetPriceWorkOrders), 'sales/setEmailReceipt'
(Sales::postSetEmailReceipt), 'sales/addPayment' (Sales::postAddPayment),
'sales/add' (Sales::postAdd), 'sales/editItem/(:segment)'
(Sales::postEditItem/$1), 'sales/complete' (Sales::postComplete),
'sales/delete/(:num)' (Sales::postDelete/$1) —and change other mutating
endpoints to appropriate verbs too (e.g., 'sales/deletePayment/(:any)'
Sales::getDeletePayment/$1 and 'sales/deleteItem/(:num)' Sales::getDeleteItem/$1
should be converted to $routes->post() or $routes->delete(),
'sales/removeCustomer' Sales::getRemoveCustomer to $routes->post(),
'sales/save/(:num)' Sales::postSave/$1 to $routes->post() if not already,
'sales/cancel' Sales::postCancel to $routes->post(), 'sales/suspend'
Sales::postSuspend and 'sales/unsuspend' Sales::postUnsuspend to
$routes->post(); keep true read-only handlers (e.g., receipt/invoice/PDF
display) as GET. Ensure you update only the registrations (route helper) leaving
controller method names intact.
In `@app/Controllers/Config.php`:
- Around line 476-523: Normalize the primary and secondary currency codes (e.g.
trim and strtoupper) and reject when they are identical: update the validation
block that runs when $secondary_currency_enabled to normalize
$secondary_currency_code and the posted currency code
(getPost('currency_code')), and if they match add a validation error (push to
$validation_errors) so saving is blocked; also ensure the value you write into
$batch_save_data['secondary_currency_code'] is the normalized code so the stored
value is consistent with the check.
In `@app/Controllers/Sales.php`:
- Around line 1743-1761: The refresh handler in Sales.php currently returns on
$result['success'] false without updating secondary_currency_last_error and
ignores the boolean return of model(Appconfig::class)->batch_save on success;
update the logic so that on refresh failure you call $appconfig->batch_save(...)
to set secondary_currency_last_error to the failure message (and
secondary_currency_last_synced_at as appropriate), and on the success path check
the boolean result of $appconfig->batch_save (the call that sets
secondary_currency_rate/last_synced_at/last_error) — if batch_save returns
false, persist a non-empty secondary_currency_last_error and return a failure
JSON via $this->response->setJSON; use the same $result['message'] or a clear
error string when writing last_error so the DB always reflects the actual stored
state.
In `@app/Language/ar-LB/Sales.php`:
- Line 115: The localization entry for the key "rate" currently uses a
nonstandard spelling "الصيرفه"; update the value for the "rate" key to a
standard Arabic term—either "الصيرفة" for the generic term or "سعر الصرف" if
this label specifically represents an exchange rate—so replace the value
associated with "rate" accordingly in the array.
In `@app/Language/bg/Config.php`:
- Line 83: The array entry line containing "secondary_currency_rate_required"
and "current_employee_only" has two entries jammed together causing a PHP parse
error; split them into two distinct array entries so each key => value pair is
on its own entry (ensure there's a trailing comma after the
"secondary_currency_rate_required" entry and that "current_employee_only" has
its own quoted key and value), locating the entries by the keys
"secondary_currency_rate_required" and "current_employee_only" in the language
Config.php array.
In `@app/Language/cs/Config.php`:
- Line 83: There is a PHP array syntax error because two entries
("secondary_currency_rate_required" and "current_employee_only") are on the same
line; edit the array in Config.php so each associative entry is a separate
element (put "secondary_currency_rate_required" => "Secondary currency rate must
be a positive number." on its own line followed by a comma, then add
"current_employee_only" => "" on the next line with a trailing comma) so the
array keys/values are properly separated and the file parses.
In `@app/Language/de-DE/Sales.php`:
- Around line 115-116: Translate the two new German language keys: replace the
English values for "rate" and "secondary_currency_update_live_rate_tooltip" with
appropriate German strings (e.g., "Kurs" or "Wechselkurs" for "rate" and
"Aktuellen Wechselkurs der Zweitwährung jetzt abrufen." for
"secondary_currency_update_live_rate_tooltip") and move these entries next to
the other currency/financial keys in the Sales language array to keep semantic
grouping; update only the values for the keys "rate" and
"secondary_currency_update_live_rate_tooltip" and ensure the keys themselves
remain unchanged.
In `@app/Language/es-ES/Config.php`:
- Around line 62-83: The Spanish locale file currently contains English literals
for the new secondary-currency keys (e.g., "secondary_currency_auto_enable",
"secondary_currency_feed_http_error", "secondary_currency_feed_invalid_json",
"secondary_currency_refresh_interval_invalid",
"secondary_currency_rate_required", etc.); replace those English text values
with empty strings ("") so the app's Language::getLine() fallback will use the
English default and translators can track missing entries; leave any keys
already translated in Spanish as-is and ensure each new secondary_currency_* key
uses "" when a Spanish translation is not yet available.
In `@app/Language/es-ES/Sales.php`:
- Around line 115-116: Translate the two new keys in
app/Language/es-ES/Sales.php: replace the English values for "rate" and
"secondary_currency_update_live_rate_tooltip" with Spanish equivalents (for
example "Tasa" or "Tipo de cambio" for "rate" and "Obtener la última tasa de la
moneda secundaria ahora." for "secondary_currency_update_live_rate_tooltip") and
move these entries so they sit with the other currency/financial keys in the
Sales.php array for consistent organization.
In `@app/Language/es-MX/Config.php`:
- Line 83: The array entry line in Config.php incorrectly places two keys on the
same line; split the entries "secondary_currency_rate_required" and
"current_employee_only" into separate array elements (one per line) and ensure
the first entry ends with a comma so the PHP array syntax is valid (adjust
surrounding indentation to match the array style in the file).
In `@app/Language/es-MX/Sales.php`:
- Around line 115-116: The Spanish (Mexico) language file contains untranslated
keys "rate" and "secondary_currency_update_live_rate_tooltip"; update their
values to Spanish (e.g., "rate" => "Tasa" or "Tipo de cambio", and
"secondary_currency_update_live_rate_tooltip" => "Obtener la última tasa de la
moneda secundaria ahora.") and move these entries to the section with other
currency/financial keys so they’re grouped logically (look for nearby keys
dealing with currency, exchange rate, or finance to place them).
In `@app/Language/fa/Sales.php`:
- Around line 115-116: Translate the two new keys "rate" and
"secondary_currency_update_live_rate_tooltip" into Persian (provide appropriate
Persian strings) in app/Language/fa/Sales.php and move their entries from
between "key_help_modal" and "key_in" into the currency/financial block near
"amount_due", "change_due", and "sales_tax" to preserve semantic grouping;
ensure the keys remain unchanged and only their values and position are updated.
In `@app/Language/it/Sales.php`:
- Around line 115-116: The Italian language file contains English text for the
keys "rate" and "secondary_currency_update_live_rate_tooltip"; change their
values to empty strings ("") so the custom Language::getLine() fallback will use
the English translations—update the entries for "rate" and
"secondary_currency_update_live_rate_tooltip" in the app/Language/it/Sales.php
array to "" instead of the English phrases.
In `@app/Language/pl/Config.php`:
- Line 83: The array in app/Language/pl/Config.php has two entries on the same
line causing a parse error; split the entries so each key/value pair is on its
own line by moving "current_employee_only" => "" onto the next line after
"secondary_currency_rate_required" => "...", ensuring proper comma placement and
alignment within the array declaration (look for the array containing the keys
"secondary_currency_rate_required" and "current_employee_only" to update).
In `@app/Language/pl/Sales.php`:
- Line 109: Fix the typo in the translation string for the array key
"key_finish_quote": change the value from "Finish Quote/Invoice witdout payment"
to "Finish Quote/Invoice without payment" so the user-facing text reads
correctly; update the string in the Language/pl/Sales.php localization array
entry for key_finish_quote.
In `@app/Language/ur/Config.php`:
- Line 83: The array line containing "secondary_currency_rate_required" and
"current_employee_only" is malformed because two entries are on the same line;
split them into two separate array entries so each key => value pair is its own
element (ensure there's a trailing comma after
"secondary_currency_rate_required" and that "current_employee_only" is on its
own line), preserving the exact strings "secondary_currency_rate_required" and
"current_employee_only" so the PHP array parses correctly.
In `@app/Libraries/SecondaryCurrencyFeedLib.php`:
- Around line 74-92: The feed URL returned by buildFeedUrl($config) (driven by
secondary_currency_feed_url) must be validated before issuing
Services::curlrequest and $client->get: ensure the URL uses only http or https
schemes, reject or normalize other schemes, and verify the host does not resolve
to private/internal addresses (or enforce a whitelist of allowed hosts) to
prevent SSRF. Implement checks using parse_url/filter_var to confirm scheme and
host, resolve the host and block RFC1918/loopback addresses (or compare against
an allowed-host list), and only proceed to call getCertificateBundlePath(),
create the client via Services::curlrequest, and $client->get when the URL
passes validation; otherwise log and abort the request.
---
Nitpick comments:
In `@app/Helpers/tabular_helper.php`:
- Around line 853-861: The foreach introduces an unused $key and shadows the
outer $expense; change the loop from "foreach ($expense->getResult() as $key =>
$expense)" to use a single, non-shadowing variable (e.g., "foreach
($expense->getResult() as $expenseRow)") and update all uses inside the loop
($expense->amount, $expense->tax_amount, $expense->secondary_currency_rate) to
use $expenseRow so $sum_amount_expense, $sum_tax_amount_expense and
$sum_secondary_amount_expense calculations remain correct.
In `@app/Language/bs/Sales.php`:
- Around line 115-116: Translate the two untranslated keys in the Bosnian locale
by replacing the English values for "rate" and
"secondary_currency_update_live_rate_tooltip" with their Bosnian equivalents;
update the entries for "rate" and "secondary_currency_update_live_rate_tooltip"
in the app/Language/bs/Sales.php locale array to provide Bosnian text (e.g.,
replace "Rate" and "Fetch the latest secondary currency rate now." with
appropriate Bosnian translations).
In `@app/Language/hr-HR/Sales.php`:
- Around line 115-116: The Croatian locale file contains two keys with English
values: "rate" and "secondary_currency_update_live_rate_tooltip"; update their
values to Croatian by replacing "Rate" with an appropriate Croatian word (e.g.,
"Tečaj" or "Stopa") and translate the tooltip to Croatian (e.g., "Preuzmi
najnoviji tečaj za sekundarnu valutu sada."), ensuring the keys "rate" and
"secondary_currency_update_live_rate_tooltip" remain unchanged.
In `@app/Language/hu/Sales.php`:
- Around line 115-116: Translate the two untranslated keys in the Hungarian
locale: replace the English values for "rate" and
"secondary_currency_update_live_rate_tooltip" in the Sales.php locale array with
appropriate Hungarian strings (e.g., "Árfolyam" for "rate" and a Hungarian
sentence for the tooltip) ensuring they match the style and grammar of existing
translations in the file; update the values for the array entries "rate" and
"secondary_currency_update_live_rate_tooltip".
In `@app/Language/id/Config.php`:
- Around line 62-83: The new secondary_currency_* language entries (e.g.,
"secondary_currency_disabled", "secondary_currency_auto_enable",
"secondary_currency_feed_invalid_json",
"secondary_currency_refresh_interval_invalid", etc.) in
app/Language/id/Config.php are left as English; update each of these keys to
either a proper Indonesian translation or set the value to an explicit empty
string ("") so fallback to English is intentional and consistent (follow the
existing pattern used by "current_employee_only"); ensure every
"secondary_currency_*" key is handled this way to avoid mixed-language UI.
- Line 83: The array entry on the same line contains two keys
("secondary_currency_rate_required" and "current_employee_only") in
app/Language/id/Config.php; split them so each key/value pair is on its own line
(one line for "secondary_currency_rate_required" => "Secondary currency rate
must be a positive number.", and a separate line for "current_employee_only" =>
""), preserving the surrounding array formatting, commas, and alignment to
reduce merge conflicts and improve readability.
In `@app/Language/it/Sales.php`:
- Around line 115-116: The two translation keys "rate" and
"secondary_currency_update_live_rate_tooltip" are misplaced among keyboard
shortcut entries; move the "rate" key so it's alphabetically grouped with other
"r" entries (e.g., near "register" or "return") and move
"secondary_currency_update_live_rate_tooltip" to the "s" section near the other
"s" entries (around the existing keys at lines ~183-192) in Sales.php so logical
grouping and alphabetical ordering are preserved.
In `@app/Language/ml/Sales.php`:
- Around line 115-116: The new keys "rate" and
"secondary_currency_update_live_rate_tooltip" are placed between keyboard
shortcut keys; move these two entries into the existing currency/financial
translations block (where other currency keys like "currency", "exchange_rate",
or similar exist) so they're semantically grouped with related strings; ensure
you preserve their current translations/empty-string fallbacks and maintain the
same array ordering/formatting used by the surrounding entries in the
Language/ml/Sales.php file.
In `@app/Language/pl/Config.php`:
- Around line 62-83: Translate the English values for the secondary currency
localization keys shown (e.g. "secondary_currency_disabled",
"secondary_currency_auto_enable", "secondary_currency_auto_enable_tooltip",
"secondary_currency_auto_refresh_disabled",
"secondary_currency_auto_refresh_disabled_force",
"secondary_currency_base_quote_required", "secondary_currency_codes_required",
"secondary_currency_codes_must_differ", "secondary_currency_feed_http_error",
"secondary_currency_feed_invalid_json", "secondary_currency_feed_invalid_rate",
"secondary_currency_feed_request_failed", "secondary_currency_feed_url",
"secondary_currency_feed_url_tooltip", "secondary_currency_last_error",
"secondary_currency_last_synced_at", "secondary_currency_refresh_failed",
"secondary_currency_refresh_successful", "secondary_currency_refresh_interval",
"secondary_currency_refresh_interval_tooltip",
"secondary_currency_refresh_interval_invalid",
"secondary_currency_rate_required") into Polish in the same associative array
(preserve placeholders like {0}, {base}, {quote} and literal flags like
--force), replacing the English strings with proper Polish translations so the
Polish locale no longer falls back to English.
In `@app/Language/th/Sales.php`:
- Around line 115-116: Translate the two English strings in the Thai locale for
the Sales module: replace the value for the "rate" key and the value for
"secondary_currency_update_live_rate_tooltip" with appropriate Thai
translations, keeping the keys unchanged; update the values in the array where
"rate" and "secondary_currency_update_live_rate_tooltip" are defined so the
tooltip reads naturally in Thai and the single-word "rate" is translated to the
correct Thai term.
In `@app/Language/tr/Sales.php`:
- Around line 115-116: Translate the two untranslated keys in the Turkish Sales
locale: replace the English values for "rate" and
"secondary_currency_update_live_rate_tooltip" in the Sales.php translations
array with appropriate Turkish strings (e.g., "Oran" or context-appropriate term
for "rate" and a Turkish sentence for the tooltip) so the keys "rate" and
"secondary_currency_update_live_rate_tooltip" are fully localized.
In `@app/Language/ur/Sales.php`:
- Around line 115-116: The added localization keys "rate" and
"secondary_currency_update_live_rate_tooltip" are in English and misplaced among
shortcut keys; either provide Urdu translations for these keys (replace the
English strings with proper Urdu text for "rate" and for "Fetch the latest
secondary currency rate now.") or relocate the key-value pairs to the locale
file/section that holds payment/currency-related strings so they sit with other
financial keys; update any references if you move them so the application still
reads the same keys.
In `@app/Language/vi/Sales.php`:
- Around line 115-116: Translate the two English values for the localization
keys "rate" and "secondary_currency_update_live_rate_tooltip" in the Vietnamese
language array: replace the value for "rate" with the Vietnamese equivalent
(e.g., "Tỷ giá" or context-appropriate term) and translate
"secondary_currency_update_live_rate_tooltip" into Vietnamese (e.g., "Lấy tỷ giá
ngoại tệ phụ mới nhất ngay bây giờ."), keeping the keys unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: acdbca33-a06a-46dd-9dc5-fad22d2f3937
⛔ Files ignored due to path filters (1)
writable/certs/cacert.pemis excluded by!**/*.pem
📒 Files selected for processing (99)
app/Commands/RefreshSecondaryCurrency.phpapp/Config/Routes.phpapp/Controllers/Config.phpapp/Controllers/Sales.phpapp/Helpers/tabular_helper.phpapp/Language/ar-EG/Config.phpapp/Language/ar-EG/Sales.phpapp/Language/ar-LB/Config.phpapp/Language/ar-LB/Sales.phpapp/Language/az/Config.phpapp/Language/az/Sales.phpapp/Language/bg/Config.phpapp/Language/bg/Sales.phpapp/Language/bs/Config.phpapp/Language/bs/Sales.phpapp/Language/ckb/Sales.phpapp/Language/cs/Config.phpapp/Language/cs/Sales.phpapp/Language/da/Config.phpapp/Language/da/Sales.phpapp/Language/de-CH/Config.phpapp/Language/de-CH/Sales.phpapp/Language/de-DE/Config.phpapp/Language/de-DE/Sales.phpapp/Language/el/Config.phpapp/Language/el/Sales.phpapp/Language/en-GB/Config.phpapp/Language/en-GB/Sales.phpapp/Language/en/Config.phpapp/Language/en/Reports.phpapp/Language/en/Sales.phpapp/Language/es-ES/Config.phpapp/Language/es-ES/Sales.phpapp/Language/es-MX/Config.phpapp/Language/es-MX/Sales.phpapp/Language/fa/Config.phpapp/Language/fa/Sales.phpapp/Language/fr/Config.phpapp/Language/fr/Sales.phpapp/Language/he/Config.phpapp/Language/he/Sales.phpapp/Language/hr-HR/Config.phpapp/Language/hr-HR/Sales.phpapp/Language/hu/Config.phpapp/Language/hu/Sales.phpapp/Language/hy/Config.phpapp/Language/hy/Sales.phpapp/Language/id/Config.phpapp/Language/id/Sales.phpapp/Language/it/Config.phpapp/Language/it/Sales.phpapp/Language/km/Config.phpapp/Language/km/Sales.phpapp/Language/lo/Config.phpapp/Language/lo/Sales.phpapp/Language/ml/Config.phpapp/Language/ml/Sales.phpapp/Language/nb/Config.phpapp/Language/nb/Sales.phpapp/Language/nl-BE/Config.phpapp/Language/nl-BE/Sales.phpapp/Language/nl-NL/Config.phpapp/Language/nl-NL/Sales.phpapp/Language/pl/Config.phpapp/Language/pl/Sales.phpapp/Language/pt-BR/Config.phpapp/Language/pt-BR/Sales.phpapp/Language/ro/Config.phpapp/Language/ro/Sales.phpapp/Language/ru/Config.phpapp/Language/ru/Sales.phpapp/Language/sv/Config.phpapp/Language/sv/Sales.phpapp/Language/ta/Config.phpapp/Language/ta/Sales.phpapp/Language/th/Config.phpapp/Language/th/Sales.phpapp/Language/tl/Config.phpapp/Language/tl/Sales.phpapp/Language/tr/Config.phpapp/Language/tr/Sales.phpapp/Language/uk/Config.phpapp/Language/uk/Sales.phpapp/Language/ur/Config.phpapp/Language/ur/Sales.phpapp/Language/vi/Config.phpapp/Language/vi/Sales.phpapp/Language/zh-Hans/Config.phpapp/Language/zh-Hans/Sales.phpapp/Language/zh-Hant/Config.phpapp/Language/zh-Hant/Sales.phpapp/Libraries/SecondaryCurrencyFeedLib.phpapp/Models/Expense.phpapp/Models/Reports/Detailed_receivings.phpapp/Views/configs/locale_config.phpapp/Views/reports/graphical.phpapp/Views/sales/quote_email.phpapp/Views/sales/register.phptests/Libraries/SecondaryCurrencyFeedLibTest.php
💤 Files with no reviewable changes (1)
- app/Language/ckb/Sales.php
✅ Files skipped from review due to trivial changes (10)
- tests/Libraries/SecondaryCurrencyFeedLibTest.php
- app/Language/km/Config.php
- app/Language/en/Reports.php
- app/Language/en/Config.php
- app/Language/nl-NL/Sales.php
- app/Language/az/Config.php
- app/Language/en/Sales.php
- app/Language/hy/Sales.php
- app/Language/tl/Config.php
- app/Language/cs/Sales.php
🚧 Files skipped from review as they are similar to previous changes (5)
- app/Views/sales/quote_email.php
- app/Models/Expense.php
- app/Models/Reports/Detailed_receivings.php
- app/Views/reports/graphical.php
- app/Views/sales/register.php
| private function persistStatus(bool $success, ?float $rate, string $error, ?string $syncedAt = null): void | ||
| { | ||
| try { | ||
| $appconfig = model(Appconfig::class); | ||
|
|
||
| $payload = [ | ||
| 'secondary_currency_last_error' => $success ? '' : $error, | ||
| ]; | ||
|
|
||
| if ($rate !== null) { | ||
| $payload['secondary_currency_rate'] = $rate; | ||
| } | ||
|
|
||
| if ($syncedAt !== null) { | ||
| $payload['secondary_currency_last_synced_at'] = $syncedAt; | ||
| } | ||
|
|
||
| $appconfig->batch_save($payload); | ||
| } catch (Throwable $throwable) { |
There was a problem hiding this comment.
Propagate batch_save() failures back to run().
persistStatus() only handles thrown exceptions. If batch_save() returns false, the command still prints a successful refresh even though the new rate/status never reached Appconfig.
Suggested fix
- private function persistStatus(bool $success, ?float $rate, string $error, ?string $syncedAt = null): void
+ private function persistStatus(bool $success, ?float $rate, string $error, ?string $syncedAt = null): bool
{
try {
$appconfig = model(Appconfig::class);
@@
- $appconfig->batch_save($payload);
+ return $appconfig->batch_save($payload);
} catch (Throwable $throwable) {
CLI::write(lang('Config.secondary_currency_feed_request_failed', [$throwable->getMessage()]), 'red');
+ return false;
}
}- $this->persistStatus(true, $rate, '', $now);
+ if (!$this->persistStatus(true, $rate, '', $now)) {
+ CLI::write(lang('Config.saved_unsuccessfully'), 'red');
+ return;
+ }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Commands/RefreshSecondaryCurrency.php` around lines 48 - 66,
persistStatus currently swallows batch_save failures because it only catches
exceptions; change it to detect Appconfig::batch_save returning false and throw
an exception so the failure propagates to run(). Specifically, in persistStatus
(function persistStatus) capture the return value from
$appconfig->batch_save($payload), and if it is === false throw a
RuntimeException (or other Throwable) containing a useful message (include the
payload/error context). This ensures run() observes the failure and doesn't
report a successful refresh when the Appconfig update failed.
| $routes->post('config/saveScale', 'Config::postSaveScale'); | ||
| $routes->post('config/saveShortcuts', 'Config::postSaveShortcuts'); |
There was a problem hiding this comment.
These routes point to controller methods that are not present.
Config::postSaveScale() and Config::postSaveShortcuts() do not exist in the current app/Controllers/Config.php, so these endpoints will fail as soon as something tries to call them.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Config/Routes.php` around lines 75 - 76, The routes
$routes->post('config/saveScale', 'Config::postSaveScale') and
$routes->post('config/saveShortcuts', 'Config::postSaveShortcuts') reference
missing controller methods; either add public methods postSaveScale(Request
$request) and postSaveShortcuts(Request $request) to the Config controller
(implementing request validation, processing, and appropriate response) or
remove/disable these route definitions if the endpoints are not intended to
exist; ensure method names exactly match Config::postSaveScale and
Config::postSaveShortcuts so the router can resolve them.
| $routes->add('sales/selectCustomer', 'Sales::postSelectCustomer'); | ||
| $routes->add('sales/changeMode', 'Sales::postChangeMode'); | ||
| $routes->add('sales/change_register_mode/(:num)', 'Sales::change_register_mode/$1'); | ||
| $routes->add('sales/setComment', 'Sales::postSetComment'); | ||
| $routes->add('sales/setInvoiceNumber', 'Sales::postSetInvoiceNumber'); | ||
| $routes->add('sales/setPaymentType', 'Sales::postSetPaymentType'); | ||
| $routes->add('sales/setPrintAfterSale', 'Sales::postSetPrintAfterSale'); | ||
| $routes->add('sales/setPriceWorkOrders', 'Sales::postSetPriceWorkOrders'); | ||
| $routes->add('sales/setEmailReceipt', 'Sales::postSetEmailReceipt'); | ||
| $routes->post('sales/refreshSecondaryCurrency', 'Sales::postRefreshSecondaryCurrency'); | ||
| $routes->add('sales/customerDisplay', 'Sales::getCustomerDisplay'); | ||
| $routes->add('sales/addPayment', 'Sales::postAddPayment'); | ||
| $routes->add('sales/deletePayment/(:any)', 'Sales::getDeletePayment/$1'); | ||
| $routes->add('sales/add', 'Sales::postAdd'); | ||
| $routes->add('sales/editItem/(:segment)', 'Sales::postEditItem/$1'); | ||
| $routes->add('sales/deleteItem/(:num)', 'Sales::getDeleteItem/$1'); | ||
| $routes->add('sales/removeCustomer', 'Sales::getRemoveCustomer'); | ||
| $routes->add('sales/complete', 'Sales::postComplete'); | ||
| $routes->add('sales/sendPdf/(:num)/(:segment)', 'Sales::getSendPdf/$1/$2'); | ||
| $routes->add('sales/sendReceipt/(:num)', 'Sales::getSendReceipt/$1'); | ||
| $routes->add('sales/receipt/(:num)', 'Sales::getReceipt/$1'); | ||
| $routes->add('sales/invoice/(:num)', 'Sales::getInvoice/$1'); | ||
| $routes->add('sales/edit/(:num)', 'Sales::getEdit/$1'); | ||
| $routes->add('sales/delete/(:num)', 'Sales::postDelete/$1'); | ||
| $routes->add('sales/restore/(:num)', 'Sales::restore/$1'); | ||
| $routes->add('sales/save/(:num)', 'Sales::postSave/$1'); | ||
| $routes->add('sales/cancel', 'Sales::postCancel'); | ||
| $routes->add('sales/discard_suspended_sale', 'Sales::getDiscardSuspendedSale'); | ||
| $routes->add('sales/discardSuspendedSale', 'Sales::getDiscardSuspendedSale'); | ||
| $routes->add('sales/suspend', 'Sales::postSuspend'); | ||
| $routes->add('sales/suspended', 'Sales::getSuspended'); | ||
| $routes->add('sales/unsuspend', 'Sales::postUnsuspend'); | ||
| $routes->add('sales/salesKeyboardHelp', 'Sales::getSalesKeyboardHelp'); | ||
| $routes->add('sales/checkInvoiceNumber', 'Sales::postCheckInvoiceNumber'); | ||
| $routes->add('sales/changeItemNumber', 'Sales::postChangeItemNumber'); | ||
| $routes->add('sales/changeItemName', 'Sales::postChangeItemName'); | ||
| $routes->add('sales/changeItemDescription', 'Sales::postChangeItemDescription'); |
There was a problem hiding this comment.
Use verb-specific routes for the mutating sales endpoints.
This block exposes state-changing actions such as selectCustomer, changeMode, addPayment, add, editItem, complete, delete, save, cancel, suspend, and unsuspend through $routes->add(). That makes them callable via GET as well as POST, which is unsafe for CSRF-sensitive sales mutations.
Suggested direction
-$routes->add('sales/selectCustomer', 'Sales::postSelectCustomer');
-$routes->add('sales/changeMode', 'Sales::postChangeMode');
-$routes->add('sales/addPayment', 'Sales::postAddPayment');
-$routes->add('sales/add', 'Sales::postAdd');
-$routes->add('sales/complete', 'Sales::postComplete');
-$routes->add('sales/delete/(:num)', 'Sales::postDelete/$1');
-$routes->add('sales/save/(:num)', 'Sales::postSave/$1');
-$routes->add('sales/cancel', 'Sales::postCancel');
-$routes->add('sales/suspend', 'Sales::postSuspend');
-$routes->add('sales/unsuspend', 'Sales::postUnsuspend');
+$routes->post('sales/selectCustomer', 'Sales::postSelectCustomer');
+$routes->post('sales/changeMode', 'Sales::postChangeMode');
+$routes->post('sales/addPayment', 'Sales::postAddPayment');
+$routes->post('sales/add', 'Sales::postAdd');
+$routes->post('sales/complete', 'Sales::postComplete');
+$routes->post('sales/delete/(:num)', 'Sales::postDelete/$1');
+$routes->post('sales/save/(:num)', 'Sales::postSave/$1');
+$routes->post('sales/cancel', 'Sales::postCancel');
+$routes->post('sales/suspend', 'Sales::postSuspend');
+$routes->post('sales/unsuspend', 'Sales::postUnsuspend');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Config/Routes.php` around lines 84 - 120, Several sales routes that
mutate server state are registered with $routes->add() and thus allow GET;
change them to verb-specific registrations (mostly $routes->post() or
$routes->delete() as appropriate) so CSRF-prone actions are not callable via
GET. Replace $routes->add(...) with $routes->post(...) for:
'sales/selectCustomer' (Sales::postSelectCustomer), 'sales/changeMode'
(Sales::postChangeMode), 'sales/setComment' (Sales::postSetComment),
'sales/setInvoiceNumber' (Sales::postSetInvoiceNumber), 'sales/setPaymentType'
(Sales::postSetPaymentType), 'sales/setPrintAfterSale'
(Sales::postSetPrintAfterSale), 'sales/setPriceWorkOrders'
(Sales::postSetPriceWorkOrders), 'sales/setEmailReceipt'
(Sales::postSetEmailReceipt), 'sales/addPayment' (Sales::postAddPayment),
'sales/add' (Sales::postAdd), 'sales/editItem/(:segment)'
(Sales::postEditItem/$1), 'sales/complete' (Sales::postComplete),
'sales/delete/(:num)' (Sales::postDelete/$1) —and change other mutating
endpoints to appropriate verbs too (e.g., 'sales/deletePayment/(:any)'
Sales::getDeletePayment/$1 and 'sales/deleteItem/(:num)' Sales::getDeleteItem/$1
should be converted to $routes->post() or $routes->delete(),
'sales/removeCustomer' Sales::getRemoveCustomer to $routes->post(),
'sales/save/(:num)' Sales::postSave/$1 to $routes->post() if not already,
'sales/cancel' Sales::postCancel to $routes->post(), 'sales/suspend'
Sales::postSuspend and 'sales/unsuspend' Sales::postUnsuspend to
$routes->post(); keep true read-only handlers (e.g., receipt/invoice/PDF
display) as GET. Ensure you update only the registrations (route helper) leaving
controller method names intact.
| $secondary_currency_enabled = $this->request->getPost('secondary_currency_enabled') != null; | ||
| $secondary_currency_auto_enabled = $this->request->getPost('secondary_currency_auto_enabled') != null; | ||
| $secondary_currency_feed_url = trim((string) $this->request->getPost('secondary_currency_feed_url')); | ||
| $secondary_currency_refresh_interval_minutes = $this->request->getPost('secondary_currency_refresh_interval_minutes', FILTER_SANITIZE_NUMBER_INT); | ||
| $secondary_currency_rate = $this->request->getPost('secondary_currency_rate', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); | ||
| $secondary_currency_code = trim((string) $this->request->getPost('secondary_currency_code')); | ||
|
|
||
| if ($secondary_currency_enabled) { | ||
| $validation_errors = []; | ||
|
|
||
| if ($secondary_currency_code === '') { | ||
| $validation_errors[] = lang('Config.secondary_currency_base_quote_required'); | ||
| } | ||
|
|
||
| if ($secondary_currency_rate === false || $secondary_currency_rate === '' || (float) $secondary_currency_rate <= 0) { | ||
| $validation_errors[] = lang('Config.secondary_currency_rate_required'); | ||
| } | ||
|
|
||
| if ($secondary_currency_auto_enabled) { | ||
| if ( | ||
| !is_numeric($secondary_currency_refresh_interval_minutes) | ||
| || (int) $secondary_currency_refresh_interval_minutes < 1 | ||
| || (int) $secondary_currency_refresh_interval_minutes > 1440 | ||
| ) { | ||
| $validation_errors[] = lang('Config.secondary_currency_refresh_interval_invalid'); | ||
| } | ||
|
|
||
| if ($secondary_currency_feed_url === '') { | ||
| $secondary_currency_feed_url = 'https://open.er-api.com/v6/latest/{base}'; | ||
| } | ||
| } | ||
|
|
||
| if (!empty($validation_errors)) { | ||
| return $this->response->setJSON([ | ||
| 'success' => false, | ||
| 'message' => implode(' ', $validation_errors) | ||
| ]); | ||
| } | ||
| } | ||
|
|
||
| $batch_save_data = [ | ||
| 'currency_symbol' => htmlspecialchars($currency_symbol ?? ''), | ||
| 'currency_code' => $this->request->getPost('currency_code'), | ||
| 'secondary_currency_enabled' => $secondary_currency_enabled, | ||
| 'secondary_currency_symbol' => htmlspecialchars($this->request->getPost('secondary_currency_symbol') ?? ''), | ||
| 'secondary_currency_code' => $secondary_currency_code, | ||
| 'secondary_currency_rate' => $secondary_currency_rate, | ||
| 'secondary_currency_decimals' => $this->request->getPost('secondary_currency_decimals', FILTER_SANITIZE_NUMBER_INT), |
There was a problem hiding this comment.
Normalize and reject identical base/secondary currency codes before saving.
This path only checks that the secondary code is non-empty. usd vs USD currently passes, and identical codes can still be persisted in manual mode even though the refresh path rejects them later. That leaves the feature enabled with duplicate totals instead of a real second currency.
Suggested fix
$exploded = explode(":", $this->request->getPost('language'));
$currency_symbol = $this->request->getPost('currency_symbol');
+ $currency_code = strtoupper(trim((string) $this->request->getPost('currency_code')));
$secondary_currency_enabled = $this->request->getPost('secondary_currency_enabled') != null;
$secondary_currency_auto_enabled = $this->request->getPost('secondary_currency_auto_enabled') != null;
$secondary_currency_feed_url = trim((string) $this->request->getPost('secondary_currency_feed_url'));
$secondary_currency_refresh_interval_minutes = $this->request->getPost('secondary_currency_refresh_interval_minutes', FILTER_SANITIZE_NUMBER_INT);
$secondary_currency_rate = $this->request->getPost('secondary_currency_rate', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
- $secondary_currency_code = trim((string) $this->request->getPost('secondary_currency_code'));
+ $secondary_currency_code = strtoupper(trim((string) $this->request->getPost('secondary_currency_code')));
@@
if ($secondary_currency_code === '') {
$validation_errors[] = lang('Config.secondary_currency_base_quote_required');
}
+
+ if ($currency_code !== '' && $currency_code === $secondary_currency_code) {
+ $validation_errors[] = lang('Config.secondary_currency_codes_must_differ');
+ }
@@
- 'currency_code' => $this->request->getPost('currency_code'),
+ 'currency_code' => $currency_code,
'secondary_currency_enabled' => $secondary_currency_enabled,
'secondary_currency_symbol' => htmlspecialchars($this->request->getPost('secondary_currency_symbol') ?? ''),
'secondary_currency_code' => $secondary_currency_code,As per coding guidelines, "Validate and sanitize all user input".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Controllers/Config.php` around lines 476 - 523, Normalize the primary and
secondary currency codes (e.g. trim and strtoupper) and reject when they are
identical: update the validation block that runs when
$secondary_currency_enabled to normalize $secondary_currency_code and the posted
currency code (getPost('currency_code')), and if they match add a validation
error (push to $validation_errors) so saving is blocked; also ensure the value
you write into $batch_save_data['secondary_currency_code'] is the normalized
code so the stored value is consistent with the check.
| if (empty($result['success'])) { | ||
| return $this->response->setJSON([ | ||
| 'success' => false, | ||
| 'message' => $result['message'] ?? lang('Config.secondary_currency_refresh_failed') | ||
| ]); | ||
| } | ||
|
|
||
| $appconfig = model(Appconfig::class); | ||
| $appconfig->batch_save([ | ||
| 'secondary_currency_rate' => $result['rate'], | ||
| 'secondary_currency_last_synced_at' => $syncedAt, | ||
| 'secondary_currency_last_error' => '' | ||
| ]); | ||
|
|
||
| return $this->response->setJSON([ | ||
| 'success' => true, | ||
| 'message' => lang('Config.secondary_currency_refresh_successful'), | ||
| 'rate' => $result['rate'], | ||
| 'synced_at' => $syncedAt |
There was a problem hiding this comment.
Persist refresh failures too, and fail the request if the config write does not stick.
This endpoint returns immediately on refresh failure without updating secondary_currency_last_error, and the success path ignores batch_save()'s boolean result. That leaves the locale status fields stale and can report a successful refresh even though the new rate was never stored.
Suggested fix
if (empty($result['success'])) {
+ model(Appconfig::class)->batch_save([
+ 'secondary_currency_last_error' => (string) ($result['message'] ?? lang('Config.secondary_currency_refresh_failed')),
+ ]);
+
return $this->response->setJSON([
'success' => false,
'message' => $result['message'] ?? lang('Config.secondary_currency_refresh_failed')
]);
}
@@
- $appconfig->batch_save([
+ $saved = $appconfig->batch_save([
'secondary_currency_rate' => $result['rate'],
'secondary_currency_last_synced_at' => $syncedAt,
'secondary_currency_last_error' => ''
]);
+
+ if (!$saved) {
+ return $this->response->setJSON([
+ 'success' => false,
+ 'message' => lang('Config.saved_unsuccessfully')
+ ]);
+ }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Controllers/Sales.php` around lines 1743 - 1761, The refresh handler in
Sales.php currently returns on $result['success'] false without updating
secondary_currency_last_error and ignores the boolean return of
model(Appconfig::class)->batch_save on success; update the logic so that on
refresh failure you call $appconfig->batch_save(...) to set
secondary_currency_last_error to the failure message (and
secondary_currency_last_synced_at as appropriate), and on the success path check
the boolean result of $appconfig->batch_save (the call that sets
secondary_currency_rate/last_synced_at/last_error) — if batch_save returns
false, persist a non-empty secondary_currency_last_error and return a failure
JSON via $this->response->setJSON; use the same $result['message'] or a clear
error string when writing last_error so the DB always reflects the actual stored
state.
| "rate" => "Rate", | ||
| "secondary_currency_update_live_rate_tooltip" => "Fetch the latest secondary currency rate now.", |
There was a problem hiding this comment.
Use empty strings for untranslated keys instead of English text.
Both new keys contain English text in the Italian language file. Per the established pattern in this codebase, untranslated keys should use empty strings (""), which will automatically fall back to the English translation via the custom Language::getLine() behavior. This approach makes it easier to track missing translations and maintain consistency across language files.
🌐 Proposed fix for proper translation fallback
- "rate" => "Rate",
- "secondary_currency_update_live_rate_tooltip" => "Fetch the latest secondary currency rate now.",
+ "rate" => "",
+ "secondary_currency_update_live_rate_tooltip" => "",Based on learnings: In OpenSourcePOS's CodeIgniter 4 language files, empty-string values ("") are intentional for untranslated keys and trigger automatic fallback to English via app/Libraries/MY_Language.php, making them the preferred pattern over directly embedding English text in non-English language files.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Language/it/Sales.php` around lines 115 - 116, The Italian language file
contains English text for the keys "rate" and
"secondary_currency_update_live_rate_tooltip"; change their values to empty
strings ("") so the custom Language::getLine() fallback will use the English
translations—update the entries for "rate" and
"secondary_currency_update_live_rate_tooltip" in the app/Language/it/Sales.php
array to "" instead of the English phrases.
| "key_browser" => "", | ||
| "key_cancel" => "Cancels Current Quote/Invoice/Sale", | ||
| "key_customer_search" => "Customer Search", | ||
| "key_finish_quote" => "Finish Quote/Invoice witdout payment", |
There was a problem hiding this comment.
Fix typo in shortcut text ("witdout").
Line 109 has a user-facing typo: Finish Quote/Invoice witdout payment → without.
Suggested fix
- "key_finish_quote" => "Finish Quote/Invoice witdout payment",
+ "key_finish_quote" => "Finish Quote/Invoice without payment",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "key_finish_quote" => "Finish Quote/Invoice witdout payment", | |
| "key_finish_quote" => "Finish Quote/Invoice without payment", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Language/pl/Sales.php` at line 109, Fix the typo in the translation
string for the array key "key_finish_quote": change the value from "Finish
Quote/Invoice witdout payment" to "Finish Quote/Invoice without payment" so the
user-facing text reads correctly; update the string in the Language/pl/Sales.php
localization array entry for key_finish_quote.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
app/Controllers/Config.php (1)
486-491: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winValidate ISO 4217 currency code format.
When
secondary_currency_enabledis true, the code checks thatsecondary_currency_codeis non-empty but does not validate it matches the expected 3-letter ISO 4217 format. Invalid codes could cause issues downstream in currency conversion or display logic.🛡️ Proposed validation
if ($secondary_currency_enabled) { $validation_errors = []; if ($secondary_currency_code === '') { $validation_errors[] = lang('Config.secondary_currency_base_quote_required'); + } elseif (!preg_match('/^[A-Z]{3}$/', strtoupper($secondary_currency_code))) { + $validation_errors[] = lang('Config.secondary_currency_code_invalid_format'); }As per coding guidelines, "Validate and sanitize all user input".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Controllers/Config.php` around lines 486 - 491, When $secondary_currency_enabled is true, add a format check for $secondary_currency_code (after trim/strtoupper) to ensure it matches ISO 4217 three-letter codes using a regex like /^[A-Z]{3}$/; if it fails, push a validation error onto $validation_errors (e.g., lang('Config.secondary_currency_code_invalid')), and optionally normalize $secondary_currency_code = strtoupper(trim($secondary_currency_code)) before further use to sanitize input; update any downstream uses to rely on the normalized value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Controllers/Config.php`:
- Around line 531-533: The feed URL is being HTML-escaped (htmlspecialchars)
which can corrupt valid URLs; instead validate and sanitize the incoming
$secondary_currency_feed_url for a proper URL format and only use it if valid.
Update the code that sets the 'secondary_currency_feed_url' (the variable
$secondary_currency_feed_url and the array entry) to trim the input, validate
with a URL validator (e.g., filter_var(..., FILTER_VALIDATE_URL) or an
equivalent validation helper), and fall back to the default
'https://open.er-api.com/v6/latest/{base}' when validation fails; do not apply
htmlspecialchars() to the value used for HTTP requests.
---
Duplicate comments:
In `@app/Controllers/Config.php`:
- Around line 486-491: When $secondary_currency_enabled is true, add a format
check for $secondary_currency_code (after trim/strtoupper) to ensure it matches
ISO 4217 three-letter codes using a regex like /^[A-Z]{3}$/; if it fails, push a
validation error onto $validation_errors (e.g.,
lang('Config.secondary_currency_code_invalid')), and optionally normalize
$secondary_currency_code = strtoupper(trim($secondary_currency_code)) before
further use to sanitize input; update any downstream uses to rely on the
normalized value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1843bb6c-24cf-409a-9787-39fc7d856c1e
📒 Files selected for processing (1)
app/Controllers/Config.php
| 'secondary_currency_feed_url' => $secondary_currency_feed_url !== '' | ||
| ? htmlspecialchars($secondary_currency_feed_url) | ||
| : 'https://open.er-api.com/v6/latest/{base}', |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Validate feed URL format rather than HTML-escaping it.
Using htmlspecialchars() on the feed URL (line 532) can corrupt valid URL characters (e.g., & becomes &), breaking API calls. Since this URL is used for HTTP requests, not HTML output, validate its format instead.
🔒 Proposed fix
+ if ($secondary_currency_auto_enabled && $secondary_currency_feed_url !== '') {
+ if (filter_var($secondary_currency_feed_url, FILTER_VALIDATE_URL) === false) {
+ $validation_errors[] = lang('Config.secondary_currency_feed_url_invalid');
+ }
+ }
+
if (!empty($validation_errors)) {
return $this->response->setJSON([
'success' => false,
'message' => implode(' ', $validation_errors)
]);
}
...
'secondary_currency_feed_url' => $secondary_currency_feed_url !== ''
- ? htmlspecialchars($secondary_currency_feed_url)
+ ? $secondary_currency_feed_url
: 'https://open.er-api.com/v6/latest/{base}',As per coding guidelines, "Validate and sanitize all user input".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Controllers/Config.php` around lines 531 - 533, The feed URL is being
HTML-escaped (htmlspecialchars) which can corrupt valid URLs; instead validate
and sanitize the incoming $secondary_currency_feed_url for a proper URL format
and only use it if valid. Update the code that sets the
'secondary_currency_feed_url' (the variable $secondary_currency_feed_url and the
array entry) to trim the input, validate with a URL validator (e.g.,
filter_var(..., FILTER_VALIDATE_URL) or an equivalent validation helper), and
fall back to the default 'https://open.er-api.com/v6/latest/{base}' when
validation fails; do not apply htmlspecialchars() to the value used for HTTP
requests.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
app/Language/fa/Config.php (1)
62-83: ⚡ Quick winPrefer empty-string placeholders for untranslated
fakeys instead of hardcoded English.These newly added Persian-locale entries are in English. In this codebase, untranslated locale keys should be left as
""so fallback-to-English is handled consistently byLanguage::getLine().Proposed pattern
- "secondary_currency_disabled" => "Secondary currency is disabled.", + "secondary_currency_disabled" => "", ... - "secondary_currency_feed_invalid_json" => "Currency feed response was not valid JSON.", + "secondary_currency_feed_invalid_json" => "", ... - "secondary_currency_rate_required" => "Secondary currency rate must be a positive number.", + "secondary_currency_rate_required" => "",Based on learnings: in
app/Language/**, empty strings are an intentional untranslated-key pattern and are resolved via English fallback inapp/Libraries/MY_Language.php Language::getLine().🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Language/fa/Config.php` around lines 62 - 83, The new Persian locale entries are left in English but should follow the project's untranslated-key convention: replace the English text for each added key (e.g., "secondary_currency_disabled", "secondary_currency_auto_enable", "secondary_currency_auto_enable_tooltip", "secondary_currency_auto_refresh_disabled", "secondary_currency_auto_refresh_disabled_force", "secondary_currency_base_quote_required", "secondary_currency_codes_required", "secondary_currency_codes_must_differ", "secondary_currency_feed_http_error", "secondary_currency_feed_invalid_json", "secondary_currency_feed_invalid_rate", "secondary_currency_feed_request_failed", "secondary_currency_feed_url", "secondary_currency_feed_url_tooltip", "secondary_currency_last_error", "secondary_currency_last_synced_at", "secondary_currency_refresh_failed", "secondary_currency_refresh_successful", "secondary_currency_refresh_interval", "secondary_currency_refresh_interval_tooltip", "secondary_currency_refresh_interval_invalid", "secondary_currency_rate_required") with empty-string values ("") so Language::getLine() will correctly fall back to English.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Language/he/Config.php`:
- Around line 62-83: Replace the English text values for the secondary currency
translation keys in app/Language/he/Config.php (keys such as
"secondary_currency_disabled", "secondary_currency_auto_enable",
"secondary_currency_auto_enable_tooltip", "secondary_currency_feed_http_error",
"secondary_currency_refresh_interval_invalid",
"secondary_currency_rate_required", etc.) with proper Hebrew translations or, if
unavailable, set them to empty strings ("") so the MY_Language.php fallback to
English kicks in; update all 22 keys in the block currently containing English
(the secondary_currency_* entries between the existing keys) so localization is
correct and avoids duplicating the source text.
In `@app/Libraries/SecondaryCurrencyFeedLib.php`:
- Around line 24-25: Normalize currency codes to a consistent case before
encoding and comparing: replace current rawurlencode(trim((string)
($config['currency_code'] ?? ''))) and rawurlencode(trim((string)
($config['secondary_currency_code'] ?? ''))) with
rawurlencode(strtoupper(trim((string) ($config['currency_code'] ?? '')))) and
rawurlencode(strtoupper(trim((string) ($config['secondary_currency_code'] ??
'')))). Ensure any equality check that enforces “must differ” compares these
normalized values ($baseCurrency, $secondaryCurrency) and apply the same
strtoupper-trim normalization to the other similar block (the code handling the
alternative parsing around the later section) so case-insensitive matches like
"usd" vs "USD" are treated as equal.
- Around line 75-76: The warning currently logs the raw user-supplied $url when
isSafeFeedUrl($url) fails; instead remove the verbatim URL from the log and log
a sanitized/redacted value or a generic rejection message. Update the
log_message call in SecondaryCurrencyFeedLib (the branch that checks
$this->isSafeFeedUrl($url)) to either log only the host (extracted and escaped
via esc()) or a fixed string like "Secondary currency feed URL rejected:
redacted" and do not include the raw $url; ensure any output uses the esc()
helper per coding guidelines.
- Around line 102-105: The catch block in SecondaryCurrencyFeedLib (the
Throwable handler returning ['success'=>false,'message'=>lang(...,
[$throwable->getMessage()])]) leaks transport details; instead, log the full
$throwable internally (e.g., via the class logger or logger()->error with the
exception) and return a generic localized failure string from
lang('Config.secondary_currency_feed_request_failed') without including
$throwable->getMessage() or other exception data; update the catch in the method
inside SecondaryCurrencyFeedLib to remove the message parameter and add an
internal log call referencing the caught $throwable.
---
Nitpick comments:
In `@app/Language/fa/Config.php`:
- Around line 62-83: The new Persian locale entries are left in English but
should follow the project's untranslated-key convention: replace the English
text for each added key (e.g., "secondary_currency_disabled",
"secondary_currency_auto_enable", "secondary_currency_auto_enable_tooltip",
"secondary_currency_auto_refresh_disabled",
"secondary_currency_auto_refresh_disabled_force",
"secondary_currency_base_quote_required", "secondary_currency_codes_required",
"secondary_currency_codes_must_differ", "secondary_currency_feed_http_error",
"secondary_currency_feed_invalid_json", "secondary_currency_feed_invalid_rate",
"secondary_currency_feed_request_failed", "secondary_currency_feed_url",
"secondary_currency_feed_url_tooltip", "secondary_currency_last_error",
"secondary_currency_last_synced_at", "secondary_currency_refresh_failed",
"secondary_currency_refresh_successful", "secondary_currency_refresh_interval",
"secondary_currency_refresh_interval_tooltip",
"secondary_currency_refresh_interval_invalid",
"secondary_currency_rate_required") with empty-string values ("") so
Language::getLine() will correctly fall back to English.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e7509e2e-693c-4c2c-8049-5ba4c9366ae2
📒 Files selected for processing (42)
app/Language/ar-EG/Config.phpapp/Language/ar-LB/Config.phpapp/Language/az/Config.phpapp/Language/bg/Config.phpapp/Language/bs/Config.phpapp/Language/cs/Config.phpapp/Language/da/Config.phpapp/Language/de-CH/Config.phpapp/Language/de-DE/Config.phpapp/Language/el/Config.phpapp/Language/en-GB/Config.phpapp/Language/es-ES/Config.phpapp/Language/es-MX/Config.phpapp/Language/fa/Config.phpapp/Language/fr/Config.phpapp/Language/he/Config.phpapp/Language/hr-HR/Config.phpapp/Language/hu/Config.phpapp/Language/hy/Config.phpapp/Language/id/Config.phpapp/Language/it/Config.phpapp/Language/km/Config.phpapp/Language/lo/Config.phpapp/Language/ml/Config.phpapp/Language/nb/Config.phpapp/Language/nl-BE/Config.phpapp/Language/nl-NL/Config.phpapp/Language/pl/Config.phpapp/Language/pt-BR/Config.phpapp/Language/ro/Config.phpapp/Language/ru/Config.phpapp/Language/sv/Config.phpapp/Language/ta/Config.phpapp/Language/th/Config.phpapp/Language/tl/Config.phpapp/Language/tr/Config.phpapp/Language/uk/Config.phpapp/Language/ur/Config.phpapp/Language/vi/Config.phpapp/Language/zh-Hans/Config.phpapp/Language/zh-Hant/Config.phpapp/Libraries/SecondaryCurrencyFeedLib.php
✅ Files skipped from review due to trivial changes (14)
- app/Language/de-CH/Config.php
- app/Language/ur/Config.php
- app/Language/ro/Config.php
- app/Language/nb/Config.php
- app/Language/bs/Config.php
- app/Language/hr-HR/Config.php
- app/Language/ru/Config.php
- app/Language/tr/Config.php
- app/Language/zh-Hans/Config.php
- app/Language/ml/Config.php
- app/Language/en-GB/Config.php
- app/Language/bg/Config.php
- app/Language/de-DE/Config.php
- app/Language/nl-BE/Config.php
🚧 Files skipped from review as they are similar to previous changes (24)
- app/Language/cs/Config.php
- app/Language/km/Config.php
- app/Language/es-MX/Config.php
- app/Language/hy/Config.php
- app/Language/pl/Config.php
- app/Language/es-ES/Config.php
- app/Language/id/Config.php
- app/Language/it/Config.php
- app/Language/fr/Config.php
- app/Language/az/Config.php
- app/Language/ar-EG/Config.php
- app/Language/pt-BR/Config.php
- app/Language/vi/Config.php
- app/Language/nl-NL/Config.php
- app/Language/th/Config.php
- app/Language/da/Config.php
- app/Language/lo/Config.php
- app/Language/tl/Config.php
- app/Language/sv/Config.php
- app/Language/hu/Config.php
- app/Language/uk/Config.php
- app/Language/ta/Config.php
- app/Language/ar-LB/Config.php
- app/Language/el/Config.php
| "secondary_currency_disabled" => "Secondary currency is disabled.", | ||
| "secondary_currency_auto_enable" => "Auto refresh secondary currency", | ||
| "secondary_currency_auto_enable_tooltip" => "When enabled, the app refreshes the secondary currency rate from the configured feed on a schedule.", | ||
| "secondary_currency_auto_refresh_disabled" => "Automatic secondary currency refresh is disabled.", | ||
| "secondary_currency_auto_refresh_disabled_force" => "Automatic secondary currency refresh is disabled. Use --force to run anyway.", | ||
| "secondary_currency_base_quote_required" => "Secondary currency code is required.", | ||
| "secondary_currency_codes_required" => "Currency codes are required before refreshing the rate.", | ||
| "secondary_currency_codes_must_differ" => "Base and secondary currency codes must be different.", | ||
| "secondary_currency_feed_http_error" => "Currency feed returned HTTP {0}.", | ||
| "secondary_currency_feed_invalid_json" => "Currency feed response was not valid JSON.", | ||
| "secondary_currency_feed_invalid_rate" => "Currency feed did not contain a valid rate for {0}.", | ||
| "secondary_currency_feed_request_failed" => "Currency feed request failed: {0}", | ||
| "secondary_currency_feed_url" => "Currency feed URL", | ||
| "secondary_currency_feed_url_tooltip" => "Use {base} for the primary currency code and {quote} for the secondary currency code.", | ||
| "secondary_currency_last_error" => "Last refresh error", | ||
| "secondary_currency_last_synced_at" => "Last refresh", | ||
| "secondary_currency_refresh_failed" => "Currency refresh failed.", | ||
| "secondary_currency_refresh_successful" => "Secondary currency rate refreshed successfully.", | ||
| "secondary_currency_refresh_interval" => "Refresh interval (minutes)", | ||
| "secondary_currency_refresh_interval_tooltip" => "How often the background job should fetch a new rate.", | ||
| "secondary_currency_refresh_interval_invalid" => "Secondary currency refresh interval must be between 1 and 1440 minutes.", | ||
| "secondary_currency_rate_required" => "Secondary currency rate must be a positive number.", |
There was a problem hiding this comment.
Translate to Hebrew or use empty strings for fallback.
All 22 new secondary currency translation keys contain English text instead of Hebrew translations. For a Hebrew language file, this creates unnecessary duplication and maintenance burden.
Based on learnings, the preferred approaches are:
- Provide actual Hebrew translations for proper localization
- Use empty strings (
"") which automatically fall back to English via the customMY_Language.phpbehavior
The current approach duplicates the English source text, requiring updates in two places if the English text changes.
Example fix using empty string fallback
- "secondary_currency_disabled" => "Secondary currency is disabled.",
+ "secondary_currency_disabled" => "",
- "secondary_currency_auto_enable" => "Auto refresh secondary currency",
+ "secondary_currency_auto_enable" => "",Apply this pattern to all 22 new keys (lines 62-83) until Hebrew translations are available.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Language/he/Config.php` around lines 62 - 83, Replace the English text
values for the secondary currency translation keys in app/Language/he/Config.php
(keys such as "secondary_currency_disabled", "secondary_currency_auto_enable",
"secondary_currency_auto_enable_tooltip", "secondary_currency_feed_http_error",
"secondary_currency_refresh_interval_invalid",
"secondary_currency_rate_required", etc.) with proper Hebrew translations or, if
unavailable, set them to empty strings ("") so the MY_Language.php fallback to
English kicks in; update all 22 keys in the block currently containing English
(the secondary_currency_* entries between the existing keys) so localization is
correct and avoids duplicating the source text.
| $baseCurrency = rawurlencode(trim((string) ($config['currency_code'] ?? ''))); | ||
| $secondaryCurrency = rawurlencode(trim((string) ($config['secondary_currency_code'] ?? ''))); |
There was a problem hiding this comment.
Normalize currency codes before comparing or templating them.
usd and USD currently bypass the “must differ” guard because the comparison is case-sensitive. That lets the same currency through and can persist a meaningless 1 exchange rate.
Proposed fix
- $baseCurrency = rawurlencode(trim((string) ($config['currency_code'] ?? '')));
- $secondaryCurrency = rawurlencode(trim((string) ($config['secondary_currency_code'] ?? '')));
+ $baseCurrency = rawurlencode(strtoupper(trim((string) ($config['currency_code'] ?? ''))));
+ $secondaryCurrency = rawurlencode(strtoupper(trim((string) ($config['secondary_currency_code'] ?? ''))));- $baseCurrency = trim((string) ($config['currency_code'] ?? ''));
- $secondaryCurrency = trim((string) ($config['secondary_currency_code'] ?? ''));
+ $baseCurrency = strtoupper(trim((string) ($config['currency_code'] ?? '')));
+ $secondaryCurrency = strtoupper(trim((string) ($config['secondary_currency_code'] ?? '')));Also applies to: 57-72
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Libraries/SecondaryCurrencyFeedLib.php` around lines 24 - 25, Normalize
currency codes to a consistent case before encoding and comparing: replace
current rawurlencode(trim((string) ($config['currency_code'] ?? ''))) and
rawurlencode(trim((string) ($config['secondary_currency_code'] ?? ''))) with
rawurlencode(strtoupper(trim((string) ($config['currency_code'] ?? '')))) and
rawurlencode(strtoupper(trim((string) ($config['secondary_currency_code'] ??
'')))). Ensure any equality check that enforces “must differ” compares these
normalized values ($baseCurrency, $secondaryCurrency) and apply the same
strtoupper-trim normalization to the other similar block (the code handling the
alternative parsing around the later section) so case-insensitive matches like
"usd" vs "USD" are treated as equal.
| if (!$this->isSafeFeedUrl($url)) { | ||
| log_message('warning', 'Secondary currency feed URL rejected: ' . $url); |
There was a problem hiding this comment.
Remove the raw feed URL from this warning log.
This path logs admin-supplied input verbatim. A custom feed URL can carry API keys/query tokens, and malformed values can inject line breaks into the log stream. Log a redacted host or a generic rejection message instead.
Proposed fix
if (!$this->isSafeFeedUrl($url)) {
- log_message('warning', 'Secondary currency feed URL rejected: ' . $url);
+ log_message('warning', 'Secondary currency feed URL rejected');
return [
'success' => false,
'message' => lang('Config.secondary_currency_feed_request_failed', ['Invalid feed URL'])
];As per coding guidelines, "Sanitize user input; escape output using esc() helper".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!$this->isSafeFeedUrl($url)) { | |
| log_message('warning', 'Secondary currency feed URL rejected: ' . $url); | |
| if (!$this->isSafeFeedUrl($url)) { | |
| log_message('warning', 'Secondary currency feed URL rejected'); | |
| return [ | |
| 'success' => false, | |
| 'message' => lang('Config.secondary_currency_feed_request_failed', ['Invalid feed URL']) | |
| ]; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Libraries/SecondaryCurrencyFeedLib.php` around lines 75 - 76, The warning
currently logs the raw user-supplied $url when isSafeFeedUrl($url) fails;
instead remove the verbatim URL from the log and log a sanitized/redacted value
or a generic rejection message. Update the log_message call in
SecondaryCurrencyFeedLib (the branch that checks $this->isSafeFeedUrl($url)) to
either log only the host (extracted and escaped via esc()) or a fixed string
like "Secondary currency feed URL rejected: redacted" and do not include the raw
$url; ensure any output uses the esc() helper per coding guidelines.
| } catch (\Throwable $throwable) { | ||
| return [ | ||
| 'success' => false, | ||
| 'message' => lang('Config.secondary_currency_feed_request_failed', [$throwable->getMessage()]), |
There was a problem hiding this comment.
Do not return transport exception details to the caller.
$throwable->getMessage() can include the resolved URL, TLS paths, or other server-side network details. That turns a failed refresh into an information leak in the admin response. Log internally and return a generic localized failure instead.
Proposed fix
} catch (\Throwable $throwable) {
+ log_message('error', 'Secondary currency feed request failed');
+
return [
'success' => false,
- 'message' => lang('Config.secondary_currency_feed_request_failed', [$throwable->getMessage()]),
+ 'message' => lang('Config.secondary_currency_feed_request_failed', ['Request failed']),
];
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Libraries/SecondaryCurrencyFeedLib.php` around lines 102 - 105, The catch
block in SecondaryCurrencyFeedLib (the Throwable handler returning
['success'=>false,'message'=>lang(..., [$throwable->getMessage()])]) leaks
transport details; instead, log the full $throwable internally (e.g., via the
class logger or logger()->error with the exception) and return a generic
localized failure string from
lang('Config.secondary_currency_feed_request_failed') without including
$throwable->getMessage() or other exception data; update the catch in the method
inside SecondaryCurrencyFeedLib to remove the message parameter and add an
internal log call referencing the caught $throwable.
|
@WebShells Looks good! I was wondering, and I might be way off here, since this feature seems optional for most deployments, I'm curious whether it might be better as a plugin once the plugin system is implemented (#4407). That could keep the core simpler while still letting users who need dual‑currency support enable it. |
@BudsieBuds Thanks for your time going over this! A good reason not to move this to a plugin is that it is not an isolated feature. It goes through the same core paths that already own money and historical snapshots. Why code feature better fits here: It changes persisted transaction data, so sales, receivings, and expenses need the same secondary rate snapshot at save time. @opensourcepos/org any thoughts on this ? |
#4407 needs code review so @WebShells and @BudsieBuds if you feel like doing one, it could both help you get familiar with the feature set and spot things that need fixing. This PR could be shoehorned into a plugin, but it doesn't fit the natural profile of a plugin. It is true that it's an optional feature, but I don't think that should make the only criteria for whether it should be a plugin. To make it a plugin we would have to inject several partial views, which #4407 already has the capability of doing, but the part that makes it more complicated is the roadmap for the feature. My understanding is that down the road @WebShells was thinking to implement payments in secondary currency which further ties it into the core functionality of the code. |
objecttothis
left a comment
There was a problem hiding this comment.
@WebShells it's late, so this is as far as I got. It gives you something to do. Request another PR once you finish with this part.
| { | ||
| protected $group = 'OSPOS'; | ||
| protected $name = 'currency:refresh-secondary'; | ||
| protected $description = 'Refreshes the configured secondary currency rate from the live feed.'; |
There was a problem hiding this comment.
I assume we are hardcoding the English here because CLI commands don't use locales. Is that correct?
| $routes->add('sales/checkInvoiceNumber', 'Sales::postCheckInvoiceNumber'); | ||
| $routes->add('sales/changeItemNumber', 'Sales::postChangeItemNumber'); | ||
| $routes->add('sales/changeItemName', 'Sales::postChangeItemName'); | ||
| $routes->add('sales/changeItemDescription', 'Sales::postChangeItemDescription'); |
There was a problem hiding this comment.
We shouldn't need to add all these sales routes. Comment them out and run php spark routes. You should see the autorouter pick it up.
| $routes->add('no_access/index/(:segment)', 'No_access::index/$1'); | ||
| $routes->add('no_access/index/(:segment)/(:segment)', 'No_access::index/$1/$2'); | ||
|
|
||
| $routes->add('reports/summary_expenses_suppliers/(:any)/(:any)/(:any)/(:any)/(:any)', 'Reports::summary_expenses_suppliers/$1/$2/$3/$4/$5'); |
There was a problem hiding this comment.
The reports routes are necessary, I believe, because they use custom patterns.
| $routes->add('no_access/index/(:segment)', 'No_access::index/$1'); | ||
| $routes->add('no_access/index/(:segment)/(:segment)', 'No_access::index/$1/$2'); | ||
|
|
||
| $routes->add('reports/summary_expenses_suppliers/(:any)/(:any)/(:any)/(:any)/(:any)', 'Reports::summary_expenses_suppliers/$1/$2/$3/$4/$5'); |
There was a problem hiding this comment.
This is a very large changeset. If you are adding new reports that don't have directly to do with Secondary Currency, please break them out into a different PR.
There was a problem hiding this comment.
If they are just changes to existing reports to display secondary currency, then that's fine.
| { | ||
| $exploded = explode(":", $this->request->getPost('language')); | ||
| $currency_symbol = $this->request->getPost('currency_symbol'); | ||
| $secondary_currency_enabled = $this->request->getPost('secondary_currency_enabled') != null; |
There was a problem hiding this comment.
All new local variables, function names and class names must use PSR compliant naming. Please go through all the new code in this PSR and refactor them.
| 'data' => $tabular_data, | ||
| 'summary_data' => $summary | ||
| 'summary_data' => $summary, | ||
| 'summary_secondary_data' => $this->_build_secondary_summary_display_map($report_data, $summary, $secondaryCurrency), |
There was a problem hiding this comment.
Hungarian notation in function names is obsolete. It was used back before PHP had private function classifiers to indicate that the function was meant to be private.
_append_secondary_currency / _secondary_currency_snapshot / _secondary_currency_for_row / _build_secondary_summary_display_map / _average_secondary_rate Leading underscore on private methods. PSR-12 explicitly says "method names MUST NOT be prefixed with a single underscore to indicate protected or private visibility." These are all violations.
| 'start_date' => $start_date, | ||
| 'end_date' => $end_date, | ||
| 'sale_type' => $sale_type, | ||
| public function summary_suppliers(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string |
There was a problem hiding this comment.
This is pre-existing code smell, not introduced by this PR. @coderabbitai please create an issue to resolve this in another PR.
summary_suppliers has 4 params — same as summary_sales, summary_categories, summary_customers, summary_items, summary_employees, and ~15 other methods. Even the TODO comment ("Too many parameters") was already there before this PR.
That said — the right fix is a parameter object:
// Instead of:
public function summary_suppliers(
string $start_date,
string $end_date,
string $sale_type,
string $location_id = 'all'
): string
// Use a DTO/value object:
class ReportFilter
{
public function __construct(
public readonly string $startDate,
public readonly string $endDate,
public readonly string $saleType = 'all',
public readonly string $locationId = 'all',
public readonly int $discountType = 0,
public readonly string $entityId = '', // customer/employee/supplier/discount ID
public readonly string $paymentType = 'all',
) {}
}
public function summary_suppliers(ReportFilter $filter): string
Why this is better:
Adding new filter param (like discount_type) doesn't break every call site
IDE completion on the object vs positional string args
One place to validate/sanitize inputs
Routes already unpack URL segments into $inputs array internally anyway — the object just formalizes that
Create the class in app/Entities/ReportFilter.php — namespace App\Entities since Entities is where CI intends on Data holding objects to be stored.
CI4 convention: Entities directory is specifically for data-holding objects
Directory doesn't exist yet but CI4 autoloads it automatically
Most semantically correct by CI4 standards
Recommendation: app/Entities/ReportFilter.php
CI4 has a first-class Entity concept for data objects. No existing pattern to follow here so better to align with the framework than invent App\Models\Filters. Future devs will know where to look.
There was a problem hiding this comment.
🧩 Analysis chain
@objecttothis, I've created the issue to track this refactoring. Here's a summary of what was captured:
🐰
The issue has been filed in opensourcepos/opensourcepos, assigned to you, and includes:
- The full list of ~15+ affected methods in
app/Controllers/Reports.php - The proposed
ReportFilterDTO with all relevant fields (startDate,endDate,saleType,locationId,discountType,entityId,paymentType) - Rationale (no call-site breakage on new params, IDE completion, single validation point)
- The recommended path
app/Entities/ReportFilter.phpper CI4 conventions - Backlinks to PR
#4535and this comment
🏁 Script executed:
gh issue create \
--repo opensourcepos/opensourcepos \
--title "Refactor: Introduce ReportFilter DTO to replace multiple positional parameters in Reports controller" \
--assignee "objecttothis" \
--body "## Summary
Several methods in \`app/Controllers/Reports.php\` accept 4+ positional string parameters (\`\$start_date\`, \`\$end_date\`, \`\$sale_type\`, \`\$location_id\`, etc.), which is a pre-existing code smell flagged in the existing TODO comments.
Affected methods include (but are not limited to):
- \`summary_sales()\`
- \`summary_categories()\`
- \`summary_customers()\`
- \`summary_suppliers()\`
- \`summary_items()\`
- \`summary_employees()\`
- \`summary_discounts()\`
- \`graphical_summary_sales()\`
- \`graphical_summary_items()\`
- \`graphical_summary_categories()\`
- \`graphical_summary_suppliers()\`
- \`graphical_summary_employees()\`
- \`detailed_sales()\`
- \`detailed_receivings()\`
- and others (~15+ methods total)
## Proposed Solution
Introduce a \`ReportFilter\` value object / DTO at \`app/Entities/ReportFilter.php\` (namespace \`App\\Entities\`, aligned with CodeIgniter 4 conventions):
\`\`\`php
<?php
namespace App\Entities;
class ReportFilter
{
public function __construct(
public readonly string \$startDate,
public readonly string \$endDate,
public readonly string \$saleType = 'all',
public readonly string \$locationId = 'all',
public readonly int \$discountType = 0,
public readonly string \$entityId = '', // customer/employee/supplier/discount ID
public readonly string \$paymentType = 'all',
) {}
}
\`\`\`
Then update all affected controller methods to accept \`ReportFilter \$filter\` instead of individual positional parameters.
## Why
- Adding a new filter field (e.g., \`discount_type\`) does not break every call site
- IDE autocompletion on the object vs. positional string arguments
- Single place to validate/sanitize report inputs
- Routes already unpack URL segments into an \`\$inputs\` array internally — the object formalizes that
## Notes
- \`app/Entities/\` does not yet exist in the codebase but CI4 autoloads it automatically
- This is pre-existing technical debt, not introduced by PR `#4535`
## References
- Flagged during review of PR `#4535`: https://github.com/opensourcepos/opensourcepos/pull/4535#discussion_r3291111462
- Requested by `@objecttothis`
"
Length of output: 59
@objecttothis, the issue has been created and assigned to you: #4561 — Refactor: Introduce ReportFilter DTO to replace multiple positional parameters in Reports controller.
It covers:
- All ~15+ affected methods (both
summary_*andgraphical_summary_*variants) - The proposed
app/Entities/ReportFilter.phpDTO with all relevant fields - The rationale you outlined (no call-site breakage, IDE completion, single validation point, CI4
Entitiesconvention) - Backlinks to PR
#4535and this comment thread
🧬 Code Graph Analysis Results
-
File: app/Controllers/Reports.php
-
Snippet: Summary methods with multi-parameter signatures (4 string parameters)
-
Description: The following public methods all take four separate string parameters (start_date, end_date, sale_type, location_id) and return a string. They illustrate the current pattern that the user is describing as a code smell. These are exact signatures and docblocks for each method:
- Summary Sales Report
- Signature:
public function summary_sales(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string // TODO: Perhaps these need to be passed as an array? Too many parameters in the signature. - Docblock (excerpt present in file):
/**- Summary Sales Report.
@paramstring $start_date@paramstring $end_date@paramstring $sale_type@paramstring $location_id@returnstring
*/
- Signature:
- Summary Categories Report
- Signature:
public function summary_categories(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string - Docblock (excerpt present in file):
/**- Summary Categories report.
@paramstring $start_date@paramstring $end_date@paramstring $sale_type@paramstring $location_id@returnstring
*/
- Signature:
- Summary Customers Report
- Signature:
public function summary_customers(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string - Docblock (excerpt present in file):
/**- Summary Customers report.
@paramstring $start_date@paramstring $end_date@paramstring $sale_type@paramstring $location_id@returnstring
*/
- Signature:
- Summary Suppliers Report
- Signature:
public function summary_suppliers(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string - Docblock (excerpt present in file):
/**- Summary Suppliers report.
@paramstring $start_date@paramstring $end_date@paramstring $sale_type@paramstring $location_id@returnstring
*/
- Signature:
- Summary Items Report
- Signature:
public function summary_items(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string - Docblock (excerpt present in file):
/**- Summary Items report.
@paramstring $start_date@paramstring $end_date@paramstring $sale_type@paramstring $location_id@returnstring
*/
- Signature:
- Summary Employees Report
- Signature:
public function summary_employees(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string - Docblock (excerpt present in file):
/**- Summary Employees report.
@paramstring $start_date@paramstring $end_date@paramstring $sale_type@paramstring $location_id@returnstring
*/
- Signature:
- Summary Taxes Report
- Signature:
public function summary_taxes(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string - Docblock (excerpt present in file):
/**- Summary Taxes report.
@paramstring $start_date@paramstring $end_date@paramstring $sale_type@paramstring $location_id@returnstring
*/
- Signature:
- Summary Sales Taxes Report
- Signature:
public function summary_sales_taxes(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string - Docblock (excerpt present in file):
/**- Summary Sales Taxes report
@returnstring
*/
- Signature:
- Summary Discounts Report
- Signature:
public function summary_discounts(string $start_date, string $end_date, string $sale_type, string $location_id = 'all', int $discount_type = 0): string - Docblock (excerpt present in file):
/**- Summary Discounts report
@returnstring
**/
- Signature:
- Summary Sales Report
-
Snippet: Graphical counterparts for the same parameter pattern
-
Description: The file also contains graphical counterparts that follow the same four-parameter pattern (start_date, end_date, sale_type, location_id) for the graphical reports:
- Graphical Summary Sales
- Signature:
public function graphical_summary_sales(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
- Signature:
- Graphical Summary Items
- Signature:
public function graphical_summary_items(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
- Signature:
- Graphical Summary Categories
- Signature:
public function graphical_summary_categories(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
- Signature:
- Graphical Summary Suppliers
- Signature:
public function graphical_summary_suppliers(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
- Signature:
- Graphical Summary Employees
- Signature:
public function graphical_summary_employees(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
- Signature:
- Graphical Summary Taxes
- Signature:
public function graphical_summary_taxes(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
- Signature:
- Graphical Summary Sales Taxes
- Signature:
public function graphical_summary_sales_taxes(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
- Signature:
- Graphical Summary Discounts
- Signature:
public function graphical_summary_discounts(string $start_date, string $end_date, string $sale_type, string $location_id = 'all', int $discount_type = 0): string
- Signature:
- Graphical Summary Payments
- Signature:
public function graphical_summary_payments(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
- Signature:
- Graphical Summary Sales
-
-
File: app/Models/Reports/Summary_report.php
- Snippet: Abstract report interface
- Description: The base abstract class defines the contract that all summary reports must implement. This is relevant for understanding how a future ReportFilter DTO could be used to drive data fetch for all these reports:
- Abstract method getData
- Signature:
public abstract function getData(array $inputs): array; - Description: Returns the detailed data rows for the report based on inputs.
- Signature:
- Abstract method getSummaryData
- Signature:
public abstract function getSummaryData(array $inputs): array; - Description: Returns the aggregated/summary data for the report.
- Signature:
- Abstract method getDataColumns
- Signature:
public abstract function getDataColumns(): array; - Description: Returns the columns/headers for the report output.
- Signature:
- Abstract method getData
-
File: app/Controllers/Reports.php
- Snippet: Example of a 4-parameter pattern in a single method header (summary_sales)
- Description: The method header and docblock show how the code currently represents a report request with explicit positional/typed parameters instead of a DTO. This illustrates the motivation for introducing a parameter object (DTO) like ReportFilter.
-
File: app/Models/Reports/Summary_discounts.php
- Snippet: Example of a data-fetch path that already uses an inputs array
- Description: This file demonstrates an alternative approach (accepting an inputs array) for flexibility. It includes:
- getData(array $inputs): array
- getSummaryData(array $inputs): array
- Context: This helps illustrate that some report classes already use an inputs array pattern, which aligns with the proposed DTO approach.
-
File: app/Models/Reports/Summary_sales.php (and other similar Summary_*.php counterparts)
- Snippet: getDataColumns and _select patterns that rely on an $inputs array
- Description: Examples showing how the code builds data for the report when given an inputs array, highlighting the contrast with the 4-parameter methods in the controller.
-
File: app/Entities/ReportFilter.php (not present in provided content)
- Snippet: Proposed DTO class (from user comment)
- Description: The user proposed introducing a DTO/value object:
- ReportFilter constructor with parameters like startDate, endDate, saleType, locationId, discountType, entityId, paymentType
- Intended usage: public function summary_suppliers(ReportFilter $filter): string
- Context: This is not present in the provided repo dump, but is the suggested direction to resolve the multi-parameter signature smell.
-
File: any relevant lines from the provided code indicating existing multi-parameter patterns
- File: app/Controllers/Reports.php
- Snippet: Summary Sales Report (docblock + signature)
- Description: Evidence of the current multi-parameter approach that the user wants to replace with a DTO.
Notes
- The user comment claims this pattern is pre-existing (i.e., not introduced by this PR) and suggests creating an issue to address it later. The snippets above illustrate that pattern in the current codebase (4-string-parameter report methods and their graphical counterparts, plus related Summary_report abstractions). The Summary_discounts and related Summary_* classes show an already more array-based input approach in some contexts, which provides context for a DTO-based refactor.
🧠 Learnings used
Learnt from: objecttothis
Repo: opensourcepos/opensourcepos PR: 4556
File: app/Config/Encryption.php:147-153
Timestamp: 2026-05-22T20:02:44.076Z
Learning: Assume the Open Source POS codebase runs on PHP >= 8.2. When reviewing PHP files, it is OK to rely on PHP 8.2+ language features/behavior; do not flag code that uses PHP 8.2-only features or relies on 8.2 semantics for backward compatibility with earlier PHP versions.
Secondary Currency and Customer Display Enhancements
This PR adds configurable secondary currency support across the sales flow, customer-facing display, receipts, documents, receiving, and reports. The feature is controlled by a global enable/disable option in Localization, with an additional receipt-level toggle for printed receipts.
What was added
Enable Secondary Currencysetting under Localization.Show Secondary Currencyoption.Where it appears
Sales register
Customer display
Short receipt
Invoices, quotes, work orders, and tax invoices
Receiving
Reports
Enable / disable behavior
When
Enable Secondary Currencyis offWhen enabled
Implementation notes
The feature uses generic labels such as:
TotalSecondary Currency TotalRateAmount DueChange DueThis keeps the implementation reusable and avoids hardcoding currency-specific wording into the UI.
Summary by CodeRabbit
New Features
Database
Reports
Tests & Translations