Every offer has three price fields, all computed server-side:
| Field | Formula | Description |
|---|---|---|
offerAmountMinorUnit | Input from buyer | What the talent receives |
zoolyFeeMinorUnit | Math.round(offerAmountMinorUnit × 0.20) | 20% Zooly platform fee |
totalBrandPriceMinorUnit | offerAmountMinorUnit + zoolyFeeMinorUnit | What the brand pays |
The client sends only offerAmountMinorUnit. The server computes and stores the other two values. Client-sent fee fields are ignored.
createOffer() in packages/db/src/access/offers.ts computes zoolyFeeMinorUnit and totalBrandPriceMinorUnit from the provided offerAmountMinorUnit before inserting the row.
updateOfferStatus() in packages/db/src/access/offers.ts recomputes both fee fields whenever a countered amount is applied to the canonical offer_amount_minor_unit column. A counter through POST /api/offers/respond (→ COUNTERED) only stores the proposal, so fees are recomputed when the other side accepts (COUNTERED → ACCEPTED).
Conversation Offer Counter reaches the same stores-only transition through the chat: the click posts a chat line to the actor's agent, Z asks last-look, and only after the answer the agent records the counter. The written amount (and fees) stay until the other side accepts — or until Offer-Update rewrites the row. See Offer approval and brand payment.
updateOfferDraft() likewise recomputes fees when a draft edit changes the amount.
POST /api/offers/submit only accepts offerAmountMinorUnit (positive integer). The submitOfferBodySchema contract has no fee fields at all, so any client-sent zoolyFeeMinorUnit / totalBrandPriceMinorUnit are dropped at parse time before reaching createOffer.
When a payment intent is created for an offer, getOfferProduct() in packages/srv-stripe-payment/src/getProductByPayForId.ts requires status ACCEPTED and charges the due phase, not necessarily the full deal:
offerDueCharge (example: 50% on approval)zoolyFeeMinorUnit matches Math.round(offerAmountMinorUnit × 0.20)totalBrandPriceMinorUnit matches offerAmountMinorUnit + zoolyFeeMinorUnitThis prevents price drift if the fee percentage changes or if stored values are manually edited. A later phase is not due until the earlier one is paid.
The computePriceData(amountMinorUnit, platformFeeMinorUnit) function in packages/srv-stripe-payment/src/getProductByPayForId.ts produces the full ProductPriceData breakdown used by Stripe:
calculateStripeFees(amount, currency) from @zooly/util — the fixed fee component is 0 for zero-decimal currencies)This function is exported from @zooly/srv-stripe-payment for use in escrow release and other payment flows.
| File | Role |
|---|---|
packages/db/src/access/offers.ts | Fee computation in createOffer and updateOfferStatus |
apps/zooly-app/app/api/offers/submit/route.ts | Input validation, strips client fee fields |
packages/srv-stripe-payment/src/getProductByPayForId.ts | Payment-time consistency assertion, computePriceData |