Set the floor once. Groundcut enforces it on every quote
A tiny drawing should never produce a tiny quote. Groundcut’s minimum price is a per-service field that floors the area math, the surcharges, and the seasonal adjustments — and a target-hourly-rate flag catches the floor-priced jobs that still won’t clear your hour rate.
Below: six pricing-engine mechanics, what they enforce, and the column or response field that backs each one.
How the floor works in the pricing engine
Every step here is shipped code in packages/shared/src/pricing.ts and rides on the /api/quote response.
- 01
min_price is a per-service field, not a global setting
Each service in `/admin/services` has its own pricing rules, and `min_price` is one of them. Basic residential mowing might floor at $45; a hedge trim service might floor at $75; a one-off bush removal at $150. The booking page picks the service first, so the floor is always the one that matches what the customer is actually buying — not an averaged minimum that under-prices small premium jobs and over-prices large basic ones.
services.pricing_rules.min_price (per-service JSON field)
- 02
The floor wins over the area math, every time
The quote engine first computes a price from `price_per_quarter_acre × (mowable_area_sqft / 10,890)`. Then it applies surcharges, distance fees, and seasonal adjustments. Then `Math.max(price, rules.min_price)` runs — the floor cannot be undercut. A customer who drew a 200-sqft lawn doesn’t pay $0.83; they pay your floor. The floor bump is absorbed into the area line item so the breakdown still sums cleanly on the customer-facing quote.
packages/shared/src/pricing.ts: Math.max(price, rules.min_price) after all surcharges
- 03
max_price is the matching ceiling
Pricing rules also have a `max_price` ceiling for the same engine. A customer outlining an absurd area (someone tracing a city block) gets clamped to the cap — the booking doesn’t produce a $4,000 instant quote that locks you into a job you can’t deliver. Big jobs route to the in-person quote flow instead, where you visit, fill in line items, and send a real quote.
services.pricing_rules.max_price; Math.min(price, rules.max_price) at the end of calculateQuote
- 04
target_hourly_rate flags quotes that clear the floor but still lose money
A quote can clear your $50 minimum and still be unprofitable — a 90-minute job that nets $48.55 after Stripe is $32/hr, not your $50 target. The pricing engine divides `net_after_fees` by `estimated_time_mins / 60` and returns `below_target_rate: true` on the `/api/quote` response. The flag surfaces on the booking page (configurable), the admin job detail, and the operator card. The minimum protects against $1 quotes; the target rate protects against floor-priced jobs that still won’t clear.
services.pricing_rules.target_hourly_rate → below_target_rate field on /api/quote response
- 05
The quote response surfaces both numbers, every time
The `/api/quote` response returns `quoted_price`, `net_after_fees`, `line_items`, `below_target_rate`, `estimated_time_mins`, `equipment_cost`, `card_fee_included`, `mowable_area_sqft`, `mowable_area_acres`, `service_name`, `distance_miles`, and `seasonal_label`. The booking page renders the line items so the customer sees how the quote was built (Area $X, Floor bump rolled in, Distance fee $Y, Surcharge $Z). No black-box pricing.
POST /api/quote returns the full QuoteResult shape on every request
- 06
Card-fee passthrough is opt-in and respects the floor
If you turn on `pass_card_fee_to_customer` at the service level, the engine adds Stripe’s 2.9% + 30¢ on top of the quoted price so you net the full quoted amount. The floor still applies to the pre-passthrough price — the passthrough is layered on after, never used to dodge the floor. `card_fee_included: true` rides on the quote response so the booking page can display "$X (card fee included)" cleanly.
pass_card_fee_to_customer flag; STRIPE_PCT + STRIPE_FLAT applied after min_price floor in pricing.ts
“What about big customers who want a discount?”
The discount conversation comes up in every lawn business. A commercial account wants a per-job rate $10 below your residential floor. A long-time customer wants a “loyalty rate.” Your neighbor wants the buddy price. The minimum-price floor sits in the booking-page flow — it doesn’t have to apply to every customer.
Recurring price overrides take precedence. Set a recurring schedule with a lower price for that customer and the booking-page floor never enters the picture — the recurring schedule carries its own price snapshot through every generated job. The floor is for new-customer instant quotes, not for established relationships.
customer_price_override on a one-off job. From `/admin/jobs` you can create a job with a custom price; the engine clamps it at your floor so a typo can’t zero the job, but otherwise the override wins. The quote-line breakdown shows “Custom rate” and the booking-page math is bypassed.
On-site adjustments are signed and auditable. The crew can adjust the price on arrival if the customer pushes back; increases require a signature, decreases don’t. Every adjustment writes to the audit log with the actor, the before/after, and the reason.
Where each field lives
/admin/services— the service card has Min price, Max price, Price-per-quarter-acre, Target hourly rate, and the surcharge fields. Edit one service to test, save, and refresh the booking page to see the new math.services.pricing_rules— the underlying JSON column. `min_price` and `max_price` are required; `target_hourly_rate` is optional (`below_target_rate` returns `false` when it’s null).POST /api/quote— the runtime endpoint. Returns the full quote shape including the floor-applied price, the line-item breakdown, and the target-rate flag.
Minimum-job-price FAQs
What about a long-time customer who wants $10 off the minimum?+
How does the floor interact with a customer who draws a tiny lawn on purpose?+
Can I have a different floor for weekly vs one-time customers?+
Where does the floor live in the database vs the UI?+
Does the minimum apply to in-person quotes too?+
What if my market is so price-sensitive that the floor loses me bookings?+
How is this different from just setting a high price-per-quarter-acre?+
Stop quoting below your floor
14-day free trial. Set min_price, max_price, and target_hourly_rate on each service; run a few real bookings; see the engine clamp small lawns to your floor and flag the rest.
14-day free trial · No card required · Cancel any time