Saltar al contenido principal

Foreign-Currency Cash Tender

A cash payment can be larger than the balance, and it can be in a currency other than the business default. The drawer then holds the bill that came in and the change that went out. Those legs must net to the amount the customer owed.

The rules live in packages/global/utils/cash-tender-settlement.ts. Checkout, sale submit, session totals, and the sale receipt all use that module. Restaurant bill payment does not: BillPaymentPanel never calls settleCashTender.

The exchange rate is base-currency units per 1 selected unit. 7 means 1 USD = 7 GTQ.

Worked example​

A Q135.00 sale paid with a $100 bill at rate 7:

Change given inLegsNet base
Quetzales (base)in $100 (Q700), out Q565Q135
Dollars (tender)in $100 (Q700), out $80.71 (Q564.97), out Q0.03Q135

The exact dollar chip for the same sale is $19.29. exactTenderAmount rounds the quotient up to the tender minor unit, so $19.29 is worth Q135.03. Settlement stores an inflow of $19.29 and a Q0.03 change leg. The net is still Q135.

receiptChangeBase prints Cambio as the sum of outflow bases (Q565 in both $100 cases, Q0.03 for the exact chip). Inflows are not change.

What settleCashTender returns​

Inputs: tenderedAmount, exchangeRate, remainingBase, changeCurrency (base or tender), and optional minor units (default 2). Amounts round half-up. A rate of 0 or less is treated as 1.

SituationResult
Tender or remaining is 0 or negativeNo legs, appliedBase 0, partial false
Tender's base value is less than the balanceOne inflow. partial is true. appliedBase is that base value
Tender covers the balance, change in base (or the rate is 1)Inflow in the tender currency, one base-currency outflow for the change. appliedBase is the full balance
Tender covers the balance and change is in the tender currencyInflow, a tender-currency outflow, and a base residual when the rounded dollar change is short of the quetzal change

Dollar change is rounded half-up. If that rounded amount would be worth more base than the change owed, the function drops one minor unit of the tender currency and puts the remainder on a base outflow. A $4 bill against a Q10 balance at rate 3 becomes in $4 (Q12), out $0.66 (Q1.98), out Q0.02.

Each leg is { direction, amount, baseAmount, currency } where currency is tender or base. direction is in or out.

Who writes the legs​

usePaymentMethodsSelector (apps/frontend-pwa/src/components/common/usePaymentMethodsSelector.ts) is the writer. PaymentMethodsSelector is embedded on sale checkout, purchase checkout, accounts-receivable receipts, and contractor-assignment checkout.

Cash over-tender is allowed only when the payment method name is cash (isCashPaymentMethod: trim, lower case, exact match). cashAndManual is not cash. Every other method is capped at exactTenderAmount of the remaining base; a larger amount is rejected before a leg is stored.

The "Give change in" control appears when the selected method is cash, the selected currency is not the business default, and the rate is not 1. The default choice is base. A rate of 1 forces base even if the control was previously set to tender.

One physical bill becomes one or more payment_detail rows that share a tenderGroupId:

  • The inflow keeps the selected currency and the selected rate. A reference, when the method requires one, is stored on the inflow only.
  • A base-currency change leg uses the default currency and rate 1.
  • Removing any row of the group removes the whole group.

The table shows one row per group. Applied amount is netSignedBase of the group's legs. When change was stored entirely in the base currency and the bill was foreign, the table also shows what that change would have been in the bill currency. It does that by re-running settleCashTender with changeCurrency: "tender". A dollar payout is listed once.

Sale submit​

SalesService submit compares payments with the header total (totalBaseAmount, otherwise totalAmount). That header is already net of a cart discount. Submit does not subtract the discount again.

netSignedBase sums inflows and subtracts outflows. A missing direction counts as an inflow. The tolerance is 0.01 in base currency:

  • A header of 0.01 or less skips the payment requirement.
  • Shortfall above 0.01 rejects with Payments do not cover the sale total.
  • Excess below -0.01 rejects with Payments exceed the sale total.

The PWA uses the same net and the same tolerance in salePaymentValidation.ts, against the cart base after the cart discount. A sale whose stored header and whose client-side discounted total disagree will pass one side and fail the other.

Session totals​

SessionTotalsService parses each payment_detail item and normalizes direction: only the string "out" is an outflow. Anything else, including a missing direction, is an inflow.

Cash-group totals and per-method totals add the signed base (and, per method, the signed selected-currency amount). A $100 bill and its Q565 change contribute Q135 to the cash group, not Q1,265.

extractCashMagnitude (credit notes, debit notes, cancellations) signs the cash legs first, then returns the absolute net. The document type decides whether that net is added or subtracted from the drawer. The proration denominator is the absolute signed net of every tender, cash and otherwise.

A method counts as cash for the drawer when its group name, lowercased, is cash or contains cash. That is wider than the checkout name check. A non-cash name in a cash group still signs in session totals; checkout will not have split it into change legs.

Receipts​

mapSalePaymentDetailToReceiptPayments lists inflow rows only. mapSalePaymentDetailToReceiptChange sums outflow baseAmount (falling back to amount), rounds to cents, and returns null when the sum is 0. Cambio on a thermal sale receipt is that base-currency figure, so a dollar change leg and a Q0.03 residual print as one quetzal total.