When One Product Needs Two Prices: A Pricing Model Bug in PichaKonnect
How a single missing field locked out an entire category of photographers from a core feature.

Three ways to charge for a photo#
This application am building, PichaKonnect, lets photographers sell their work through three distinct billing contracts:
Hourly: a rate per hour, billed against session length. A two-hour shoot at KSh 5,000/hr costs KSh 10,000. The meter runs.
Fixed: a flat session fee, regardless of how long you shoot. KSh 15,000 is KSh 15,000, whether you're done in 45 minutes or wrapping at sunset.
Per-photo: a price per delivered image. The client pays for what they keep, not for time on set.
The three contracts behave differently. A per-photo photographer shooting a wedding might deliver 400 images at KSh 500 each. An hourly shooter at the same rate costs KSh 2,000 for four hours. Treating those numbers as comparable at browse time produces the confusion that makes bookings feel like a bait-and-switch.
The feature that locked out a third of photographers#
PichaKonnect has a go-live feature: a live map where photographers broadcast their location and clients request a session on the spot. Sessions are time-boxed, spontaneous, and need a time-based rate to compute a cost before either party commits.
The session-request handler had a guard clause from early development:
if (photographer.pricingType === 'per_photo') {
throw appError(400, 'This photographer charges per photo. Use the standard booking flow…');
}
That guard clause is a product decision: per-photo photographers cannot go live. No ticket captured this gap. The 400 message sounded plausible enough that clients likely assumed a profile problem, and photographers who tried enabling go-live probably gave up. Nobody filed a bug because the error appeared to explain itself.
The tempting wrong fix#
The first instinct was to require all three pricing fields at onboarding, hourly, fixed, and per-photo, all mandatory. Then go-live always has a number to work with.
It breaks things. A wedding photographer builds their business around fixed packages. Ask them to invent an hourly rate and they'll type something to get past the form. That number doesn't reflect how they work or what they'd charge. The team collects a field that lies, and those figures poison price comparisons and the go-live pricing engine. The display formatting work (one formatter, one currency casing, no bare numbers) unravels the moment photographers submit prices they don't stand behind.
A second problem: a photographer with three prices on their public listing invites clients to arbitrage. "Is 5,000/hr or 15,000 flat cheaper for a two-hour shoot?" should never appear on a browse screen. It moves cognitive load to the client and makes the listing feel like a puzzle.
Forcing uniform data collection to solve a problem that affects a subset of users repeats the display-layer mistake. The team ends up with more fields and less honest information.
The actual problem#
Go-live and the standard marketplace were two products sharing one pricing model. The guard clause was the only thing holding that seam together.
On-demand sessions have a different character: spontaneous, often carrying a premium for availability, always time-bounded. Modelling on-demand pricing as its own field reflects that difference rather than papering over it.
We added liveRate and liveRateType to the photographer profile and wrote a resolver that the go-live handler, the session-request pricing engine, and the nearby-photographers endpoint all call:
function resolveLiveRate(p) {
if (p.liveRate != null && p.liveRateType) return { rate: p.liveRate, type: p.liveRateType };
if (p.pricingType !== 'per_photo' && p.priceMin != null) return { rate: p.priceMin, type: p.pricingType };
return null;
}
The fallback logic handles the practical cases without a data migration. Hourly and fixed photographers need no changes; their existing rate falls through as the live rate until they set a dedicated one. Per-photo photographers can now enable go-live by setting one field. Anyone charging a premium for on-demand availability can set a separate live rate without touching their marketplace listing.
The guard clause as a design signal#
The guard clause if (per_photo) throw carried an unexamined assumption: that on-demand sessions and marketplace bookings price the same way.
Guard clauses that reject valid cases usually mean the model doesn't fit the problem. The code ran fine. The team had modelled on-demand sessions as a variant of marketplace bookings when they were a separate product. A per-photo photographer is a valid photographer. The missing piece was a field for a different product context.
A feature that excludes valid users rarely needs those users to change. Modelling the different thing as different, rather than demanding conformity, produces data the system can trust.
The result#
The go-live feature works for photographers across all three billing models. The marketplace listing stays unambiguous. Per-photo photographers who want on-demand sessions set one field; those who don't, skip it. The team changed no existing profiles.
The original guard clause was three lines. Fixing what it concealed took two fields and one resolver, reasonable for a gap that had probably existed since launch.
1 comment