Primitive spec
Bayta
The customer-voice primitive. Bayta owns what customers say back to the business and the work that answering them generates — internal support cases, an automated post-visit feedback funnel, consent-gated public testimonials, and processor disputes handled as cases. A 2★ rating is an incipient case; a chargeback is a case with a legal deadline.
What it owns
Bayta owns the workflow around customer conflict and customer praise. A complaint at the front desk, a refund request, a chargeback, and a low post-visit rating are all the same shape of work — something a staff member must take to resolution — so they all become a SupportCase in one queue, linked to the booking, order, invoice, or dispute they are about.
The primitives of record keep owning their facts. Payments owns the dispute wire object and evidence machinery; Mallow owns the refund money; Speaker owns every outbound send (consent-gated, fail-closed); Terminus owns the person and their consent record. Bayta composes them into "what does this customer need from me today?"
Concepts
- SupportCase
- A unit of customer-conflict work. Typed by
kind(general,complaint,refund_request,dispute,feedback_followup) andsource(manual,dispute,feedback,system), with a lifecycle ofopen → in_progress → waiting → resolved → closed(reopen allowed), apriority, a nullablepersonId(anonymous walk-in complaints are fine), an assignee, an optionaldueAtdeadline, and last-activity queue sorting. - CaseLink & CaseNote
- A
CaseLinkties a case to anything by opaqueentityType/entityId—seldon_booking,hober_order,mallow_invoice,payments_dispute— so "cases about this booking" is one lookup from either side.CaseNoteis the append-only timeline: human notes, auto-recorded status changes, and system annotations, with Media attachments. Corrections are new notes; nothing is deleted. - Feedback routing engine
- Three entities run the funnel.
FeedbackSettings(per tenant) holds the triggers, send delay (default 2h), per-person throttle (default 30 days), the happy/unhappyratingThreshold(default 4), the public review URL keyed by sub-tenant, andautoCaseOnLowRating.FeedbackRequestis the ask — one per visit ever, with a tokenized link (hashed at rest) that expires after 14 days.FeedbackResponseis the answer — a 1–5 rating plus comment, theroutingoutcome recorded (public_redirect/private/none), the auto-openedcaseIdif the rating was low, and the customer's explicitconsentPublicDisplaycheckbox. Exactly one response per request. - Testimonial
- Minted only from consented responses — never without
consentPublicDisplay. Enters a moderation queue (pending_review → approved | rejected) with an operator-editabledisplayName(defaults to first name + last initial) and a trimmableexcerpt. Only approved rows are publicly readable, through a sub-tenant-scoped projection of stars, excerpt, first name, and a coarse date — rendered on booking sites by thetestimonialscatalog component. - Disputes are cases
DisputeCaseSubscriberlistens on the Payments outbox:dispute.openedidempotently opens aSupportCase(kind=dispute, priority=high)linked to the dispute, setsdueAtto the evidence deadline, and schedules staff nudges at −72h and −24h via Speaker. Eachdispute.<status>update appends a system note; a win auto-resolves the case, a loss flags it and leaves it open for reconciliation. The case detail's evidence panel prefills from the linked transaction, invoice, order, person, and Media uploads — a signed waiver is exactly the evidence a no-show chargeback wants. Evidence is assembled and recorded in Foundation; sending it to the processor over the wire is on the Payments roadmap, and the console says so.- Triggers over the outbox
- The funnel starts itself.
FeedbackTriggerSubscriberwatches Hoberorder.closedand Seldonbooking.completedon the transactional outbox, applies throttle and one-per-visit checks, and schedules afeedback.request_dueemission at+delaySec.FeedbackSendSubscriberthen dispatches through Speaker — consent-gated, fail-closed; a consent skip marks the requestskippedwith the audit preserved. The send link carries the token in the URL fragment, so it never leaks via Referer.
API surface
Endpoints are versioned under /bayta/v1/. The public fill-out and testimonial routes are served through the render proxy with no login; everything else is operator-facing.
Quick reference
| Method | Path | Purpose |
|---|---|---|
| POST / GET | /bayta/v1/cases | Open a case; list the queue with status/kind/assignee filters. |
| GET | /bayta/v1/cases/{id} | Case detail with timeline and links. |
| POST | /bayta/v1/cases/{id}/notes · /assign · /status · /links | Work the case: append a note, assign, move status, link an entity. |
| GET / PUT | /bayta/v1/cases/{id}/dispute-evidence (+ POST …/submit) | Prefill, draft, and record the dispute evidence bundle. |
| GET / PUT | /bayta/v1/feedback/settings | The funnel config: triggers, delay, throttle, threshold, review URL. |
| GET / POST | /bayta/v1/public/feedback/{token} | Public tokenized fill-out — fetch context, submit the rating (uniform 404, idempotent replay). |
| GET / POST | /bayta/v1/testimonials, …/{id}/approve, …/{id}/reject | The moderation queue. |
| GET | /bayta/v1/public/testimonials | Approved-only public projection for the testimonials site component. |
Example: a 2★ rating becomes a case
A salon visit closes in Hober; two hours later the customer gets a feedback link. A happy answer routes out to the public review page; an unhappy one stays private and opens work.
POST /bayta/v1/public/feedback/{token}
{
"rating": 5,
"comment": "Best color I've had in years.",
"consentPublicDisplay": true
}
→ 200 OK
{
"routing": "public_redirect",
"redirectUrl": "https://g.page/r/…/review",
"thankYou": "Thanks for your feedback!"
}
POST /bayta/v1/public/feedback/{token}
{
"rating": 2,
"comment": "Waited 40 minutes past my appointment time.",
"consentPublicDisplay": false
}
→ 200 OK
{
"routing": "private",
"redirectUrl": null,
"thankYou": "Thanks for your feedback!"
}
GET /bayta/v1/cases?status=open&kind=feedback_followup
→ 200 OK
{ "items": [ {
"id": "bcas_01JZW8…",
"subject": "Low rating follow-up (2★)",
"kind": "feedback_followup",
"status": "open",
"priority": "normal",
"source": "feedback",
"personId": "per_01JX2K…"
} ] }
The auto-opened case arrives already linked to the feedback response and the visit it was about; the 5★ consented comment lands in the testimonial moderation queue.
How it fits with the rest
flowchart TB
Hb[Hober order.closed] --> OB[(Audit outbox)]
Se[Seldon booking.completed] --> OB
Pay[Payments dispute.opened] --> OB
OB --> By(Bayta)
By -- consent-gated ask --> Sp[Speaker]
Sp --> Cust[Customer]
Cust -. tokenized fill-out .-> By
By --> Dz[Demerzel cases queue]
By -- approved testimonials --> Site[Booking-site component]
Everything inbound rides the same transactional outbox: Hober order closes and Seldon booking completions trigger feedback asks, and Payments dispute events open and update dispute cases. Speaker sends every ask and every deadline nudge with the Terminus consent gate enforced fail-closed at dispatch time. Operators work the queue in the Demerzel console at /dashboard/cases, and approved testimonials flow back out to booking sites as a render component — the loop from a gym class attended to a review published, with the unhappy branch landing as work instead of a public one-star.