Deposit and pay-in-full choice
At the cart stage a customer can choose to pay a deposit now or pay in full. Choosing deposit reduces the amount charged today to the sum of the per-holiday deposits, and lands the order in a custom deposit-paid status with the balance due 56 days before the earliest arrival.
This is new in [[woocommerce-holiday-checkout]] — the legacy flow had no such option. The balance is later collected through [[booking-portal-balance-payments]].
Why it exists
Section titled “Why it exists”Deposits were available over the phone but not online, so customers who did not want to commit the full amount abandoned the web checkout. The complication is that a basket can hold up to three holidays with different deposit amounts, different arrival dates, and promotions that may forbid one payment mode or the other — so “deposit” is a single order-level decision computed from several per-line facts.
Entry points
Section titled “Entry points”All paths relative to wp-content/themes/lovat-parks/inc/lovat-checkout/.
| Path | What it does |
|---|---|
blocks/deposit-choice/render.php | The radio UI. Registered as lovat-parks/deposit-choice in lovat-checkout.php:95. Renders on the extras page, above the totals box. |
blocks/deposit-choice/deposit-choice.js | Writes the lovat_payment_choice cookie on change and on init. |
src/Order/DepositCalculator.php:22 | The whole decision: totals, eligibility, promo conflicts, balance date. Pure function, unit tested. |
src/Order/DepositCartFee.php:22 | Applies a negative cart fee at checkout so the displayed total is the deposit. |
src/Checkout/PaymentSuccessHandler.php:84 | capturePaymentChoice() — reads the cookie onto the order and recomputes the total. |
src/Order/DepositPaidStatus.php:8 | Registers the wc-deposit-paid order status. |
src/Order/DepositChoiceAutoRender.php | Vestigial — register() is now empty; the block moved to the extras page. |
How it works
Section titled “How it works”flowchart TD
A[Extras page: deposit-choice block] -->|radio change| B[lovat_payment_choice cookie]
B --> C[DepositCartFee on woocommerce_cart_calculate_fees]
C --> D[DepositCalculator::compute]
D -->|eligible| E[negative fee 'Balance due later date']
E --> F[Checkout shows deposit as the total]
F --> G[capturePaymentChoice on order create]
G --> H[_lovat_payment_choice meta + set_total]
H --> I[Payment]
I --> J[overrideStatusForDeposit: completed → deposit-paid]
J --> K[PaymentSuccessHandler charges lineAmount per line to EP]
The calculation
Section titled “The calculation”DepositCalculator::compute() takes every cart line as {arrival_date, deposit_amount, line_total, promo_code} plus the promo config map, and returns deposit_total, full_total, has_deposit_option, hide_pay_in_full, hide_pay_deposit and balance_due_date.
Rules, in the order they matter:
- A line has a real deposit only if
0 < deposit_amount < line_total. Otherwise the line contributes its full total to the deposit sum and setshasNoDepositLine. - The deposit option is offered only when: at least one real deposit exists, no line lacks one, the saving is at least £5, and no promo hides it. The £5 floor exists because EP sometimes returns a deposit that is 95%+ of the total on short-notice bookings, producing a saving of pence (
DepositCalculator.php:59-68). - Promo conflicts are order-wide and sticky. If any line’s promo sets
hide_pay_deposit, deposit is disabled for the whole order. Same forhide_pay_in_full. The flags OR together across lines and never un-set. - Balance due date is arrival minus 56 days, taking the earliest across all lines. A basket with an August and a December holiday must pay both balances on the August schedule. Intentional.
Display vs charge
Section titled “Display vs charge”Two independent mechanisms keep the number right:
DepositCartFeeadds a negative fee labelledBalance due later (12 August 2026)so the WooCommerce review-order table already totals to the deposit before the customer pays.capturePaymentChoice()recomputes the deposit from order item meta at order creation and callsset_total()if it differs by more than 0.1p. This is the belt-and-braces fallback and the authoritative figure.
Note the two use different fallbacks for a line with no deposit: the calculator adds the line total and marks the whole order ineligible; capturePaymentChoice adds line_subtotal + subtotal_tax for that line and continues. In practice the eligibility gate means an ineligible basket never gets here, but the asymmetry is real (PaymentSuccessHandler.php:94-105).
Status and EP
Section titled “Status and EP”overrideStatusForDeposit() hooks woocommerce_payment_complete_order_status and rewrites completed → deposit-paid when the choice was deposit. DepositPaidStatus registers the post status and inserts “Deposit paid” into the admin dropdown immediately after “Processing”.
Per line, PaymentSuccessHandler::lineAmount() decides what is sent to EP’s MakePayment: the line’s _lovat_deposit_amount when the choice is deposit and that amount is greater than zero, otherwise the full line subtotal. So a mixed basket bills deposit for eligible lines and full for the rest, in one order.
The frozen deposit
Section titled “The frozen deposit”lovat_deposit_amount is captured once, from GetBooking at the moment the holiday is added to the cart, and then never updated. Both ExtrasHandler::update() and PromoHandler::apply() explicitly re-write the original value back after their EP calls, because EP recalculates deposit_amount to include extras — which would inflate the deposit the customer pays today. This is called out in comments in both files; it is a decision, not an oversight.
Configuration
Section titled “Configuration”| Setting | Where | Purpose |
|---|---|---|
checkout_promo_code_config | ACF options repeater | Rows of code, hide_pay_in_full, hide_pay_deposit. Read in DepositCartFee.php:50. |
lovat_payment_choice | Cookie | deposit or full. Anything else is treated as full. |
_lovat_payment_choice | Order meta | The persisted choice. |
_lovat_deposit_amount | Cart item meta → order item meta | Per-line frozen deposit. |
wc-deposit-paid | Order status key | Constant DepositPaidStatus::STATUS_KEY. |
Deposit amounts themselves come from EP; there is no site-side deposit rule or percentage.
Invariants and gotchas
Section titled “Invariants and gotchas”- The choice travels in a cookie, not a form field. It is set on the extras page and read at checkout and at order creation. Cookie-blocking, a cross-domain checkout, or an aggressive privacy setting silently degrades to “full” — which overcharges relative to what the customer selected. Any change to the checkout host or cookie policy must be tested against this.
wc-deposit-paidis notcompleted. Anything filtering orders by status — reporting, exports, the DotDigital sync, fulfilment — must include it explicitly or deposit orders vanish.- The 56-day balance window is hard-coded at
DepositCalculator.php:73(modify('-56 days')). It is not configurable. - The £5 minimum saving is hard-coded at
DepositCalculator.php:64. - Promo conflict flags OR across lines and are order-wide. One restricted promo on one line changes the payment options for the entire basket.
- Do not re-sync
lovat_deposit_amountfrom EP after extras or promos. See above. DepositChoiceAutoRender::register()is intentionally empty. The class is kept for the documented history of where the block used to render and what would be needed to move it back into the Block checkout.
Changing it safely
Section titled “Changing it safely”- All new deposit logic belongs in
DepositCalculator::compute(). It takes plain arrays and returns plain arrays, has no WordPress dependencies, and is the one place unit tests can reach. Resist adding rules intoDepositCartFeeorPaymentSuccessHandler. - If you change the shape of a cart line, update both callers:
DepositCartFee::applyDepositDiscount()(builds lines from the cart) andcapturePaymentChoice()(builds them from order items). - A new promo restriction needs: an ACF sub-field on
checkout_promo_code_config, mapping inDepositCartFee.php:56, and handling incompute(). - Run
cd wp-content/themes/lovat-parks && vendor/bin/phpunit --filter Deposit. - Manual check, per the QA brief in the repo README: all-eligible basket → deposit reduces the total and the order lands
deposit-paid; one ineligible line → that line bills full and the rest bill deposit; ahide_pay_depositpromo on any line → deposit disabled entirely; balance date shows the earliest across lines. - Deliberately not abstracted: the negative cart fee. It looks like a hack next to
set_total(), but it is what makes the review-order table, the gateway’s amount, and Apple/Google Pay sheets all agree before the order exists. Removing it makes the displayed total wrong until after payment.
tests/unit/Order/DepositCalculatorTest.php— eligibility, mixed baskets, the £5 floor, promo conflicts, earliest balance date.tests/unit/Order/DepositPaidStatusTest.php— status registration and dropdown ordering.tests/unit/Checkout/PaymentSuccessHandlerTest.php— coverslineAmount()and the split helpers.
Not covered: DepositCartFee (needs a live WC_Cart), the cookie round-trip, and the block’s JS. All verified by hand.