Pet services scheduling and billing

FetchFlow

Fill schedules. Fetch payments faster.

FetchFlow is a mobile, SMS-first platform for independent groomers, walkers, and trainers who run schedules by text. It unifies booking, two-way reminders, and payments in one dashboard, auto-collects vaccine records and preferences via Pet Profile Smart Cards with each pet’s name and photo, and applies packages and no-show fees—cutting no-shows 38% and speeding checkout 22% while improving cash flow.

Subscribe to get amazing product ideas like this one delivered daily to your inbox!

FetchFlow

Product Details

Explore this AI-generated product idea in detail. Each aspect has been thoughtfully created to inspire your next venture.

Vision & Mission

Vision
Transform independent groomers, walkers, and trainers into thriving, effortless businesses that delight clients and grow sustainably through trusted, humane automation.
Long Term Goal
By 2028, power 25,000 independent pet-care businesses to cut no-shows 40%, process $1B in on-time payments annually, and reclaim 5 million hours for higher-quality care and growth.
Impact
For independent groomers, walkers, and trainers, FetchFlow cuts no-shows by 38%, speeds checkout 22%, and lifts repeat bookings 15% in two months. Owners reclaim 4+ hours weekly as SMS responses arrive 5x faster than email, boosting schedule utilization and accelerating cash flow.

Problem & Solution

Problem Statement
Independent groomers, walkers, and trainers running schedules via text juggle bookings, vaccine records, and payments across paper calendars, DMs, and clunky salon software, causing no-shows and unpaid invoices. Generic tools ignore pet-specific workflows and SMS-first client behavior.
Solution Overview
FetchFlow unifies scheduling, reminders, and payments in a mobile-first, SMS-first dashboard, replacing paper calendars and scattered DMs. Two-way SMS Pet Profile Smart Cards auto-collect vaccine records and preferences, and an auto-fill checkout applies packages and no-show fees instantly, reducing no-shows and unpaid invoices.

Details & Audience

Description
FetchFlow is a lightweight platform that unifies scheduling, reminders, and billing. For independent groomers, walkers, and trainers who run on text, not spreadsheets. It slashes no-shows, speeds checkout, and improves cash flow by centralizing booking, two-way texts, and payments in one mobile-first dashboard. Pet Profile Smart Cards auto-collect vaccine records and preferences via SMS, then personalize reminders with the pet's name and photo.
Target Audience
Independent pet-care groomers, walkers, trainers (25-55) battling no-shows, late payments, running schedules entirely via SMS.
Inspiration
Friday at my sister’s grooming shop, a no-show after two voicemails left an empty slot, and a walk-in left because the dog’s vaccine record wasn’t on hand. Sticky notes carpeted the register. Yet every client replied to texts in seconds. That night I hacked a Google Form + SMS flow to collect records and preferences. One week later, no-shows dipped, and reminders with each pet’s photo made owners smile—FetchFlow was born.

User Personas

Detailed profiles of the target users who would benefit most from this product.

D

DM-Driven Diego

- 28, male-identifying, urban Chicago - Independent dog walker/trainer, 3 years self-employed - Income $55–70k; 70% clients from social - iPhone user; unlimited data, always on-the-go

Background

Started posting training tips during lockdown; demand surged from DMs. Lost bookings to forgotten times and unpaid sessions until adopting SMS links and packages.

Needs & Pain Points

Needs

1. Convert DMs to confirmed SMS bookings fast 2. Auto-collect pet details without extra typing 3. One-tap payment after each session

Pain Points

1. Lost prospects stuck in DMs 2. No-shows from vague social agreements 3. Chasing payments across apps

Psychographics

- Hustle-minded, measures impact by repeat bookings - Values social proof over formal credentials - Loves quick wins, hates slow back-and-forth - Brand-conscious, aims for polished client experience

Channels

1. SMS primary 2. Instagram DMs prospecting 3. TikTok comments discovery 4. WhatsApp chats select-clients 5. Google Business Messages inquiries

R

Rescue-Partner Priya

- 35, female, suburban Austin - Runs micro-studio; volunteers with two rescues - Income $60–80k; seasonal spikes around drives - Android user; texts dominate client communications

Background

Former shelter coordinator turned independent pro to speed rehoming. Learned hard lessons from missing vaccine proofs and last-minute foster changes.

Needs & Pain Points

Needs

1. Bulk-text scheduling for foster cohorts 2. Auto-capture vaccines and waivers pre-appointment 3. Apply rescue-specific discounts automatically

Pain Points

1. Scrambling for missing records day-of 2. Volunteer no-shows derail schedules 3. Manual discount tracking causes payout errors

Psychographics

- Mission-first, prioritizes animal welfare impact - Craves order amid chaotic rescue logistics - Community-centric, values transparency and trust - Time-poor, favors automation over customization

Channels

1. SMS cohorts 2. Facebook Groups rescue-updates 3. WhatsApp volunteer-chats 4. Email newsletters coordination 5. Google Calendar invites

S

Senior-Care Sam

- 42, male, coastal Seattle - Certified fear-free handler; solo practice - Income $75–90k; high repeat clientele - Serves weekdays, midday and evenings

Background

Cared for his own arthritic lab; built a niche from vet referrals. Burned by a missed medication once, he now double-checks everything.

Needs & Pain Points

Needs

1. Medication notes surfaced before each visit 2. Reminder cadences tailored per household 3. Easy photo updates within SMS thread

Pain Points

1. Critical care notes buried in texts 2. Owners anxious without timely updates 3. Conflicts from rigid, overlapping schedules

Psychographics

- Precision-driven, obsessed with consistency and care - Empathy-led communicator, over-communicates reassurance constantly - Risk-averse; favors documented protocols, checklists - Loyal; values long-term client relationships

Channels

1. SMS daily 2. iMessage photos 3. Google Maps navigation 4. Yelp profile 5. Email summaries

R

Rural-Route Rosa

- 33, female, rural Nebraska counties - 40-mile service radius; 12–15 stops/day - Income $45–60k; mileage-heavy expenses - Android with offline maps; limited LTE coverage

Background

Former USPS rural carrier turned pet pro; timing windows are second nature. Missed signals once cost two bookings and a tank of gas.

Needs & Pain Points

Needs

1. Windowed arrival confirmations via SMS 2. Offline-friendly reminders and receipts 3. Route batching to minimize backtracking

Pain Points

1. Signal dead zones break app flows 2. Last-minute cancels waste long drives 3. Manual routing increases fuel costs

Psychographics

- Pragmatic, efficiency-obsessed road warrior and planner - Values reliability over bells and whistles - Frugal; scrutinizes every mile and minute - Independent, prefers low-touch client contact

Channels

1. SMS confirmations 2. Google Maps offline 3. Facebook Marketplace listings 4. Nextdoor recommendations 5. Phone calls voicemails

P

Pop-Up Parker

- 29, nonbinary, metro Phoenix - Weekend events; weekdays mobile microgrooming - Income $50–65k; event-driven spikes - Uses Square reader; heavy SMS coordination

Background

Started with a farmers’ market booth; grew through store partnerships. Chaos at peak hours taught them to demand deposits and clear time slots.

Needs & Pain Points

Needs

1. QR booking links for on-site signups 2. Auto-deposits per slot to reduce no-shows 3. One-tap checkout with no receipts

Pain Points

1. Walk-up crowd overwhelms manual scheduling 2. No-shows waste limited event hours 3. Line bottlenecks from slow payments

Psychographics

- Event-minded; thrives in bustling environments - Short-attention; favors simple tap-and-go workflows - Reputation-driven; wants five-star throughput and reviews - Opportunistic; capitalizes on foot traffic

Channels

1. SMS blasts 2. Instagram Stories announcements 3. Facebook Events RSVPs 4. Google Maps updates 5. Eventbrite pages

C

Clinic-Collab Claire

- 38, female, mid-sized Ohio town - Part-time clinic, part-time mobile - Income $65–85k; shared clientele with clinic - Works Tue/Thu in-clinic, Sat mobile

Background

Former clinic vet tech who launched her grooming brand. Learned to navigate double-booking risks and compliance across separate systems.

Needs & Pain Points

Needs

1. Shared Smart Cards tied to clinic records 2. Split payments and deposit handling 3. Hold deposits on peak clinic days

Pain Points

1. Duplicate data entry across systems 2. Confusion over who collected payment 3. Vaccine mismatches delay services

Psychographics

- Collaborative; values professional alignment with vets - Compliance-minded, but avoids adding client friction - Systems-thinker; loves tidy reconciliation and clarity - Client-first; wants clear, transparent communications

Channels

1. SMS primary 2. Google Business Messages intake 3. Facebook Page messages 4. Clinic website referrals 5. Email confirmations

Product Features

Key capabilities that make this product valuable to its target users.

Smart Radius

Automatically targets and adjusts who receives an offer based on distance and live travel time to the canceled slot, filling gaps with nearby clients who can actually make it—reducing dead time and drive-backs.

Requirements

Live Travel Time Scoring
"As an independent groomer using FetchFlow, I want Smart Radius to score clients by live travel time so that I only reach out to people who can realistically make the canceled slot."
Description

Compute live, per-client travel time to the open slot’s service location using traffic-aware mapping APIs, ranking candidates by estimated arrival time within the slot’s start window. Incorporate client saved addresses, shop vs. mobile route context, service prep buffers, and historical average parking/load time by service type. Provide graceful degradation to distance-based heuristics if API limits or outages occur, with result caching and TTL to control cost and latency. Normalize outputs into an ETA score consumed by Smart Radius selection and outreach, and log inputs/outputs for auditability. Integrate with Client Profiles for address validation, with Calendar for slot boundaries, and with Privacy controls to ensure no personal data is persisted outside FetchFlow systems. Expected outcome: accurate, current rankings that target clients who can arrive on time, reducing dead time and unnecessary drive-backs.

Acceptance Criteria
Live Traffic ETA Ranking Within Start Window
Given a canceled slot with a Calendar-defined start window and service type And a set of candidate clients with validated addresses When live traffic ETAs are fetched from the mapping API And service prep buffer and historical parking/load time are applied Then the system computes arrival_time per candidate = travel_time + prep_buffer + parking_load And includes only candidates where arrival_time <= window_end And ranks included candidates in ascending order of arrival_time And produces an ETA score per candidate normalized to 0.00–1.00 (two decimal places), where 1.00 corresponds to the earliest arrival within the window and 0.00 corresponds to arrival at or after window_end And exposes the ranked candidates and scores to Smart Radius
Client Address Validation and Geocoding
Given a candidate client address stored in Client Profiles When the address is validated Then the address is geocoded to latitude/longitude with confidence >= 0.90 And the geocoded coordinates are stored within FetchFlow systems And if confidence < 0.90 or geocoding fails, the client is marked ineligible with reason "Invalid address" and excluded from scoring And revalidation is triggered automatically upon address update And no raw addresses are sent to logging or analytics systems
Shop vs Mobile Route Context
Given a slot modality of Shop When computing the route Then origin = candidate client primary address and destination = shop location And travel_time is computed using traffic conditions at the slot start time Given a slot modality of Mobile When computing the route Then origin = provider expected location at slot start time (last job end location or designated base if none) And destination = candidate client primary address And travel_time is computed using traffic conditions at the slot start time And in both modalities, prep_buffer and parking/load adjustments are applied identically
Parking/Load Adjustment by Service Type
Given a service type with at least 30 completed appointments in the last 60 days When computing ETA Then add parking_load_time equal to the 60-day rolling average for that service type, capped at 10 minutes Given a service type with fewer than 30 historical appointments When computing ETA Then add a default parking_load_time of 5 minutes configured for that service type And the applied parking_load_time value is recorded in the audit log per candidate
Graceful Degradation to Distance Heuristics
Given the mapping API returns a non-2xx response, times out after 3000 ms, or a rate-limit error is received When computing ETAs Then fall back to a distance-based heuristic using Haversine distance and mode-specific average speeds (Shop: 25 mph; Mobile: 22 mph), plus the same buffers And set degradation_flag = true and data_source = "heuristic" for each candidate And continue to rank and score candidates using the heuristic ETAs And emit a single operational alert per org per 5-minute window while degradation is active
Caching, TTL, and Cost/Latency Controls
Given two identical scoring requests with the same fingerprint (slot_id, modality, candidate_ids set, provider_expected_location, start_window) When the second request occurs within a TTL of 300 seconds Then results are returned from cache with cache_hit = true and no mapping API calls are made Given the TTL has expired or the fingerprint differs When a scoring request is made Then fresh mapping API calls are made and results are cached with cache_hit = false And the 95th percentile end-to-end scoring latency per request is <= 800 ms under normal load And the average mapping API cost per scored slot is <= $0.01 USD over a 7-day rolling window And the mapping API call rate does not exceed 600 calls per minute per org
Logging, Auditability, and Privacy Controls
Given a scoring run completes When audit logs are written Then for each candidate the following fields are logged: slot_id, timestamp, modality, origin_geohash(7), destination_geohash(7), travel_time_ms, prep_buffer_ms, parking_load_ms, arrival_time_ms, score, algorithm_version, data_source, cache_hit, candidate_id And no raw addresses, names, phone numbers, or photos are logged And logs are retained for 90 days and accessible to authorized admins via an audit endpoint filtered by slot_id And no personal data is persisted outside FetchFlow systems; third-party requests include only the minimum necessary routing coordinates and no-store headers And clients with privacy_opt_out = true are excluded from scoring and outreach
Eligibility & Radius Rules Engine
"As a business owner, I want to define clear eligibility and radius policies so that only qualified, nearby clients receive slot offers."
Description

Provide a configurable rules engine that determines which clients are eligible to receive an offer based on radius/time thresholds, service compatibility (e.g., dog size, temperament flags, trainer specialization), valid vaccine records from Pet Profile Smart Cards, client payment standing, package balance, quiet-hour windows, and appointment conflicts. Support default policies with per-service overrides, frequency caps per client, and fairness controls to avoid repeatedly pinging the same clients. Exclude clients with recent declines or no-show penalties if policy dictates. Emit an explainable decision for each client (include/exclude plus reason) and expose admin UI controls and presets. Integrate with Profiles, Payments, Packages, and Calendar to ensure compliance with business rules and operational realities. Expected outcome: a precise, policy-driven candidate pool that improves acceptance rates while protecting client experience.

Acceptance Criteria
Distance and Live Travel Time Eligibility
Given a canceled slot with start_at, location_id, and service_id S, and resolved Smart Radius thresholds threshold_max_eta_min M and threshold_max_distance_km D (from per-service override else default) And a client C with origin determined as last_known_location within 24h else home_address, both geocoded When the rules engine evaluates eligibility at evaluation_time Then it requests ETA_minutes and distance_km from the configured maps provider with traffic_model=best_guess at evaluation_time And if the provider errors or times out (>1500 ms), it falls back to haversine distance and converts to ETA using fallback_speed_kmh And C is included only if ETA_minutes <= M AND distance_km <= D And the decision record stores origin_used, ETA_minutes, distance_km, M, D, source=live|fallback, provider_request_id
Calendar Conflicts and Travel Window Checks
Given the canceled slot has an assigned staff member T, start_at, location, and service S with duration_min And a candidate client C with computed travel_time_to and travel_time_from based on the maps provider (or fallback) When checking schedule feasibility Then exclude C if C has any calendar event overlapping the window [evaluation_time, start_at + duration_min + travel_time_from] And exclude C if T has any conflicting event overlapping the window [evaluation_time, start_at + duration_min + travel_time_from] And include C only if both calendars are free across the computed windows And the decision record captures conflict_type=client|staff, conflicting_event_id, window_start, window_end when exclusion occurs
Service Compatibility and Trainer Constraints
Given service S defines allowed_pet_sizes, disallowed_temperament_flags, required_trainer_specializations, and equipment_requirements And client C’s active Pet Profile(s) provide dog_size, temperament_flags, and any special handling notes And staff member T has trainer_specializations and certifications When evaluating service compatibility Then include C only if dog_size ∈ allowed_pet_sizes AND temperament_flags ∩ disallowed_temperament_flags = ∅ AND required_trainer_specializations ⊆ T.trainer_specializations AND equipment_requirements are available at the slot location And otherwise exclude C with reason codes for each unmet rule (e.g., SIZE_MISMATCH, TEMPERAMENT_FLAG, TRAINER_SPECIALIZATION_MISSING, EQUIPMENT_UNAVAILABLE) recorded in the decision
Vaccine and Pet Profile Compliance
Given service S requires vaccines_required = {vaccine_code: min_valid_through_date_rule} And C’s Pet Profile Smart Card contains vaccine_records with vaccine_code and valid_through dates When validating compliance for appointment start_at Then include C only if for every vaccine_code in vaccines_required, a corresponding record exists with valid_through ≥ start_at And exclude otherwise, logging missing_or_expired_vaccines = [codes] and each record’s valid_through in the decision And the decision includes source=PetProfileSmartCard and profile_version used
Payment Standing and Package Balance Enforcement
Given policy defines delinquency_threshold_amount, require_valid_payment_method=true|false, apply_packages_first=true|false, and allow_pay_at_visit_overrides per service And client C has payments_profile (default_payment_method_status, delinquent_balance) and package_balances per service When evaluating financial eligibility for service S Then exclude C if delinquent_balance > delinquency_threshold_amount And exclude C if require_valid_payment_method=true and default_payment_method_status ∉ {valid} And if apply_packages_first=true and package_balances[S] ≥ 1, mark package_to_apply=1; else if service requires package and package_balances[S] = 0 and allow_pay_at_visit_overrides=false, exclude C And record financial fields in the decision: delinquent_balance, payment_method_status, package_balance_for_S, rule_path_taken
Quiet Hours, Frequency Caps, and Fairness Controls
Given policy defines quiet_hours per client timezone, max_offers_per_rolling_days N over window_days W, decline_cooldown_days Dd, and no_show_cooldown_days Dn And selection_fairness is enabled with tie_breaker=least_recently_offered When assembling the send_batch at evaluation_time Then exclude C if evaluation_time falls within C’s quiet_hours, with reason=QUIET_HOURS And exclude C if offers_sent_to_C in the past W days ≥ N, with reason=FREQUENCY_CAP and count recorded And exclude C if C has a recorded decline within the past Dd days or a no_show_penalty within the past Dn days when policy dictates exclusion, with reason=RECENT_DECLINE or NO_SHOW_COOLDOWN And among equally eligible clients, order ascending by last_offered_at to enforce fairness; record selection_order_reason for each client And the decision summary aggregates counts by reason code
Explainable Decisions, Audit Trail, and Admin Controls
Given each evaluation produces a decision record per considered client with fields {client_id, include:boolean, reasons:[{code, message, data}], policy_id, service_id, thresholds, metrics} When the evaluation completes for a canceled slot Then persist all decision records and a summary for at least 90 days, retrievable by slot_id within 200 ms for up to 500 records And expose an admin UI where an admin can: (a) select a preset policy, (b) create/edit per-service overrides, and (c) view include/exclude counts and top 5 reason codes matching backend summaries And ensure policy changes propagate to new evaluations within 60 seconds and are tagged with policy_version in decisions And provide an API endpoint to fetch decisions with pagination and filtering by include/exclude and reason code
Wave-based SMS Offers
"As a groomer, I want Smart Radius to send SMS offers in prioritized waves so that open slots get filled quickly without me managing individual texts."
Description

Automatically launch SMS outreach in ranked waves when a slot is canceled, starting with the shortest ETA candidates and widening as needed. Personalize messages with pet name, service type, slot time, and location, and include one-tap confirm/decline links or reply keywords. Enforce send throttling, delivery retries, and per-client cooldowns. Cancel pending messages the moment the slot is filled and notify non-selected respondents that the slot is no longer available. Provide operator overrides (pause, cancel, resend, manual add) and template management with localization. Log full message history and outcomes for compliance and analytics. Expected outcome: fast, low-friction outreach that fills gaps without manual texting.

Acceptance Criteria
Wave Ranking and Expansion on Slot Cancellation
Given a booked slot is canceled and outreach is triggered And Smart Radius provides live ETA and distance for all eligible, opted-in clients And default thresholds are configured as R1: ETA ≤ 10 min & distance ≤ 5 mi; R2: ETA ≤ 20 min & distance ≤ 10 mi; R3: ETA ≤ 30 min & distance ≤ 15 mi; MaxWaves = 3; InterWaveDelay = 90s When Wave 1 is built Then recipients are clients with ETA ≤ 10 min and distance ≤ 5 mi, ranked by ascending ETA, excluding anyone in cooldown or previously messaged for this event And messages are sent preserving rank at a throttle ≤ 20 SMS/min per business And if the slot remains unfilled 90s after Wave 1 launch, Wave 2 is built with ETA ≤ 20 min and distance ≤ 10 mi, excluding already contacted clients And if the slot remains unfilled 90s after Wave 2 launch, Wave 3 is built with ETA ≤ 30 min and distance ≤ 15 mi, excluding already contacted clients And no more than 3 waves are launched for a single cancellation And each client appears in at most one wave per cancellation event
Personalized Message Composition with One‑Tap Actions
Given a wave recipient is selected When composing the SMS Then the message includes {pet_name}, {service_type}, {slot_time}, {location}, and unique {confirm_link} and {decline_link} And reply keywords CONFIRM and DECLINE are accepted as alternatives to links And links are single‑use, bound to recipient and slot, and expire upon slot fill or after 30 minutes, whichever comes first And if any token value is missing, the template falls back gracefully (e.g., pet_name → "your pet", location → business name) without rendering errors And total message length is ≤ 320 GSM‑7 characters; messages exceeding 160 characters are sent as concatenated segments with links intact And campaign attribution includes slot_id and wave_number on links for analytics And messages are only sent to recipients with valid SMS opt‑in
Send Controls: Throttling, Retries, Cooldowns, and Quiet Hours
Given messages are queued for delivery When sends are processed Then per‑business send rate does not exceed 20 SMS/min; excess messages remain queued in order And transient delivery failures are retried up to 2 times with 30s backoff; permanent failures are not retried And a per‑client cooldown of 24 hours prevents sending another offer to the same client within that window And quiet hours 21:00–08:00 local to the business are enforced; messages scheduled in this window are deferred to 08:00 unless an operator explicitly applies an After‑hours Override And each send decision is recorded with a reason code (sent, queued_throttle, cooldown_block, quiet_hours_deferral, hard_fail, retry)
Slot Fill Race Handling and Cancellation of Pending Outreach
Given multiple confirmations may arrive from different recipients When the first valid confirmation is received Then the slot is assigned to that recipient using server receive timestamp as the tie‑breaker And all pending queued or in‑flight messages for the cancellation are canceled within 2 seconds And any subsequent confirmations receive an immediate "slot no longer available" notification within 5 seconds And DECLINE responses are acknowledged and do not hold the slot And duplicate confirmations from the same recipient are idempotent (single booking, single acknowledgement) And audit logs capture the winning recipient, tie‑break rationale, and IDs of canceled pending messages
Operator Overrides and Manual Controls
Given an active outreach exists for a canceled slot When an operator with permissions clicks Pause Then further sends stop within 2 seconds and the current queue is preserved When the operator clicks Resume Then sending restarts from the same wave and order When the operator clicks Cancel Outreach Then all queued sends are dropped and no further waves are launched for that event When the operator selects Resend Failures Then only previously failed recipients are resent, honoring opt‑out and cooldown rules When the operator uses Manual Add Then a selected client can be sent an immediate personalized offer regardless of wave membership, unless DNC/opt‑out applies And all override actions are audited with user, timestamp, action, and effect
Template Management and Localization
Given an operator manages SMS templates When creating or updating a template Then required placeholders {pet_name}, {service_type}, {slot_time}, {location}, {confirm_link}, {decline_link} are validated; missing required placeholders prevent save And localized variants can be defined per locale (e.g., en-US, es-US) with preview showing sample data, character count, and segment count And at send time, the client’s locale selects the matching variant; if unavailable, the business default locale is used; if still unavailable, en-US fallback is applied And date/time and number formats follow the chosen locale And the template version used is recorded on every message so that all messages in an event render consistently from the same version
Compliance, Logging, and Analytics
Given wave‑based SMS offers are sent When viewing message records and reports Then each message record includes: message_id, slot_id, wave_number, recipient_id, opt‑in status at send, template_id and version, rendered_text_hash, queue/sent/delivered/failed timestamps, status, retry_count, reason_codes, link_click events, response_type and timestamp And logs are retained for 24 months and can be exported as CSV And metrics are computed and visible: time‑to‑fill, fill rate by wave, response rate by wave, delivery failure rate, opt‑out rate, average messages per filled slot, cooldown deferrals And compliance is enforced: STOP unsubscribes are processed within 1 second and honored; HELP replies return support info; DNC list is enforced before send
Atomic Slot Lock & Confirmation
"As a groomer, I want the first client who confirms to automatically lock the slot and pay any required deposit so that there’s no double-booking or confusion."
Description

Implement transactional booking logic that atomically locks the open slot for the first qualifying confirmation, preventing race conditions and double-booking. On confirmation, apply the appropriate package credit or collect a deposit via stored payment credentials, update the calendar and route buffers, and send confirmations to the winner while gracefully notifying others. Handle timeouts and abandoned holds by releasing the lock and resuming outreach. Persist an auditable event trail (offer sent, response received, lock acquired, payment outcome, notifications) and ensure idempotent webhooks for external payment gateways. Expected outcome: reliable first-confirmed wins behavior that secures commitment and reduces no-shows.

Acceptance Criteria
First-Responder Atomic Lock
Given a canceled slot is broadcast to qualifying clients via Smart Radius And two or more clients reply CONFIRM within the hold window When the first valid confirmation is received by the backend Then the system acquires a single distributed lock for that slot within 1 second And creates exactly one booking record linked to the slot and client And all subsequent confirmations for that slot during the lock window are rejected with a 'slot already taken' message And no duplicate bookings exist for the slot in the calendar And the lock TTL equals the configured hold window (default 3 minutes) and is released on success or failure
Payment Application on Confirmation
Given the confirming client has an active package with remaining credits applicable to the service When the slot lock is acquired Then the appropriate package credit is deducted and linked to the booking And a receipt is sent via SMS including remaining credits Given the confirming client has no applicable package credits but has stored payment credentials When a deposit of the configured amount is required Then the system initiates an authorization/capture for the deposit with an idempotency key And on payment success the booking is confirmed and a receipt is sent And on payment decline the booking is canceled, the lock is released within 2 seconds, and outreach resumes And no client is charged more than once for the same booking
Timeout and Lock Release
Given a slot is locked for a client pending payment or credit application When no successful payment/credit application occurs within the hold window (3 minutes by default) Then the lock auto-expires and any tentative booking is removed And the client is notified that the hold expired And Smart Radius outreach resumes to remaining candidates And the event trail records the timeout and lock release
Idempotent Payment Webhooks
Given the payment gateway sends duplicate webhook notifications for the same transaction (success, failure, or timeout) When the webhook is processed one or more times Then the booking state transitions at most once And the ledger records a single charge or credit entry per transaction id And the API responds idempotently (HTTP 2xx) to duplicate webhooks without side effects And retries are safe across service restarts
Notification Routing and Messaging
Given a winner is determined for the slot When confirmation is finalized (credit applied or deposit succeeded) Then the winner receives an SMS confirmation with date, time, location, service, and deposit/credit details plus cancellation policy within 30 seconds And all non-winning responders receive a 'slot taken' SMS within 30 seconds that offers the next best available times or a waitlist option And messages are personalized with the pet’s name and include opt-out instructions And no more than one winner confirmation is sent per slot
Auditable Event Trail Persistence
Given the system processes outreach to fill a canceled slot When events occur (offer sent, response received, lock acquired, payment initiated, webhook received, booking created/canceled, notifications sent, lock released) Then each event is appended immutably with timestamp (UTC), actor/system, slot id, booking id, client id, correlation id, and payload hash And events are queryable by slot id and booking id within 5 seconds of occurrence And exporting the event trail for a slot yields a complete, gap-free sequence
Calendar and Route Buffer Update
Given a booking is confirmed for the previously open slot When the calendar is updated Then the slot is marked as booked and visible across mobile and web within 10 seconds And route buffers and travel times are recalculated so no buffer constraints are violated And Smart Radius eligibility recalculations are triggered for adjacent slots And any conflicting tentative holds are cleared
Adaptive Radius Expansion
"As a scheduler, I want the radius to expand automatically if a slot doesn’t fill quickly so that I maximize the chance of filling it without blasting my entire client list."
Description

If a slot remains unfilled, automatically expand the effective radius and/or relax ETA thresholds over configurable intervals, informed by historical acceptance and arrival reliability by time of day, day of week, and service type. Provide guardrails for maximum expansion, stop conditions (e.g., X minutes before slot start), and dynamic pacing to limit message volume. Offer a preview of the next expansion cohort and allow one-click adjustments. Respect quiet hours, client frequency caps, and compliance constraints throughout. Expected outcome: higher fill rates with minimal over-messaging by progressively widening the audience only as needed.

Acceptance Criteria
Staged Expansion Based on Historical Reliability
Given a canceled slot with start_time, service_type, location, and configured initial thresholds R0 (radius) and E0 (ETA), and an expansion schedule S referencing historical acceptance and arrival reliability by time_of_day, day_of_week, and service_type When the slot remains unfilled at the scheduled expansion time S[i].at Then the system computes next thresholds Ri and Ei using the historical model; if the model is unavailable it uses configured defaults ΔR and ΔE And executes the expansion within 60 seconds of S[i].at And persists the chosen Ri/Ei, model inputs, outputs, and source (model|default) to the audit log And ensures Ri <= MaxRadius and Ei <= MaxETA
Guardrails and Stop Conditions Enforcement
Given configured guardrails MaxRadius, MaxETA, MaxExpansionsPerSlot, and StopLeadTimeMinutes When any of the following occur: the slot is filled; current_time >= start_time - StopLeadTimeMinutes; cumulative expansions >= MaxExpansionsPerSlot; Ri >= MaxRadius; or Ei >= MaxETA Then no further expansions are scheduled or sent for the slot And any pending preview actions are disabled with a visible stop_reason And the slot timeline reflects the stop event within 15 seconds
Quiet Hours, Consent, and Frequency Caps Compliance
Given each candidate client has timezone, sms_consent status, DND/quiet hours window [Qstart, Qend], and frequency caps {MaxOffersPerDay, MaxOffersPerWeek} When computing an expansion cohort Then exclude clients without valid sms_consent or who are in opt-out/suppression lists And exclude clients if their current local time is within [Qstart, Qend] And exclude clients who would exceed frequency caps if messaged now And surface counts by exclusion reason in the preview and audit log
Dynamic Pacing and Message Volume Limits
Given a configured pacing profile with MaxMessagesPerMinute, MaxMessagesPerSlot, MinExpansionIntervalSeconds, and acceptance-rate thresholds {A_high, A_low} with corresponding delay and cohort multipliers When sending expansion messages Then the send rate never exceeds MaxMessagesPerMinute and total messages per slot never exceed MaxMessagesPerSlot And consecutive expansions are spaced by at least MinExpansionIntervalSeconds unless manually applied And if last cohort acceptance_rate >= A_high, then the next expansion delay increases by at least the configured high_delay and the cohort size multiplier is reduced to <= high_multiplier And if last cohort acceptance_rate <= A_low, then the next expansion delay decreases by at least low_delay (bounded by MinExpansionIntervalSeconds) and the cohort size multiplier increases to >= low_multiplier (bounded by aggregate caps)
Preview of Next Cohort with One-Click Adjustments
Given a slot with an upcoming expansion When the user opens the Smart Radius panel Then the UI shows the next cohort preview including: client_count, min/max ETA, mean ETA, radius Ri, ETA Ei, scheduled send time, and expected messages respecting caps And provides actions: Apply Now, Delay by +Δ, Skip this expansion, and Edit thresholds (Ri, Ei) with validation against guardrails And on Apply Now, Delay, or Skip, the system executes the action within 60 seconds and writes an audit entry with actor, action, before/after thresholds, and affected counts
Live Travel Time Targeting and Recalculation
Given a list of candidate clients with current locations or home bases and the slot location When computing an expansion cohort Then live travel time is fetched from the configured provider for each candidate (or a cached value within TTL_seconds) And only clients with predicted arrival_time <= start_time - prep_buffer and <= current ETA threshold Ei are eligible And if live travel time retrieval fails for a client and no cache within TTL_seconds exists, exclude the client and log the reason And live travel times are re-evaluated on each expansion; previously messaged clients are not re-messaged even if they remain eligible
Deduplication, Suppression, and Auditable Metrics
Given a slot and outreach across Smart Radius and other messaging features When targeting and sending offers Then each client receives at most one offer per slot (deduplicated across cohorts and channels) and per-channel constraints are honored And suppression rules (opt-out, bounced, blocked, carrier errors) prevent sends and are recorded per client And the system records per-slot metrics: messages_sent, messages_suppressed_by_reason, acceptances, time_to_fill, and arrival reliability, available in the dashboard within 5 minutes
SMS Compliance & Preference Controls
"As a client, I want control over receiving offer texts and when they’re sent so that I only get relevant messages at appropriate times."
Description

Ensure all Smart Radius outreach adheres to SMS regulations and client preferences. Enforce opt-in status, easy opt-out handling, quiet hours by client timezone, frequency caps, and template approval workflows. Store consent provenance and maintain immutable message logs for audit. Support language preferences, accessibility guidance, and merge-tag safeguards to prevent sending offers that violate eligibility or payment rules. Provide a privacy-by-design data model and documented DSR processes. Expected outcome: compliant, respectful communication that protects the business and maintains client trust.

Acceptance Criteria
Enforce Opt-In and Consent Provenance
Given a contact without active SMS opt-in, When Smart Radius computes recipients for a canceled slot, Then the contact is excluded and zero messages are queued to that number. Given a contact with SMS opt-in, When a message is queued, Then the send job references a consent record that includes source, timestamp, method (e.g., START keyword, web form, staff capture), policy version, and capturing actor or IP. Given an audit request for a specific message ID, When consent provenance is retrieved, Then the system returns the exact consent snapshot used at send-time and the snapshot is immutable. Given imported contacts with unknown consent, When outreach is attempted, Then the system blocks the send and flags the contact as "opt-in unverifiable".
Opt-Out Handling and Immediate Suppression
Given an inbound SMS containing STOP, UNSUBSCRIBE, END, QUIT, or CANCEL (case-insensitive, trimmed), When received, Then the system marks the client as opted-out within 1 second, sends a single confirmation message, and suppresses all future Smart Radius sends. Given an opted-out client replies START, UNSTOP, or YES, When received, Then the system re-enables opt-in, captures consent provenance, and allows future messages. Given a Smart Radius template missing opt-out instructions, When submitted for approval, Then approval is blocked with a clear error indicating missing compliance text. Given a client who opted out, When a Smart Radius outreach is initiated, Then the client is excluded and the message log records "suppressed: opt-out" as the reason.
Quiet Hours by Client Timezone
Given quiet hours are set to 8:00 PM–8:00 AM local time, When Smart Radius prepares to send to a client, Then it resolves the client’s timezone and does not send during quiet hours. Given a send falls into quiet hours, When deferral is applied, Then the message is scheduled for the next allowed window and will not be delivered after the offer’s slot start minus 15 minutes; if missed, it is canceled with reason "expired due to quiet hours." Given a client has no timezone stored, When preparing to send, Then timezone is inferred from address or area code; if unresolved, default to business timezone and apply quiet hours.
Frequency Caps Across Smart Radius Outreach
Given frequency caps are configured to max 2 Smart Radius messages per client per day and 5 per week, When outreach is generated, Then no client exceeds these caps across all Smart Radius campaigns. Given a manual override is attempted by a user without Compliance role, When sending, Then the override is rejected; with Compliance role, Then the override requires a reason and is logged and still respects global regulatory caps. Given multiple canceled slots trigger overlapping outreach, When deduplication runs, Then each client receives at most one message per 2 hours unless override applies.
Template Approval, Localization, and Accessibility Checks
Given a new Smart Radius SMS template, When submitted, Then it cannot be used until approved by a user with Compliance role; changes to an approved template reset status to "Pending Approval." Given a template under review, When automated checks run, Then the system verifies presence of required merge tags, opt-out/help text, link domains allowlist, max length 320 chars, and readability at or below 8th-grade; failures block approval with specific reasons. Given a client language preference (e.g., es, en), When sending, Then the localized template variant is used; if unavailable, the default language is used and logged as "fallback: localization missing." Given a template fails accessibility checks but a Compliance override is allowed, When approved, Then an override note is required and stored with approver, timestamp, and check results.
Merge-Tag Safeguards and Eligibility/Payment Rules
Given a recipient has missing data for a required merge tag, When rendering the message, Then the send to that recipient is skipped, no partial content is delivered, and the log records the missing field. Given business rules require up-to-date vaccines, active package credits or payment method on file, and no delinquent balance, When Smart Radius selects recipients, Then only clients who satisfy all applicable rules are included. Given a rule evaluation error occurs for a recipient, When selecting, Then that recipient is excluded and the error is logged with a machine-readable code and human-readable message.
Privacy-by-Design, Immutable Logs, and DSR
Given any Smart Radius message is sent, When the log entry is created, Then it is write-once and immutable, containing message ID, recipient, template ID/version, rendered body, send/deferral timestamps, consent snapshot hash, outcome, and suppression or rule-evaluation reason if applicable. Given an auditor requests logs for a date range and client, When export is initiated, Then the system provides a CSV/JSON export within 60 seconds including consent provenance and a cryptographic hash to detect tampering. Given a Data Subject Request for access or deletion, When processed, Then the system can export all related data within 30 days and delete or anonymize personal data within 30 days while retaining a minimal non-personal suppression token to honor opt-outs. Given the data model is reviewed, When inspecting, Then personally identifiable data is minimized, purpose-limited to outreach, and segregated from immutable logs via hashed references.
Smart Radius Analytics Dashboard
"As an owner, I want clear analytics on Smart Radius campaigns so that I can optimize settings and prove ROI."
Description

Deliver a real-time dashboard and reports for Smart Radius performance: active campaigns, time-to-fill, fill rate, response rate by wave and radius, deposit capture rate, travel distance saved, and revenue recovered. Provide cohort analyses by service type, time window, and neighborhood, plus trend lines and suggested policy tweaks (e.g., optimal starting radius, ideal wave size). Enable CSV export and API access, and surface per-campaign drill-down with message transcripts and decision explanations. Expected outcome: actionable insights that help owners tune Smart Radius settings to reduce dead time and improve cash flow.

Acceptance Criteria
Real-Time Smart Radius Performance Overview
- Given the owner opens the Analytics Dashboard for the last 7 days, When the page loads, Then the following KPI tiles are visible with values: active_campaigns, median_time_to_fill, p90_time_to_fill, fill_rate, response_rate_by_wave_and_radius, deposit_capture_rate, avg_travel_distance_saved_km, revenue_recovered_usd. - Given a Smart Radius event occurs (campaign created, message sent, response, slot filled, payment captured), When the event is ingested, Then affected KPIs and charts update within 15 seconds and the Last Updated timestamp reflects the change. - Given the user changes any filter (date range, service type, neighborhood, time window), When the change is applied, Then all widgets update consistently within 2 seconds server response time and reflect the selected scope. - Given the selected date range has no data, When the dashboard loads, Then an empty state appears with zero values and a prompt to adjust filters. - Rules: - time_to_fill = accepted_at - canceled_at (per campaign); median and p90 computed across campaigns in scope. - fill_rate = filled_campaigns / total_campaigns in scope. - response_rate_by_wave_and_radius = responses / recipients for each wave number and radius bin. - deposit_capture_rate = deposits_collected / offers_accepted in scope. - avg_travel_distance_saved_km = average(max(baseline_route_km - actual_route_km, 0)). - revenue_recovered_usd = sum(payment_amounts captured for filled canceled slots).
Campaign Drill-Down with Transcripts and Decision Rationale
- Given a campaign row is selected, When the user clicks Drill Down, Then a detail view loads within 2 seconds (p95) showing: - campaign metadata (canceled_at, service_type, time_window, neighborhood, starting_radius_km, wave_size), - a timeline of waves with send timestamps, - per wave: recipients_count, responses_count, accepts_count, declines_count, no_response_count, response_rate, - response table with client_id, wave_number, radius_km, distance_km_at_send, live_eta_min_at_send, decision_reason, outcome, response_timestamp. - Given the detail view is open, When the user selects Messages, Then all message transcripts for the campaign display in chronological order with timestamp, sender (client/staff/system), and message body; system-generated messages are labeled; payment tokens and card numbers are redacted. - Given the detail view is open, When the user selects Map, Then client pins and wave radii render on a map with hover tooltips showing distance_km and eta_min captured at send time. - Given the campaign is filtered, When the user exports from detail view, Then a CSV containing the wave metrics and transcripts downloads with the current filters applied.
Cohort Analysis and Trend Insights with Suggested Policy Tweaks
- Given the user opens Cohorts, When they choose groupings (service_type, time_window, neighborhood), Then a cohort table renders with metrics per cohort: campaigns, median_time_to_fill, fill_rate, response_rate_by_wave_and_radius, deposit_capture_rate, avg_travel_distance_saved_km, revenue_recovered_usd. - Given a cohort is selected, When the user views Trends, Then 7/30/90-day trend lines render for time_to_fill (median), fill_rate, response_rate (by top 3 waves), and revenue_recovered, with tooltips and exact values. - Given at least 30 campaigns exist within a cohort and an observed improvement of ≥10% vs baseline is detected for alternate settings, When Suggestion Engine runs nightly, Then the dashboard displays suggested starting_radius_km and wave_size per cohort with expected impact ranges and the top contributing factors; suggestions include the data window used and are timestamped. - Given the user clicks a suggestion, When details open, Then the method summary shows comparison cohorts, sample sizes, effect sizes, and guardrails (min campaigns >= 30) to validate the recommendation.
CSV Export for Dashboard and Drill-Down Views
- Given any dashboard view (overview, cohorts, drill-down) is active, When the user clicks Export CSV, Then a CSV downloads within 30 seconds for up to 100,000 rows with columns matching the on-screen data plus IDs and timestamps; values reflect current filters and time zone settings. - Given numeric fields exist, When exported, Then numbers use dot decimal, no thousands separator; timestamps are ISO 8601 with time zone offset; line endings are LF; UTF-8 encoding is used; empty values are blank. - Given a multi-sheet export is triggered from the overview, When completed, Then the ZIP contains separate CSVs for kpis.csv, waves.csv, transcripts.csv, and cohorts.csv along with a metadata.json describing columns and generation_time. - Given an export fails, When retried, Then the user sees a clear error message and an option to retry; partial files are not delivered.
Analytics API Access with Filtering and Pagination
- Given a valid API key, When the client requests GET /analytics/kpis, /analytics/cohorts, or /analytics/campaigns endpoints with filters (date_range, service_type, neighborhood, time_window, wave, radius_bin), Then the API returns 200 with JSON matching the dashboard definitions and consistent totals. - Given large result sets, When requested, Then endpoints support pagination via cursor with page_size up to 5,000; next_cursor is provided until completion. - Given rate limits of 60 requests per minute per key, When exceeded, Then the API returns 429 with retry-after header; otherwise p95 latency is ≤ 800 ms for responses up to 1 MB. - Given invalid parameters or unauthorized keys, When requested, Then the API returns 400/401 with machine-readable error codes and messages; no partial data is returned.
Metric Definitions, Data Quality, and Freshness SLAs
- Given a user hovers any KPI or column header, When the tooltip appears, Then the metric definition, formula, and scope are displayed and link to a Definitions panel with full documentation for all metrics used by Smart Radius Analytics. - Given the dashboard is compared against raw event logs for a sampled day, When validation runs nightly, Then metric totals differ by ≤ 0.1% and any discrepancies are flagged in a Data Health banner. - Given new events are generated, When observed, Then the median data freshness (ingest-to-visibility) is ≤ 10 seconds and p95 ≤ 30 seconds during business hours; a Last Updated indicator shows the most recent event time processed. - Given daylight saving transitions or cross-time-zone usage, When data is grouped by day/hour, Then grouping respects the selected account time zone with unambiguous ISO timestamps; no duplicated or missing hours. - Given outliers exist (e.g., time_to_fill > 24h or negative values), When displayed, Then values are capped or excluded per documented rules and flagged via an Outliers badge.

Priority Score

An adjustable scoring engine ranks waitlist clients by package status, distance, reliability, and no‑show/tip history, so your best-fit clients see offers first and slots fill faster.

Requirements

Scoring Weights Configuration
"As an independent groomer, I want to adjust how different client factors influence the ranking so that my highest-value nearby clients get first access to openings."
Description

Provide a dashboard module where operators can enable Priority Score, configure factor weights (package status, distance/travel time, reliability, no‑show rate, tip history), choose presets per service type (grooming/walking/training), and set guardrails. Includes live preview on a sample waitlist, validation to ensure weights sum to 100, role-based access control, and organization-level defaults that can be overridden per staff member or location. Persist configurations with versioning for rollback and audit.

Acceptance Criteria
Enable/Disable Priority Score at Organization Scope
Given I have the Configure Priority Scoring permission at the organization scope When I toggle Priority Score ON and click Save Then the configuration version increments and an audit entry is recorded with user, scope=Org, and timestamp And new waitlist offer rankings within the organization use Priority Score within 1 minute of save And the UI displays a success confirmation Given Priority Score is ON When I toggle it OFF and click Save Then offer rankings revert to the pre-existing non-scored ordering within 1 minute
Weight Sum Validation and Factor Guardrails
Given I am editing weights for package status, distance/travel time, reliability, no-show rate, and tip history When I change any value Then the displayed total equals the sum of all factor weights And the Save action is disabled unless the total equals exactly 100 Given guardrails with min/max are defined for each factor When I enter a value outside its min/max or a non-numeric value Then the input prevents committing the value, shows an inline error with the allowed range, and Save remains disabled Given all factor values are within guardrails and the total equals 100 When I click Save Then the configuration saves successfully and persists on page reload
Service-Type Presets Application and Customization
Given I have selected the service type Grooming When I apply the Grooming preset Then all factor weights populate to the preset values and the preset name is displayed Given a preset has been applied When I modify any factor value Then the preset indicator changes to Custom Given I switch to service type Walking or Training When I view the weights Then I see the last-saved configuration for that service type Given I save a preset or custom configuration for a service type When I return later Then the saved values are loaded correctly
Live Preview Updates With Scoring Changes
Given a sample waitlist preview is visible When I adjust any factor weight or apply a preset Then the preview updates within 500 ms to reflect new total scores and ranking And each preview row shows factor contributions and a total score for at least the top 10 clients Given I click Reset Changes When there are unsaved edits Then the preview and inputs revert to the last-saved configuration
Role-Based Access Control for Scoring Configuration
Given I am a user with Configure Priority Scoring permission (Admin/Manager) When I open the Scoring Weights Configuration module Then I can view and edit settings and the Save action is available Given I have only View Scoring permission When I open the module Then all inputs are read-only and Save is hidden or disabled Given I lack both permissions When I attempt to access the module or a deep link Then I receive a 403 error and the module is not shown in navigation And all access attempts are recorded in the audit log with user, time, and outcome
Organization Defaults with Location and Staff Overrides
Given organization-level defaults exist for each service type When no overrides are defined Then staff and locations inherit the organization defaults Given a location-level override exists When computing the effective configuration for that location Then the location override takes precedence over the organization default Given a staff-level override exists When computing the effective configuration for that staff member Then the staff override takes precedence over the location and organization levels And the UI displays an Effective Source indicator showing Staff, Location, or Organization for each value set Given I clear a staff or location override and Save Then the effective configuration reverts to the next-lower scope
Configuration Versioning, Rollback, and Audit Trail
Given I Save any change at Organization, Location, or Staff scope Then a new version is created with a unique ID, scope, user, timestamp, changed fields, and a diff from the prior version Given a list of prior versions is visible When I select a version and confirm Rollback Then the selected version becomes the active configuration, a new version is recorded noting the rollback, and the live preview reflects the reverted values Given I open the audit log When I filter by scope, user, and date range Then matching entries are returned and exportable to CSV
Scoring Data Pipeline
"As a walker managing a waitlist by text, I want the system to pull accurate, up-to-date client info automatically so that rankings reflect reality without manual data entry."
Description

Aggregate and normalize input signals required for scoring: active package status and remaining credits; client location and service radius constraints; real-time distance or travel time from provider’s base; reliability score derived from response time, confirmation rate, and cancellation history; no‑show fee history; and tip history with outlier handling. Ensure data freshness via event-driven updates (package purchase/use, profile edits, schedule changes) and define defaults for missing data. Implement geocoding and distance calculation with fallbacks when SMS-only clients lack addresses (e.g., use last service location). Expose a unified, cacheable profile for each waitlisted client to the ranking engine.

Acceptance Criteria
Package Events Refresh Package Status and Credits
Given client C has an active package with remainingCredits=5 in the source system When a credit_consumed event for C with 1 credit is received Then the unified profile for C shows package.remainingCredits=4 within 10 seconds and profile.version is incremented by 1 And package.status remains "active" and package.lastEventId equals the event id And profile.lastUpdatedAt equals the event timestamp truncated to milliseconds Given a package_purchased event for C with 10 credits When the event is processed Then package.status="active", package.remainingCredits=10, and package.expiry is populated if provided by the event And if no package exists for C, defaults are applied: package.status="none", package.remainingCredits=0, package.expiry=null
Geocoding With Fallbacks for SMS‑Only Clients
Given client C has a validated address in profile When the geocoding job runs Then C.location.lat and C.location.lng are set by the primary geocoding provider with precision >= 5 decimal places and location.source="address" And travelDistanceMeters and travelTimeSeconds from provider.baseLocation to C.location are computed by the routing engine and timestamped as distance.updatedAt Given C lacks an address but has lastServiceLocation When the geocoding job runs Then C.location is set to lastServiceLocation and location.source="last_service" Given both address and lastServiceLocation are missing When the geocoding job runs Then C.location.status="unknown", travelDistanceMeters=null, travelTimeSeconds=null, and location.source="unknown" Given the routing provider times out or fails When distance is computed Then fall back to straight-line distance (haversine) and an estimated travelTimeSeconds using defaultSpeedKph, and set distance.method accordingly
Reliability Score Calculation and Normalization
Given a 90-day window of messaging and appointment data for client C When reliability is computed Then reliability.score is a 0-100 integer derived as 40% responseTimeScore + 40% confirmationRateScore + 20% cancellationScore And responseTimeScore maps median response time: <=5 minutes = 100, 5-30 minutes linear to 60, 30-120 minutes linear to 20, >120 minutes = 0 And confirmationRateScore maps confirmed/offers sent: 100% = 100, 80% = 80, 50% = 50, 0% = 0 (linear interpolation) And cancellationScore maps 1 - cancellations/confirmed: 0 cancellations = 100, 10% = 70, 20% = 40, >=40% = 0 (linear) And if fewer than 5 data points exist for any component, impute that component with cohort median and set reliability.defaultApplied=true And reliability.updatedAt reflects the computation time and updates within 60 seconds of new relevant data arriving
No‑Show Fee and Tip History with Outlier Handling
Given the last 180 days of completed jobs for client C When tip statistics are computed Then tips.percent values exclude outliers beyond 1.5x IQR or robust z-score > 3 And tip.medianPercent and tip.p70Percent are stored as whole numbers (0-100) rounded to the nearest integer Given no-show and fee events for C over the last 180 days When no-show metrics are computed Then noShow.count, noShow.paidCount, noShow.unpaidCount, and noShow.rate = count / (count + completed) are stored And if there is no history, set tip.medianPercent=0, noShow.rate=0, and flags.defaultApplied=true
Service Radius Constraint Enforcement
Given provider P has serviceRadiusMeters=R and baseLocation BL When client C has travelDistanceMeters=D computed Then profile.scheduling.eligible=true if D <= R else false And if eligible=false, profile.scheduling.reason="outside_radius" And if distance is unknown, profile.scheduling.eligible=true and profile.scheduling.distanceUnknown=true
Unified Cacheable Client Profile Exposed
Given the ranking engine requests the profile for waitlisted client C When the profile is fetched via the profile API Then a single JSON document is returned with schemaVersion, clientId, package, location, distance, reliability, noShow, tip, scheduling, and timestamps populated And response time p50 <= 150 ms and p99 <= 500 ms measured over 1,000 requests And profile includes cache headers (ETag and max-age=300) and supports If-None-Match returning 304 when unchanged And profile.version increments on any field change and cache is invalidated within 5 seconds of event processing
Event‑Driven Data Freshness and Idempotency
Given events package_purchase, credit_consumed, profile_edit, and schedule_change for client C When any event is produced by upstream systems Then the unified profile is updated end-to-end within 30 seconds for p95 and 60 seconds for p99 And event processing is idempotent: re-delivering the same eventId does not change the profile twice And out-of-order events are ordered by eventTimestamp; older events do not overwrite newer values And failed events are retried with exponential backoff up to 5 times and sent to a dead-letter queue after final failure
Real-time Ranking Engine
"As a trainer filling last-minute slots, I want the waitlist to re-rank instantly when details change so that I can send the right offers without delay."
Description

Build a low-latency service that computes Priority Scores and maintains an ordered waitlist whenever triggers occur (new opening, client added, weight change, package update, location change, or status events). Support recalculation under 300 ms for up to 5,000 waitlisted clients per business, with idempotent operations and backoff under load. Provide deterministic scoring with tiebreakers (time on waitlist, most recent visit) and configurable decay for aging events. Offer APIs to fetch top N, page through ranked results, and subscribe to updates.

Acceptance Criteria
Re-rank on Trigger Under Load
Given a business with 5,000 waitlisted clients and stable inputs When any supported trigger occurs (new opening, client added, weight change, package update, location change, status event) Then Priority Scores are recomputed and the ordered waitlist is persisted within 300 ms p95 across 1,000 trigger events And the updated ranking is immediately available via the fetch APIs and eligible for subscription notifications And no trigger events are dropped for that business during the test window
Deterministic Ranking and Tiebreakers
Given identical input data and configuration When the ranking is recomputed multiple times Then the resulting order and scores are identical across runs And for clients with equal Priority Score, the client with longer time on the waitlist ranks higher And if still tied, the client with the more recent visit ranks higher And, absent new triggers, repeated fetches return a stable order
Configurable Decay for Aging Events
Given a business configures aging-event decay with a defined rate When no new events occur and time advances by T Then the contributions of aging events to Priority Score are reduced according to the configured decay And disabling decay yields identical rankings between t0 and t0+T under the same inputs And changing the decay configuration triggers a recompute and the new configuration is applied on subsequent scores
Idempotent Event and Request Handling
Given a trigger event with a unique eventId E is delivered N≥2 times When the engine processes E Then the ranking is updated at most once and at most one update notification is emitted And subsequent deliveries of E are acknowledged without state change And duplicate client or configuration updates submitted with the same requestId return the same response without creating duplicate work
Graceful Backoff Under Load
Given incoming trigger rate exceeds the configured processing capacity for a business When the engine schedules recomputes and encounters retries Then an exponential backoff policy is applied between retries to avoid thrashing And no duplicate recomputes or notifications are produced due to retries And the service continues to serve the last committed ranking via fetch APIs without error And after load subsides, the backlog is fully processed without data loss
Fetch and Paginate Ranked Results API
Given a current ranking exists for business B and no new triggers occur during the test When the client requests the top N via the fetch API Then exactly N clients (or all if fewer) are returned in correct order by Priority Score and tiebreakers And when the client pages through the results using the API's pagination mechanism with page size P Then the concatenated pages contain each waitlisted client exactly once in the correct order with no gaps or overlaps And repeated fetches with the same parameters return identical results
Subscribe to Ranking Updates
Given an active subscription to ranking updates for business B When a ranking change is committed due to any supported trigger Then exactly one update notification is delivered to the subscriber for that change And multiple changes for B are delivered to that subscriber in commit order And if the subscriber disconnects and reconnects, it can recover the latest state by fetching the current ranking via the fetch API
Score Explainability
"As a business owner, I want to understand why a client ranked above another so that I can trust and tune the scoring model."
Description

Display a transparent breakdown of each client’s Priority Score in the dashboard, showing factor contributions, caps, and any applied guardrails. Provide a one-tap “Why ranked here?” panel and an exportable audit trail recording inputs and weights used at the time of ranking. Include tooltips and help text to guide interpretation and prevent misuse. Ensure no customer-facing SMS reveals sensitive scoring details while allowing operators to preview non-sensitive templated explanations if desired.

Acceptance Criteria
View Priority Score Breakdown
Given an operator is viewing the waitlist and selects a client When they tap "Why ranked here?" Then a panel opens within 2000 ms showing: final score, rank position, and a list of factors where each factor displays label, input_value, normalization method, weight, capped_contribution, and any guardrails applied And the sum of capped_contribution across factors equals the displayed final score within ±0.01 And missing inputs display "Not available" and contribute 0.0 to the score And each factor includes an info icon tied to a tooltip anchor And the panel header displays "As ranked on <UTC timestamp>" reflecting the ranking snapshot time
Open and Dismiss Explainability Panel
Given a client card in the waitlist When the operator taps the "Why ranked here?" control Then the explainability panel opens as a modal or side sheet, traps focus, and is fully keyboard navigable And when the operator presses Escape, clicks the overlay, or performs a downward swipe gesture on mobile Then the panel closes within 500 ms and focus returns to the previously focused control on the client card And the panel renders correctly on viewport widths from 320px to 1440px without horizontal scrolling
Export Audit Trail
Given the explainability panel is open for a client When the operator clicks Export Then the operator can choose CSV or JSON and scope of "Selected client" or "Entire current waitlist" And upon confirming, a file download begins within 5 seconds And the export contains for each factor: client_id, ranking_id, ranked_at_utc, factor_key, input_value, normalization, weight, capped_contribution, guardrails_applied, engine_version, factor_config_checksum And the export metadata includes checksum_sha256 and exported_by_user_id And an audit log entry is recorded with user_id, timestamp, scope, format, and checksum
Contextual Tooltips and Help
Given the explainability panel is open When the operator hovers, focuses, or taps an info icon for a factor, weight, cap, or guardrail Then a tooltip appears within 150 ms and contains: a plain-language definition (≤160 characters) and a "Learn more" link to Help Center And the tooltip is dismissible via Escape and loses focus correctly And all tooltip triggers have accessible names and aria-describedby mappings; contrast ratio is ≥4.5:1; keyboard tab order is logical And the Help Center link opens in a new tab and is tracked in analytics as help_tooltip_opened
Prevent Sensitive Scoring in SMS; Allow Safe Preview
Given an operator is composing an SMS using templates When inserting variables Then only the approved safe variables are available: client_first_name, pet_name, service_type, appointment_time, generic_offer_reason And variables exposing sensitive scoring details (e.g., score, rank, weights, factor names, no_show_history, tip_history, reliability_score, distance_score) are not listed And if the operator types an unsupported placeholder like {{score}} or {{weight}}, the system blocks insertion, highlights the token, and shows an inline error explaining it is restricted And the message preview never renders numeric scores, factor names, or weights; any such tokens are redacted as [restricted] And on send, outbound SMS content is scanned; if restricted terms/tokens are detected, sending is blocked with an error and no message is delivered
Score Versioning and Reproducibility
Given an existing ranking snapshot with an associated audit record When an authorized operator triggers Recompute using the snapshot inputs and factor configuration Then the recomputed final score matches the snapshot within ±0.10 for each client And the audit record includes engine_version, factor_config_checksum, dataset_checksum, timezone, and user_id And if the current configuration differs from the snapshot, the explainability panel shows a non-dismissable banner: "Score settings changed since calculation" And the Recompute action is disabled unless the exact snapshot configuration is selected or a new snapshot will be created
Data Freshness Indicator
Given the explainability panel is open for a ranked client When any scoring input has changed since the ranking time Then the panel displays an "As of" timestamp equal to ranked_at_utc and a Changes since ranking section listing each changed field with before → after values And if ranked_at_utc is older than 24 hours, a "Stale" badge appears with a tooltip explaining how to refresh And a "Recompute with current data" button is visible; clicking it performs recomputation and updates the panel values, timestamp, and audit log entry within 5 seconds
Ranked Offer Dispatch
"As a groomer managing cancellations, I want offers to go to my best-fit clients first and then widen out automatically so that openings fill faster without spamming everyone."
Description

Integrate Priority Score with SMS offer workflows to send availability to the top-ranked clients first, with configurable batch size (e.g., top 5), throttling windows (e.g., 10 minutes), and automatic escalation to the next cohort until the slot fills. Respect per-client quiet hours and opt-outs, prevent duplicate offers, and lock the slot when a client confirms. Provide metrics on time-to-fill, acceptance rate by cohort, and impact on no‑shows.

Acceptance Criteria
Configurable Cohort Selection via Priority Score
Given a published slot and a Priority Score list, and batch_size set to X, When dispatch starts, Then exactly X eligible clients with the highest scores are selected in descending order, excluding any suppressed by quiet hours or opt-out. Given batch_size is updated to a new positive integer, When the next dispatch runs, Then the new batch_size is used to form the first cohort. Given batch_size is set to a non-integer or less than 1, When attempting to save, Then the system rejects the value with a validation error and retains the previous valid value.
Throttled Escalation Until Slot Fills
Given throttle_window is configured to T minutes, When the first cohort is sent, Then the next cohort is scheduled exactly T minutes later if the slot is still unfilled. Given a client confirmation locks the slot before T elapses, When the scheduler reaches the next send time, Then no further cohorts are sent and all scheduled sends are canceled. Given all eligible clients are exhausted without a confirmation, When the final cohort completes, Then the workflow ends, the slot remains open on the calendar, and the run is marked as unfilled. Given throttle_window is set to a non-numeric value or less than 1 minute, When attempting to save, Then the system rejects the value with a validation error and retains the previous valid value.
Quiet Hours and Opt-out Suppression
Given a client is opted out of SMS or has revoked consent, When they would otherwise be targeted for an offer, Then they are excluded from the cohort and a suppression reason of OptedOut is logged. Given a client is within their defined quiet hours at the scheduled send time, When forming a cohort, Then they are excluded from that cohort with a suppression reason of QuietHours and no message is sent. Given the slot remains unfilled after the client’s quiet hours end, When forming a subsequent cohort, Then the previously suppressed client becomes eligible for inclusion if all other eligibility conditions are met.
Duplicate Offer Prevention
Given a client has an active offer for a specific slot, When subsequent cohorts are sent for the same slot, Then the client is not targeted again for that slot. Given parallel workflows (e.g., manual and automated) address the same slot, When messages are prepared, Then idempotency by slot_id + client_id prevents more than one offer from being sent to the same client, and duplicates are logged with a suppression reason of DuplicateOffer. Given the SMS provider retries delivery of the same message, When the system receives the retry, Then idempotency by offer_id prevents multiple deliverable messages to the client.
Atomic Slot Lock on Confirmation
Given multiple clients reply CONFIRM to active offers for the same slot, When the system processes confirmations, Then the slot is awarded to the earliest valid confirmation by server-received timestamp, the slot is locked atomically, and later confirmations receive a SlotTaken response without a booking. Given a confirmation is accepted, When the slot is locked, Then all pending and scheduled cohorts for that slot are immediately canceled and no new offers for that slot are sent. Given a client replies after the slot has been locked by another client, When processing the message, Then no booking is created for that client and a SlotTaken response is sent.
Dispatch Workflow Metrics: Time-to-Fill and Acceptance Rate
Given a slot is filled via ranked dispatch, When the slot is locked, Then time_to_fill is recorded as the elapsed time from first offer sent to lock time with second-level precision. Given cohorts are sent, When viewing analytics for a selected date range, Then acceptance rate per cohort is displayed as confirmations divided by offers sent in that cohort, and totals match underlying message/booking records. Given metrics are exported from the dashboard, When comparing export to on-screen values for identical filters and time zone, Then the values match exactly.
No-show Impact Reporting
Given appointments booked via ranked dispatch reach their scheduled time, When computing analytics, Then the no-show rate for ranked-dispatch appointments is calculated and displayed alongside the baseline no-show rate for comparable non-ranked appointments over the same period. Given a user applies date range, location, and service filters, When viewing the no-show impact report, Then both ranked and baseline no-show rates recalculate using the selected filters.
Rollout Controls and Experiments
"As an operations lead, I want to test Priority Score with a subset of clients and compare outcomes so that I can roll it out confidently."
Description

Add organization and staff-level toggles to enable/disable Priority Score, percentage-based rollouts, and A/B testing of weight presets. Define success metrics (time-to-fill, no‑show rate, revenue per slot) and automated comparisons against holdout groups. Provide safe-rollback and change logs, with scheduled activation windows for busy periods.

Acceptance Criteria
Org and Staff Toggles for Priority Score Activation
- Given an org admin turns Priority Score ON at the organization level and a staff member has no override, When a waitlist offer is created for that staff, Then the client ranking uses Priority Score and the UI shows an indicator "Priority Score: On" for that staff. - Given Priority Score is OFF at the org level and a specific staff override is set to ON, When offers are generated for that staff, Then Priority Score is applied only to that staff while others remain unaffected. - Given Priority Score is ON at the org level and a specific staff override is set to OFF, When offers are generated, Then that staff uses chronological ranking and the UI shows "Priority Score: Off (override)". - Given any toggle state is changed, When the change is saved, Then a change log entry is recorded with user, timestamp, scope (org/staff), previous value, new value, and reason. - Given a toggle is changed, When new offers are generated after the change, Then the new state is applied within 2 minutes across all clients and devices.
Percentage-Based Rollout with Stable Cohorting
- Given an admin sets a rollout percentage P (e.g., 30%) at the org level, When eligibility is evaluated, Then users are deterministically bucketed by stable hashing on user or pet profile ID and approximately P% (±2%) are exposed. - Given the rollout increases from P1 to P2 (P2>P1), When exposure is recalculated, Then previously exposed users remain exposed (monotonic expansion) and only additional users are added to reach P2. - Given the rollout is set to 0%, When new offers are generated, Then no users are exposed and Priority Score is not applied. - Given a list of holdout tags/groups is configured, When rollout assignment occurs, Then members of holdout groups are excluded from exposure regardless of percentage. - Given a rollout percentage change is saved, When system propagation occurs, Then the new exposure takes effect within 2 minutes and the exposure counts are visible to admins.
A/B Testing of Priority Weight Presets
- Given an admin creates an experiment with variants A and B (distinct weight presets) and a 50/50 split, When the experiment is started, Then new eligible offers are assigned to A or B via deterministic bucketing and remain in the same variant on subsequent interactions. - Given minimum sample size per variant (e.g., 200 filled offers) and a maximum duration are configured, When either threshold is met, Then the system marks the experiment ready for analysis and freezes assignments if stopped. - Given a staff member is excluded via toggle or is outside scheduled activation, When offers are generated, Then that staff is not exposed to any variant and uses the non-experimental ranking. - Given the experiment is paused or stopped, When new offers are generated, Then control (current default weights) is used within 2 minutes and no new exposures are logged. - Given experiment traffic and assignments occur, When logs are reviewed, Then each exposure records timestamp, user/staff, variant, and experiment ID in the change log.
Automated Metric Computation and Holdout Comparison
- Given success metrics are defined as Time-to-Fill (offer sent to acceptance), No-Show Rate (no-shows/confirmed), and Revenue per Slot (gross per filled slot), When an experiment or rollout runs, Then these metrics are computed per variant and for the holdout/control group. - Given sufficient sample size is reached, When analysis runs, Then the system calculates lift versus holdout and performs a two-sided significance test with alpha ≤ 0.05, surfacing effect size and confidence intervals. - Given the dashboard is viewed during an active test, When data updates occur, Then metrics refresh at least every 15 minutes and display the last refresh time. - Given an admin requests a CSV export, When the export is generated, Then it contains per-variant daily aggregates and raw exposure counts for the selected date range.
Safe Rollback for Priority Score and Experiments
- Given an admin triggers a one-click rollback, When the rollback is confirmed, Then Priority Score, percentage rollouts, and active experiments are disabled org-wide and new offers revert to chronological ranking within 2 minutes. - Given rollback is executed, When logs are inspected, Then a rollback entry is recorded with user, timestamp, affected scopes, and optional reason. - Given scheduled activations exist, When rollback completes, Then all pending schedules are paused and will not auto-activate until explicitly re-enabled. - Given rollback occurs mid-offer, When the next offer is generated, Then the non-Priority Score ranking is used and no experimental exposure is recorded.
Change Logs and Audit Trail for Rollouts and Experiments
- Given any change to toggles, rollout percentages, experiment configs, or schedules, When the change is saved, Then an immutable audit entry is created with who, when, what changed, previous value, new value, scope (org/staff), and environment. - Given audit logs are queried, When filters for date range, user, scope, and change type are applied, Then results return only matching entries and can be exported as CSV. - Given a log entry contains structured configs (e.g., weight presets), When viewed, Then the diff of key weights is displayed to show exactly what changed.
Scheduled Activation Windows with Timezone Awareness
- Given an admin schedules Priority Score or an experiment to run between Start and End in the org’s timezone, When the Start time occurs, Then the feature activates automatically and deactivates at End, restoring the prior state. - Given overlapping schedules or manual overrides occur, When conflicts are evaluated, Then the system applies the latest explicit manual override and flags the conflict in the schedule list. - Given a schedule is set, When it is within 10 minutes of Start, Then an in-app and email notification is sent to org admins summarizing the upcoming activation. - Given scheduled activation executes, When verification runs, Then activation/deactivation completes within 2 minutes of the scheduled time and is recorded in the audit log.
Performance, Monitoring, and Bias Safeguards
"As a product owner, I want built-in monitoring and bias safeguards so that the feature is reliable, compliant, and fair to clients."
Description

Instrument the scoring pipeline and dispatch flow with metrics, logs, and alerts for latency, error rates, and stale data. Implement fairness guardrails: cap the influence of tipping, exclude protected-class proxies, and prevent permanent demotion from a single no‑show. Provide configuration to ignore tips entirely if required by policy. Include privacy controls that limit score visibility, and maintain audit logs for compliance. Set SLOs (p95 recalculation <300 ms; offer dispatch initiation <1 s) and dashboards to track them.

Acceptance Criteria
p95 Score Recalculation Latency SLO & Alerting
- Given production traffic >= 1000 recalculations/hour, when scores are recalculated, then the p95 recalculation latency is < 300 ms over a rolling 60-minute window. - Given a latency SLO breach persists for 5 consecutive minutes, when alerting rules evaluate, then a high-severity alert is sent to on-call via PagerDuty and Slack within 2 minutes and a dashboard annotation is created. - Given the dashboard is loaded, when viewing the Scoring Latency panel, then p50, p95, and p99 latencies plus error budget burn rate are displayed for the last 24 hours with 1-minute resolution. - Given a latency alert fires, when the runbook link is followed, then steps to capture top traces and recent deploys are provided and the alert includes the active service ownership.
p95 Offer Dispatch Initiation Latency SLO & Tracing
- Given a waitlist fill is initiated, when the dispatch flow starts, then the p95 time from eligible client selection to first offer enqueue is < 1 second over a rolling 60-minute window with >= 200 dispatches/hour. - Given distributed tracing is enabled, when viewing a single dispatch request, then scoring, queueing, SMS provider, and database operations share a common trace_id with durations and status codes. - Given p95 dispatch initiation latency exceeds 1 second for 5 consecutive minutes, when alert rules evaluate, then a high-severity alert is sent with a link to the tracing query filtered to the affected timeframe and tenant.
Error Rate and Stale Data Detection in Scoring Pipeline
- Given >= 1000 scoring events in a 5-minute window, when processing completes, then the processing error rate is < 0.5%. - Given processing error rate >= 1% for 2 consecutive 5-minute windows, when alerting evaluates, then a high-severity alert fires including the top 3 error classes by count. - Given any feature timestamp is older than 15 minutes at time of use, when this occurs for >= 1% of scoring requests over a 10-minute window, then an alert tagged 'stale-data' fires and the scoring engine uses a documented fallback input without throwing. - Given stale-data fallback is applied, when logs are inspected, then an INFO log is present per event including trace_id, feature_name, age_ms, fallback_reason, and tenant_id.
Fairness Guardrail: Tip Influence Cap and Ignore-Tips Policy Switch
- Given tips are included in features, when a score is computed, then the tip-derived contribution is capped at <= 10% of the total score range for all clients and tenants. - Given tenant policy ignore_tips=true, when scores are computed, then the tips feature is excluded (effective weight 0) and offer ordering matches a tips-free baseline for the same inputs. - Given the tip policy is changed, when the change is saved, then an audit entry records actor, timestamp, tenant_id, previous_value, new_value, and justification. - Given a user opens the admin fairness settings with proper permission, when viewing configuration, then the current tip policy (enabled/ignored) and cap percentage are visible and validated on edit (range 0–10%).
Fairness Guardrail: Protected-Proxy Exclusion and Enforcement
- Given a feature is tagged as protected or proxy in the feature registry, when the model schema is validated in CI, then the check fails if the feature is present in the scoring pipeline or training schema. - Given runtime scoring, when features are materialized, then any blocked feature is omitted and a WARN log with feature_name, reason=blocked, and trace_id is emitted once per deployment. - Given the daily fairness audit job runs, when it scans feature-usage logs for the last 24 hours, then the count of blocked-feature references equals 0; otherwise an alert is sent with offending features and tenants. - Given telemetry is shipped, when inspecting logs, then no PII or protected attributes appear; only hashed IDs and whitelisted feature names are present.
No-Show Penalty Decay (No Permanent Demotion)
- Given a client has exactly one recorded no-show, when 30 days elapse or the client completes 3 subsequent appointments (whichever occurs first), then the no-show penalty contribution to the score decays to 0. - Given multiple no-shows, when penalties are applied, then the cumulative penalty cannot reduce the total score by more than 20% of the score range at any time. - Given the nightly decay job runs, when it completes, then 100% of eligible client records are updated within 24 hours and the job emits a summary metric (updated_count, skipped_count, duration_ms). - Given two otherwise identical clients after the decay condition is met, when ranked, then their scores differ by <= 1e-6.
Privacy and Auditability of Score Access
- Given RBAC is enforced, when a user without 'Score.View' permission requests scores via API or UI, then the response omits score fields and returns HTTP 403 with a redaction reason. - Given a permitted user views or exports scores, when the request completes, then an immutable audit log entry is written with user_id, subject_id, action, timestamp, purpose, and trace_id to an append-only store with a daily SHA-256 chain. - Given audit retention is configured, when compliance requests records, then audit entries are retrievable for at least 400 days and a 10,000-record export completes in <= 60 seconds. - Given a public API response includes ranking, when delivered, then score values are bucketed/rounded per policy and never include intermediate feature values or raw tips.

One‑Tap Claim

Sends a unique SMS link that holds the slot with a countdown, pre-fills pet and service details, and can pre‑auth a deposit—confirming in seconds without back‑and‑forth.

Requirements

Time-Bound Claim Link & Slot Hold
"As a busy pet parent receiving an appointment offer by text, I want a link that holds the slot for a few minutes so that I can confirm without losing it while I grab my card."
Description

Generate a unique, signed SMS link that reserves the offered appointment slot for a configurable hold window (e.g., 5–15 minutes) and displays a live countdown on the mobile claim page. The link embeds slot, location, and service context; is single-use; and auto-expires to release inventory if not confirmed. The hold is enforced by the scheduling engine to prevent other bookings during the countdown. The link respects timezone, DST, and multi-location settings and records creation, open, claim, and expiry events for system visibility. This integrates with the existing SMS workflow so staff can send offers without leaving the thread, and customers can confirm in seconds without back-and-forth.

Acceptance Criteria
Unique, Signed, Single‑Use Claim Link
Given a staff user sends an offer from an SMS thread When the system generates a claim link Then the link is unique per offer, cryptographically signed, and includes offer_id, slot_id, location_id, service_id, customer_id, and expires_at Given the claim link is tampered (any parameter changed) When the customer opens it Then the link is rejected and no hold or booking is created Given the claim link is opened before expires_at for the first time When the customer visits the page Then the page loads successfully and the link is marked as opened Given the claim link is used to confirm the slot When the booking is created Then subsequent opens of the same link show a "Link used or expired" state and cannot create another booking Given the claim link has passed expires_at without confirmation When it is opened Then the page shows an "Expired" state and the link cannot create a booking
Configurable Hold Window and Live Countdown
Given organization or location settings specify a hold window between 5 and 15 minutes (default 10 minutes) When an offer is sent Then the hold window is set to the configured duration and expires_at is calculated from link creation time Given a customer opens the claim page When the countdown is displayed Then it shows remaining time in mm:ss, updates every second, and is based on server time Given the countdown reaches 00:00 When the page is active or refreshed Then the page transitions to an "Expired" state and the Confirm action is disabled Given the claim page is closed and reopened during the hold When it is loaded again Then the countdown reflects the correct remaining time from the original expires_at
Scheduling Hold Enforcement and Auto‑Release
Given a hold is active for a slot When availability is requested via dashboard, API, or online booking Then the held slot does not appear as available to anyone except the held offer flow Given two customers attempt to confirm the same held slot concurrently When one confirmation succeeds Then the other attempt is rejected with a "No longer available" message and no duplicate booking is created Given a hold expires without confirmation When the expiry occurs Then the slot is released back to inventory within 2 seconds and becomes available to book Given a hold exists for a slot When another hold attempt for the same slot is initiated from any channel Then the system prevents creating a second overlapping hold for that slot
Timezone, DST, and Multi‑Location Accuracy
Given a slot belongs to a specific location with a defined timezone When the claim link is opened from any device timezone Then the slot time is displayed in the location’s local time with timezone label (e.g., 3:00 PM America/Los_Angeles) Given a slot occurs on a daylight saving time transition date for the location When the claim page renders Then the displayed time reflects the correct post‑rules local time (no +/‑1 hour errors) Given events are recorded for the claim flow When they are stored Then event timestamps are saved in UTC with location_id for traceability Given an organization has multiple locations with different timezones When an offer is sent for a location Then the link uses that location’s timezone for display and hold computation
Event Tracking: Creation, Open, Claim, Expiry
Given an offer is sent When the claim link is generated Then a ClaimLinkCreated event is recorded with offer_id, link_id, slot_id, location_id, customer_id, created_at_utc, and expires_at_utc Given a recipient opens the link When the page loads Then a ClaimLinkOpened event is recorded with link_id, opened_at_utc, and user_agent Given the customer confirms the slot When the booking is created Then a ClaimLinkClaimed event is recorded with link_id, booking_id, claimed_at_utc Given the link expires without confirmation When expires_at passes Then a ClaimLinkExpired event is recorded with link_id, expired_at_utc and the slot is marked released Given any event is delivered When the staff timeline is viewed Then the latest claim‑flow events are visible within 5 seconds of occurrence
SMS Workflow Integration and Pre‑Filled Context
Given a staff user is in an SMS conversation with a customer When they send an offer Then the SMS includes a shortened claim URL and a human‑readable summary of service, date/time, and location Given the customer taps the claim link When the claim page loads Then the pet name and photo, service, duration, price (if available), and location name/address are pre‑filled and read‑only Given the offer references a specific pet and service When the customer views the form Then they are not required to re‑select pet or service to confirm Given the user proceeds to confirm When the confirmation succeeds Then a booking confirmation screen is shown and a confirmation SMS is sent to the customer
Contextual Prefill from Pet Profile Smart Cards
"As a returning customer, I want my pet and service info pre-filled so that I can confirm the appointment quickly without retyping details."
Description

Pre-populate the claim page with the customer’s pet(s) and service details based on the SMS thread and existing Pet Profile Smart Cards, including pet name, photo, breed, weight, coat notes, vaccine status, prior preferences, and add-ons. Present a one-tap selector if multiple pets exist and surface required updates (e.g., expired vaccines) inline. Prefill owner contact details and preferred groomer/tech. This reduces friction, ensures data accuracy, and leverages stored profiles to shorten confirmation time while keeping records current.

Acceptance Criteria
Prefill Single Pet Details from Pet Profile
Given a One‑Tap Claim link is opened by a customer with exactly one active Pet Profile When the claim page loads Then the pet's name, photo, breed, weight, coat notes, and vaccine status are pre-populated And the prior service preferences for the selected service type are pre-populated And the service type and held date/time from the countdown slot are pre-populated And all prefilled values match the latest stored Pet Profile and booking context
Multi‑Pet Selector with One‑Tap Switch
Given a One‑Tap Claim link is opened by a customer with multiple active Pet Profiles When the claim page loads Then a one-tap selector displays each pet's name and thumbnail photo And the pet most recently referenced in the SMS thread is preselected; if none, the most recently booked pet is preselected And tapping a different pet updates all prefilled fields immediately without a full page reload And the Confirm action always reflects the currently selected pet
Inline Vaccine Status Validation and Update
Given the selected pet has missing or expired required vaccines for the chosen service When the claim page loads Then an inline warning lists the specific vaccine(s) and status (missing/expired) next to Vaccine Status And the Confirm action is disabled until the vaccine requirement is satisfied And the user can upload proof or enter/update dates inline without leaving the flow And upon successful update, the Pet Profile is updated and the warning clears
Owner Contact and Preferred Staff Prefill
Given a One‑Tap Claim link is opened When the claim page loads Then the owner's name, mobile number, and email (if on file) are prefilled and editable And the preferred groomer/tech (if on file) is preselected And if any item is not on file, the corresponding field is blank and indicates if required And edits made before confirmation are saved to the owner's profile upon confirmation
Prior Preferences and Add‑Ons Prefill with Sync
Given the selected pet has historical preferences and add-ons for the selected service type When the claim page loads Then those preferences and add-ons are preselected And the user can modify selections prior to confirming And upon confirmation, changes are written back to the Pet Profile and tied to the pet and service type
Deposit Pre‑Authorization with Prefilled Amount
Given the selected service requires a deposit and a payment method is on file When the user taps Confirm Then the prefilled deposit amount is pre-authorized on the default payment method And the authorization status is displayed inline without leaving the claim flow And if pre-authorization fails, the user is prompted to update/add a payment method and the booking remains unconfirmed until success
Graceful Fallback When No Profile Match
Given a One‑Tap Claim link is opened and no Pet Profile can be matched from the SMS context When the claim page loads Then a minimal form requests pet name, breed, weight, vaccine status, and optional photo And the user can create a new Pet Profile inline without leaving the flow And upon confirmation, the new profile is stored and associated to the booking and future messages
Deposit Pre-Authorization & Card on File
"As a solo groomer, I want a deposit pre-auth at claim time so that I reduce no-shows without chasing payments later."
Description

Allow merchants to require a deposit or card pre-authorization during claim to reduce no-shows, with configurable rules by service, time, or client status. If a card is on file, pre-auth seamlessly; if not, collect card details via a secure, mobile-first form with 3DS/SCA support and PCI-compliant tokenization. Handle success, decline, and fallback flows, and tie the authorization to the appointment for later capture, release, or application to the final invoice and no-show fees. Support multi-currency, taxes/fees, and receipts, and reflect deposit status in the dashboard and confirmation messages.

Acceptance Criteria
Deposit Rules Evaluated on One‑Tap Claim Open
Given a One‑Tap Claim link with an offered service, start time, merchant, and client and a configured deposit policy by service/time/client status When the client opens the link before expiry Then the system determines whether a deposit or card pre‑auth is required according to active rules and computes the required amount in the merchant’s currency including applicable taxes/fees And the countdown timer displays the remaining hold time in mm:ss and decrements each second until zero And the pet(s) and selected service details are prefilled and read‑only And the primary call‑to‑action is labeled to reflect the requirement (e.g., “Confirm & Hold with $X Deposit” or “Confirm” when no deposit required)
Seamless Pre‑Auth Using Card on File
Given the client has at least one valid card token on file and a deposit is required for the offered slot When the client taps Confirm within an active hold Then a pre‑authorization for the exact computed deposit amount (±0.01 of currency minor unit) is attempted via the payment gateway within 5 seconds P95 And 3DS/SCA is applied as required by region/gateway; if exemption is accepted, no challenge is shown And on approval, the appointment is confirmed, and the authorization ID, amount, currency, and expiration date are stored and linked to the appointment ID And the client receives an SMS confirmation including deposit status “Pre‑Authorized” and a receipt link; the dashboard shows a “Deposit Pre‑Auth” badge with amount
New Card Collection with 3DS/SCA and Tokenization
Given no valid card is on file or the on‑file card is unavailable/declined and a deposit is required When the client chooses to add a card Then a PCI SAQ‑A compliant, mobile‑first form is presented supporting card number, expiry, CVC, ZIP/postcode, and 3DS/SCA challenge when required And submitted card details are tokenized; no raw PAN is stored; the token is attached to the client profile with explicit consent to “Save card on file” per merchant policy And upon successful tokenization and any required 3DS/SCA challenge, a pre‑authorization for the deposit amount is immediately attempted and the result is shown And Apple Pay and Google Pay are offered when device/browser support and merchant settings enable them
Decline, Retry, and Fallback Handling
Given a pre‑authorization attempt fails due to decline, 3DS failure, network error, or timeout When the client is notified of the failure Then a human‑readable reason is displayed (e.g., “Insufficient funds”) without exposing sensitive data, and options to Retry with same card, Use different card, or Cancel are offered And if merchant policy allows “hold without deposit” fallback, selecting it keeps the slot until the countdown ends; otherwise the slot is released immediately And no appointment is created nor status set to Confirmed unless a successful pre‑auth exists when deposit is required by policy And all attempts (timestamp, outcome, reason code) are logged for audit
Authorization Lifecycle: Link, Capture, Release, No‑Show Fee
Given an appointment with a linked authorization When the appointment is marked Completed Then the merchant can capture the authorization either for the deposit amount only or adjust to the final invoice total (up to gateway limits), applying captured funds to the invoice And if the appointment is canceled within the refundable window, the authorization is voided/released the same day; otherwise, if late‑cancel/no‑show rules apply, the merchant can capture the configured fee from the authorization And if the authorization expires before capture, the dashboard flags “Auth expired” and prompts to request a new payment And all captures, partial captures, voids, and expirations are reflected in the appointment timeline and exported reports with gateway reference IDs
Multi‑Currency, Taxes/Fees, and Rounding
Given the merchant’s base currency and tax/fee rules When computing the deposit amount for a claim Then the amount is calculated in the merchant currency, includes configured taxes/fees per taxability of deposits, and is rounded per gateway currency rules And the currency ISO code and symbol are shown consistently in client UI, SMS, dashboard, and receipts And cross‑border cards are supported; any FX conversion is handled by the cardholder’s bank; the platform does not convert deposit amounts And amounts shown to the client match the authorized amount within ±0.01 of the currency minor unit
Dashboard and Messaging Reflect Deposit Status
Given an appointment created via One‑Tap Claim When the deposit state changes (Pre‑Authorized, Captured, Released, Expired, Failed) Then the dashboard updates the appointment badge and amount in under 5 seconds and the activity feed adds a new event entry And the client receives an SMS/email notification on initial pre‑auth and on capture or release, with receipt links And staff can filter appointments by deposit state and export a CSV including appointment ID, client, amount, currency, status, and gateway reference And the Pet Profile and appointment detail view display deposit status, authorization amount, and last update timestamp
Instant Confirmation & Calendar Writeback
"As a walker managing a full day, I want confirmed claims to auto-update my calendar and notify the client so that I stay on schedule without extra messages."
Description

Upon successful claim, atomically convert the hold into a confirmed booking, write back to the calendar, and trigger confirmation SMS to both client and staff with appointment details, deposit status, and policy reminders. Generate and attach an ICS event, update package balances or credits, and queue any required Smart Card prompts (e.g., missing vaccines) for completion. Update the dashboard in real time and provide a quick link to reschedule or cancel per policy. This ensures accurate schedules and closes the loop without manual steps.

Acceptance Criteria
Atomic Conversion and Calendar Writeback
Given a valid One‑Tap claim link that holds a slot for pet(s) and service(s) When the client confirms within the countdown window Then the hold is atomically converted to a confirmed booking with the same start/end, staff, and resources And the booking is written to the staff calendar within 2 seconds (p95) And the original hold is released and cannot be reclaimed by any other link And if calendar writeback fails, the conversion is rolled back and the client is shown a failure message with no confirmation sent And conflicting simultaneous claims are prevented; only one booking can be created for the held slot
Dual‑Side Confirmation SMS with ICS Attachment
Given a booking is confirmed When notifications are triggered Then the client and assigned staff each receive an SMS within 30 seconds (p95) containing date, time, pet names, service, location (if applicable), deposit status, and a policy reminder link And an ICS event is generated for the booking and delivered via a downloadable link in the SMS And the ICS contains Summary, Description (including deposit status and policy URL), Start/End with correct timezone, Organizer, Location (if set), and a unique UID And importing the ICS into Google Calendar and Apple Calendar produces a single event at the correct time without duplication
Deposit Pre‑Authorization and Status Propagation
Given the booked service requires a deposit and the client has a payment method on file (or provides one during claim) When the client confirms the One‑Tap claim Then a pre‑authorization for the deposit amount is successfully created before calendar writeback And only upon successful pre‑auth does the booking confirm and notifications send And deposit status (Pre‑authorized/Captured) is displayed in the SMS and dashboard booking card And on pre‑auth failure, the booking is not confirmed, the hold remains until expiry, and the client receives a failure message with a secure retry link And retries are idempotent and do not result in duplicate charges
Package/Credit Balance Updates
Given the client has an applicable package or credit balance When the booking is confirmed Then the correct number of credits are deducted immediately and recorded on the booking And the updated balance is shown on the dashboard and in staff-facing details And if credits are insufficient, the system applies remaining credits and reflects any required deposit/fee per policy without double-charging And all package/credit transactions are idempotent and auditable with timestamp and actor
Smart Card Prompt Queuing for Missing Requirements
Given one or more required Smart Card items (e.g., vaccines, preferences) are missing or expired for the pet(s) When the booking is confirmed Then a Smart Card prompt is queued listing the specific missing items And the client receives a link to complete the Smart Card, and the booking shows an “Action Required” badge on the dashboard And the appointment remains confirmed but flagged until completion And upon completion, the badge is cleared and the booking details reflect updated records within 1 minute (p95)
Real‑Time Dashboard Refresh with Policy‑Compliant Quick Links
Given a booking confirmation event occurs When the business user is viewing the dashboard Then the booking card appears or updates in real time within 3 seconds (p95) without manual refresh And the card displays deposit status, package deductions, Smart Card status badge, start/end time, staff, and a unique booking ID And quick links for Reschedule and Cancel are present; Cancel is enabled/disabled per policy rules and warning text is shown before action And selecting Reschedule opens an available-slot picker constrained to the same service and staff (unless changed by policy) and preserves deposit/policy rules And all actions (confirm/reschedule/cancel) are logged with timestamp, actor, and reason code where applicable
Secure Link & Identity Binding
"As a trainer, I want claim links to work only for the intended client so that my limited slots aren’t taken by mistake or misuse."
Description

Protect claim links with signed, short-lived tokens bound to the recipient’s phone number and offer context. Enforce single-use and device/number checks where possible, with server-side validation to prevent guessing, replay, or sharing. Provide safe recovery if the client forwards the link (e.g., verification code challenge) and enable staff-driven invalidation and reissue. Log security events and throttle attempts. This ensures only the intended recipient can claim the slot, preserving merchant trust and reducing fraud or accidental double claims.

Acceptance Criteria
Short‑Lived Signed Token with Context
Given a one‑tap claim link is generated When the link is created Then the token is signed server‑side and contains claim_id, recipient_phone_hash, exp (<= 10 minutes from issuance), and a unique jti nonce, and any tampering invalidates it Given current time is after exp or the hold countdown has finished When the link is accessed Then the server responds 410 Expired, shows merchant and appointment context with a reissue option, and the slot remains unclaimed Given a valid link is opened before expiry When the claim screen renders Then it displays merchant name, pet name(s), service, date/time, location, and a masked recipient phone indicator prior to confirm
Single‑Use and Replay Protection
Given a valid token is redeemed once When any subsequent request with the same jti arrives Then the server responds 409 Already Used, makes no state changes, and the slot is not double‑booked Given a token is not redeemed and the hold expires When the link is accessed after expiry Then the token is invalid and cannot be reused Given concurrent redeem attempts occur within a short window When two or more requests race to confirm Then only the first validated request succeeds and all others receive 409 with no partial state
Phone Number Verification and Safe Recovery on Forwarding
Given the link is accessed on a device not verified for the recipient number When the user attempts to proceed Then require entry of a 6‑digit SMS code sent to the original recipient number; allow max 3 attempts within 5 minutes Given the link is accessed on the intended recipient device (auto‑verified where the platform supports it) When the token validates Then bypass manual code entry and proceed to confirmation Given verification fails 3 times or times out When the threshold is reached Then block the claim, invalidate the token, and present a safe reissue path
Device Consistency Check and Soft‑Fail Path
Given a previously validated view of the claim link exists on Device A When the same link is opened on Device B or a new browser profile Then require SMS re‑verification before allowing confirm Given device or browser fingerprint cannot be established When consistency cannot be checked Then fall back to SMS code verification with the same attempt and timeout limits
Server‑Side Brute‑Force Protection and Throttling
Given repeated invalid token or OTP attempts originate from the same IP, phone, or jti When 5 invalid attempts occur within 1 minute Then throttle further attempts for 15 minutes with a generic error and do not reveal which factor failed Given request volume exceeds thresholds When >60 requests/min per IP or >20 requests/min per jti are detected Then return 429 Too Many Requests and make no booking state changes Given a guessed or malformed token is submitted When validation runs Then respond with uniform 401 Invalid Link timing (constant within ±50 ms) to avoid timing leaks
Staff Invalidation and Reissue
Given staff selects Invalidate Link on a pending claim When the action is confirmed Then any existing token for that claim is immediately revoked, cannot be redeemed, and an audit record captures staff user, timestamp, and reason Given staff selects Reissue Link When the action is completed Then a new token with a fresh jti and exp is minted, previous tokens are revoked, and a replacement SMS is sent to the selected number Given a revoked link URL is visited When the page loads Then show Link Replaced messaging with last reissue time and provide no path to confirm
Security Event Logging and Auditability
Given any token mint, validate, redeem, OTP send/success/failure, throttle, revoke, or reissue event occurs When the event completes Then write an immutable security log entry with timestamp (UTC), claim_id, jti, phone hash, IP, user agent, outcome, and actor (system/staff) Given 3 or more failed verifications or a throttle trigger occurs for a claim When the threshold is met Then surface a security alert in the merchant dashboard tied to the appointment reference Given an admin queries security logs for the last 30 days When filtering by claim_id or phone hash Then results return within 2 seconds
Concurrency & Double-Booking Protection
"As a dispatcher sending multiple offers, I want the system to prevent overlaps so that no two customers can end up with the same time slot."
Description

Implement atomic hold/claim transitions with optimistic locking and idempotent endpoints to handle rapid clicks, multiple opens, or simultaneous offers. Validate slot availability at claim, invalidate other holds and links upon success, and gracefully recover if conflicts occur (e.g., show a friendly expiry message and alternative times). Ensure behavior is consistent across devices, locations, and timezones, and stress-test under load to maintain integrity of the schedule.

Acceptance Criteria
Atomic Hold Countdown and Expiry Releases Slot
Given a recipient taps the unique SMS link for an available slot When the link loads Then a server-side hold is created atomically for that slot with a countdown of 5 minutes tied to the link token And the countdown displayed is derived from server time And only one active hold exists per slot per offer Given the countdown reaches zero without a claim When the page is refreshed or the link is re-opened Then the hold is released server-side and the slot becomes available to others And the user sees an Expired message with no option to confirm Given the link is opened on multiple devices/browsers When all clients display the countdown Then all show the same remaining time within ±1 second And no additional holds are created beyond the first
Idempotent Claim Under Rapid Repeats and Multiple Opens
Given a recipient taps Confirm multiple times rapidly or resubmits due to a network retry When POST /claim is called with the same link token and idempotency key within 10 minutes Then at most one booking is created and one payment pre-authorization is initiated And subsequent duplicate requests return the original confirmation payload and booking ID And no duplicate charges or duplicate schedule entries exist Given the client retries after a 5xx or timeout When the same idempotency key is reused Then the server returns the original outcome (success or failure) without creating a new side effect
Optimistic Locking Conflict Handling with Alternatives
Given two users attempt to claim the same slot concurrently When both submit Confirm nearly simultaneously Then exactly one claim succeeds based on optimistic locking and version checks And the other receives a friendly message indicating the slot was just taken And at least three alternative time suggestions for the same service and pet are returned, sorted soonest-first And any pending payment pre-authorization for the losing attempt is voided or released within 5 seconds And no partial bookings remain for the losing attempt
Invalidate Competing Holds and Links on Success
Given a claim succeeds for a slot via one link When other links or devices associated with the same slot/offer are opened or active Then they immediately (≤5 seconds) reflect status as Claimed by someone else or Expired And all competing holds are invalidated server-side And further claim attempts via those links return HTTP 410 Gone with a user-friendly message And no additional bookings can be created from those links
Timezone and Device Clock Consistency for Countdown and Timestamps
Given the recipient’s device timezone differs from the business timezone When the link is opened Then the countdown duration matches the configured hold window using server time regardless of device clock And displayed appointment times are localized to the business’s timezone with clear labels Given the device clock is skewed by ±15 minutes When viewing the countdown Then the remaining time shown is accurate within ±1 second relative to server time And no hold exceeds the configured duration on the server And all audit log timestamps are stored in UTC with timezone offset included for display
Schedule Integrity Under Load and Race Conditions
Given a load test with 500+ concurrent claim attempts targeting the same slot and 5,000 RPS platform-wide When claims are processed for 10 minutes Then there are 0 double-booked slots and 0 duplicate payment pre-authorizations for the same booking And p95 claim latency ≤ 800 ms and p99 ≤ 2 s And error rate for claim attempts ≤ 0.5% And database constraints, locks, and retries complete without deadlocks that cause conflicting commits And all claim outcomes are recorded with correlation IDs for traceability
Merchant Configuration, Branding & Templates
"As a small shop owner, I want to customize claim rules and messaging so that the experience matches my policies and brand while maximizing conversions."
Description

Provide a settings panel to configure hold duration, deposit requirements, pre-auth amounts, cancellation/no-show policies, and which services are eligible for One‑Tap Claim. Enable editable SMS templates with variables (pet name, time, location, deposit) and preview, plus brand the claim page with logo, colors, and business name. Support localization for time/date formats and languages. Expose basic analytics (sends, opens, claims, conversion rate, average time to claim) to inform tuning and A/B testing of hold duration and messaging.

Acceptance Criteria
Hold Duration Configuration and Countdown Enforcement
- Given a merchant sets hold duration to N minutes in Settings > One-Tap Claim, When a claim link is generated, Then the hold lasts exactly N minutes from send time and the claim page shows a live countdown of remaining time. - Given the countdown reaches zero, When the recipient taps the link, Then the page shows "Offer expired", prevents confirmation, and the slot is released to the schedule. - Given the merchant changes the hold duration, When new links are sent after the change, Then they use the new duration; previously sent links retain their original duration. - Rule: Hold duration accepts integers 1-120; invalid entries are blocked with inline validation. - Rule: Countdown updates at least once per second and never displays negative time.
Deposit Requirement and Pre-Authorization Handling
- Given "Deposit required" is enabled with pre-auth amount $X, When the recipient claims, Then the flow requests a card, authorizes $X, and confirms only after authorization succeeds. - Given authorization fails, When the recipient retries with a different card, Then the system re-attempts pre-auth and only confirms on success; if no success, the hold remains until expiry. - Given cancellation/no-show policies are configured, When a claim is cancelled or marked no-show, Then the system voids or captures the pre-auth per policy and records the action in the booking timeline. - Rule: Pre-auth amount must be > 0 and <= service price; validation prevents save otherwise. - Rule: The claim page and SMS clearly display the deposit amount and a one-line policy summary before authorization.
Service Eligibility Controls for One-Tap Claim
- Given a merchant marks Service A as eligible for One-Tap Claim, When staff composes a claim link for Service A, Then the send action is available; for ineligible services it is disabled with a reason tooltip. - Given eligibility is toggled, When the settings are saved, Then the change applies immediately to new sends. - Rule: Eligibility is stored per service ID and persists across sessions and devices.
SMS Template Editor with Variables and Preview
- Given the merchant opens the SMS template editor, When inserting variables {pet_name}, {appointment_time}, {location}, {deposit_amount}, Then the preview renders with sample data and unknown tokens are highlighted as errors. - Given a template saves without errors, When a claim SMS is sent, Then the delivered message matches the saved template with variables resolved from the booking and merchant settings. - Rule: Character count and estimated segments update live as the template changes. - Rule: Date/time variables in preview and sends respect the selected locale and time zone.
Claim Page Branding (Logo, Colors, Business Name)
- Given a merchant uploads a logo, sets a primary color, and enters a business name, When a recipient opens the claim page, Then the logo displays, primary UI elements use the selected color, and the business name appears in the header. - Rule: Logo accepts PNG or SVG up to 2 MB; invalid files are rejected with an error. - Rule: Primary color accepts valid hex codes; invalid input is blocked. - Rule: The claim page maintains a minimum 4.5:1 text contrast; if the chosen color fails, a warning is shown and an accessible fallback shade is applied.
Localization of Dates, Times, and Language
- Given the merchant selects a language and locale, When the claim page and SMS preview render, Then all static text appears in the chosen language and dates/times display in the locale’s format (e.g., DD/MM/YYYY, 24h). - Given "Use business time zone" is enabled, When recipients in other time zones view the claim page, Then appointment time is shown in the business time zone with an explicit label. - Rule: If a translation key is missing, content falls back to English and the missing key is logged for remediation.
Analytics for Sends, Opens, Claims, Conversion, and Time to Claim
- Given the analytics date range is set, When the merchant views the dashboard, Then it shows total sends, unique opens, claims, conversion rate, and average time to claim for the period. - Rule: Metrics can be filtered by service, SMS template, and hold duration to support A/B comparisons. - Rule: Conversion rate = claims/sends; average time to claim = mean of (claim timestamp - send timestamp) for claimed links; both are displayed with one decimal precision. - Given filters change, When the dashboard refreshes, Then all metrics update to reflect the selected filters.

Route Saver

Optimizes offers using your live route (or staff routes) and traffic to suggest fills that minimize detours and auto-insert travel buffers, keeping the day smooth and on time.

Requirements

Live Route & Traffic Ingestion
"As a mobile groomer, I want FetchFlow to use my live route and traffic so that suggested fills fit my day without causing delays."
Description

Continuously ingest each selected staff member’s live location, planned stops, and current traffic conditions to construct an up-to-the-minute route model. Pull travel times and ETAs at a configurable refresh interval, with graceful degradation when GPS or network is unavailable (fallback to last known position and scheduled stop locations). Respect user permissions and privacy settings, and sync with FetchFlow schedules to align service durations and time windows. Expose a normalized route/traffic data service to downstream components for scoring fills, inserting buffers, and updating reminders.

Acceptance Criteria
Configurable Real-Time Refresh of Live Route and Traffic
Given a staff member with location sharing enabled and at least one planned stop today When the route ingestion refresh interval is configured to 60 seconds Then the system ingests live location, planned stops, and traffic every 60 ± 5 seconds And recomputes travel times and ETAs for at least the next 5 upcoming stops And updates the route model within 2 seconds of each ingestion cycle And the route model includes a lastUpdated timestamp within 5 seconds of wall-clock time Given the refresh interval is changed to 120 seconds at runtime When the configuration is saved Then the next ingestion cycle starts within 120 ± 5 seconds without service restart And subsequent cycles adhere to the new interval Given a refresh interval outside the allowed range 15–300 seconds is submitted When saving configuration Then the save is rejected with a validation error and the prior value remains active
Graceful Degradation on GPS or Network Unavailability
Given network connectivity is lost for up to 10 minutes When an ingestion cycle executes Then the system falls back to the last known GPS position timestamped within the prior 15 minutes And uses scheduled stop locations to estimate travel times with historical speeds And marks the route model mode as "degraded" and ETA confidence as "low" And downstream consumers receive a degraded=true flag Given GPS is unavailable but network is available When an ingestion cycle executes Then the system uses the device’s last known coarse location (>=1 km accuracy) if permitted Or the next scheduled stop location if not And continues refreshing on the configured interval Given connectivity is restored When the next ingestion cycle runs Then the system resumes live mode within one cycle And backfills missed ingestion gaps without duplicating stops or reordering
Permissions and Privacy Enforcement for Location and Route Data
Given a staff member has not granted precise location permission When ingestion runs Then no precise latitude/longitude is stored or exposed And scheduled stop coordinates are used for routing in place of device GPS And downstream responses omit or round coordinates to >= 1 km precision Given a staff member has paused sharing or is outside configured sharing hours When ingestion runs Then no live location is pulled And the route model indicates sharingPaused=true Given a user without route-view permission requests a staff route When the request is made Then the service returns 403 or a redacted payload with no coordinates, path, or exact ETAs
Schedule Sync and Time Window Alignment
Given today’s FetchFlow schedule contains N planned stops with service durations and time windows When the route model is built Then each planned stop is matched by unique stopId And the route model includes serviceDuration and timeWindow for each stop And ETAs falling outside a stop’s timeWindow are flagged with windowBreach=true And missing or duplicate stopIds result in a validation error emitted to logs and metrics Given a stop is added, removed, or reordered in the schedule When the next ingestion cycle runs Then the route model reflects the change within one cycle And recalculates travel times, ETAs, and downstream freshness timestamps
Normalized Route/Traffic Data Service Contract and Performance
Given a downstream component requests normalized route data for staffId S When the data service is called Then the response includes staffId, mode (live|degraded), lastUpdated, refreshIntervalSec, currentPosition {lat, lon, accuracy, source, timestamp}, plannedStops [{stopId, location {lat, lon}, serviceWindow {start, end}, durationSec, plannedArrival, eta, etd, travelTimeFromPrevSec, trafficConfidence}] And the payload conforms to the published JSON schema version v1 And the response time is <= 500 ms p95 and <= 1 s p99 under 100 concurrent requests And the response includes freshnessAgeSec <= refreshIntervalSec + 5 Given the service is called with an unknown staffId When the request is processed Then the service returns 404 with an error code ROUTE_NOT_FOUND
Concurrency and Isolation Across Multiple Staff
Given 100 staff members are concurrently sharing locations with a 60-second refresh interval When the system operates for 30 minutes Then ingestion cycles complete on schedule with >= 95% of cycles starting within the configured ±5-second window And no staff member’s route data appears in another staff member’s response And per-staff event ordering is preserved (monotonic lastUpdated) And system CPU and memory remain within predefined operational thresholds (CPU < 70% avg, memory < 75% of allocated) Given downstream scoring requests spike to 200 RPS for 60 seconds When the data service is called Then the system maintains correctness and isolation And applies backpressure (HTTP 429) rather than returning stale or partial data And recovers to normal latency targets within 2 minutes after the spike
ETA Accuracy and Traffic Incident Freshness
Given an approved reference service provides travel times for the same origin-destination pairs When comparing the system’s ETAs for the next two upcoming stops in live mode Then ETAs are within the greater of ±10% or ±2 minutes of the reference Given the system is in degraded mode When comparing ETAs for the next two upcoming stops Then ETAs are within the greater of ±20% or ±5 minutes of the reference Given a new traffic incident affecting the route is published by the provider When ingestion cycles continue Then affected segment travel times and ETAs reflect the incident within 5 minutes And the route model increments routeVersion and updates lastUpdated
Gap Scoring & Smart Fill Suggestions
"As a walker managing a busy day, I want ranked fill suggestions that minimize detours so that I can fill gaps without running late."
Description

Analyze the day’s open gaps against the live route to rank candidate appointments that minimize detour and preserve on-time performance. Score options using detour minutes, downstream schedule impact, client time windows, service duration, location proximity, package balances, and no‑show rules. Surface top suggestions with predicted arrival window, incremental travel time, and confidence. Provide an API and UI action (Suggest Fills) to generate, refresh, and accept suggestions, updating the calendar and blocking the selected slot.

Acceptance Criteria
Ranked Suggestions Using Detour and Constraints
Given a staff member’s live route for a day with real-time traffic and at least one open gap And a set of candidate appointments with locations, durations, client time windows, package balances, and no-show risk flags When Suggest Fills is requested for that gap Then the system computes a composite score for each eligible candidate using at minimum: detour minutes, downstream schedule impact, time-window alignment, location proximity, and service duration fit And candidates violating hard constraints (outside client time window, duration exceeding gap, geofence exclusion) are excluded from results And the results are sorted by best score deterministically for identical inputs And each suggestion includes predicted arrival window, incremental travel time in minutes, and a confidence value between 0 and 100
UI Suggest Fills Panel Shows Top Options
Given the dispatcher taps Suggest Fills on a specific gap in the calendar When suggestions are returned Then the UI displays the top 5 suggestions by score with pet name, client name, service type, predicted arrival window, incremental travel minutes, confidence, and a reason tag (e.g., “closest”, “fits window”) And the panel supports Refresh to re-query and re-score and returns results within 2 seconds for up to 50 candidates And selecting Accept on a suggestion previews the updated route with travel buffer insertion before confirmation And Decline closes the preview without altering the schedule
API: Generate and Refresh Suggestions
Given a valid access token and a staffId, date, and gap identifier When the client POSTs to the Suggestions API with the gap context Then the response returns HTTP 200 with a JSON array of suggestions each containing: candidateId, score, predictedArrivalStart/End (ISO-8601), incrementalTravelMinutes, downstreamImpactMinutes, confidence (0–100), and rationale tags And the endpoint responds within 1500 ms for up to 200 candidate records and includes an ETag for caching And a subsequent request with If-None-Match and no scoring changes returns HTTP 304 And invalid input returns HTTP 400 with a machine-readable error code; unauthenticated requests return 401
Accept Suggestion Updates Calendar and Blocks Slot
Given a dispatcher selects Accept on a suggestion When the dispatcher confirms Then the system creates the appointment in the selected gap with the suggested start time and assigned staff, and inserts the configured travel buffer before and/or after as needed And the chosen slot is blocked from further suggestions and double-booking And downstream appointments remain within their committed client time windows And the route polyline, ETAs, and reminders are recalculated within 5 seconds And an audit log entry is recorded with suggestionId, userId, timestamp, and before/after route metrics
No Viable Suggestions Handling
Given all candidates violate hard constraints or exceed the maximum allowed downstream impact threshold When Suggest Fills is requested Then the API returns an empty array and the UI shows “No viable fills” with the top two exclusion reasons (e.g., time window mismatch, duration too long) And a control to Expand Criteria is shown to widen search without auto-applying changes And the system does not modify the calendar
Live Traffic Re-score and Consistency
Given suggestions were generated using traffic data timestamped T0 And traffic conditions change causing any affected leg ETA to vary by at least 10% When the user taps Refresh or the API is called to refresh Then affected suggestions are re-scored and re-ordered accordingly, and their predicted arrival windows and incremental travel minutes reflect the updated ETAs And each response includes a scoringVersion and trafficTimestamp And identical inputs (route, traffic, candidates) produce identical scores and ordering across requests
Scoring Factors: Package Balance and No-show Risk Weighting
Given two otherwise equivalent candidates, one with an active package balance for the requested service and one without When suggestions are computed Then the candidate with an active package balance is ranked equal to or higher than the one without, all else equal And candidates flagged with recent no-show risk are penalized in score by a configurable weight and are not ranked above a candidate without such risk when other factors are equal
Auto Travel Buffer Insertion & Maintenance
"As a trainer, I want travel buffers to auto-adjust to traffic so that I arrive on time without manually editing my schedule."
Description

Automatically insert and maintain travel buffers between appointments based on real-time travel estimates and configurable per‑service minimums/maximums. Recalculate buffers as traffic changes, preventing overlaps and preserving locked customer time windows. Propagate updates to two‑way SMS reminders and ETAs, and flag conflicts with recommended reschedules when adequate buffer cannot be maintained. Ensure buffers are non-billable and do not affect pricing or package redemption.

Acceptance Criteria
Initial Buffer Insertion on Schedule Creation
Given a user creates or imports a route with at least two appointments having locations and service types When live travel time is computed between each adjacent pair using current traffic Then insert a travel buffer between the pair rounded up to the nearest minute, not less than the downstream service’s configured min and not more than its configured max And ensure the buffer eliminates overlap between the adjacent appointments And ensure all appointment times remain within each customer’s requested/locked time window And create the buffer as a non-billable "Travel" block And ensure prices, taxes, tips, and package redemptions on appointments remain unchanged
Ongoing Buffer Recalculation with Traffic Changes
Given an existing route has buffers inserted When live travel estimates between any adjacent stops change before the upstream appointment completes Then recalculate the affected buffer within the downstream service’s configured min and max And if no overlap results, update only the buffer and dependent ETAs without moving locked appointment windows And record an audit entry showing prior and new travel estimate and buffer duration And display the updated buffer and ETA in the dispatcher and staff views
Conflict Flagging and Reschedule Recommendations
Given recalculation determines the required buffer cannot be maintained without causing overlap or violating a locked customer window When this condition is detected Then flag a Buffer Conflict on the affected appointments/segment And generate at least three reschedule options that satisfy configured min/max buffers and all locked windows, sorted by minimal total schedule displacement And allow the user to apply a chosen option or dismiss the conflict And do not send any customer updates until an option is applied And upon apply, update all affected ETAs and reminders
SMS Reminder and ETA Propagation
Given buffers are inserted or updated on a route When an appointment’s ETA or communicated arrival window changes Then update the dashboard ETA, staff mobile ETA, and two-way SMS templates to reflect the new time/window And update any pending reminders before they are sent so they contain the new ETA/window And send a change notification SMS only if the new window differs from the previously communicated window And log the outbound messages and new ETA in the message history
Non-Billable Buffer Accounting Integrity
Given a schedule includes travel buffers and billable appointments When generating invoices, charging payments, or redeeming packages Then exclude buffers from line items, totals, taxes, tips, and package debit calculations And exclude buffer durations from revenue, earnings, and billable hours reports And exclude buffers from package redemption counts and remaining balances
Manual Locks and Buffer Overrides
Given a dispatcher locks an appointment’s time window or manually edits a buffer duration When subsequent recalculations occur due to traffic or route changes Then preserve locked appointment windows And preserve manually set buffer durations unless they violate min/max or cause overlap, in which case raise a Buffer Conflict with recommendations And upon unlocking, re-enable automatic buffer maintenance for that segment
Route Reorder and Multi-Stop Handling
Given the route order or staff assignment changes for a set of stops When the new sequence is saved Then recalculate buffers between all newly adjacent pairs using live travel within configured min/max And do not insert a buffer after the final stop And insert a pre-start travel buffer before the first stop if travel is required from the staff’s start location And update ETAs and pending reminders for all affected stops And flag any resulting buffer conflicts with recommendations
Offer Composer & SMS Outreach
"As an independent walker, I want ready-to-send SMS offers for nearby clients so that I can quickly fill openings without leaving my route."
Description

Generate personalized, compliant SMS offers to eligible clients along the current route for selected gaps. Pre-fill arrival window, service length, pricing or package credit usage, and Pet Profile Smart Card link. Support quick templates, opt-out compliance, reply keywords (YES/NO), auto-reserve the suggested slot with hold and expiry, and instant booking on acceptance. Deduplicate outreach to avoid spamming and log outcomes for performance analysis.

Acceptance Criteria
Route-Optimized Gap Fill Suggestions
Given a live staff route with current traffic data and a selected open gap, When the system generates offer suggestions, Then each suggestion must add no more than the configured maximum detour time to the route and auto-insert travel buffers before and after the fill. Given multiple valid suggestions, When they are displayed, Then they are ranked by least added travel time and highest on-time arrival likelihood. Given a suggestion is chosen, When composing the offer, Then the arrival window must reflect the route ETA plus/minus the inserted buffers and be included in the SMS body.
Eligible Client Targeting Along Route
Given a selected gap on the route, When determining eligible recipients, Then only clients within the configured service radius of the gap corridor who have not opted out (DNC/STOP) and who have at least one active pet profile are included. Given historical services and staff capabilities, When filtering recipients, Then only clients whose requested or past service types match the staff’s allowed services for that gap are included. Given a client already has a booking overlapping the offered window, When computing eligibility, Then that client is excluded from outreach.
Pre-Filled Offer Details and Smart Card Link
Given an eligible client and selected service, When composing the SMS, Then the message must include the client’s pet name(s), the arrival window, the service length, and pricing. Given the client has available package credits for the service, When composing the offer, Then the pricing must show package-credit usage and remaining balance after booking; otherwise show standard price. Given the pet’s Smart Card, When composing the offer, Then include a unique, shortened Smart Card link parameterized to the pet/client and provider that opens to vaccine/preference status. Given tokenized quick-fill fields, When generating the message, Then all tokens resolve without placeholders remaining.
Quick Templates with Required Compliance Language
Given a library of quick templates, When a user selects a template, Then the system must render a preview with all merge fields resolved for the selected client and gap. Given SMS compliance requirements, When sending any outreach, Then the SMS must include business identification and opt-out language (e.g., “Reply YES to book, NO to decline. Reply STOP to opt out.”). Given message length constraints, When generating the final SMS, Then the content including the shortened link must not exceed the configured character limit for a single or concatenated SMS policy threshold.
Reply Keywords Handling and Instant Booking
Given an outreach SMS was sent with an active hold, When the client replies YES (case-insensitive) before hold expiry, Then the system instantly creates a booking in the held slot, applies package credits or standard pricing, updates the route and buffers, and sends a confirmation SMS. Given an outreach SMS was sent with an active hold, When the client replies NO (case-insensitive) before hold expiry, Then the system cancels the hold, frees the slot, and sends a polite decline confirmation SMS. Given a non-recognized reply, When received during an active hold, Then the system sends a clarification SMS instructing the client to reply YES or NO and does not change the hold state.
Auto-Reserve Hold with Timed Expiry and Release
Given an outreach SMS is sent, When the system targets a client for a specific gap, Then it must place a hold on that slot for that client with a configurable expiry time and prevent double-booking by others during the hold. Given the hold expires without a YES reply, When expiry occurs, Then the system automatically releases the slot, marks the outreach as expired, and (optionally) notifies the client that the offer is no longer available. Given a YES reply after expiry, When processed, Then the system responds that the slot is unavailable and proposes the next best route-optimized option if one exists.
Outreach Deduplication and Outcome Logging
Given multiple gaps or staff routes could target the same client, When preparing outreach, Then the system must send at most one active offer per client for overlapping time windows within a configurable cooldown period and suppress duplicates across staff. Given outreach events occur, When logging, Then the system records composed, sent, delivered (if available), reply received (content), hold created, hold expired, booked, declined, and failure events with timestamps, message IDs, recipient IDs, and template IDs. Given logged events, When viewing analytics, Then the dashboard must display send count, acceptance rate, median time-to-accept, fill rate, and average added travel minutes, and allow CSV export for the selected date range.
Multi-Staff Route Selection & Assignment
"As an owner managing a team, I want to optimize fills across staff routes so that I place new bookings where they cause the least disruption."
Description

Allow dispatchers and owners to select one or multiple staff routes for optimization, view each route with live ETAs, and assign suggested fills to the appropriate staff member. Enforce skills, service zones, and availability constraints, prevent double-booking, and respect role-based permissions. Provide a consolidated dashboard to compare detour impact across staff and choose the best assignment.

Acceptance Criteria
Select Multiple Staff Routes for Optimization
Given I am logged in as an Owner or Dispatcher And there are 2–10 active staff with routes for the selected day When I select 2 or more staff routes and click Optimize Then the system returns suggestions scoped to the selected staff only within 5 seconds And each selected staff is visually tagged as "Included", others as "Excluded" And I can add/remove staff and re-run optimization, with updated results returned within 5 seconds And if no staff is selected, the Optimize action is disabled with helper text "Select at least one route"
Live ETAs Display per Staff Route
Given live location and traffic data are available When I open a staff route Then the next two stop ETAs and current leg travel time are displayed And ETA and travel time update at least every 60 seconds or when change exceeds 2 minutes And the timestamp of last traffic data refresh is shown and is no older than 2 minutes And if live data is unavailable, the UI falls back to schedule-based ETA and displays "Live data unavailable" within 5 seconds
Enforce Skills, Service Zones, and Availability Constraints
Given a proposed fill with service type S, location L, duration D, and window [Tstart, Tend] When Route Saver evaluates eligibility for staff set R Then only staff whose profile includes skill S, whose service zone contains L, and whose availability includes a slot for D plus travel buffers within [Tstart, Tend] are marked Eligible And all ineligible staff are labeled with specific reason codes (MissingSkill, OutOfZone, Unavailable) visible on hover/click And the Assign action is disabled for ineligible staff And the eligibility decision is consistent across reruns with the same inputs
Prevent Double-Booking on Assignment and Conflict Resolution
Given staff X has existing bookings and buffers on the selected day When I attempt to assign a fill that overlaps any existing booking or buffer by ≥1 minute Then the system blocks the commit and shows an error message listing the conflicting appointment ID(s) and overlap minutes And when two users attempt to assign to the same time slot concurrently, the second commit is rejected with HTTP 409 Conflict and a prompt to refresh And successful assignments result in zero overlapping events on staff X's calendar as verified via calendar API
Consolidated Dashboard Detour Comparison and Ranking
Given multiple Eligible staff for a fill When I open the Consolidated Comparison view Then each row shows Additional Travel Time (minutes), Additional Distance (miles/km), Impact on downstream on-time probability (%), and Updated End-of-Day ETA And rows are sorted by Additional Travel Time ascending by default, with controls to sort by any column And selecting a row updates the route map preview and recalculates ETAs within 1 second And the Additional Travel Time equals (optimized route travel − baseline route travel) using current traffic and excludes service duration
Assign Suggested Fill with Auto-Inserted Travel Buffers
Given organization padding settings define PrePad and PostPad minutes When I confirm assignment of a selected fill to staff Y Then pre- and post-travel buffers are created with durations = predicted travel time to/from adjacent stops + PrePad/PostPad And the appointment start time is adjusted within the client's availability window to accommodate buffers without creating overlaps And the buffers and adjusted times are saved and visible on staff Y's calendar and in the client notification And if buffer editing is allowed by settings, I can adjust buffers within configured bounds and ETAs recalculate immediately
Role-Based Permission Controls for Dispatchers and Owners
Given roles Owner, Dispatcher, and Staff exist When a Staff user attempts to select multiple routes or assign a fill to another staff member Then the action is disabled in the UI and any direct API attempt returns HTTP 403 Forbidden And Owner and Dispatcher users can perform route selection, optimization, comparison, and assignment And all assignment attempts (success or failure) are written to the audit log with user ID, timestamp, staff target, and outcome
Real-time Re-optimization & On-time Risk Alerts
"As a groomer on the road, I want proactive alerts and one-tap fixes when my schedule slips so that I can stay on time and keep clients informed."
Description

Continuously monitor drift between planned and actual progress. When traffic spikes or an appointment runs long, re-score the route to propose re-ordering, buffer adjustments, or alternative fills. Trigger proactive alerts when on-time risk exceeds a threshold, with one-tap actions to apply the recommended change and auto-notify affected clients via SMS. Maintain an audit trail of changes and outcomes.

Acceptance Criteria
Traffic Spike Triggers Re-optimization
- Given an active route with upcoming stops and live traffic updates enabled, When predicted travel time for any upcoming leg increases by ≥15% or ≥5 minutes over plan, Then the system recomputes route scores and produces at least one ranked recommendation within 10 seconds. - Given recommendations are generated, Then each recommendation includes proposed stop order, inserted travel buffers (minutes), projected ETAs, per-stop on-time risk (%), and total detour (minutes). - Then no recommendation may violate hard constraints: service windows, staff working hours, pet-specific requirements, and max daily capacity. - When no recommendation can reduce overall on-time risk or detour, Then the system records "No beneficial change" with reason and does not prompt the user.
Appointment Overrun Adjusts Buffers and Order
- Given an appointment is in progress, When the actual duration exceeds planned by ≥20% or ≥3 minutes (whichever is greater), Then downstream ETAs are recalculated and travel buffers auto-adjusted within 5 seconds. - When the overrun causes any downstream stop's on-time risk to exceed 30%, Then a re-ordering recommendation is generated that keeps affected stops within their service windows. - Then the user can preview the before/after timeline and accept or dismiss the recommendation.
On-time Risk Alert with One-Tap Apply
- Given the on-time risk threshold is configured (default 25%), When any upcoming stop's on-time risk exceeds the threshold, Then an in-app alert is displayed to the assigned staff within 15 seconds showing impacted stops, risk percentages, the top recommendation, and an Apply button. - When the user taps Apply, Then the recommendation is applied in ≤2 seconds, ETAs are recalculated, and the route is locked to the new order. - When the user taps Dismiss, Then the same recommendation is suppressed for 10 minutes unless conditions change by ≥10% risk delta or a new conflict appears.
Auto-Notify Clients via SMS on Schedule Change
- Given a route change is applied, Then clients whose ETAs shift by ≥5 minutes receive an SMS update within 30 seconds using the configured template including pet name(s), new ETA window, and a reply-to number. - Then the system respects client opt-outs and business quiet hours; if within quiet hours, messages are queued and sent when the window opens, and the appointment card shows "notification queued". - Then delivery status (sent, delivered, failed) is recorded per message; on failure, the system retries up to 2 times and surfaces an in-app alert to contact the client manually.
Audit Trail Captures Changes and Outcomes
- For every re-optimization attempt (auto or user-initiated), Then an immutable audit entry is created capturing: timestamp, actor, trigger (traffic spike, overrun), original vs new stop order, buffers, ETAs, risk scores, recommendations presented, user action (applied/dismissed), and client notifications sent. - Then audit entries are searchable by date, route, staff, client, or trigger and exportable as CSV. - When an applied change is reverted within 10 minutes by the user, Then a new audit entry links the revert to the original change and restores prior plan and ETAs; follow-up notifications are sent accordingly.
Alternative Fill Suggestions Minimize Detour
- Given a gap of ≥15 minutes opens in the route within today's working hours, Then the system suggests at least 3 fill candidates from waitlist/open requests within a 5-mile radius or ≤10-minute detour, ranked by lowest added travel time. - For each candidate, Then the system displays expected detour minutes, new ETA to subsequent stops, and on-time risk impact; applying a fill auto-inserts travel buffers and sends offer/confirmation SMS per configuration. - Then suggestions must respect package balances, service types the staff can perform, vaccination requirements, and client preferences.

PreCheck Gate

Verifies vaccines, waivers, package credits, and a payment method on file before any offer is sent, ensuring every claimed slot is service‑ready and checkout stays fast.

Requirements

PreCheck Orchestrator Service
"As a groomer, I want an automatic precheck that verifies client and pet readiness before offers go out so that every claimed slot is service-ready and I avoid manual follow-ups."
Description

A synchronous backend service that aggregates and evaluates all readiness checks—vaccination validity, signed waivers, package credit availability, payment method on file, and account standing—before any booking offer is sent. It exposes a single idempotent API that returns a pass/fail decision with standardized reason codes and remediation hints, meeting a sub-500 ms p95 latency SLA. Integrates with Pet Profile Smart Cards for medical records, the payments vault for tokenized methods, and the packages ledger for credits. Supports multi-pet and multi-service scenarios, caches non-sensitive results for short TTLs, and emits structured events for analytics and monitoring. Includes rate limiting, circuit breakers, and detailed error mapping to ensure reliability across SMS-first workflows.

Acceptance Criteria
Pass decision for multi-pet multi-service within 500 ms p95
Given a booking request with 2–3 pets and 1–2 services per pet, all pets having valid vaccines and signed waivers, an account in good standing, and either sufficient package credits or a valid payment method on file When the client calls POST /precheck with a valid payload and X-Correlation-Id Then the API responds 200 within a measured p95 latency of < 500 ms over 1,000 requests and a timeout rate < 0.1% And the response decision equals "pass" And the response contains perPetService entries for every pet-service pair with status=pass And the response includes requestId, correlationId echo, and evaluatedAt timestamps
Fail decision with standardized reason codes and remediation hints
Given any single readiness condition is not met (e.g., expired vaccine, missing waiver, insufficient package credits, missing payment method, or delinquent account) When the client calls POST /precheck with X-Correlation-Id Then the API responds 200 with decision="fail" And the response includes at least one reason code from the allowlist {VACCINE_MISSING, VACCINE_EXPIRED, WAIVER_MISSING, PACKAGE_CREDITS_INSUFFICIENT, PAYMENT_METHOD_MISSING, ACCOUNT_DELINQUENT} And each reason code includes a non-empty human-readable hint actionable via SMS (e.g., link to Pet Profile Smart Card, add card link, purchase package link) And perPetService entries indicate fail only for the impacted pet-service pairs while others pass when applicable
Idempotent API behavior with repeat submissions
Given a valid POST /precheck request with an Idempotency-Key header and identical payload repeated within 24 hours When the request is retried 1–N times due to client/network retries Then the API returns HTTP 200 with an identical response body and status as the first successful call And no duplicate side effects occur (no duplicate analytics events without a dedup marker) And the response includes idempotencyKey and a stable requestGroupId linking all repeats And changing any request field or Idempotency-Key yields a new evaluation and a new requestGroupId
Non-sensitive caching with short TTLs and event-driven invalidation
Given repeated POST /precheck calls for the same pets and account within a short interval When evaluating readiness, the service may use cached, non-sensitive results with TTLs: vaccines/waivers=120s, paymentMethodOnFile=60s, packageCredits=30s, accountStanding=60s Then decisions reflect cached values only within TTL windows, after which fresh reads occur And upon receiving update events (PetProfileUpdated, WaiverSigned, PaymentMethodUpdated, PackageLedgerChanged, AccountStandingChanged), relevant cache entries are invalidated within 5s And no sensitive data (payment PANs, tokens, PII beyond IDs) is cached or logged
Downstream resiliency: rate limiting, timeouts, and circuit breakers
Given high request volume from SMS-first workflows When requests exceed 10 RPS per business (burst 20, token bucket) Then the API returns 429 with a Retry-After header and reason code RATE_LIMITED And downstream calls have a per-call timeout of 300 ms and a single retry with jitter when safe And a circuit breaker opens for a downstream after 5 consecutive timeouts/errors within 10s, remains open 30s, and fails fast with reason DOWNSTREAM_UNAVAILABLE and an actionable hint And overall decision defaults to fail with SYSTEM_ERROR if resiliency policies prevent a reliable evaluation
Structured events and observability for analytics and monitoring
Given any decision (pass or fail) When the API completes Then it emits a CloudEvents 1.0-compliant event to analytics and monitoring with fields {eventType=PrecheckEvaluated, decision, reasonCodes[], latencyMs, idempotencyKey, correlationId, requestId, perPetServiceCount} And pass decisions are sampled at 20% and fail decisions at 100% And service-level metrics are recorded: request count, error rate, p50/p95/p99 latency, downstream call success/timeout/error rates per dependency And traces include spans for each downstream call and propagate correlationId
Error mapping and response contract validation
Given any failure at the orchestrator or dependency level When the API responds Then HTTP status is 200 for business-rule outcomes (decision=pass|fail) and 5xx only for unrecoverable system failures And reason codes map deterministically: 408/504 from dependencies -> DOWNSTREAM_TIMEOUT; 5xx -> DOWNSTREAM_ERROR; 4xx schema/validation -> INVALID_REQUEST; auth failures -> UNAUTHORIZED; rate limit -> RATE_LIMITED; unknown -> SYSTEM_ERROR And the JSON schema includes {decision, reasons[], hints[], perPetService[], requestId, correlationId, evaluatedAt, version} and validates against OpenAPI contract And responses never include sensitive tokens/PII and truncate arrays to configured limits with Next-Page tokens when needed
SMS Offer Gatekeeper
"As a business owner, I want offers to be sent only when clients meet requirements so that I don’t waste slots or delay checkouts."
Description

An enforcement layer that intercepts outbound SMS offers and consults the PreCheck Orchestrator; only messages associated with a pass decision are sent. Failed checks are converted into guided remediation flows, including dynamic SMS with secure short links to update records, sign waivers, add payment methods, or purchase/adjust packages. Prevents bypass via dashboard or API by requiring a valid precheck token on all offer-creation calls. Provides configurable copy, localized templates, rate-limit handling, retries with exponential backoff, and delivery/engagement metrics to optimize conversion.

Acceptance Criteria
Gatekeeper Allows Offer Only With Valid Precheck Pass
Given an outbound offer creation request from API or dashboard includes a precheck_token bound to the customer, pet(s), service(s), appointment datetime, and business And the PreCheck Orchestrator returns pass for that token When the Gatekeeper processes the request Then the SMS offer is enqueued to the messaging provider within 2 seconds And exactly one SMS is sent per unique idempotency_key And the offer audit log records precheck_token_id, orchestrator_decision=pass, template_id, locale, and timestamps And the API responds 202 Accepted with offer_id and message_id
Gatekeeper Blocks Offer Without Valid Precheck Token
Given an outbound offer creation request lacks a precheck_token or uses an expired/invalid token When the Gatekeeper validates the request Then the request is rejected with 403 PRECHECK_REQUIRED and no SMS is sent And an audit entry records reason=missing_or_invalid_token and zero outbound messages And reuse of a token with mismatched customer/pet/service/time returns 409 TOKEN_MISMATCH and no SMS is sent And the dashboard UI displays a Run PreCheck prompt within 500 ms
Failed Precheck Triggers Guided Remediation SMS
Given an outbound offer creation request includes a precheck_token whose orchestrator decision is fail with one or more causes (vaccines_missing, waiver_unsigned, payment_missing, package_insufficient) When the Gatekeeper processes the request Then the offer SMS is not sent And a remediation SMS is sent within 5 seconds containing secure short links for each missing item and a help contact And the remediation SMS copy is selected from the configured template for the customer's locale with variable substitution for pet names and business branding And completion of all required items triggers issuance of a new pass precheck_token and notifies the business dashboard in real time And if all items are completed within 30 minutes, the original offer is auto-sent using the new token; otherwise it remains blocked
Secure Short Links With Expiring Tokens
Given a remediation SMS is generated When secure short links are created Then each link is signed, single-use, device-agnostic, and expires in 24 hours or immediately upon successful completion And opening an expired or already-used link returns HTTP 410 with a branded expired page and a CTA to request a new link And link clicks are uniquely tracked (de-duplicated per recipient) with timestamp, device type, and locale And personally identifiable information is not present in the link path or query string
Configurable, Localized Offer and Remediation Templates
Given a business has configured custom offer and remediation templates for one or more languages When the Gatekeeper renders a message Then the message uses the customer's preferred locale with fallback to business default, and if missing, to en-US And all variables (customer_first_name, pet_names, appointment_time, business_name, short_link) are rendered and safe for SMS (UTF-16/emoji) And an admin preview shows final rendering for a sample contact before publishing And template changes take effect within 2 minutes and are versioned; each sent message logs template_version
Rate Limiting and Exponential Backoff Policies
Given the messaging provider or PreCheck Orchestrator returns 429 or transient 5xx When the Gatekeeper schedules retries Then it uses exponential backoff with jitter starting at 1s, doubling up to 32s, max 5 attempts And per-business offer send rate does not exceed the configured limit (default 30 SMS/minute) And duplicate retries do not create multiple messages; idempotency is preserved And permanent errors (4xx other than 429) are not retried and are surfaced with error codes and guidance
Delivery and Engagement Metrics for Optimization
Given offers and remediation messages are sent When delivery receipts and click events are received Then the system records for each message: sent_at, delivered_at, failed_at (with reason), unique_clicks, first_click_at, precheck_completion, and conversion_to_booking And metrics are available in the dashboard within 5 minutes and via API with pagination and filters by date range, template_version, locale, business, and failure_reason And a standard report shows remediation completion rate and offer conversion rate by failure cause And PII is redacted from logs and metrics exports; access is permissioned
Smart Card Auto-Collection Flow
"As a pet parent, I want a simple link to update my pet’s records and waivers so that I can confirm a booking without back-and-forth."
Description

Automated collection workflow that triggers when vaccines or waivers are missing or expired. Sends a personalized SMS linking to the Pet Profile Smart Card where clients can upload vaccine proof (photo/PDF with OCR), capture digital signatures for waivers, and confirm pet preferences. Uses real-time webhooks to update the precheck decision and unblock the offer immediately upon completion. Includes reminders, expiration detection, accessibility-compliant forms, and secure file storage with retention policies. Surfaces completion status and issues in the provider dashboard with clear next steps.

Acceptance Criteria
Auto-Collection Trigger Prior to Offer Send
Given a provider attempts to send an offer for a pet whose required vaccines are missing or expired, or whose required waiver is not on file, When the offer is initiated, Then the PreCheck Gate blocks the offer and triggers the Smart Card Auto-Collection flow within 2 seconds. And the blocked state lists each unmet item by type and name (e.g., Rabies vaccine expired on YYYY-MM-DD; Liability Waiver missing). And no offer is sent until all blocking items are completed or an authorized override is recorded.
SMS Delivery and Reminder Cadence
Given the auto-collection flow is triggered, When the system sends the first message, Then the SMS includes the client first name, pet name(s), and a unique, single-use Smart Card link tied to the booking context. And the link token expires after 7 days or upon successful completion, whichever comes first; expired tokens redirect to a page to request a new link. And reminders are sent at T+24h and T+72h if incomplete, with a maximum of 3 messages in 7 days, respecting quiet hours (no sends between 8pm–8am recipient local time). And if the recipient replies STOP or the number is undeliverable, no further reminders are sent; delivery status is logged with reason codes; email fallback is attempted if available.
Vaccine Proof Upload with OCR Extraction
Given a client opens the Smart Card link, When uploading vaccine proof, Then JPG, PNG, and PDF files up to 10 MB each are accepted; users can add multiple files, crop, and rotate before submission. And OCR extracts vaccine names, dates administered, and expiration dates; extractions with confidence >= 0.9 auto-populate fields; confidence < 0.9 prompts client review and confirmation. And the system validates dates and flags expired or soon-to-expire vaccines (expiring within 14 days) with clear guidance; submission is blocked until required vaccines are provided. And files are virus-scanned, encrypted in transit and at rest, and retained for 24 months after vaccine expiration or account deletion (whichever comes first), after which they are automatically purged; all events are audit-logged.
Digital Waiver E-Signature Capture
Given a required waiver is outstanding, When the client views the waiver in the Smart Card, Then the content is accessible (WCAG 2.1 AA), readable on mobile, and requires scrolling through the body before enabling signature. And the client can sign by draw or type; upon submit, the system records signer name, timestamp (UTC), IP address, device/browser fingerprint, waiver version, and consent checkboxes. And a tamper-evident PDF with the signed content and audit trail is generated and stored securely; a receipt link is sent to the client; retention is at least 7 years unless the provider’s policy supersedes it.
Preferences Review and Confirmation
Given the Smart Card is open, When the client reviews pet preferences and medications, Then previously known values are prefilled and editable; required fields are indicated and validated client-side and server-side. And the form is fully accessible (labels, keyboard navigation, ARIA), supports screen readers, and prevents submission until all required fields pass validation. And on submit, the system records a versioned change set and emits a webhook containing only changed fields with timestamps.
Real-Time PreCheck Unblock via Webhooks
Given all blocking items (vaccines, waivers) are completed, When the client submits the last required item, Then a precheck.updated webhook with decision=pass is delivered within 5 seconds, signed (HMAC) and idempotent. And failed deliveries are retried with exponential backoff for up to 24 hours; duplicate deliveries are safely ignored by consumers via idempotency keys. And the booking UI unblocks automatically within 2 seconds of receiving the pass decision; if the UI is open, the state changes without reload; if closed, the state is correct on next open.
Provider Dashboard Status and Next Steps
Given a booking is on precheck hold, When a provider opens the dashboard, Then a status panel shows each requirement, its state (missing, pending, verified, failed), last update time, and clear next actions (review, resend link, override). And clicking a requirement opens details with artifacts (thumbnails, OCR data, signed waiver PDF) and allows verify/flag actions with role-based permissions; all actions write to an immutable audit log. And providers can resend the Smart Card link with one click and schedule an additional reminder; overrides require a reason and are visibly marked in the timeline.
Payment Method On-File Enforcement
"As a walker, I want clients to add a payment method before confirming a slot so that checkout is fast and payment is guaranteed."
Description

Verification that a valid tokenized payment method (card or supported wallets) exists before an offer can be sent or accepted. If absent or expired, sends a secure PCI-compliant payment sheet via SMS to add or refresh the method, supporting $0 authorizations or small reversible verifications and 3DS where required. Stores tokens in the existing vault, associates them with the client, and validates chargeability for checkout and no-show fees. Handles errors, retries, and fallbacks, and exposes status to both the orchestrator and the dashboard.

Acceptance Criteria
Block Offer Send and Accept Without Chargeable Payment Method
Given a client has no stored payment token or the token is flagged not chargeable When a provider attempts to send an offer for a slot Then PreCheck blocks the send, no offer SMS is dispatched, and the orchestrator receives reason code payment_method_missing or payment_method_not_chargeable And the dashboard thread displays a banner with a CTA to add a payment method Given a client without a chargeable payment method When the client attempts to accept an offer Then acceptance is blocked and a secure SMS link to the payment sheet is sent automatically
Add or Refresh Payment Method via Secure SMS Payment Sheet
Given a client receives a secure payment link by SMS When the link is opened within its TTL Then a PCI-hosted payment sheet renders with card entry and supported wallets (e.g., Apple Pay, Google Pay) Given the client submits valid payment details or authorizes a supported wallet When tokenization completes Then a token is stored in the existing vault, associated to the client as default And a $0 or <=$1 reversible verification is performed successfully And the client’s payment status becomes chargeable and the orchestrator is notified to resume any blocked flow Given the link is expired or already used When it is opened Then an "Link expired" state is shown and the user can request a new link Given verification fails or is declined When the sheet receives a failure response Then an actionable error is shown, the client may retry up to 3 times, and status remains not_chargeable
Validate Chargeability for Checkout and No-Show Fees
Given a client has a stored token marked chargeable When checkout captures the service amount Then the charge succeeds without requiring re-entry of payment details and a receipt is recorded Given a no-show is recorded for a booked slot When a no-show fee is assessed Then the stored token is charged; on decline, the dashboard shows the decline code and a refresh payment method SMS is sent Given processor policy requires re-verification When PreCheck runs prior to sending an offer Then a $0 or small reversible verification is attempted; on failure, the offer is blocked and a secure payment link is sent
Detect and Resolve Expired or Invalid Payment Tokens
Given the processor flags the stored token as expired, deleted, or otherwise invalid When PreCheck runs or a charge is attempted Then the token is marked not_chargeable, removed as default, and a refresh payment method SMS is sent; offers are blocked until a chargeable method exists Given multiple tokens exist for a client When one token becomes invalid Then another chargeable token is set as default automatically; if none exist, the client is prompted to add a new method
Error, Retry, Idempotency, and Fallback Handling
Given a transient network error or timeout occurs during verification When the system attempts to verify chargeability Then it retries up to 3 times with exponential backoff and jitter and preserves idempotency via a unique request key Given the payment gateway is unavailable after retries When verification still cannot complete Then the status becomes error with reason gateway_unavailable, offer sending is prevented, and the client is notified via SMS to try again later Given a user submits the payment sheet multiple times (e.g., double taps) When token creation requests are received Then only one token is stored and only one verification is performed Given a fallback processor is configured When the primary gateway is degraded Then verification is routed to the fallback processor and the processor_used is recorded
Expose Payment-On-File Status to Orchestrator and Dashboard
Given the orchestrator queries a client’s PreCheck state When the payment method status changes Then events payment_method_added, verification_succeeded, verification_failed are emitted with timestamps and reason codes And the dashboard reflects the new status within 2 seconds Given a user views a client in the dashboard When the client lacks a chargeable method Then a visible Not chargeable badge and a Send payment link action are displayed; when chargeable, a Chargeable badge is shown Given webhooks are configured When payment status transitions occur Then a signed webhook with HMAC is delivered; 2xx acks stop retries; non-2xx responses are retried with backoff for up to 12 hours
PCI/3DS Compliance and Secure Link Controls
Given the payment sheet is rendered Then it is hosted by a PCI DSS SAQ A–eligible provider; FetchFlow never handles raw PAN/CVV; only tokens are stored in the vault Given an SMS payment link is generated Then it uses HTTPS, contains a signed nonce tied to the client, is single-use, and expires in 24 hours; an expired or reused link returns an error state and cannot submit data Given the issuer or region requires SCA/3DS for card addition When the client attempts to add a card Then a 3DS challenge is performed; on success, tokenization proceeds; on failure, no token is stored and the user can retry or choose another method Given a small verification authorization is placed (e.g., $1) Then the authorization is reversed within 24 hours and never settled; logs capture verification_id and reversal status
Package Credit Hold & Ledger
"As a trainer, I want package credits auto-reserved during booking so that I don’t oversell and clients see accurate balances."
Description

A reservation system that places a temporary hold on applicable package credits during precheck to prevent double-spend when multiple offers are active. On claim, the hold converts to a debit; on cancel/expiry, the hold auto-releases. Supports multi-pet bookings, partial credits, and cross-service rules. Fully integrated with the existing packages ledger for atomicity and auditability, with idempotent operations, concurrency safeguards, and clear balance visibility in both client communications and the provider dashboard.

Acceptance Criteria
Hold Placement on Precheck for Eligible Package Credits
Given a client has eligible package credits for the requested service lines and an idempotency key for the precheck request When PreCheck Gate runs for the offer Then the system creates temporary hold(s) per eligible service line for the exact credit quantities required (up to the available balance) And a ledger entry of type hold_created is recorded with hold_id, package_id, service_code, pet_id(s), quantity, expires_at, and correlation_id And balances reflect available = previous_available − sum(hold.quantity) and on_hold = previous_on_hold + sum(hold.quantity) And the precheck response includes the hold_id(s), per-line quantities held, and expires_at for each hold
Hold Conversion to Debit on Offer Claim
Given active hold(s) exist for an offer When the client claims the offer (appointment is confirmed) Then each hold is atomically converted into a single debit ledger entry (event debit_posted) linked to hold_id and appointment_id And the original hold(s) are marked converted and removed from on_hold balances And operation is idempotent by correlation_id; retries do not create additional debits And receipt and provider dashboard display debited credits and updated remaining package balance
Auto-Release on Cancel or Offer Expiry
Given active hold(s) exist for an offer When the offer is canceled before claim or expires at the hold's expires_at Then the system posts a hold_released ledger entry with reason cancel or expiry And balances are restored (available increased by released quantity; on_hold decreased accordingly) And subsequent release attempts for the same hold_id are idempotent and produce no additional entries And automatic expiry-based release occurs within 60 seconds of expires_at
Multi-Pet and Partial Credit Allocation
Given a booking contains multiple pets and service lines and the client has limited and/or fractional package credits When PreCheck runs Then holds are allocated per line in deterministic request order until credits are exhausted And fractional credit quantities are supported to two decimal places and correctly reflected in ledger entries And any unheld remainder is marked payable at checkout and requires a valid payment method on file for service-ready status And the ledger records one hold_created entry per line with the exact quantity held; the sum equals total credits held across the booking
Cross-Service Eligibility and Rules Enforcement
Given packages define eligible service codes and cross-service rules/limits When holding credits for a mixed-service booking Then holds are applied only to eligible service lines according to the rules (including max-usage limits and pet-specific restrictions) And ineligible lines receive no hold and are marked payable at checkout And the rules version (or policy id) applied is recorded on each hold ledger entry And a 422 error is returned if no eligible credits exist and no valid payment method is on file
Concurrency, Idempotency, and Double-Spend Prevention
Given multiple precheck requests for the same client/pet/package may occur concurrently When 10 or more requests attempt to create holds simultaneously Then total on_hold across all holds never exceeds the starting available balance And only one hold is created per unique idempotency key; duplicate requests return the same hold details without new ledger entries And competing requests that would exceed balance fail with 409 Conflict and no hold_created entry is written And no two different offers can convert the same hold to debit
Ledger Atomicity, Auditability, and Balance Visibility
Given any hold, release, or debit operation is performed When the operation completes Then ledger writes are atomic with related appointment/offer state changes; either both persist or neither does And each ledger entry includes ISO8601 UTC timestamp, actor, event_type, hold_id/debit_id, package_id, correlation_id, service_code, pet_id(s), quantity, and pre/post balances And the provider dashboard displays available, on_hold, and used counts per package (and per pet where applicable) and lists active holds with source offer and expiry And client-facing communications for the offer show the number of credits to be applied and the projected remaining balance upon claim
No-Show Fee Pre-Authorization
"As an operator, I want a pre-auth for no-show fees so that I can enforce policies without awkward collection later."
Description

Configurable pre-authorization of a no-show fee or card verification at the time of precheck for services with a no-show policy. Supports amount tiers by service type, auto-voids on check-in, and gracefully falls back to card verification for wallets that don’t support preauth. Stores authorization IDs for reconciliation, includes retry/timeout policies, and complies with regional rules. Surfaces preauth status to staff and clients to set expectations and reduce post-service collections.

Acceptance Criteria
Preauth Gating Before Slot Offer
Given a client selects a service with an active no-show policy and a default payment method is on file And amount tiers are configured by service type When PreCheck Gate runs before sending a slot offer Then the system calculates the preauthorization amount using the correct tier for the selected service And sends a preauthorization request for that amount to the payment gateway And blocks the slot offer until a definitive result is received (Authorized, Verified Fallback, or Declined) And if no payment method is on file, the system halts and requests a payment method update via SMS link
Amount Tier Selection by Service Type
Given multiple service types each have configured no-show preauthorization amounts When a client is prechecked for a specific service type Then the preauthorization request amount equals the configured amount for that service type And the amount uses the correct currency for the business location And changes to tier configuration take effect for new prechecks within 1 minute
Fallback to Card Verification When Preauth Unsupported
Given the client's wallet or card brand does not support preauthorization holds When PreCheck Gate attempts to preauthorize Then the system performs a card verification per regional rules (zero-amount or minimal-amount) And no funds are held And the result is recorded as "Verified (Fallback)" with gateway response codes And the slot offer proceeds only if verification succeeds; otherwise it is blocked and the client is prompted to update payment
Auto-Void on Check-In
Given a pending preauthorization exists for the booking When the client is checked in for service Then the system voids the preauthorization within 10 seconds And records the void transaction ID and timestamp And updates status to "Voided" for staff and client And if the gateway does not support void, the authorization is marked to auto-expire and no capture is attempted
Declines, Retries, and Timeouts Policy
Given a preauthorization attempt returns Declined or times out When the retry policy is configured per environment settings Then the system retries according to the configured policy without creating duplicate holds And idempotency keys are used per attempt to prevent duplicate authorizations And after the final failed attempt, the slot offer is not sent and staff and client are notified with actionable guidance And all attempts and outcomes are logged with timestamps and gateway codes
Authorization ID Storage and Reconciliation
Given a successful preauthorization or fallback verification When the transaction completes Then the system stores authorization_id or verification_id, payment_method_id, amount, currency, gateway, and timestamps And values are immutable and retrievable via dashboard, CSV export, and API And each precheck has a unique attempt_id and a reconciliation_status field And missing or mismatched authorization records are flagged for review
Status Visibility to Staff and Clients
Given a preauthorization or verification is in progress or has completed When staff view the booking in the dashboard Then they see a status badge: Pending, Authorized, Verified (Fallback), Declined, Voided, or Expired And tapping or hovering reveals amount, method last4, and timestamp And clients receive SMS notifications for Authorized or Verified (Fallback), Declined requiring action, and Voided on check-in, with localized copy And all statuses update in under 2 seconds after gateway response
Staff Override & Audit Trail
"As a shop manager, I want to override the gate with an audit trail so that I can handle edge cases while maintaining accountability."
Description

Role-based controls in the dashboard that allow authorized staff to override specific precheck failures with mandatory reason codes, notes, and optional evidence attachments. Every override generates an immutable audit record with timestamp, user, affected checks, and downstream actions taken. Includes configurable permissions, alerting, and reporting to track override frequency, reasons, and outcomes, enabling policy tuning without compromising accountability.

Acceptance Criteria
Role-Based Override Access Control
Given a precheck failure is visible in the dashboard, When a user with the required override permission for that precheck type views it, Then the Override action is displayed and enabled. Given a precheck failure is visible in the dashboard, When a user without the required permission attempts to open the override dialog or call the override API, Then the system blocks the action with 403 Forbidden and no override is applied. Given override permissions are updated in Settings, When the same user retries the action, Then the new permissions take effect on the next request without requiring a service restart. Given permissions are scoped by precheck type, When a user has permission for vaccines but not payment method, Then only vaccine failures present an Override option and payment method failures do not.
Mandatory Reason and Notes with Optional Evidence
Given an authorized user initiates an override, When the form is submitted without selecting a reason code from the active list, Then validation fails and the override is not saved. Given the override form is open, When notes are fewer than 10 characters, Then submission is blocked with a clear validation message. Given the override form is open, When evidence files exceed 10 MB each or are not jpg/png/pdf, Then those files are rejected with an error and cannot be uploaded. Given a valid submission (reason code selected, notes >= 10 chars), When the user confirms, Then the override is recorded and the reason, notes, and evidence metadata (filename, size, content hash) are stored linked to the audit record. Given evidence is optional, When no files are attached, Then submission succeeds if all required fields are valid.
Immutable Audit Record Creation and Append-Only Amendments
Given an override is successfully applied, When the system writes the audit entry, Then it includes a unique ID, UTC timestamp, acting user ID and role, booking/offer ID, affected checks, reason code, notes, evidence metadata, and environment, and returns the ID. Given an audit record exists, When any user attempts to edit or delete it, Then the action is blocked (405/forbidden) and the original record remains unchanged. Given additional context is needed, When an authorized user submits an amendment, Then a new append-only audit event references the original record ID, and both records are visible in chronological order. Given downstream actions occur (e.g., booking accepted, no-show fee waived/applied, package credit applied, checkout completed), When they are detected for the same booking/offer, Then they are appended to the original audit record as child events with timestamps and actor/system attribution.
Partial Override of Specific PreCheck Failures
Given multiple precheck failures exist (vaccines, waiver, package credits, payment method), When an authorized user selects one or more failures to override, Then only the selected failures are marked Overridden and all others remain blocking. Given certain checks are configured as non-overridable, When a user attempts to override one of these, Then the system prevents submission and shows an error stating the check cannot be overridden. Given overrides are applied, When the PreCheck Gate re-evaluates the booking, Then the overall status is Pass only if all remaining failures are either resolved or explicitly overridden. Given a booking with overridden checks is later viewed, When the details panel is opened, Then each overridden check displays an Overridden badge with the overriding user and timestamp.
Override Alerting and Delivery
Given alert rules are configured (e.g., reason code = 'Emergency', missing payment method override, staff daily overrides > threshold), When an override matches a rule, Then alerts are sent to configured channels (in-app and email) within 60 seconds of override creation. Given an alert dispatch attempt fails, When retry logic executes, Then delivery is retried up to 3 times with exponential backoff and the final delivery status is recorded. Given an alert is received, When opened by a recipient, Then it contains audit record ID, staff name, role, location, reason code, affected checks, and a deep link to the audit record. Given an override does not match any alert rule, When it is saved, Then no alert is sent and no delivery is attempted.
Override Reporting and Outcomes
Given override events exist, When a report is generated with filters (date range, location, staff, role, precheck type, reason code), Then the results honor the filters and return within 5 seconds at the 90th percentile. Given a summary view shows counts and rates, When compared to raw audit events for the same filters, Then totals match within 1% and any discrepancy is flagged for review. Given a user exports the report, When CSV export is requested, Then the file downloads with normalized columns (UTC timestamp, staff, role, location, precheck type, reason code, overridden items, downstream outcomes) and the row count matches on-screen totals. Given a user lacks reporting permissions, When they attempt to access override reports, Then the system returns 403 Forbidden and no data is exposed.

Roll‑Down Offers

If the first client doesn’t claim, offers cascade automatically to the next best matches with smart timing, quiet-hour respect, and gentle heads‑up pings to boost acceptance.

Requirements

Match Scoring & Eligibility Filters
"As a groomer, I want roll-down offers to target only eligible, high-likelihood clients so that empty slots fill quickly without manual outreach or unproductive messages."
Description

Implements a server-side scoring engine that ranks potential recipients for a vacant slot using historical acceptance rates, proximity to service area, service compatibility, active packages, pet-specific constraints (size/breed/temperament), and client reliability (no-show history, outstanding balance). Applies hard filters to exclude ineligible clients, including expired vaccine records pulled from Pet Profile Smart Cards, conflicting appointments, Do-Not-Disturb flags, opt-outs, and blocked numbers. Exposes tunable weights per business, supports staff/resource constraints, and returns a deterministic, idempotent ordered list to seed the cascade. Integrates with existing CRM, calendar, Pet Profiles, and messaging services to ensure only high-likelihood, compliant candidates receive offers.

Acceptance Criteria
Hard Filters Exclude Ineligible Clients Pre‑Scoring
Given a vacant slot and a candidate with an expired vaccine record When eligibility filters run Then the candidate is excluded and does not appear in the ordered list Given a candidate with an appointment overlapping the slot When eligibility filters run Then the candidate is excluded and does not appear in the ordered list Given a candidate with Do‑Not‑Disturb active for the slot window When eligibility filters run Then the candidate is excluded and does not appear in the ordered list Given a candidate who has opted out of SMS or has a blocked number When eligibility filters run Then the candidate is excluded and does not appear in the ordered list
Deterministic, Idempotent Ordered List
Given identical inputs (same slot and unchanged data) When scoring is invoked multiple times Then the ordered list and scores are identical Given two candidates with equal scores When ordering the results Then the tie is broken by ascending client_id deterministically Given no input changes between evaluations When invoked at T and again at T+1 Then the outputs (order and scores) are identical
Business‑Tunable Weights Persist and Apply
Given default weights When no business overrides exist Then the engine uses the default weights Given an admin updates the proximity weight to 0.4 and saves When the next scoring run executes Then the proximity factor contributes at 0.4 in the score calculation Given an invalid weight value (e.g., negative or > 1) When saving weight changes Then validation fails and previously active weights remain in effect Given weights are updated successfully When fetching the current configuration Then the returned weights reflect the new values
Composite Scoring Ranks Higher‑Likelihood Candidates
Given weights acceptance=0.35, proximity=0.25, compatibility=0.15, packages=0.10, pet_constraints=0.10, reliability=0.05 And Candidate A has factor metrics 0.9, 0.8, 1, 1, 1, 0.8 respectively And Candidate B has factor metrics 0.7, 0.9, 1, 0, 1, 0.6 respectively When scoring runs Then Candidate A’s total score is higher than Candidate B’s and A appears earlier in the ordered list Given a candidate lacks an active package for the requested service When scoring runs Then the packages factor contributes 0 for that candidate Given a candidate has an outstanding balance and two no‑shows in the last 90 days When scoring runs Then the reliability factor is lower than for a candidate with no balance and no recent no‑shows
Staff and Resource Constraints Limit Eligibility
Given a slot requires specific staff skills or resources that are unavailable at the slot time When eligibility filters run Then candidates requiring those unavailable constraints are not included in the ordered list Given at least one qualified staff/resource combination is available for the slot When eligibility filters run Then candidates are evaluated and scored; otherwise the result is an empty ordered list
Integration‑Backed Data Drives Eligibility
Given vaccine expiration is retrieved from Pet Profile Smart Cards When a pet’s record is expired Then the associated candidate is excluded from the ordered list Given the CRM calendar shows a conflicting appointment for the candidate at the slot time When eligibility filters run Then the candidate is excluded from the ordered list Given the messaging service marks a client as opted‑out or blocked When eligibility filters run Then the candidate is excluded from the ordered list Given an integration read timeout or error for any eligibility data source When evaluation runs Then candidates whose compliance cannot be confirmed are excluded for that run
Cascading Offer Queue & Timeout Logic
"As a groomer, I want offers to cascade automatically when unclaimed so that I can keep my schedule full without constant follow-ups."
Description

Builds and manages the sequential offer queue for a single open slot, including configurable claim windows (e.g., 10–30 minutes), dynamic timeouts based on slot proximity, and automatic progression to the next candidate upon expiry or decline. Supports stop conditions (slot start imminent, max recipients reached, staff cancels, slot filled elsewhere), deduplication across households, and prevention of concurrent cascades for the same slot. Provides persistent state, idempotent retries, and recovery on service restarts. Emits events for notifications, analytics, and calendar updates and exposes administrative controls to pause, resume, or cancel a cascade.

Acceptance Criteria
Create Sequential Offer Queue for a Single Slot
Given an open slot with slotId and a ranked candidate list [r1, r2, r3] When a cascade is initiated for slotId Then the system creates a cascade with a unique cascadeId and a persisted ordered queue [r1, r2, r3] And sends an OfferSent to r1 only And sets cascade state to offered with currentRecipient=r1 And does not send offers to r2 or r3 until r1 declines or the claim window expires And emits an OfferSent event with {eventId, cascadeId, slotId, recipientId=r1, sentAt}
Configurable Claim Windows and Dynamic Timeouts
Given defaultClaimWindowMinutes is configured between 10 and 30 (inclusive) And proximityThresholdMinutes and proximityClaimWindowMinutes are configured And decisionBufferMinutes is configured When an offer is sent at time T for a slot starting at slotStart Then the expiryAt is T + claimWindow where claimWindow = (slotStart - decisionBufferMinutes - T) bounded by [proximityClaimWindowMinutes if (slotStart - T) <= proximityThresholdMinutes else defaultClaimWindowMinutes] And expiryAt is strictly < slotStart - decisionBufferMinutes And the offer record persists expiryAt and the active timer is scheduled accordingly
Auto-Progress on Decline or Timeout
Given r1 has an outstanding offer in cascadeId When r1 explicitly declines via any supported channel Then r1 is marked Declined with reason=UserDeclined And the system emits OfferDeclined and CascadeProgressed events And the next recipient r2 is offered within 5 seconds with a fresh claim window Given r1 does not respond until expiryAt When the timer fires Then r1 is marked Expired with reason=NoResponse And the system emits OfferExpired and CascadeProgressed events And the next recipient r2 is offered within 5 seconds with a fresh claim window
Stop Conditions Halt Cascade
Given an active cascade for slotId When any of the following occurs: (a) a booking is created that fills slotId, (b) staff cancels the cascade, (c) maxRecipientsPerCascade is reached without acceptance, (d) current time >= slotStart - stopWindowMinutes Then the cascade status transitions to Stopped with a specific stopReason And all pending offers are revoked/canceled and no further offers are sent And the system emits CascadeStopped event with {cascadeId, slotId, stopReason} And any subsequent trigger to progress the cascade is a no-op
Household Deduplication Across Candidates
Given the candidate list contains multiple recipients with the same householdId When the cascade queue is created Then only the highest-ranked recipient per householdId is included in the queue Given a household’s recipient has declined or expired in the current cascade When progression continues Then no further recipients from the same householdId are offered within this cascade And the queue and audit log reflect the deduplication decisions
Concurrency and Idempotency Safeguards
Given a cascade already exists in an active state for slotId When a second startCascade command is received (with same or different requestId) Then the system returns/uses the existing cascadeId and does not create a duplicate cascade or send duplicate offers Given a timer/webhook/command is retried with the same idempotency key When it is processed Then state transitions and events are not duplicated (idempotent handling) Given the service restarts while a cascade is active When it comes back online Then the cascade state is restored from persistence And pending timers are rescheduled within 30 seconds And no duplicate offers are sent on recovery And a CascadeRecovered event is emitted
Administrative Controls: Pause, Resume, and Cancel
Given an active cascade When an admin pauses the cascade Then no new offers are dispatched and active timers are suspended And the cascade status becomes Paused and a CascadePaused event is emitted Given a paused cascade When an admin resumes the cascade Then the currentRecipient is re-evaluated and timers are reinstated with correct remaining time And the next progression follows normal rules And a CascadeResumed event is emitted Given an active or paused cascade When an admin cancels the cascade Then the cascade status becomes Canceled, all pending offers are revoked, and no further offers are sent And a CascadeCancelled event is emitted
Event Emission for Notifications, Analytics, and Calendar Updates
Given any state transition (OfferSent, OfferDeclined, OfferExpired, CascadeProgressed, CascadePaused, CascadeResumed, CascadeCancelled, CascadeStopped, CascadeRecovered) When the transition occurs Then exactly one event with a unique eventId is emitted per transition and published to the event bus within 2 seconds And each event payload includes {eventId, eventType, occurredAt, cascadeId, slotId, recipientId (when applicable), reason (when applicable)} And events are persisted for auditing and are retriable by consumers without causing duplicate state changes due to idempotency keys
Quiet Hours & Smart Send Windows
"As a client, I want offers to arrive only at reasonable times so that they are helpful and not intrusive."
Description

Respects per-client and business-level quiet hours with timezone awareness to schedule offer and reminder sends only within approved windows. Defers or reschedules sends that would violate quiet hours, and provides safe fallback windows (e.g., next morning). Supports per-channel throttling and daily/weekly frequency caps across heads-up pings, offers, and nudges. Honors opt-out keywords and consent status, and records send decisions for auditability. Integrates with the messaging service to ensure compliance and with the cascade engine to avoid advancing during muted periods.

Acceptance Criteria
Client Quiet Hours Deferral with Local Timezone
Given a client has quiet hours set 21:00–08:00 in their profile and their timezone is America/Los_Angeles And the cascade engine attempts to send a heads-up ping at 22:15 PT When send evaluation runs Then the message is not sent immediately And a send task is scheduled for 08:00 PT the next calendar day And the attempt is recorded with decision=deferred reason=client_quiet_hours window=21:00–08:00 next_window_start=08:00 timezone=America/Los_Angeles And no other messages in the same campaign are sent to this client during the quiet window And if a DST transition occurs before the next window, the schedule remains at 08:00 local wall time
Business-Level Quiet Hours Enforcement
Given business quiet hours are configured 21:00–07:00 America/New_York and apply to all clients And it is 06:30 ET and an offer step is scheduled When evaluation runs Then the message is deferred until 07:00 ET And decision is recorded with decision=deferred reason=business_quiet_hours And if a client’s personal quiet hours are stricter, the later window prevails And the cascade does not advance due to this deferral
Frequency Caps Across Channels and Message Types
Given per-client caps: SMS daily=3 weekly=10; Email daily=2; Pings+Offers+Nudges share counters per channel And the client has already received 3 SMS today from these types When the system attempts to send a new SMS offer Then the send is blocked And decision is recorded with decision=blocked reason=frequency_cap_exceeded cap_type=daily channel=SMS current_count=3 attempted=1 window_resets_at=<timestamp> And the cascade engine evaluates the next candidate as if this client declined And no counter is incremented for the blocked send
Per-Channel Throttling and Queue Management
Given SMS provider rate limit is 30 messages per minute and throttling is configured accordingly And there are 200 eligible SMS to send at 09:00 in the same minute When dispatch runs Then no more than 30 SMS are sent per minute And remaining messages are queued and drained within the next 6 minutes respecting the limit And provider throttling errors are zero And send timestamps are recorded per message with queue_latency_ms
Opt-Out and Consent Compliance
Given a client’s consent state is opted_out or they have sent STOP to the SMS number When any heads-up ping, offer, or nudge is evaluated for send Then the send is not attempted on opted-out channels And decision is recorded with decision=blocked reason=opt_out channel=SMS opt_out_timestamp=<timestamp> And the cascade skips this client and moves to the next candidate without counting as a decline And re-consent is required before future sends can be attempted
Cascade Hold During Muted Periods
Given a roll-down offer is at candidate A And candidate A is in a quiet window (client or business) When the cascade engine tick occurs Then the cascade does not advance to candidate B due to no response And a timer is scheduled to re-evaluate at the start of candidate A’s next send window And a heads-up ping is scheduled within the open window according to smart timing rules And audit log records cascade_state=paused reason=muted_period next_check_at=<timestamp>
Decision Logging and Audit Export
Given any message is evaluated for send When a decision is made (sent, deferred, blocked, failed) Then a decision record is stored with fields: message_id, client_id, channel, message_type, decision, reason_code, evaluated_at, scheduled_for, timezone_used, quiet_hours_applied, caps_snapshot, throttle_snapshot, policy_version, actor=system And records are queryable via API and dashboard by date range, client, decision, reason_code And export is downloadable as CSV for a given date range and contains at least 10,000 rows without truncation And a sample audit can be produced within 48 hours of request
Heads‑Up Pings & Nudge Cadence
"As a groomer, I want gentle heads-up pings and limited reminders so that more clients accept offers without feeling overwhelmed."
Description

Introduces optional pre-offer heads-up pings and post-offer nudges to increase acceptance without spamming. Configures cadence (e.g., heads-up 30 minutes before likely slot, one reminder before timeout), content personalization (pet name/photo from Smart Cards, service and time), and frequency caps per client. Supports A/B testing of copy and timing, tracks engagement (reply, tap, claim), and suppresses pings for clients with recent interactions or within quiet hours. Fully integrated with the cascade queue, analytics, and consent management.

Acceptance Criteria
Heads‑Up Ping Timing and Enable/Disable
Given heads-up pings are enabled with lead_time=30 minutes and a candidate has a predicted slot at 10:00 local, When the clock reaches 09:30±2 minutes and the candidate is eligible, Then exactly one heads-up SMS is enqueued to that client, containing service name, slot start time in the client’s timezone, the primary pet name, an image/thumbnail URL if available, and a unique claim link, and the send is logged with a message ID. Given heads-up pings are enabled with lead_time=20 minutes (custom override), When the clock reaches 20 minutes before the predicted slot start, Then the heads-up is sent using the override and logged. Given heads-up pings are disabled globally or for the client, When the trigger time occurs, Then no heads-up is sent and a suppression event with reason=heads_up_disabled is logged.
Post‑Offer Nudge Before Timeout
Given an offer was sent at T0 with timeout T1 and nudge_offset=5 minutes and the client has not replied, clicked, or claimed, When the clock reaches T1−5 minutes, Then exactly one nudge SMS is sent and logged with reference to the originating offer/message ID. Given an engagement (reply, click, or claim) occurs before T1−5 minutes, When the nudge trigger time occurs, Then no nudge is sent and a suppression event with reason=already_engaged is logged. Given the offer is canceled, claimed by any candidate, or the slot expires before T1−5 minutes, When the nudge trigger time would occur, Then no nudge is sent and a suppression event with the appropriate reason is logged.
Send Suppression: Quiet Hours, Recent Interactions, and Frequency Caps
Given client quiet hours are 21:00–08:00 local, When a heads-up or nudge would be sent at 22:00, Then no SMS is sent and a suppression event with reason=quiet_hours is logged. Given a suppressed send due to quiet hours and the offer remains active and relevant at 08:00, When quiet hours end, Then a single deferred send is attempted at 08:00±5 minutes; if the offer is no longer active/relevant, no deferred send occurs and a suppression event with reason=no_longer_relevant is logged. Given a recent interaction cooldown window C=2 hours and the client had any inbound or outbound message within C before the scheduled send, When the heads-up or nudge trigger occurs, Then the message is not sent and reason=recent_interaction is logged. Given per-client caps are daily=X and weekly=Y, When a planned heads-up or nudge would exceed either cap within the client’s local day/week windows, Then the message is not sent and reason=frequency_cap is logged; cap counters increment only on successful enqueue events and are visible in logs.
Personalized Content from Smart Cards
Given the client has a Pet Profile Smart Card with name and photo, When a heads-up or nudge is sent, Then the SMS includes the pet’s name and a photo thumbnail URL; if multiple pets are associated to the service, include up to two pet names with a "+N more" suffix when N>0. Given a pet photo is unavailable, When the message is composed, Then the content falls back to pet name only without broken links or placeholders. Given the service and time window for the offer, When the message is composed, Then it includes the correct service name and the start time in the client’s timezone. Given GSM-7 encoding, When the message is composed, Then the total SMS length does not exceed 320 characters (or is split with proper UDH if exceeding) and the claim link is short and unique per message.
A/B Testing and Engagement Attribution
Given an active experiment with variants A and B at 50/50 split and eligibility rules matching the client, When a heads-up or nudge is sent, Then the client is deterministically bucketed, receives the assigned variant content, and the experiment ID, variant, and message ID are logged; no cross-over occurs for the client within that experiment. Given delivery receipts, keyword replies, link clicks, and claims, When these events occur, Then they are attributed to the originating message ID and experiment variant and appear in analytics within 5 minutes; deduplication ensures one claim per offer per client. Given experiment results, When metrics are viewed, Then the dashboard shows send, delivery, reply, click, and claim rates by variant with time range filters and exportable CSV including ISO 8601 timestamps and variant labels.
Cascade Queue Coordination and Auto‑Cancel Pings
Given Roll‑Down Offers manage a candidate queue for a slot, When any candidate claims or the slot is filled or canceled, Then all pending and scheduled heads-ups/nudges for that slot across all other candidates are canceled and cancellations are logged with reason=slot_unavailable. Given a heads-up is scheduled for the top candidate and capacity or ranking changes before send time, When the candidate is no longer next-in-line, Then the pending heads-up is canceled or reassigned per the new queue rules, and only the new top candidate can receive a heads-up. Given an offer cascades to the next candidate, When scheduling nudges for that candidate, Then their configured nudge offsets and suppression rules are applied independently of prior candidates, and no messages are sent to any candidate after a successful claim.
Consent and Opt‑Out Respect
Given the client’s consent status is opt-in, When a heads-up or nudge trigger occurs, Then the message may be sent; if consent status is opt-out, Then no message is sent and a suppression event with reason=consent is logged. Given the client replies STOP (or a local equivalent), When the STOP is received, Then consent status changes to opt-out within 1 minute, a confirmation is sent, and all pending/scheduled heads-ups and nudges are canceled with reason=consent. Given the client re-opts-in through compliant flow, When consent status is updated to opt-in, Then future heads-ups and nudges may be sent subject to all other suppression and cadence rules.
SMS Claim/Decline & Auto‑Confirm Flow
"As a client, I want to claim or decline a roll-down offer with a single text or tap so that booking is effortless and clear."
Description

Delivers a frictionless claim/decline experience via SMS keywords and a one-tap Smart Link. Presents slot details, price, location, applicable package credits, and policies; on claim, immediately confirms, reserves the slot, applies packages or deposits, and updates the calendar and payment state. On decline or expiry, gracefully thanks the client and advances the cascade. Handles edge cases such as simultaneous claims, expired links, or changed slot details with clear messaging. Supports multi-language templates and fallback to pure-SMS flows when links are unsupported.

Acceptance Criteria
SMS Keyword Claim: Instant Confirm, Apply Credits/Deposit, Update Calendar & Payment
Given an offer SMS including slotId, price, location, packageCreditCount, and policy summary is sent to a client with an active profile and a payment method on file When the client replies CLAIM within the offer validity window Then the system confirms by SMS within 5 seconds, reserves the slot, sets the event to Booked on the provider calendar, applies available package credits first (showing remaining credits) or otherwise authorizes the configured deposit, and updates the job/payment state in the dashboard And Then send a confirmation message containing date/time, location, price after credits/deposit, cancellation/no‑show policy, and a unique confirmation code And Then audit log records "claimed-via-sms" with timestamp, clientId, slotId, and payment action id And Then the offer is removed from all other candidates and the cascade stops for this slot Given no payment method on file and no package credits When the client replies CLAIM Then a secure payment link is sent immediately and the slot is held for 10 minutes And Then if payment completes within 10 minutes the booking confirms and deposit authorizes; else the hold releases and the cascade resumes
SMS Decline or Offer Expiry Advances Cascade
Given a client receives an offer SMS When the client replies DECLINE Then send a thank‑you message within 5 seconds, mark the candidate as declined for this offer, and advance the cascade to the next best match within 60 seconds Given no response before the offer expiry timestamp When the offer expires Then send an expiration notice to the client, prevent further claims for this offer from that client, and advance the cascade Given a late CLAIM arrives after expiry or after the slot is booked When processed Then respond with "Sorry, taken/expired" messaging and include a Smart Link to view other openings or opt into alerts; and do not create a booking or charge Then audit log records "declined" or "expired" with timestamps and nextCandidateId
Smart Link One‑Tap Claim with Full Offer Details
Given an offer SMS includes a Smart Link When the client opens the link Then the page displays pet name(s) and photo(s), date/time, duration, price, location, package credit availability and how many will be applied, and policy summaries, all localized to the client's language And Then the page loads in under 2 seconds on a 4G connection for the 90th percentile When the client taps Confirm Then booking is confirmed within 5 seconds, the slot reserves, applicable credits or deposit are applied, the dashboard and calendar update, and a confirmation SMS is sent When the client taps Decline or closes without confirming Then no booking is created and the cascade advances to the next candidate Given the link is opened after expiry When Confirm is tapped Then show an "Offer expired" message with a button to view other openings and do not book
Concurrent Claim Collision Handling
Given two or more clients attempt to claim the same slot concurrently via SMS or Smart Link When claims are processed Then exactly one booking is created and the slot is reserved once Then the winner receives a confirmation SMS; and losers receive a polite "already taken" SMS within 5 seconds Then any temporary payment holds for losers are voided/released within 15 minutes and no charges or credits are applied to them Then the audit log includes a collision record with slotId, winnerClientId, loserClientIds, and processing times And Then all claim endpoints are idempotent with respect to repeated CLAIM messages or repeated Confirm taps within a 60‑second window
Slot Details Changed After Offer Sent
Given an offer was sent and the slot's time, price, location, or policies change before claim When the client attempts to claim Then the system detects the change, invalidates the original offer, and presents updated details requiring an explicit re‑confirm When the client re‑confirms Then booking proceeds with the new details and a change notice is included in the confirmation SMS When the client does not re‑confirm Then no booking is created and the cascade continues with updated offers to remaining candidates Then audit log records "offer-updated" with old vs new fields and client response
Multi‑Language Templates and Pure‑SMS Fallback
Given a client has a preferred language in profile or inferred from locale When sending offer, confirmation, decline, and expiry messages Then the system uses the corresponding template set and falls back to English if the target language template is unavailable Then template tokens for {petName}, {date}, {time}, {price}, {location}, {creditsApplied}, {policySummary}, {confirmCode} render correctly in all languages Given the client's device or carrier blocks links or the client indicates links unsupported When sending an offer Then the system provides a pure‑SMS flow including the required details in at most 3 messages and supports CLAIM/DECLINE keywords without any link Then STOP and HELP keywords are honored in all languages per carrier compliance; and no further messages are sent after STOP Then all messages stay within 160 characters per segment and are concatenated safely where needed
Slot Locking & Conflict Resolution
"As a groomer, I want slot locking and fair conflict handling so that my calendar stays accurate and clients aren’t double-booked."
Description

Implements short-lived holds when an offer is sent and a stronger lock upon claim to prevent double bookings across staff, resources, and services. Uses transactional operations with server-side timestamps to resolve simultaneous claims, notifying non-winning recipients and offering waitlist alternatives. Automatically releases holds on decline, timeout, or cascade cancellation and reconciles changes if the slot is edited mid-cascade. Ensures calendar, reminders, and payment/fee rules remain consistent and auditable.

Acceptance Criteria
Short‑Lived Offer Hold on Send
Given a roll‑down offer is sent for an open timeslot When the offer is dispatched to recipient A Then the system creates a hold on the slot with status=held-offer scoped to staff, required resources, and service constraints And the hold uses a server-enforced TTL of 120 seconds (configurable) And while the hold is active, conflicting offers or bookings for the same scope cannot be confirmed And the hold is visible in the dashboard with timestamp, TTL countdown, and correlation ID to the offer And the hold is recorded in the audit log with server timestamp
Strong Claim Lock on Accept
Given recipient A taps Claim for the offered slot When the claim request reaches the server Then the system upgrades the hold to a claim-lock with status=claimed-pending And the lock prevents any other claims or bookings on the same scope And the lock persists until booking is confirmed or 180 seconds of inactivity elapse (configurable) And booking confirmation is atomic and idempotent via a single server transaction And on success the slot state becomes booked and all other holds on the same scope are released
Simultaneous Claim Conflict Resolution
Given recipients A and B submit Claim within a close interval for the same slot When the server processes both requests Then the winner is determined by the earliest server-received timestamp using a strictly monotonic clock And both recipients receive a definitive outcome within 1 second of resolution And the non-winning claimant is notified with reason=claimed-by-other within 5 seconds And the non-winning claimant is offered a one-tap waitlist option for that job
Auto‑Release on Decline, Timeout, or Cascade Cancel
Given an active hold or claim-lock exists on a slot When the recipient declines, the TTL expires, or the cascade is cancelled/passes the candidate Then the system releases the hold/lock within 1 second And downstream candidates are evaluated and messaged per smart timing and quiet-hour rules And calendars and availability are immediately updated to reflect the release And an audit log entry records release reason, actor, and timestamps
Mid‑Cascade Slot Edit Reconciliation
Given the slot’s time, duration, staff, or required resources are edited while offers/claims are in flight When the edit is saved Then the system atomically recalculates holds and eligibility against the new slot attributes And invalid or conflicting holds/claims are cancelled with notification and waitlist option And still-valid claims retain precedence and are migrated to the updated slot if compatible And affected recipients are notified of material changes within 10 seconds And the audit trail links the edit to all affected holds/claims via correlation IDs
Cross‑Staff/Resource/Service Double‑Booking Prevention
Given a slot hold or lock exists for staff S and resources R for service X over time window T When any user or automation attempts to confirm another booking overlapping T that requires S or any r in R or violates service X constraints Then the system blocks the confirmation with error=conflict and identifies the conflicting hold/booking And no calendar entries or reminders are created for the blocked attempt And the attempt is logged with actor, timestamp, and conflict reference
Calendar/Reminders/Payments Consistency & Auditability
Given a slot transitions from held/claimed to booked or released When the final state is committed Then calendar entries are created or freed to reflect the state And reminder jobs are scheduled, updated, or cancelled accordingly within 5 seconds And package application, deposits, and no‑show fee rules are applied consistently and idempotently And all state transitions (offer, hold, claim, win/loss, release, book) are recorded with server timestamps, actor, and payload snapshots for audit
Offer Analytics & Audit Trail
"As a business owner, I want visibility into roll-down performance so that I can optimize timing, targeting, and messaging for better fill rates and revenue."
Description

Captures end-to-end funnel metrics for roll-down offers, including candidate list composition, send/receive timestamps, opens/clicks, claims/declines, time-to-fill, and ultimate revenue impact. Breaks down performance by time of day, client segment, service type, and messaging variant. Provides an in-dashboard view with export, filters, and cohort comparisons, plus a complete message and decision audit trail for support and compliance. Surfaces actionable insights to tune scoring weights, timing windows, and copy to maximize acceptance and reduce no-shows.

Acceptance Criteria
End-to-End Funnel Metrics Capture
Given a roll-down offer with a defined candidate list is initiated When the first offer is sent Then a candidate_list_snapshot is stored with offer_id, candidate_id, rank, match_score, and total_candidates Given carrier delivery receipts are received for SMS messages When a delivery receipt arrives Then delivered_at is stored per candidate within 2 minutes of receipt and delivery_status is updated Given client interactions occur When an open/read (where supported), link click, claim, decline, or timeout event happens Then the corresponding timestamp and channel are recorded against the candidate and offer Given a slot is claimed When the claim is finalized Then time_to_fill is computed as claimed_at minus first_offer_sent_at and stored at offer level Given the booked service tied to the offer is completed and payment is processed When revenue is posted Then revenue_attributed_to_offer is recorded in currency units on the offer with reference to the transaction_id
Dashboard Filters and Cohort Comparisons
Given the analytics dashboard is open When a user applies filters for date range, time-of-day (hour bucket), client segment (new/returning/VIP), service type, and messaging variant Then KPIs (acceptance_rate, median_time_to_fill, no_show_rate, revenue_per_offer) update within 2 seconds and reflect only the filtered dataset Given a filtered dataset When totals and charts are displayed Then acceptance_rate = claims_delivered / messages_delivered, no_show_rate = no_shows / completed_appointments, and calculations match the underlying rows exactly Given two cohorts are selected via Compare (e.g., Variant A vs Variant B) When comparison is enabled Then side-by-side metrics are displayed with absolute and relative lift for acceptance_rate, time_to_fill, no_show_rate, and revenue_per_offer; if either cohort has <100 offers, an "insufficient sample" notice is shown
Analytics Data Export (CSV)
Given any set of dashboard filters is applied When the user clicks Export CSV Then a CSV is generated within 60 seconds for datasets up to 100,000 rows and download begins automatically Given the CSV is generated When the file is opened Then it contains a header row and the following columns at minimum: offer_id, org_id, created_at_utc, first_offer_sent_at_utc, candidate_id, candidate_rank, match_score, delivered_at_utc, read_at_utc, click_at_utc, claim_at_utc, decline_at_utc, timeout_at_utc, client_segment, service_type, messaging_variant, accepted_flag, no_show_flag, time_to_fill_seconds, revenue_attributed_cents Given the export is filtered When row counts are compared Then the number of rows and aggregates in the CSV exactly match the on-screen filtered totals Given exports may be re-imported to BI tools When timestamps are parsed Then all timestamps are ISO 8601 in UTC, numeric fields use dot decimal separators, and text fields are UTF-8 encoded without embedded newlines in cells Given role-based access control When a user without Analytics Export permission attempts export Then the Export option is hidden and direct export endpoints return 403
Offer-Level Audit Trail for Support and Compliance
Given a support agent opens an offer detail view When the Audit Trail tab is selected Then a chronological, immutable log is displayed including event_id, timestamp, actor (system/client/user), channel (SMS/in-app), event_type (message_sent, delivered, read, click, claim, decline, timeout, config_change), status, and content_preview (first 160 chars) Given sensitive content may exist When full message content is viewed or exported Then PII masking rules apply (phone numbers masked to last4, tokens masked) and the original unmasked content is never editable Given audit integrity requirements When any user attempts to modify or delete an entry Then the system prevents changes; only additive admin annotations are allowed and are logged with user_id and timestamp Given compliance export is requested When the user clicks Download Transcript Then a PDF and CSV of the full audit trail for the offer, including metadata and hash of content for integrity, are generated within 30 seconds
Data Quality, Timezone, and Late Event Handling
Given duplicate events are received from providers When events share the same event_id and source Then only one record is stored and aggregates are unaffected (idempotent ingestion) Given late-arriving events up to 24 hours after occurrence When such events are ingested Then aggregates and charts are recalculated within 5 minutes and the affected time buckets are marked as backfilled Given a selected date range and filters When totals are compared to the sum of underlying rows Then discrepancies equal 0 for counts and <=0.1% rounding variance for rates due to formatting Given a user with timezone preference America/Los_Angeles When viewing dashboards Then all displayed times are localized to the user timezone while raw export timestamps remain in UTC
Actionable Insights and Recommendations
Given the last 30 days contain at least 200 offers per cohort When a messaging variant or timing window shows >=5 percentage point absolute acceptance lift with p<0.05 Then the Insights panel surfaces a recommendation summarizing the change, expected impact, sample size, and a link to the relevant settings page Given a recommendation is accepted When the user clicks Apply Then the corresponding configuration (e.g., copy variant weighting or timing window) is updated immediately, the change is logged in the audit trail with user_id and timestamp, and a success toast confirms the update Given a recommendation is not wanted now When the user clicks Dismiss Then the recommendation is snoozed for 30 days and will resurface only if the observed lift increases by >=3 points or the sample size doubles Given no-shows are tracked for offers When a segment shows a no_show_rate >=2 percentage points above baseline Then an insight is surfaced proposing scoring weight or timing adjustments aimed at reducing no-shows, with an estimated impact range

Fill Rate Insights

A dashboard highlights gaps filled, revenue recovered, time-to-fill, and top-performing rules, helping you tune priority weights and timing for even better results.

Requirements

Real-time Fill Rate Overview
"As a small pet-service business owner, I want a live view of my fill rate and gaps filled so that I can quickly see how well my schedule is being recovered and where to focus."
Description

A mobile-ready dashboard module that aggregates openings created, gaps filled, and overall fill rate percentage across selectable date ranges, staff, and services. It pulls events from bookings, cancellations, no-shows, waitlist confirmations, and rule-engine actions, normalizes time zones, and refreshes within a 15-minute SLA. Visualizations include KPI tiles, trend lines, and heatmaps by hour and day-of-week. Filters and drill-downs allow viewing by service type, staff member, location, and channel (automation vs manual). Access is role-based for owners and managers, and all values are exportable for further analysis.

Acceptance Criteria
15-Minute Data Refresh SLA
- Given new events (bookings, cancellations, no-shows, waitlist confirmations, rule-engine actions) are recorded, when 15 minutes elapse, then the dashboard refresh includes all events up to the last 15-minute window. - Given an event at time T in any staff/service/time zone, when viewed at T+15 minutes or later, then KPI tiles, trends, and heatmaps reflect the event. - Given heavy load ≥10k events/hour, when refresh runs, then data latency remains ≤15 minutes p95 and ≤20 minutes p99. - Given a failed refresh cycle, when the next cycle runs, then missed events are backfilled with no duplicates and the last successful sync time is displayed.
Time Zone Normalization and Date Range Accuracy
- Given a user with profile time zone X selects date range A–B, when viewing metrics, then all aggregations are computed in X regardless of event source time zones. - Given a DST transition within the range, when rendering hourly heatmaps, then counts reflect 23/25-hour days without double-counting or missing hours. - Given a manager in time zone X views data for a location in Y, then filters and exports reflect X, while drill-down rows include original event timestamps labeled with their source time zone. - Given the same filters and range, then totals on KPI tiles, trend lines, and heatmaps match within 0.1% rounding tolerance.
Filter, Drill-down, and Channel Segmentation
- Given filters for service type, staff, location, and channel are applied, then all visualizations update within 2 seconds p95 on mobile and include only matching data. - Given channel=Automation, then gaps filled from rule-engine actions are included and manual entries excluded; vice versa for channel=Manual. - Given a user taps a KPI tile or heatmap cell, then a drill-down appears with a paginated list of contributing events showing: event type, pet, staff, service, channel, timestamp. - Given multiple filters are combined, then exporting the drill-down yields a CSV containing only the filtered subset with totals equal to on-screen values.
Fill Rate Computation and Data Consistency
- Given openings created=O and gaps filled=F within the selected range and filters, then Fill Rate=(F/O)*100 displayed with one decimal; if O=0, display 0% and tooltip "No openings." - Given cancellations/no-shows rebooked from waitlist within the range, then recovered fills count toward F once and are not double-counted in trends or heatmaps. - Given booking edits (reschedules), then metrics adjust to reflect net openings created and fills without creating duplicate events. - Given identical filters, then KPI tile totals, trend line aggregates, and heatmap sums are equal within 0.1% rounding tolerance.
Role-Based Access and Mobile Readiness
- Given user role∈{Owner, Manager}, then the module is accessible; for other roles, access is denied with HTTP 403 and no data leakage. - Given mobile viewport widths 320–768px, then KPI tiles, trend lines, and heatmaps render without horizontal scroll and with text size ≥12pt. - Given device rotation, then visualizations reflow within 500 ms without layout overlap or truncation. - Given an authorized session expires, then accessing the module prompts re-authentication before any data is fetched.
Exportability and Data Parity
- Given a user taps Export on any visualization, when choosing CSV, then a file downloads within 10 seconds containing metrics scoped by active filters, date range, and time zone noted in a header row. - Given exports of KPI tiles, trends, and heatmaps for the same filters/range, then totals match on-screen values within rounding rules. - Given multi-location data, then each exported row includes location ID and name; timestamps are ISO 8601 with time zone offset. - Given datasets >100k rows, then export streams or provides a downloadable link within 60 seconds with a progress indicator; on failure, a retry option is presented.
Event Ingestion and Source Integrity
- Given a booking, cancellation, no-show, waitlist confirmation, or rule-engine action occurs, then an event with unique ID, type, timestamp, staff, service, location, and channel is stored within 2 minutes of occurrence. - Given duplicate webhook deliveries or retries, then ingestion is idempotent and produces a single stored event. - Given a delayed event (>1 hour late), then it is included in the next refresh and backfilled into historical aggregates without altering unrelated periods. - Given data corruption or schema mismatch at ingest, then the event is quarantined, an alert is logged, and quarantined counts are visible to owners while excluded from metrics.
Recovered Revenue Attribution
"As a business owner, I want to see how much revenue each automation actually recovers so that I can invest in the rules that generate the most cash flow."
Description

An attribution layer that calculates revenue recovered from filled gaps and ties it to the originating rule, campaign, or manual action. It reconciles invoices, payments, packages, discounts, and no-show fees to present net recovered revenue per fill. The engine excludes simple reschedules that do not backfill a net-new gap and de-duplicates multi-touch interactions using a last-touch within a configurable lookback window. Outputs include totals and breakdowns by rule, service, staff, and channel, with traceable audit detail down to the appointment ID.

Acceptance Criteria
Last-Touch Attribution within Configurable Lookback
Given a configurable lookback window of 72 hours And a previously open gap is filled by appointment A1 at 2025-08-11T15:20:00Z And recorded touches for the pet owner are: R1 (Rule) at 2025-08-10T09:00:00Z, C1 (Campaign) at 2025-08-11T14:00:00Z, M1 (Manual) at 2025-08-11T15:00:00Z When the attribution engine evaluates A1 Then M1 is credited as the single last-touch attribution within the lookback window And R1 and C1 receive no attribution credit for A1 And the attribution result persists channel="Manual", source_id="M1", lookback_window_hours=72 And if all touches fall outside the 72-hour lookback, A1 is marked unattributed (channel="None")
Exclude Simple Reschedules with No Net Backfill
Given appointment A2 originally scheduled at 2025-08-11T14:00:00Z is rescheduled to 2025-08-11T15:00:00Z And no other appointment is booked into the original 14:00–14:30 slot before or after the move When the attribution engine processes A2 Then recovered revenue for A2 equals $0.00 And A2 is excluded from the "gaps filled" count And A2 appears in the audit trail with reason="reschedule-no-backfill" And overall recovered revenue totals are unchanged by A2
Net Recovered Revenue Computation and Reconciliation
Given appointment A3 fills a previously open gap And invoice I3 includes service lines totaling $60.00, a discount of $10.00, a package redemption of $20.00, and a no-show fee of $15.00 And payments applied toward I3 total $30.00 And a refund of $5.00 is applied after checkout When the engine computes net recovered revenue for A3 Then net_recovered_revenue for A3 equals $10.00 And the formula used is (payments + no_show_fees) - (discounts + package_redemptions + refunds) And the amounts reconcile to invoice I3 and payment/refund records by their IDs And the stored value is rounded to two decimal places using bankers rounding
Multi-Touch De-duplication across Rule, Campaign, and Manual
Given a 48-hour lookback window And the pet owner receives touches: R2 (Rule) at T-36h, C2 (Campaign) at T-4h, R2 (Rule) again at T-2h, and M2 (Manual) at T-90m And appointment A4 fills a gap at time T When the engine attributes A4 Then M2 receives 100% attribution credit as the most recent touch within the window And all other touches are marked seen-but-not-attributed in the audit for A4 And if M2 is deleted and attribution is recomputed, credit shifts to the R2 touch at T-2h And the audit history preserves prior attribution snapshots with versioning
Audit Trail Traceability to Appointment and Source
Given recovered revenue record RR5 exists for appointment A5 that filled a gap When fetching the audit details for RR5 Then the audit includes: appointment_id, fill_id, source_type, source_id, rule_id (if applicable), channel, attribution_timestamp (ISO-8601 UTC), lookback_window_hours, invoice_id(s), payment_id(s), discount_id(s), package_redemption_id(s), no_show_fee_id(s), refund_id(s), and net_recovered_value And each referenced ID resolves to an existing record via API links And exporting the audit for A5 as CSV reproduces the same values with consistent rounding and timestamp format And single-fill audit retrieval p95 latency is <= 500 ms in a dataset of 100k fills
Breakdown and Totals Consistency by Rule, Service, Staff, and Channel
Given dataset D with five fills and net recovered values: F1=$10 (Rule R1, Service S1, Staff ST1, Channel SMS), F2=$25 (Rule R1, Service S2, Staff ST1, Channel SMS), F3=$40 (Campaign C1, Service S1, Staff ST2, Channel SMS), F4=$30 (Manual, Service S2, Staff ST2, Channel Manual), F5=$20 (Unattributed, Service S1, Staff ST1, Channel None) When generating totals and breakdowns for D Then overall total equals $125.00 And breakdown by rule equals: R1=$35.00, C1=$40.00, Manual=$30.00, Unattributed=$20.00 (sum=$125.00) And breakdown by service equals: S1=$70.00, S2=$55.00 (sum=$125.00) And breakdown by staff equals: ST1=$55.00, ST2=$70.00 (sum=$125.00) And breakdown by channel equals: SMS=$75.00, Manual=$30.00, None=$20.00 (sum=$125.00) And each fill appears exactly once in every dimension; rounding differences do not exceed $0.01
Time-to-Fill Tracking
"As a scheduler, I want to know how quickly open slots get filled so that I can adjust my outreach timing for faster recovery."
Description

Accurate measurement of the elapsed time from gap creation (cancellation, no-show, or new availability) to confirmed rebooking, with metrics including average, median, and p90. The system tags each gap with creation and fill timestamps, channel of fill, and service context, and visualizes distributions and heatmaps by daypart and weekday. Supports comparisons across periods and cohorts to assess operational improvements.

Acceptance Criteria
Accurate Gap Tagging and Time-to-Fill Computation
Given a gap is created due to cancellation, no_show, or new_availability When the event is recorded Then the system stores gap_id, creation_timestamp in ISO 8601 (UTC and org_tz), gap_reason ∈ {cancellation,no_show,new_availability}, service_id, staff_id, location_id, pet_id(s), duration_minutes, and price_cents Given a subsequent booking fully occupies the gap’s free minutes on the same resource and is set to Confirmed When the booking is saved Then the system stores fill_timestamp (UTC and org_tz) and channel_of_fill ∈ {SMS, Web, Auto_Rule, Waitlist, Admin_Manual} on the gap record Given a gap record has both creation and fill timestamps When time-to-fill is computed Then time_to_fill_minutes = max(0, floor((fill_timestamp_utc - creation_timestamp_utc)/60)) and is persisted Given a gap is not filled by the end of its availability window or is archived When aggregates are computed Then the gap remains with null fill_timestamp, is counted in Unfilled metrics, and is excluded from elapsed-time aggregates
Timezone and DST-Safe Elapsed Calculation
Given org timezone is America/Los_Angeles and a gap is created at 2025-03-09T01:30-08:00 and filled at 2025-03-09T03:10-07:00 (after DST jump) When time_to_fill_minutes is computed Then the result equals 40 minutes (actual wall-clock elapsed), not 100 Given a gap is created at 23:50 local time and filled at 00:05 the next day When time_to_fill_minutes is computed Then the result equals 15 minutes Given the org timezone setting is changed after events are recorded When time_to_fill_minutes is computed Then elapsed uses stored UTC timestamps and is invariant to timezone configuration; only display offsets change Given timestamps include seconds When time_to_fill_minutes is computed Then rounding policy is floor to whole minutes
TTF Metrics Aggregation (Average, Median, p90)
Given a selected date range and filters (org, location, staff, service, channel_of_fill) When the dashboard loads Then it displays average_minutes, median_minutes, and p90_minutes for all filled gaps in-filter Given a sample set of time_to_fill_minutes = [5, 10, 20, 100] When aggregates are computed Then average_minutes = 33.75, median_minutes = 15, and p90_minutes = 100 using nearest-rank (ceil(p*n)) on the sorted list Given any records with null or negative elapsed values are present When aggregates are computed Then those records are excluded from aggregates and surfaced in a Data Quality alert with the count Given a user updates any filter When aggregates recompute Then metrics refresh within 2 seconds for datasets with ≤10,000 gaps
Distribution and Heatmap Visualizations
Given TTF data is available When the user opens the Distribution view Then a histogram renders with default 15-minute bins up to 24 hours, shows counts per bin, and hover reveals bin range and count; user can switch bin width to 5 or 60 minutes Given TTF data is available When the user opens the Heatmap view Then a heatmap renders showing median TTF by weekday (Mon–Sun) vs daypart buckets: Morning (06:00–11:59), Afternoon (12:00–17:59), Evening (18:00–21:59), Overnight (22:00–05:59) Given a heatmap cell is hovered When the tooltip appears Then it shows cohort label, sample size n, median, and p90; cells with n < 5 display “n<5” and are visually muted Given a heatmap cell is clicked When the detail pane opens Then it lists up to 50 contributing gaps with links to the underlying gap records
Period and Cohort Comparisons
Given Period A and Period B are selected When metrics render Then the dashboard shows side-by-side average, median, p90 and deltas where delta_minutes = A − B and delta_percent = (A − B)/B rounded to 1 decimal; B = 0 yields N/A for delta_percent Given a cohort dimension is selected (staff, location, service, channel_of_fill) When metrics render Then the dashboard lists cohorts with their metrics and supports sorting by median or delta Given the cohort dimension is changed When charts and tables update Then all views reflect the new dimension consistently within 3 seconds for ≤50 cohorts
Edge Cases: Partial Fills, Reopens, and Merges
Given a gap is partially filled by a shorter booking that does not eliminate the original free minutes When evaluating fill status Then the gap remains Unfilled; fill_timestamp is set to the time of the booking that fully eliminates the gap’s free minutes Given a filled gap’s booking is later cancelled within the original gap window When the cancellation occurs Then the gap reopens with a new creation_timestamp equal to the cancellation time; the prior fill is retained in audit history but excluded from aggregates Given two adjacent gaps are merged by an operator action When the merge is saved Then a new gap_id is created with creation_timestamp = merge time and the prior gaps are archived and excluded from new TTF aggregates Given multiple bookings could be associated to a gap When associations are saved Then each booking can fulfill at most one gap_id and each gap_id links only to the minimal set of bookings required to eliminate its free minutes
Data Freshness, Access, and Auditability
Given a gap create/fill/reopen event occurs When data pipelines process the event Then it appears in the dashboard within 5 minutes at the 95th percentile and within 10 minutes at the 99th percentile Given a user with Insights permission accesses the TTF dashboard When authorization is checked Then only organization-scoped data is visible; users without permission receive an access denied message Given an audit export is requested When the CSV is generated Then each row includes gap_id, creation_timestamp_utc, creation_timestamp_org_tz, fill_timestamp_utc, fill_timestamp_org_tz, time_to_fill_minutes, gap_reason, channel_of_fill, service_id, staff_id, location_id with timestamps in ISO 8601 Given a random audit sample of 20 gaps is selected When manual recalculations are performed Then system time_to_fill_minutes matches within ±1 minute for 100% of the sample
Top Rule Leaderboard
"As a marketer for my grooming business, I want to see which rules perform best so that I can optimize my automations and retire weak ones."
Description

A ranked view of rule performance showing conversions, recovered revenue, time-to-fill reduction, and response rates per rule. The leaderboard enforces minimum sample sizes and highlights statistically meaningful differences, surfacing the top-performing rules and underperformers. Users can drill into a rule’s history, triggers, audience, and message variants, with links to edit or pause the rule.

Acceptance Criteria
Leaderboard Ranks and Shows Core Metrics
Given the user has access to Fill Rate Insights and data is available for the selected period When the user opens the Top Rule Leaderboard Then a ranked table is displayed with columns: Rank, Rule Name, Conversions, Recovered Revenue (currency), Time-to-Fill Reduction (minutes and %), Response Rate (%), Sample Size (messages delivered), Rule Status And the default sort is Recovered Revenue descending; ties are broken by Conversions descending, then Response Rate descending And each metric value is present for every rule meeting the sample threshold And an empty state "No data for selected period" is shown if no rules meet the criteria
Enforce Minimum Sample Size Threshold
Given a minimum sample size of 50 messages delivered per rule for the selected period When a rule has Sample Size < 50 Then the rule is excluded from the ranked list And the rule appears in a collapsible "Below Sample Size" section with metrics greyed and labeled "Insufficient sample" And a tooltip on the threshold label displays "Minimum sample size: 50 deliveries" When a rule reaches Sample Size >= 50 Then it appears in the ranked list on the next data refresh
Highlight Statistically Meaningful Performance
Given eligible rules (Sample Size >= 50) and a computed portfolio average conversion rate for the selected period When comparing each rule's conversion rate to the portfolio average using a two-sided z-test at alpha = 0.05 Then rules with significantly higher conversion are badged "Top Performer" and highlighted in green And rules with significantly lower conversion are badged "Underperformer" and highlighted in red And rules without significance show no badge And significance is only computed if both the rule and portfolio sample sizes are >= 50
Drill Into Rule Details and Manage Rule
Given the leaderboard is visible When the user clicks a rule row Then a details panel opens showing tabs: History, Triggers, Audience, Message Variants And History displays time series for Conversions, Recovered Revenue, Time-to-Fill Reduction, and Response Rate for the selected period And the panel includes actions "Edit Rule" and "Pause Rule" When the user selects Edit Rule Then the rule editor opens pre-populated with the selected rule's settings When the user toggles Pause Rule and confirms Then the rule's status updates to Paused, future sends for that rule stop, and an audit entry is recorded with user, timestamp, and reason
Metric Definitions and Attribution Windows Are Applied
Given backend metric jobs have completed for the selected period Then the leaderboard uses these definitions: - Conversions = bookings created within 72h after a rule message, attributed by last touch to that rule - Recovered Revenue = sum of gross booking value from attributed conversions, excluding refunds/cancellations recorded within 7 days - Response Rate = unique recipients who replied within 24h / messages delivered, expressed as % - Time-to-Fill Reduction = (baseline mean time-to-fill for comparable slots over prior 28 days) - (mean time-to-fill for slots filled after the rule), shown as minutes and % And values match a validation dataset within ±1% or ±$0.01 for currency
Data Freshness, Loading, and Error Handling
Given normal network conditions When the user opens the leaderboard with up to 500 active rules Then initial content renders within 2 seconds at the 95th percentile And the page shows a "Last updated" timestamp reflecting the most recent metric job; timestamp is <= 24 hours old When a data load fails Then an error banner appears with a retry control and no partial metrics are shown in the ranked list
Priority Weight Tuning Sandbox
"As an owner, I want to safely experiment with priority weights and timings so that I can improve results without risking my live schedule."
Description

An interactive simulator that lets users adjust rule priority weights and send windows, backtests the changes against recent history, and estimates expected impact on fill rate, recovered revenue, and time-to-fill. The sandbox provides confidence ranges, highlights trade-offs, and supports saving scenarios. Applying changes requires confirmation and appropriate permissions, writes to an audit log, and supports rollback to prior settings.

Acceptance Criteria
Adjust Weights and Send Windows Simulation
- Given a user with Manage Optimization access opens the Sandbox, when the sandbox loads, then it displays current rule priority weights (0–100) and send windows (HH:MM–HH:MM) for all active outreach rules. - When the user adjusts a weight within 0–100 or changes a send window within 06:00–21:00 local time, then the simulator recalculates projected metrics and visualizations within 3 seconds without persisting changes to production settings. - When an invalid value is entered (non-numeric, out-of-range, overlapping or inverted windows), then the field is rejected with inline error text and the Recalculate action remains disabled. - When the user clicks Reset, then all values revert to the current production baseline.
Backtest Against Recent History
- Given the user selects a lookback of 30, 60, or 90 days, when they click Run Backtest, then the system evaluates the proposed settings against historical message and booking data and returns baseline vs proposed metrics: fill rate (%), recovered revenue ($), and median time-to-fill (hours), within 30 seconds for up to 100,000 historical engagements. - Then absolute and percent deltas for each metric are displayed to two decimal places. - If fewer than 500 engagements exist in the selected lookback, then a Data Insufficient warning is shown and results still display with an indicator of lower confidence.
Confidence Ranges Display
- Given backtest results, then each reported metric shows a 95% confidence interval computed via bootstrap resampling with at least 1,000 resamples. - The UI displays lower and upper bounds numerically and a visual band; bounds update on each parameter change within 3 seconds of recalculation. - If CI computation fails, an inline error is shown and the metric is presented without CI; a retry control is provided.
Trade-off Highlighting
- Given at least one metric improves by ≥2% and at least one metric declines by ≥2% relative to baseline, then a Trade-off notice is displayed summarizing impacted metrics and their deltas. - Metrics with positive deltas are colored green and negative deltas red; tooltips explain each trade-off in plain language. - If all metric deltas are within ±2%, then a Neutral Impact notice is displayed instead of a trade-off banner.
Save and Manage Scenarios
- Given the user enters a scenario name (3–50 characters, unique per org) and clicks Save, then the system stores: weights, send windows, selected lookback, timestamp (UTC), user ID, optional notes, and computed metrics/CIs; save completes within 2 seconds. - Then the scenario appears in the Scenario list with a version ID and can be reopened to reproduce the same results from stored data. - If a duplicate name is entered, the user is prompted to choose a unique name and save is blocked until resolved.
Apply Changes with Permissions, Confirmation, and Audit Logging
- Given the user holds the Apply Optimization Settings permission, when they click Apply on a saved scenario, then a confirmation modal presents before/after values and projected impact and requires typing APPLY to proceed. - If the user lacks permission, Apply controls are disabled and a tooltip indicates the required permission. - When confirmed, production settings are updated within 5 seconds and an immutable audit log entry is created containing: org ID, user ID, scenario ID, UTC timestamp, before/after JSON payload, projected deltas, and optional reason; the audit entry ID is returned to the UI. - If production settings changed since the scenario was last refreshed, Apply is blocked with a conflict message and a Refresh to Re-compare action is provided.
Rollback to Prior Settings
- Given the version history view, when the user selects a prior settings version and confirms by typing ROLLBACK, then the system restores those settings to production within 30 seconds and records a rollback audit entry linking both versions. - After rollback, active settings match the selected version and new optimization runs use them immediately; previously scheduled messages remain unaffected. - Only users with Apply Optimization Settings can perform rollback; without permission, the action is disabled with explanatory tooltip.
Anomaly Alerts and Weekly Digest
"As a busy operator, I want proactive alerts and summaries so that I can react quickly to issues and stay informed without checking the dashboard constantly."
Description

Automated detection of significant deviations in fill rate, recovered revenue, or time-to-fill versus a trailing baseline, with notifications via SMS and email. Users can configure thresholds, quiet hours, and recipients. A weekly digest summarizes key metrics, trends, and top-performing rules, and includes suggested actions and quick links to the dashboard and sandbox.

Acceptance Criteria
Anomaly Detection vs Trailing Baseline
Given historical data exists for fill rate, recovered revenue, and time-to-fill And a trailing baseline window of the prior 28 days excluding the current day is used And a minimum of 7 baseline days is required per metric When today’s metric deviates from its baseline mean by more than the configured percentage threshold Then the system flags one anomaly per metric per business day per account And the anomaly record includes metric name, direction (increase/decrease), absolute delta, percent delta, timestamp, and baseline sample size And no alert is generated if the baseline minimum is not met
Per-Metric Threshold and Baseline Configuration
Given a user with the Admin role opens Alert Settings When they set distinct upper and/or lower deviation thresholds per metric (fill rate, recovered revenue, time-to-fill) And optionally adjust the trailing baseline window between 7 and 90 days And set a minimum daily sample size per metric And save changes Then the settings are validated (numeric ranges, required fields) and persisted And the new settings take effect for detections within 5 minutes And an audit entry records who changed what and when
Quiet Hours and Timezone Enforcement
Given quiet hours and a timezone are configured in Alert Settings When an anomaly is detected during quiet hours Then outbound SMS and email notifications are queued and not sent immediately And queued notifications are delivered at the end of quiet hours in the configured timezone And multiple queued alerts for the same recipient are collapsed into a single summary per metric to avoid notification spam
Real-time SMS and Email Notifications
Given an anomaly is detected outside quiet hours and at least one recipient channel is configured When the system dispatches notifications Then each recipient receives exactly one SMS and/or one email per anomaly according to their channel preferences And the message includes metric name, direction, percent delta, absolute delta, detection time, and a deep link to the dashboard view And delivery failures are retried up to 3 times over 15 minutes and logged; permanently failing recipients are skipped and reported in the alert log
Weekly Digest Content and Delivery
Given Weekly Digest is enabled for a recipient group When the digest for the prior Monday–Sunday period is generated Then it includes for each metric: period average/total, change vs prior week, and change vs trailing baseline And it lists the top 5 performing rules with their contribution to recovered revenue and time-to-fill improvement And it summarizes anomaly counts and links to the anomaly log And the digest is delivered by Monday 08:00 in each recipient’s timezone via email (full detail) and SMS (short link)
Suggested Actions in Weekly Digest
Given the weekly digest is generated When underperforming metrics or optimization opportunities are identified Then the digest includes at least three suggested actions with rationale and an expected impact estimate And each suggestion provides a one-click link to the sandbox or settings preloaded with relevant rules/parameters for quick tuning
Deep Links to Dashboard and Sandbox
Given a user clicks a link from an alert or the weekly digest When the link opens Then the dashboard deep link loads with metric, time range, and filters matching the alert/digest context And the sandbox deep link opens with the referenced rule preselected and editable And links are valid for 14 days; expired links show a friendly message with an option to regenerate from the app

Live Waitboard

A real-time, branded display shows current wait times, queue position, and which station you’ll visit next. Guests stay informed at a glance, reducing “how long?” interruptions and keeping the pop-up flowing smoothly.

Requirements

Real-time Queue Sync
"As a guest waiting for service, I want the Waitboard to update instantly when the queue changes so that I always know my place and estimated wait without asking staff."
Description

Continuously sync the Waitboard with FetchFlow’s SMS-first booking pipeline and on-site check-ins, pushing position and wait-time updates within 2 seconds of any change. Implement a publish/subscribe channel (e.g., WebSockets or Server-Sent Events) that emits updates for location-specific queues; fall back to short-polling when push is unavailable. Calculate ETA using service type, pet profile attributes (size, coat, temperament), groomer availability, package selections, and historical durations. Automatically reflect events such as arrivals, walk-ins, reschedules, no-shows, and early pickups. Ensure timezone awareness, per-location configuration, and idempotent updates to prevent flicker. Provide instrumentation for latency, accuracy drift, and event throughput. Outcome: guests and staff see accurate, immediate state, reducing “how long?” interruptions and improving flow.

Acceptance Criteria
Sub-2s Real-Time Updates SLA
Given any queue mutation (new booking, on-site check-in, arrival, walk-in, reschedule, no-show, early pickup) When the event is accepted by FetchFlow Then the Waitboard reflects the affected entry’s position and ETA within 2 seconds end-to-end for at least 99% of events per hour And the P95 end-to-end latency is <= 2.0s and the maximum observed latency under normal network conditions is <= 4.0s And updates are applied in causal order per location and per pet using versioned events, with no state reversion after a newer version is applied And the Waitboard shows a last-updated timestamp that increments with each applied change
Push Channel with Seamless Fallback to Short-Polling
Given the client cannot establish or maintain a WebSocket/SSE connection When 3 consecutive ping timeouts or a 4xx/5xx response is detected or the connection is interrupted for >3 seconds Then the client switches to short-polling within 3 seconds without user intervention And while on short-polling, the poll interval is 3 seconds under activity, backing off to a maximum of 10 seconds when idle, maintaining P95 data staleness <= 5 seconds And when push becomes available, the client upgrades back to push within 5 seconds, resuming from the last acknowledged version without missing or duplicating updates And the transition between push and polling does not produce visible UI jumps or duplicate renders
ETA Calculation Accuracy and Inputs
Given a queue entry with a known service type, pet profile (size, coat, temperament), groomer availability, package selections, and historical duration data When an ETA is computed for display Then the median absolute ETA error over the last 200 completed services per location is <= 4 minutes and the P90 error is <= 8 minutes And ETAs are recomputed and updated on the Waitboard within 2 seconds of any relevant change (assignment, check-in, walk-in insertion, early pickup, no-show, reschedule) And the ETA computation uses the listed inputs and records model version and last recalculation timestamp in an internal debug payload for QA verification
Idempotent, Stable Rendering (No Flicker)
Given duplicate, retried, or out-of-order versioned events for the same queue entry When the client processes incoming updates Then each version is applied at most once and out-of-order versions are ignored if older than the last applied version And identical state messages do not trigger a re-render; the same row does not visually change more than once within 200ms for identical state And rapid successive updates for a single entry are coalesced to at most 2 visual paints per second for that entry during soak testing, eliminating visible flicker And no ghost rows appear or disappear without a corresponding versioned event
Location-Scoped Streams and Timezone-Aware Display
Given a Waitboard subscribed for a specific location When the client connects to the real-time stream Then only events for that location are authorized and delivered; cross-location events are rejected server-side and not rendered And all times (e.g., ETAs, last updated) are displayed in the location’s configured timezone with correct DST handling and date rollover And server event payloads include ISO-8601 timestamps with timezone offsets; the client renders them in the location’s local time consistently And switching the Waitboard to a different location updates the subscription and all displayed times within 3 seconds with no leakage of prior location data
Event Coverage: Arrivals, Walk-ins, Reschedules, No-Shows, Early Pickups
Given the active queue for a location When any of the following occurs: arrival, walk-in, reschedule, no-show, or early pickup Then the Waitboard updates membership, ordering, and ETAs within 2 seconds for the affected entries and downstream entries And Arrival: entry status changes to Arrived; position and ETA adjust per current grooming station availability And Walk-in: new entry is inserted according to policy; downstream positions and ETAs recalculate accordingly And Reschedule: entry moves to its new slot (or is removed from today), with downstream recalculations applied And No-show: entry is marked as No-show and removed from the active queue; no-show fee flag is set when policy is active And Early pickup: entry is marked complete and removed from the active queue; ETA is cleared
Observability: Latency, ETA Drift, and Throughput Metrics
Given the service is running under typical load When platform metrics are queried or dashboards viewed Then metrics are available per location for update latency (server-to-client and end-to-end), ETA error distribution (median, P90, P95), and event throughput by type And metrics are emitted at 1-minute resolution with at least 24-hour retention and support alerting And alerts trigger within 5 minutes when P95 update latency > 2.5s for 10 consecutive minutes or ETA P90 error > 10 minutes for 30 minutes And an audit log retains at least the last 1,000 versioned events per location for 24 hours to reproduce client state during incident review
Station Routing Display
"As a pet parent in the lobby, I want to see which station we’re headed to next so that I understand the process and feel confident we’re progressing."
Description

Show each guest’s next station (e.g., Check-in, Bath, Dry, Groom, Training, Payment) alongside queue position and ETA, updating automatically as assignments change in FetchFlow. Integrate with resource and bay assignment logic, respecting staff calendars, station capacity, and service prerequisites. Support multi-pet and multi-service visits by sequencing stations and indicating when siblings are grouped or split. Provide clear visual states for 'On Deck', 'In Service', 'In Transit', and 'Complete'. Handle unknown routes gracefully with a generic 'Next Up' label and conservative ETAs. Outcome: guests understand where they are headed next and why, reducing confusion and walk-ups.

Acceptance Criteria
Display Next Station with Queue Position and ETA
Given a guest has an assigned next station in FetchFlow When the Live Waitboard is loaded or refreshed Then the guest’s card displays the next station name, the current queue position number, and an ETA in minutes And the ETA displayed is within ±1 minute of the routing engine’s ETA at render time And the queue position reflects the guest’s rank among others assigned to the same station at that moment And timestamps and ETAs are shown in the location’s timezone
Auto-Update on Assignment Changes
Given a guest’s next station assignment, queue position, or ETA changes in FetchFlow When the change is saved Then the Live Waitboard updates the guest’s next station, queue position, and ETA within 3 seconds without requiring a page reload And no stale values for station name, position, or ETA remain visible after the update And if the guest leaves the queue (e.g., completes service), their card is removed within 3 seconds
Calendar/Capacity/Prerequisite-Compliant Routing
Given a staff member is off-shift or a station is at capacity per configured limits When routes are computed and displayed Then no guest is shown with a next station that requires an unavailable staff member or exceeds station capacity And if a service prerequisite is unmet (e.g., Bath before Groom), the next station respects the prerequisite order And if the currently scheduled next station becomes unavailable, the display switches to the next valid station in the sequence, or falls back to unknown route handling
Multi-Pet and Multi-Service Sequencing and Grouping Indicator
Given a visit includes multiple pets When the Live Waitboard displays their next steps Then pets routed together to the same next station show a clear textual indicator of being Grouped And pets routed to different stations show a clear textual indicator of being Split And each pet’s queue position and ETA are displayed per-pet when Split And for a single pet with multiple services, the next station displayed is the next incomplete prerequisite in the service sequence (not Payment until prior services are complete)
Visual State Indicators and Transitions
Given the visual states On Deck, In Service, In Transit, and Complete When a guest becomes next in line at a station Then their card shows On Deck When the guest is checked in at a station and work starts Then their card shows In Service When the guest leaves a station and is moving to the next Then their card shows In Transit When all required services and payment are finished Then their card shows Complete And only one state is active at any time per guest And state transitions appear on the waitboard within 3 seconds of the triggering event
Unknown Routes Fallback with 'Next Up' and Conservative ETA
Given the routing engine cannot determine the next station for a guest When the Live Waitboard displays the guest Then the card shows the label Next Up in place of a station name And the ETA displayed is conservative: not less than the location’s recent median wait for entry stations and rounded up to the nearest 5 minutes And when the route becomes known, the station name and ETA update within 3 seconds
Real-Time Sync Performance and Data Integrity
Given normal operating conditions and up to 100 concurrent viewers When staff perform 100 route-affecting updates within 5 minutes Then 99% of Live Waitboard updates render to viewers within 3 seconds and none exceed 10 seconds And no duplicate, missing, or orphaned cards are displayed during rapid updates And the fields displayed (next station, queue position, visual state, ETA) match the routing engine at the time of render
Branded Display Themes
"As a shop owner, I want the Waitboard to match my brand so that the in-store experience feels professional and consistent."
Description

Enable businesses to brand the Waitboard with their logo, colors, typography, and background imagery while preserving readability and accessibility. Offer preset themes and a simple editor with live preview, including dark mode and large-text options for lobby TVs. Allow per-location theme overrides and scheduling (e.g., day/night palettes). Support safe areas for different screens (TV, tablet) and automatic scaling for common resolutions. Persist brand assets in FetchFlow’s existing organization settings and apply them via a theming system that does not require code changes. Outcome: a professional, on-brand experience that fits seamlessly into the lobby.

Acceptance Criteria
Apply Organization Brand Theme to Waitboard
- Given an organization with saved brand assets (logo, primary/secondary colors, typography, background image) in FetchFlow settings, When a user opens the Live Waitboard for that organization, Then those assets are applied to all branded UI elements of the Waitboard. - Given one or more brand attributes are missing in settings, When the Waitboard loads, Then only the missing attributes fall back to the default theme, and a non-blocking warning is logged to telemetry. - Given any brand asset is updated in organization settings, When the Waitboard is refreshed, Then the updated asset is reflected without code changes and within 5 seconds. - Given an organization has no brand assets stored, When the Waitboard loads, Then the FetchFlow default theme is applied consistently.
Preset Themes and Theme Editor Live Preview
- Given a Theme Editor with preset themes, When the editor is opened, Then at least 4 preset themes are listed, including light and dark variants with accessible defaults. - Given a user selects a preset theme in the editor, When selection changes, Then the live preview updates within 250 ms to reflect the selection. - Given a user adjusts colors, typography, or background in the editor, When the user saves, Then the Waitboard display reflects the saved changes within 5 seconds and persists after reloads. - Given unsaved changes exist, When the user cancels or navigates away, Then the editor prompts to discard or save and reverts to last saved if discarded.
Accessibility: Contrast, Dark Mode, Large Text
- Given any active theme, When text is rendered, Then color contrast meets WCAG 2.1 AA: normal text >= 4.5:1 and large text >= 3:1. - Given Dark Mode is toggled on, When the Waitboard is displayed, Then background luminance and text colors meet the same contrast thresholds and no component becomes illegible. - Given Large Text mode is enabled, When the Waitboard is displayed on 1920x1080 and 1280x800 screens, Then queue position and wait-time text scales by at least 30% (minimum 32 px) without clipping, overlap, or truncation. - Given colors are customized, When a chosen combination would violate contrast thresholds, Then the editor displays a blocking validation error and suggests accessible alternatives.
Per-Location Overrides and Scheduled Palettes
- Given a multi-location organization, When a location-specific theme override is saved, Then Waitboards scoped to that location use the override and others use the org default. - Given a day/night schedule (e.g., 07:00–18:59 day; 19:00–06:59 night) is configured for a location, When local time crosses a schedule boundary, Then the theme switches within 60 seconds without requiring a manual refresh. - Given no active schedule at a location, When the Waitboard loads, Then it uses the location override (if any) or org default. - Given the organization changes time zone for a location, When the time zone is updated, Then scheduled theme switches align to the new time zone immediately.
Responsive Scaling and Safe Areas Across Displays
- Given a supported display resolution (1280x720, 1920x1080, 3840x2160, 1200x800), When the Waitboard loads, Then all branded elements auto-scale and remain readable with no overlap, truncation, or distortion. - Given a TV display with overscan, When the Waitboard is shown, Then all critical UI stays within a 5% safe area margin on each edge. - Given the display orientation changes between landscape and portrait, When the Waitboard reflows, Then brand elements and content adapt without clipping and maintain minimum text sizes (normal ≥ 24 px; large-text mode ≥ 32 px). - Given the editor's Safe Area overlay is toggled, When enabled, Then the live preview shows the safe area boundaries for TV and tablet profiles.
Brand Asset Upload, Optimization, and Persistence
- Given a user uploads a logo or background image, When the file type and size are validated, Then only SVG/PNG/JPG up to 10 MB are accepted; others are blocked with a clear error message. - Given a high-resolution image is uploaded, When saved, Then it is optimized for target displays and served via CDN with a versioned URL to ensure instant cache busting on updates. - Given a logo is placed over a background, When contrast is insufficient, Then the editor displays a warning and offers an optional semi-transparent backing plate to reach contrast compliance. - Given brand assets are saved, When the organization settings are queried, Then assets and theme configuration are persisted and retrievable via API within 200 ms P95.
Zero-Code Theming, Caching, and Performance
- Given theme configuration is updated, When the Waitboard is relaunched, Then the new theme is applied at runtime without any code deployment. - Given a cold load on a 10 Mbps connection, When the Waitboard opens, Then the initial render with applied theme completes in under 2 seconds P95 and any flash of unstyled content is under 100 ms. - Given temporary network loss, When the Waitboard loads, Then the last cached theme is used and a retry is initiated in the background until connectivity returns. - Given a theme fails to load, When the error occurs, Then the Waitboard falls back to the default theme and surfaces a non-intrusive error banner with a retry control.
QR and Mobile View Access
"As a guest waiting nearby, I want to check my wait and place on my phone so that I don’t have to repeatedly ask staff for updates."
Description

Provide a public, read-only Waitboard link and QR code that guests can scan in-store or receive via SMS to view the queue on their phones. Deliver a responsive, mobile-optimized version that auto-refreshes and respects the same privacy rules as the in-lobby display. Optionally gate detailed position via a time-limited token tied to the guest’s appointment to prevent scraping or cross-tenant viewing. Allow operators to include the link in reminder texts and auto-replies. Outcome: guests can self-serve from anywhere, decreasing front-desk interruptions.

Acceptance Criteria
Public Read-Only Waitboard Link and QR Code Generation
Given an operator with permission opens Live Waitboard settings, When they click "Copy Public Link", Then a public HTTPS URL scoped to their tenant is copied to the clipboard. Given the operator clicks "Generate QR Code", When they select "Download", Then a QR code image is provided in PNG (>=1024x1024 px) and SVG formats encoding the same URL. Given the QR code is printed at 5 cm (2 in) with high-contrast dark-on-light, When scanned by default iOS and Android camera apps from 1 meter under typical indoor lighting, Then the link opens successfully in under 2 seconds on LTE. Given a guest opens the public link, When the endpoint is requested with methods other than GET, Then the server responds 405 Method Not Allowed.
Mobile-Optimized, Auto-Refreshing Waitboard View
Given a device width between 320px and 768px, When the public link is opened, Then the waitboard renders without horizontal scrolling and all tap targets are at least 44px by 44px. Given simulated 4G (400 ms RTT, 1.6 Mbps) on a mid-tier device, When the page loads cold, Then First Contentful Paint is <= 2.5 s and Total Blocking Time is <= 300 ms. Given the device stays on the page, When underlying queue data changes, Then the view auto-refreshes at least every 10 seconds and reflects the change within 2 seconds of the refresh, showing a "Last updated" timestamp. Given the device loses connectivity, When it is offline, Then an offline banner is shown and automatic retries occur every 15 seconds until reconnection, after which the latest data is displayed without manual reload. Given a screen reader user opens the page, When navigating, Then all content is reachable with a logical focus order and color contrast meets WCAG 2.1 AA.
Privacy Parity with In-Lobby Display
Given the lobby display privacy settings specify what client and pet details are shown, When the public mobile view renders, Then it displays no more personally identifiable information than the lobby display. Given the lobby display is configured to mask last names and hide contact details, When the public view renders, Then last names are masked (e.g., "Sam T.") and no phone numbers, emails, or addresses are shown. Given pets’ photos are disabled in lobby privacy settings, When the public view renders, Then pet photos are not displayed and placeholders are used. Given an appointment identifier or internal notes exist, When rendering the public view, Then neither internal notes nor appointment IDs are exposed in the DOM or network responses.
Optional Token-Gated Detailed Position Access
Given token gating is disabled, When a guest opens the public link, Then only aggregate information (e.g., estimated wait ranges and general queue list) is shown without revealing the guest’s exact position. Given token gating is enabled and a guest opens a tokenized link tied to their appointment, When the token is valid and unexpired, Then the page shows the guest’s exact queue position and next station. Given a token is created, When no TTL is specified, Then it expires 90 minutes after issuance; When a custom TTL is set (15–240 minutes), Then the token expires accordingly. Given an appointment is cancelled or checked out, When a previously issued token is used, Then access is revoked within 60 seconds and the page shows a friendly "Link expired" message with a 401 status. Given a token is presented to a different tenant or different appointment, When the link is opened, Then the server responds 404 without disclosing existence of other tenants or appointments.
Cross-Tenant Isolation and Link Security
Given a public link for tenant A exists, When the same path is requested under tenant B’s host or slug, Then the server responds 404/403 and no cross-tenant data is returned. Given a client opens the public or tokenized link over HTTP, When the request reaches the server, Then it is redirected to HTTPS and HSTS is enabled for subsequent requests. Given repeated requests are made to the public waitboard endpoint, When more than 120 requests per IP occur within 5 minutes, Then subsequent requests receive 429 with a Retry-After header. Given the tokenized endpoint is used, When responses are returned, Then Cache-Control is no-store and no token appears in client-side logs, URLs beyond fragment, or referrers to third-party origins.
Operator Messaging Integration (SMS Templates and Auto-Replies)
Given an operator edits a reminder SMS template, When they insert the {waitboard_link} placeholder and save, Then preview shows the resolved per-tenant link and test sends include it. Given token gating is enabled and the reminder is appointment-specific, When the SMS is sent, Then {waitboard_link} resolves to a valid tokenized link for that appointment that expires per TTL settings. Given an operator enables "Include Waitboard Link in Auto-Reply", When an inbound SMS is received outside business hours, Then the auto-reply includes the public link (or tokenized link if an active appointment context exists) exactly once. Given an operator opens the Waitboard settings, When they click "Copy Link" or "Copy QR to Clipboard", Then the actions are available and succeed within 1 second.
Branding, Timezone, and Error Handling
Given a tenant has configured branding, When the public view loads, Then the tenant logo and colors are applied and maintain WCAG AA contrast; When branding is not configured, Then default branding is applied. Given the tenant timezone is set, When estimated times are displayed, Then they are shown in the tenant’s timezone and locale format and update as the queue changes. Given a guest opens an invalid, expired, or revoked link, When the server responds with 401/403/404, Then the page shows a clear, branded error message with guidance, without revealing internal details. Given a server error occurs, When a 5xx is returned, Then the page displays a retry option and logs a client-side error event with anonymized context and a correlation ID.
Privacy and Anonymization Controls
"As a privacy-conscious guest, I want my information anonymized on the Waitboard so that I feel safe seeing my place publicly."
Description

Protect personal data on public displays by showing only safe identifiers such as pet name and first initial, with per-client opt-outs. Provide configurable display formats per shop (e.g., 'Milo S.' or ticket number), hide sensitive notes and contact info, and obscure records flagged as private. Enforce tenant isolation on public endpoints, add rate limits, and support tokenized access for mobile views. Ensure compliance with applicable privacy standards and provide audit logs for display access and configuration changes. Outcome: wait transparency without exposing personal information.

Acceptance Criteria
Shop-Configurable Safe Identifier Format
Given a shop selects the "PetName + FirstInitial" format, When a waitboard entry is rendered on any public view, Then the entry displays "<PetName> <FirstInitial>." and no other personal identifiers. Given a shop selects the "Ticket Number" format, When a waitboard entry is rendered, Then only an alphanumeric ticket like "A105" is displayed and no names are shown. Given any format is selected, When a waitboard entry is rendered, Then full last names, phone numbers, emails, addresses, vaccine document links, and freeform notes are not displayed. Given a new shop is created, When the public waitboard is first shown, Then the default identifier format is "Ticket Number" until an admin selects another safe format.
Per-Client Opt-Out of Public Display
Given a client has the "Public Display Opt-Out" flag enabled, When their appointment appears on the waitboard, Then their row shows the shop’s configured fallback (e.g., "Private Guest" or ticket number) and does not reveal pet or owner identifiers. Given staff disables the client's opt-out, When the waitboard refreshes, Then the row reverts to the shop’s standard safe identifier format within one refresh cycle (<= 10 seconds).
Redaction and Privacy Flags Behavior
Given an appointment contains staff-only notes, medical/vaccine details, or contact information, When the public waitboard is viewed, Then those fields are never rendered or retrievable from the public endpoint payload. Given a client or appointment is flagged as "Private," When the public waitboard is viewed, Then the entry is either omitted or displayed as an anonymized placeholder per shop setting, with position and wait time preserved, and no identifiers shown. Given a privacy flag is toggled on or off, When the public waitboard refreshes, Then the change is reflected within one refresh cycle (<= 10 seconds).
Tenant Isolation on Public Endpoints
Given a request to a public waitboard endpoint for Tenant A, When identifiers from Tenant B are probed or enumerated, Then the response does not include Tenant B data and returns 404 or 403. Given a request host or subdomain does not match a configured Tenant domain, When the public waitboard is requested, Then the request is rejected with 403 and no queue data is returned. Given multi-tenant load, When concurrent requests are made, Then data returned is scoped strictly to the authenticated token or tenant context.
Tokenized Mobile View Access Control
Given a mobile waitboard link is generated, When the link is shared, Then it includes a signed, tamper-evident token scoped to tenant and permission "waitboard.read". Given the token has expired (default <= 24 hours, configurable), When the link is used, Then the endpoint returns 401/expired and no data. Given a token is revoked by staff, When the link is used, Then access is denied within 60 seconds of revocation. Given a tokenized mobile link is used, When other API endpoints are requested with the same token, Then access is denied (least-privilege scope).
Rate Limiting and Abuse Protection
Given repeated requests from the same IP exceed 60 requests per minute to a public waitboard endpoint, When additional requests arrive, Then the service responds 429 with a Retry-After header. Given a configured allowlist CIDR for an in-store display device, When that device polls the endpoint, Then it is exempt from rate limiting within configured bounds. Given bursts up to 10 requests within 1 second, When they occur, Then token bucket settings allow short bursts while enforcing the minute cap.
Audit Logging for Display Access and Config Changes
Given any access to a public waitboard endpoint, When a response is served, Then an audit log entry records timestamp, tenant, path, status code, IP, and user agent or token identifier. Given any change to privacy configuration or display format, When the change is saved, Then an audit log entry records timestamp, tenant, actor (user ID/API key), old value, new value, and result. Given an admin requests the audit log, When filtered by date range and action type, Then entries are retrievable and exportable (CSV/JSON) with at least 365 days of retention.
Resilience and Offline Fallback
"As a front-desk associate, I want the Waitboard to keep working even if the internet blips so that guests stay informed and calm."
Description

Ensure the Waitboard remains usable during network interruptions or service degradation. Cache the last known queue state locally and display a clear banner when estimates are paused, automatically reconciling when connectivity returns. Implement exponential backoff, heartbeat health checks, and graceful error states. Optimize for high-traffic periods with lightweight payloads, CDN caching for static assets, and backpressure on event streams. Provide observability dashboards and alerts for update latency, disconnect rates, and client error codes. Outcome: reliable lobby displays that don’t fail during busy times.

Acceptance Criteria
Offline Interruption With Cached Queue State
Given the waitboard has successfully synced within the last 5 minutes and a local cache exists When network connectivity is lost for more than 5 seconds Then the waitboard displays the last known queue, wait times, and station assignments from local cache with a "Last updated <timestamp>" indicator And a persistent banner "Live updates paused" is shown at the top within 1 second of detecting the disconnect And estimated wait times stop incrementing and are labeled "paused" And the UI does not crash and remains responsive And the banner text is announced by screen readers and meets WCAG AA contrast
Automatic Reconciliation After Connectivity Restored
Given the waitboard is in offline fallback mode displaying cached data When connectivity is restored and a heartbeat plus a successful GET /queue occurs within 2 seconds Then the client fetches the latest queue state and reconciles differences within 2 seconds And the display updates without duplicates, missing entries, or out-of-order positions And the "Live updates paused" banner is removed within 1 second after successful refresh And the "Last updated" timestamp reflects the server time of the new state
Exponential Backoff and Stream Backpressure
Given the client is connected to a real-time event stream or polling endpoint When a request fails due to network error, 429, or 5xx Then retries follow exponential backoff with jitter: 1s, 2s, 4s, 8s, up to a maximum delay of 30s And no more than one concurrent retry attempt is active at any time And upon receiving a server backpressure signal, the client reduces update consumption to at most one update per 3 seconds or switches to delta polling as directed And during backoff, client network usage stays below 150 KB/min on a reference device
Heartbeat Health Checks and Degraded Mode Banner
Given heartbeats are expected every 15 seconds from the service When two consecutive heartbeats are missed or median update latency exceeds 10 seconds over the last 1 minute Then the client enters degraded mode, pauses estimate updates, and shows an "Updates delayed" banner within 1 second And the client continues recovery attempts using the backoff policy And upon receiving two consecutive successful heartbeats and a fresh state, the banner is cleared and normal updates resume
Graceful Error States Without Data Loss
Given the server responds with a 4xx validation error or the payload fails schema validation When parsing or validation fails Then the client preserves and displays the last good state without blank screens And a non-blocking message indicates "Temporarily unavailable" with a Retry control that respects backoff rules And the error code and correlation ID are captured in client telemetry And all error states remain branded and accessible
High-Traffic Performance: Lightweight Payloads and CDN Caching
Given peak lobby traffic and first-time load When the waitboard loads static assets and initializes Then all static assets are served via CDN with Cache-Control max-age >= 7 days and immutable where applicable And the total initial payload (HTML+CSS+JS) is <= 200 KB gzipped And subsequent update payloads average <= 5 KB using ETag/If-None-Match or real-time deltas And P95 time to interactive is <= 2 seconds on a reference device And if the origin is unavailable, static assets still load from CDN cache
Observability Dashboards and Alerting
Given the system is live in production When SREs view monitoring Then dashboards expose update latency (p50/p95), disconnect rate, retry/backoff counts, client error codes, and offline/degraded banner uptime And alerts trigger within 5 minutes when disconnect rate > 3% over 10 minutes, p95 update latency > 5 seconds over 10 minutes, or client error rate > 1% of sessions And client telemetry includes app version, hashed device ID, network type, and error codes without collecting PII

Scan Check‑In

After booking at the kiosk, clients receive an SMS pass with a QR. Staff scans it on arrival to auto pull the Pet Profile, confirm deposit, and flip the status to ‘In Service’—preventing line-jumps and shaving minutes off each handoff.

Requirements

Secure QR Pass Delivery
"As a client, I want to receive a secure QR pass via SMS after booking so that check-in is fast and I don’t have to explain my details at the counter."
Description

Generate a unique, signed, time-bound QR pass immediately after kiosk booking and deliver it via SMS as a branded link to a mobile pass page that renders the QR and appointment details. Ensure the token is PII-free and revocable on cancel/reschedule, with old passes invalidated and expiry windows configurable by location. Provide SMS delivery status tracking, automatic retries, short-linking, and a fallback plaintext code if rich content fails. Support re-send from staff dashboard and client self-service, and localize content where applicable.

Acceptance Criteria
Immediate Signed QR Pass Generation
Given a kiosk booking is successfully completed When the booking confirmation event is emitted Then the system generates a unique QR pass token within 5 seconds And the token is cryptographically signed and time-bound And the token payload contains no PII (no client name, phone, pet info, or appointment details) And the token includes only an opaque identifier and expiry timestamp And an association between token and appointment ID is stored for revocation/auditing
Branded SMS Delivery with Short Link and Tracking
Given a QR pass token is created for an appointment When the system sends the SMS to the client’s phone on file Then the SMS body includes the business brand name and a branded short link domain And the short link resolves to the mobile pass URL for that token And the system records status transitions (Queued, Sent, Delivered, Failed) with timestamps And if not Delivered within 1 minute, the system retries up to 3 times with exponential backoff And if final status is Failed, the pass is marked Not Delivered and an alert appears in the staff dashboard
Mobile Pass Page Rendering and Security
Given the recipient opens the SMS short link When the pass page loads over HTTPS Then the page renders a scannable QR code for the token and shows appointment date/time and location brand And no PII is present in the QR payload or URL query string And the page loads in under 2 seconds on a 4G connection (95th percentile under 3 seconds) And if the token is expired or revoked, the page displays an Invalid/Expired state and does not render a usable QR
Validity Window Configuration and Revocation
Given a location has an expiry window configured When a pass is generated for that location Then the token’s expiry equals the configured window relative to the appointment time And changing the location’s expiry setting only affects passes generated after the change Given an appointment is canceled or rescheduled When the change is saved Then all previously issued tokens for that appointment are revoked immediately And if rescheduled, a new token is generated and sent, and previous links/QRs are invalid And scanning a revoked or expired token returns Invalid/Expired and does not change appointment status
Fallback Plaintext Code on Rich Content Failure
Given SMS delivery of the short link fails after all retries or is reported as unsupported by the carrier When a fallback is triggered Then the system sends a new SMS containing a plaintext alphanumeric pass code (8–12 chars) with brief instructions And the code maps to the same appointment and uses the same expiry as the QR token And staff can enter the code in the scanner UI to retrieve the Pet Profile and proceed with check-in
Re-send from Staff Dashboard and Client Self-Service
Given staff clicks Re-send Pass for an upcoming appointment When the action is confirmed Then the system revokes any existing token and issues a new token and SMS, logging actor and timestamp And delivery status for the new SMS is tracked and visible to staff Given a client texts RESEND from the phone number on file or uses the self-service link When the system verifies an upcoming appointment exists Then the system reissues the token and SMS and rate-limits to a maximum of 3 resends per hour per appointment And all previously issued links become invalid
Localized SMS and Pass Content
Given a location default language and/or a client preferred language is set When a QR pass is generated and sent Then the SMS body and pass page content are localized to that language, including date/time formatting and directionality And if a translation key is missing, the system falls back to English without exposing placeholder keys And right-to-left languages render correctly on the pass page
Unified Scanner Experience (Mobile & Web)
"As a staff member, I want a reliable scanner in our app and web dashboard so that I can scan passes quickly in any environment without disrupting the line."
Description

Provide a built-in camera-based QR scanner in the FetchFlow staff mobile apps and web dashboard that decodes quickly and reliably in varied lighting. Include low-light mode, autofocus guidance, haptic/audio feedback, large visual confirmations, and accessibility support. Accept external HID scanners as an alternative input. Queue scans offline with secure local storage and synchronize when connectivity returns, preserving event order. Enforce role-based access so only permitted staff can scan and check in clients.

Acceptance Criteria
Fast QR Decode in Varied Lighting
Given the staff uses the built-in scanner on iOS, Android, or Web When shown a valid FetchFlow QR at 5–40 cm under 50–1000 lux Then the QR decodes within 500 ms on 90% of attempts and within 1 s on 99% of attempts Given glare or partial shadow When at least 60% of the QR area is visible to the camera Then decoding succeeds within 1.5 s Given any decoded payload When signature verification is performed locally Then unverified payloads are rejected with an "Invalid Pass" error and no status change occurs Given a successful decode When the payload is verified Then pet name, photo thumbnail, and booking ID render within 300 ms and the Check In action is enabled Given an invalid or unsupported code When scanning occurs Then an error displays within 500 ms and the scan session remains active
Low-Light Mode and Torch Control
Given ambient light is below 50 lux When the scanner opens Then a low-light indicator appears and a torch control is offered on supported mobile devices, or exposure gain increases where supported on web Given the user toggles the torch When the control is pressed Then the flashlight state changes within 300 ms and the UI reflects the new state Given the device lacks a torch capability When the user attempts to enable torch Then a non-blocking notice is shown with guidance and scanning continues Given low-light mode is active When scanning a valid QR at 5–30 cm Then decode performance meets a 1 s success target for 95% of attempts
Autofocus Guidance and Feedback
Given the camera autofocus hunts for more than 800 ms When the scanner is active Then a guidance overlay appears instructing the user to move closer/farther and frame the QR within the bracket Given a successful scan When decoding completes Then a single haptic tap (mobile) and a 150–250 ms audio chirp play respecting system mute/accessibility settings, and a high-contrast green check with border displays for at least 800 ms Given a scan attempt fails after 3 s When no valid QR is detected Then a subtle vibration (mobile) and text prompt display without repeating audio if the device is in silent mode
Accessible Scanner Interface (WCAG 2.2 AA)
Given a screen reader is active (VoiceOver/TalkBack) When the scanner opens Then Back, Torch, and Manual Entry controls expose descriptive labels, roles, and hints, and the live camera view is marked as not accessibility-focusable Given keyboard-only navigation on web When using Tab/Shift+Tab Then focus order is logical, a visible focus indicator with at least 3:1 contrast is present, and Esc closes the scanner Given users with color-vision deficiencies When success or error states occur Then feedback is conveyed with iconography and text, not color alone Given system text size up to 200% When the scanner UI renders Then all critical controls maintain a minimum 44x44 pt hit area without overlap or truncation
External HID Scanner Input Support
Given a HID barcode/QR scanner is focused on the web dashboard input When a valid FetchFlow QR is scanned Then the decoded payload auto-populates, trailing CR/LF is stripped, and submission triggers within 200 ms Given a malformed payload or invalid signature When a scan is submitted Then an inline error displays and no booking status change occurs; the event is logged Given multiple scans occur within 2 s When duplicate payloads are received within a 5 s window Then only the first unique payload is processed and duplicates are ignored with a non-blocking notice Given offline mode is active When HID scans occur Then entries are queued identically to camera-based scans
Offline Scan Queue and Ordered Sync
Given the device is offline When a valid QR is scanned by an authorized user Then an encrypted local queue stores the event (timestamp, user ID, payload checksum) and the UI shows a "Queued" confirmation without flipping server status Given queued items exist and connectivity returns When sync begins Then all events transmit within 5 s, preserving original event order using monotonic timestamps, and each receives server acknowledgment Given a conflict is detected server-side (already checked in) When processing a queued event Then the event is marked "Skipped—Already Checked In" with a link to the record and no duplicate status change occurs Given secure local storage requirements When inspecting device storage Then queued data is encrypted at rest with a device-bound key and is unreadable from platform backups; the queue survives app restarts and is cleared on logout
Role-Based Access Enforcement for Check-In
Given a user lacks the Check-In permission When they scan a valid FetchFlow QR Then the Pet Profile displays in read-only mode, the Check In action is disabled, and a tooltip indicates the required role Given a user has the Check-In permission When a verified QR is scanned Then the booking status flips to "In Service" within 500 ms, actor ID/time/device are recorded, and a large visual confirmation displays Given a user account is suspended or permissions are expired/revoked When attempting to scan Then scanning is blocked with an error message and the attempt is audit-logged without exposing sensitive payload contents Given audit requirements When any scan attempt occurs (success, error, or offline) Then an immutable audit log entry records timestamp, user ID, device, method (camera/HID), online/offline state, result, and payload checksum
Instant Pet Profile Retrieval & Display
"As a receptionist, I want the pet’s profile to auto-appear after a scan so that I can confirm vaccines and preferences instantly."
Description

On successful scan, automatically fetch and render the Pet Profile Smart Card with pet name, photo, vaccine status, preferences, alerts, and special handling notes in a single, legible view. Surface owner contact details with click-to-call/SMS, and link to edit when permissions allow. Optimize for sub-1.5s P50 latency from scan to visible profile using caching and prefetching. Respect privacy scopes by redacting sensitive fields based on role and location policy.

Acceptance Criteria
Successful Scan: Profile Visible Under 1.5s P50
Given a valid, unexpired QR pass tied to a booking and normal network conditions When a staff member scans the QR at check-in Then the Pet Profile Smart Card becomes fully visible within 1.5 seconds for the 50th percentile measured over at least 200 production scans during business hours And the view includes pet name, photo, vaccine status, preferences, alerts, and special handling notes populated from the latest records And no loading placeholder remains visible after 2.0 seconds
Smart Card Layout: Single, Legible View With Alerts Prominent
Given the profile renders on a 5–7 inch device in portrait orientation When the Smart Card is displayed Then all required fields appear on a single screen without horizontal scrolling And text elements meet a minimum 12pt font size and WCAG AA contrast And critical alerts and special handling notes appear above the fold with an icon and are not truncated And the vaccine status badge reflects current validity based on expiration dates and shows "Expired" when past due
Owner Contact Actions: Click-to-Call and Click-to-SMS
Given the viewer has permission to view owner contact details When the staff taps Call Then the device invokes the default dialer with the correct E.164-formatted number And when the staff taps SMS Then the device opens the default messaging app with the correct number and a prefilled greeting containing the pet name And on devices without telephony capability, both actions are disabled with a contextual tooltip
Privacy Scopes: Role- and Location-Based Redaction
Given a user role and location policy with defined sensitive fields When the profile is displayed Then fields marked sensitive for that role/location (e.g., owner phone, address, medical notes) are redacted or hidden per policy And any attempt to expand or copy redacted content is blocked and shows an "Insufficient permissions" notice And no redacted values are present in client-side storage, DOM, network responses, or analytics events
Edit Link: Permissioned Visibility and Navigation
Given a user with edit permission for the pet profile at the current location When the Smart Card is displayed Then an Edit link is visible And when tapped, the user is navigated to the edit screen preloaded with the correct pet profile within 1.0 second P50 And for users without permission, no Edit link is shown and direct deep links return 403 And an audit log entry records viewer ID, timestamp, and action for successful edits
Prefetch and Caching: Hit Rate and Latency Targets
Given a QR pass was issued within the last 30 minutes and the SMS pass link was opened at least once When the QR is scanned Then the profile request is served from cache at least 80% of the time over a 1,000-scan cohort And cache-served responses have TTFB <= 200 ms P50 and <= 500 ms P95 And stale-while-revalidate ensures data freshness such that vaccine status and alerts are no older than 5 minutes
Resilience: Invalid/Expired QR and Network Failure Handling
Given an invalid or expired QR is scanned When processing the scan Then an error message is shown within 500 ms with options to retry or locate booking by phone number And no profile fields or PII are displayed And for network timeouts > 3 seconds, a non-blocking retry banner appears and the system continues polling until 15 seconds or user cancellation
Deposit & Payment State Verification on Scan
"As a manager, I want deposits and package credits verified on scan so that we prevent unpaid starts and keep billing accurate."
Description

Validate that the required deposit or prepayment is present at scan time and surface the current payment state, including applied packages, remaining credits, and any outstanding balance or no-show fee rules. If deposit is missing or partial, prompt staff to collect before service begins using existing payment rails and card-on-file. Handle declines, membership exceptions, and partial credits gracefully, writing outcomes back to the ledger without unintended auto-charges unless a policy is configured.

Acceptance Criteria
Deposit Verified on Scan Before Status Flip
Given an appointment that requires a deposit and a valid SMS pass QR When staff scans the QR at check-in Then the system verifies the deposit requirement against configured policy for that service, client, and time And if the deposit is satisfied, the appointment status changes to "In Service" and a visible "Deposit verified" indicator is shown And if the deposit is missing or partial, the status does not change and the UI presents a collection prompt with the remaining amount And if a membership benefit waives the deposit, the status changes to "In Service" and the UI shows "Deposit waived by membership"
Payment State Summary Display on Scan
Given a booked appointment and valid SMS pass QR When the QR is scanned Then the scan screen displays: deposit status (met/missing/partial) with amounts; applied packages and remaining credit counts; any prepaid balance/credits; outstanding balance estimate; no-show fee rule state (pending/applied/waived); and presence of card-on-file (masked) And the information is read from the ledger/config and reflects the latest state at time of scan
Collect Missing or Partial Deposit Using Existing Rails
Given a deposit shortfall is detected at scan When staff selects "Collect" on the scan screen Then the system calculates the exact remaining deposit amount And offers payment options: card-on-file, in-person terminal, SMS pay link, and cash And upon successful capture, a deposit transaction is written to the ledger linked to the appointment and payer, and the deposit status updates to satisfied And the appointment can then move to "In Service"
Graceful Declines and Retry Without Unintended Auto‑Charges
Given a payment attempt at scan is declined When the decline response is received Then the UI shows a clear decline message with reason code if available and offers retry or alternate method And the failed attempt is recorded in the ledger as a non-captured attempt with no financial impact And no automatic retries or charges are made to card-on-file unless an "auto-charge at scan" policy is enabled And if such a policy is enabled, auto-charge attempts follow policy limits and each outcome (success/failure) is recorded in the ledger
Package and Partial Credit Application at Scan
Given the client holds eligible package credits or prepaid credits When the QR is scanned Then credits are applied to the appointment according to configured application order and eligibility rules And full credits reduce the required prepayment/deposit as allowed by policy; partial credits are applied and the remainder is calculated for collection And the remaining credit balance is displayed and over-redemption is prevented And all applications are recorded in the ledger as adjustments
No‑Show Fee Rules Evaluated and Surfaced
Given no-show fee rules exist for the client or appointment type When the QR is scanned Then the system evaluates the rules including grace windows and membership exceptions And the scan screen indicates the rule outcome as pending/applicable/waived with the fee amount if applicable And no fee is charged at scan unless a configured policy authorizes auto-charge; otherwise the fee is scheduled or left pending per rule And the evaluation and any charge/schedule outcome is written to the ledger and audit log
Ledger Writeback and Idempotent Re‑Scan
Given verification and any collections have been performed for an appointment When the same QR is scanned again Then no duplicate charges or duplicate ledger entries are created (idempotent behavior) And the latest payment state is re-fetched and displayed And the appointment remains in its current status (e.g., "In Service") unless an authorized user takes an explicit action to change it And all scan events (including user, timestamp, and actions taken) are recorded for audit
Status Automation & Queue Enforcement
"As an operations lead, I want check-in to automatically update appointment status and enforce queue rules so that line-jumping is prevented and the board stays accurate."
Description

Upon a verified scan, automatically transition the appointment status to In Service by default, with an admin setting to gate through an Arrived state first. Enforce anti line-jump rules by comparing scan time to scheduled time with configurable early/late grace periods; hold early arrivals in a virtual queue and flag late arrivals for escalation. Broadcast status changes to the calendar, operations board, and client SMS, prevent duplicate check-ins with idempotency, and require reason codes for manual overrides.

Acceptance Criteria
Verified Scan Status Routing
Given an appointment scheduled for time T with a valid, unexpired QR pass linked to it And the appointment has not yet been checked in And the org setting "Arrived Gate" is disabled When staff scans the QR and the pass is verified Then the system sets the appointment status to "In Service" within 1 second of scan time And no "Arrived" state is applied Given the same prerequisites except the org setting "Arrived Gate" is enabled When staff scans the QR and the pass is verified Then the system sets the appointment status to "Arrived" within 1 second of scan time And no transition to "In Service" occurs until a subsequent explicit action
Early Arrival Virtual Queue
Given Early Grace is configured to G minutes And an appointment is scheduled at time T And a verified scan occurs at time S where S < T - G When the scan is processed Then the appointment is placed in a "Queued (Early)" hold and does not transition to "Arrived" or "In Service" And the client appears in the Early Queue ordered by (scheduled time asc, then scan time asc) And attempts to start service before time (T - G) are blocked with message "Early arrival — held in queue" unless a manual override is performed
Late Arrival Escalation Flag
Given Late Grace is configured to L minutes And an appointment is scheduled at time T And a verified scan occurs at time S where S > T + L When the scan is processed Then a Late flag is applied to the appointment for escalation And the appointment’s check-in status is routed per the Arrived Gate setting (Arrived when enabled, In Service when disabled) And an escalation banner is shown on the operations board for this appointment
Idempotent Duplicate Scan Handling
Given a first verified scan for an appointment has completed and a status change has occurred When the same QR pass is scanned again for the same appointment (any number of times) Then the system returns success with idempotency_hit=true And no additional status transitions, queue movements, or broadcasts are triggered And a duplicate scan event is recorded in the audit log
Multi-Channel Status Broadcast
Given a status transition occurs as a result of scan routing, queue release, or manual override When the status changes Then the calendar view reflects the new status within 2 seconds And the operations board reflects the new status within 2 seconds And an SMS containing the new status is sent to the client within 5 seconds And no duplicate notifications are sent for idempotent duplicate scans And any SMS delivery failure is logged and surfaced to staff
Manual Override Reason Codes
Given a staff user attempts a manual status change that bypasses early-hold, changes queue order, or reverses an automated transition When the user initiates the override Then the system requires selection of a reason code from a predefined list before saving And an optional free-text note up to 250 characters can be added And the change is blocked until a reason code is provided And upon save, the reason code, note, user, timestamp, and prior/new status are recorded And the resulting status change triggers the standard broadcasts
Grace Periods Configuration
Given an admin user with permissions opens the check-in rules settings When they edit Early Grace (minutes) and Late Grace (minutes) Then values must be integers within the allowed range (0–120) or validation prevents save with inline error And saved values take effect for scans occurring after the save time and do not retroactively alter existing check-ins And current values are retrievable via configuration API used by the scanner
Multi-Pet Appointment Handling
"As a staff member handling families with multiple pets, I want to check in pets together or individually from one pass so that handoffs are smooth and records stay correct."
Description

Support a single pass for households with multiple pets, allowing staff to check in all pets together or select specific pets. Display per-pet vaccine and preference exceptions, apply package credits to the correct pet-service lines, and permit split statuses when some pets are ready while others wait. Provide consolidated handoff notes with per-pet highlights to speed the exchange without losing detail.

Acceptance Criteria
Check-In All Pets via Single Pass
Given a household booking with two or more pets and a valid SMS QR pass When staff scans the pass and chooses "Check in all" Then the system pulls each Pet Profile for pets with appointments at the current location within the allowed time window And confirms deposit coverage per pet-service line And sets each selected pet’s appointment status to "In Service" within 2 seconds And records a single household check-in event with pet-level entries and staff ID And displays a success summary listing each pet and status
Selective Pet Check-In from Household Pass
Given a household pass with multiple pets scheduled for today When staff scans the pass and selects a subset of pets to check in Then only the selected pets’ appointments move to "In Service" And unselected pets remain in their prior status (e.g., "Scheduled") And the deposit (if present) is applied only to the selected pets’ service lines per allocation rules, leaving others untouched And an audit log records the pets checked in and pets deferred with timestamp and staff ID
Per-Pet Vaccine and Preference Exceptions Display
Given per-pet vaccine records and preference flags exist When staff initiates check-in from the scanned household pass Then any pet with expired or missing required vaccine is flagged as Blocking with pet name, vaccine type, and expiry date And any non-blocking preference exception is shown as Advisory per pet (e.g., anxious, muzzle required) And staff cannot complete check-in for blocking pets without override permission and a required reason And overrides are logged with staff ID, timestamp, and reason And staff may proceed to check in non-blocking pets while blocked pets remain not checked in
Package Credits Applied Per Pet-Service Line
Given available package credits are tied to specific pets and service types When completing check-in for multiple pets from one household pass Then credits are applied only to matching service lines for the correct pet And credits are not cross-applied between pets And remaining credit balance per package is recalculated and displayed post-application And a line-item ledger shows package ID, pet, service, quantity, and remaining credits And if credits are insufficient, the unpaid portion remains on the service line for later payment
Split Status During Service
Given multiple pets from the same household are in service When staff marks one pet as "Ready for Pickup" Then that pet’s status changes to "Ready for Pickup" while other pets remain in their current statuses And the household summary shows mixed statuses with per-pet badges And an SMS can be sent containing only the ready pet’s details and pickup instructions And payment and checkout can be processed per pet without requiring all pets to be ready
Consolidated Handoff Notes With Per-Pet Highlights
Given household-level notes and per-pet highlights are stored When the check-in confirmation view is opened after scanning Then a consolidated panel displays household notes followed by grouped per-pet highlights with name and photo And the panel supports expand/collapse per pet And the view renders within 2 seconds on a standard mid-tier device And staff can copy or print the consolidated notes in a single action And any edits made at handoff are saved to the appropriate household or pet record and are time-stamped
Household Pass Validity and Anti-Line-Jump Controls
Given a household SMS QR pass exists When the QR is scanned outside the allowed check-in window (e.g., more than 15 minutes before or after the appointment start) Then the system shows an Out-of-Window warning and blocks auto check-in without manager override And if the pass is scanned at a different location than scheduled, a location mismatch alert is shown and auto check-in is blocked And if the pass is scanned again after a completed check-in, reuse is denied and the last check-in timestamp is displayed
Audit, Logging, and Operational Analytics
"As an owner, I want audit logs and analytics on scans so that I can measure time saved and identify bottlenecks."
Description

Record each scan event with timestamp, staff identity, device type, outcome, latency, and resulting status transitions. Surface analytics such as scans per day, success rate, median check-in time, early/late arrival distribution, and deposit exception rates in dashboard widgets and exports. Provide CSV and API access, enforce retention policies, and support privacy requests (redaction/deletion) to meet compliance expectations.

Acceptance Criteria
Successful Scan Event Logging
- Given a booked appointment with an issued SMS QR pass and a signed-in staff member, when the QR is scanned successfully, then a scan event is persisted within 2 seconds containing: event_id (UUID), timestamp (ISO 8601 with timezone), staff_id, staff_role, location_id, device_type (kiosk|mobile), app_version, pet_id, pet_profile_version, client_id, appointment_id, scan_outcome = "success", latency_ms (scan read -> event write), prior_status, new_status, deposit_confirmed (boolean), correlation_id. - Then the associated appointment status transitions to "In Service" and the recorded prior_status->new_status matches the actual transition. - And the event write is idempotent: retrying the same event_id does not create duplicates. - And all fields are non-null except those explicitly optional (app_version, staff_role).
Failed Scan Event Logging and Outcomes
- Given an attempted scan that cannot complete, when the QR is invalid, expired, mismatched to appointment, duplicate, deposit not confirmed, or a backend error occurs, then a scan event is persisted with scan_outcome ∈ {"invalid_qr","expired","mismatch","duplicate","deposit_pending","error"} and includes error_code and error_message. - Then no appointment status transition occurs for outcomes other than "success". - And latency_ms is recorded for each failed attempt and event is persisted within 2 seconds. - And duplicate submissions of the same failed scan (same correlation_id) do not create multiple records.
Operational Analytics Widgets Computation
- Scans per day widget displays counts per org, location, and staff with timezone-correct bucketing and updates within 60 seconds of new events. - Success rate = successful scans / total scans over the selected filter; matches a manual recomputation on a controlled dataset to within 0.1%. - Median check-in time is computed as median(minutes_between(scheduled_start, scan_timestamp)) over successful scans; matches a manual recomputation on a controlled dataset. - Early/late distribution uses bins: early < -5 min, on-time -5 to +5 min inclusive, late > +5 min; percentages sum to 100% (±0.1%). - Deposit exception rate = count of successful scans with deposit_confirmed = false divided by total successful scans; displayed as count and percentage. - Widgets support filters: date range, location_id(s), staff_id(s), and pet_id; applying filters updates results in ≤ 2 seconds for last 30 days of data.
CSV Export of Scan and Analytics Data
- User can export scan events filtered by date range, location_id(s), staff_id(s), outcome, and pet_id; the CSV includes a header row and the columns: event_id, timestamp, staff_id, staff_role, location_id, device_type, app_version, pet_id, pet_profile_version, client_id, appointment_id, outcome, error_code, error_message, latency_ms, prior_status, new_status, deposit_confirmed, correlation_id. - CSV formatting: UTF-8, comma delimiter, RFC 4180 quoting, LF newlines. - For up to 100k rows, the file is generated and downloadable in ≤ 30 seconds; larger exports stream and complete without server timeouts. - Exported values reflect current redactions/deletions; no PII is present for subjects who have been redacted. - An export audit entry is recorded with requester, time, filters, and row_count.
API Access to Events and Analytics
- Provide authenticated endpoints: GET /v1/scan-events and GET /v1/analytics/checkin with OAuth2 bearer; unauthorized requests return 401, forbidden scopes return 403. - /v1/scan-events supports filters: date_from, date_to, location_id(s), staff_id(s), pet_id(s), outcome; pagination via opaque cursor and page_size up to 1000; results include total_estimate and next_cursor when applicable. - /v1/analytics/checkin returns aggregates: scans_per_day, success_rate, median_checkin_minutes, early_late_distribution, deposit_exception_rate for the supplied filters; responses include timezone used. - Performance SLOs: 95th percentile response ≤ 500 ms for cached analytics and ≤ 1500 ms for raw events for queries within last 30 days at ≤ 100 RPS per org. - Rate limiting: 600 requests/minute per org; responses include standard rate limit headers. - Schemas are versioned; unknown query params are ignored; invalid params return 400 with error details.
Retention Policies and Privacy Requests
- Each org has a retention_days setting (default 730); events older than retention_days are purged at least daily; dashboards and APIs exclude purged data within 24 hours of purge. - Privacy deletion/redaction request for a client or pet is accepted via admin UI/API; within 7 days, PII fields (client name, phone, email; pet name, photo) are irreversibly redacted in scan events, exports, and analytics caches while preserving non-identifying metrics. - Backup media containing redacted subjects are overwritten or expired within 30 days; until then, backups are inaccessible to analytics/export endpoints. - Every deletion/redaction is itself auditable: request_id, requester, scope, submitted_at, completed_at, and outcome are recorded and retrievable via admin audit view/API. - Attempting to access redacted PII via API/CSV returns nulls for protected fields and does not leak identifiers (e.g., no hashes reversible to original values).

Offline Ticketing

If venue Wi‑Fi drops, QuickQueue keeps running. Signups, deposits, and waivers cache locally, issue a numbered ticket, and auto sync/send SMS once back online—so you don’t lose the line or the sale.

Requirements

Offline Data Cache & Mode Switch
"As a groomer working an on-site event with spotty Wi‑Fi, I want the app to keep capturing client, pet, and deposit info offline so that I don’t lose the line or the sale."
Description

Detects connectivity loss and switches the app into offline mode, persisting signups, deposit intents, waivers, and minimal Pet Profile Smart Card data (pet name/photo, owner phone) to an encrypted local store. Ensures crash-safe writes, schema versioning, and queue integrity with size limits and eviction rules. Records timestamps, device/user IDs, and dependencies to enable reliable replay. On reconnection, initiates background sync to create/update server records in the correct order while preserving user workflow continuity within QuickQueue.

Acceptance Criteria
Automatic Offline Mode Switch on Connectivity Loss
Given the device has an active session in QuickQueue and network connectivity is lost for more than 3 seconds When reachability checks fail and no data can be sent to the server Then the app enters Offline Mode within 2 seconds and displays a persistent offline banner and icon Given intermittent connectivity flaps When connectivity toggles multiple times within 10 seconds Then the app debounces mode changes so the mode does not switch more than once in any 10‑second window Given the app is in Offline Mode When sustained connectivity is detected for at least 5 seconds Then the app automatically returns to Online Mode and removes the offline indicators without disrupting the current workflow
Encrypted Local Cache with Crash-Safe Writes
Given the app is offline and a user creates a signup, deposit intent, waiver, or minimal Pet Profile (name, photo, owner phone) When the record is saved locally Then the record is written atomically to an encrypted local store and is readable only by the app Given the app is force-closed or the device loses power during a write When the app restarts Then the local store is consistent with no partial records; the last acknowledged write is present and in-flight writes are retried or rolled back Given a security inspection of the device file system When attempting to read cached records outside the app sandbox Then the data at rest is encrypted and cannot be interpreted without app context
Queue Size Limit and Eviction Policy Enforcement
Given an offline queue size limit L is configured (default L = 500 records) and a warning threshold W = 80% of L When the queue size reaches W Then the app displays a non-blocking warning indicating remaining capacity Given the queue would exceed L with a new record When the record is enqueued Then the app first downscales optional media (e.g., pet photo thumbnails <= 64 KB) to fit; if still exceeding L, the enqueue is blocked with a clear error and guidance, and no existing unsynced records are lost Given the queue is at capacity and enqueue is blocked When the user frees space or the app syncs and reduces queue size below L Then new records can be enqueued without data loss
Replay Metadata Captured for Offline Records
Given a new offline record is created When it is added to the queue Then it includes ISO‑8601 UTC timestamp, deviceId, userId, stable client recordId, operation type (create/update), and dependency references (e.g., petProfileId for a waiver) Given device clock skew When multiple records are created offline Then each record also receives a monotonically increasing local sequence number to ensure deterministic replay order Given a diagnostic export of the offline queue When inspected Then all required metadata fields are present for every record
Ordered Background Sync and Idempotent Replay on Reconnection
Given the app is in Offline Mode with N>0 queued records and connectivity is restored and stable for 5 seconds When reconnection is detected Then background sync begins within 3 seconds and processes records in dependency order (e.g., Pet Profile before Waiver; Signup before Deposit Intent update) Given a record is retried due to transient failures When the same client recordId is replayed multiple times Then the outcome is idempotent on the server (no duplicate records, SMS, or charges) Given a record fails due to a server conflict or validation error When processing the queue Then the failing record is marked Failed with an actionable message, subsequent independent records continue, and the user can retry after resolution with no reordering issues
Auto Dispatch of Deferred SMS Notifications Post-Sync
Given SMS notifications were queued as intents while offline (e.g., signup confirmation, ticket number) When the corresponding records are successfully synced Then the SMS messages are automatically sent and delivery status is updated in the timeline within 10 seconds, without duplicate sends Given an SMS send fails during post-sync dispatch When retry policy is applied Then the app retries up to 3 times with exponential backoff and surfaces a failure state if all retries fail, without blocking the rest of the sync
Offline QuickQueue Workflow Continuity and Numbered Ticketing
Given venue Wi‑Fi drops while operators are using QuickQueue When they create signups, capture deposit intents, collect waivers, and add minimal Pet Profile data Then all actions are allowed offline, a numbered ticket is issued and visible within 2 seconds, and no sensitive payment data (e.g., full PAN) is stored locally Given multiple devices are issuing tickets offline for the same event When tickets are generated Then each ticket number is globally unique via device-scoped prefix plus local sequence, with zero duplicates across 1,000 consecutive tickets in a test cohort Given connectivity is restored When the offline tickets and related records sync Then server timelines reflect the correct order and ticket numbers, and the operator workflow continues without requiring manual reconciliation
Instant Offline Ticket Numbering
"As a staffer managing a busy line, I want instant ticket numbers even without internet so that I can keep arrivals ordered and served fairly."
Description

Generates sequential, location-scoped ticket numbers while offline to keep the queue organized. Displays the ticket on-screen with optional QR code encoding the local ticket ID and basic metadata; supports quick re-open and lookup. Guarantees uniqueness per device/session via a prefix scheme and reconciles to server-issued IDs during sync. Associates the ticket with the offline records (signup, waiver) to maintain order and service priority within QuickQueue.

Acceptance Criteria
Offline Sequential Numbering per Location
Given the device is offline at location L When the user creates a new ticket Then the ticket number equals the last persisted number for L plus 1 Given no prior offline tickets for location L on this device When the first ticket is created Then the device initializes from the last-known sequence for L persisted locally and starts at the next number Given the app is force-closed and reopened while still offline When a new ticket is created for L Then the sequence resumes without gaps or duplicates Given a ticket creation is canceled before save When another ticket is created for L Then the previously allocated number is not consumed Given the device clock or timezone changes When a ticket is created for L Then numbering remains monotonic by sequence and is unaffected by time changes Given multiple service types are used under the same location When tickets are created offline Then they share one sequential series scoped to location L
Device/Session Prefix Ensures Uniqueness
Given a ticket is created offline When the local ticket ID is generated Then it is composed as P-N where P includes location code, device ID, and session ID, and N is the location sequence number Given two devices at the same location are offline When they each create tickets Then no two local ticket IDs are identical Given a session restarts on a device When new tickets are created Then a new session ID is used in P and previously issued IDs remain resolvable Given up to 10,000 tickets are issued in one offline session When ticket IDs are generated Then the system prevents prefix exhaustion or warns with a clear call to action before the limit is reached Given sync completes When any local ID collision is detected Then it is resolved deterministically without data loss and without changing the customer-visible sequential ticket number
Ticket Screen and QR Encoding
Given a ticket is created offline When the ticket screen is shown Then it renders within 500 ms and displays the ticket number prominently at a minimum 24 pt size with WCAG AA contrast Given Show QR is enabled When a ticket is created offline Then a QR code is displayed encoding {local_ticket_id, ticket_number, location_id, created_at_epoch, checksum} with error correction level M or higher Given the QR code is scanned within the FetchFlow app on the same device while offline When the scan succeeds Then the corresponding ticket opens immediately (≤200 ms) Given the QR code is scanned on another device that has not yet synced the ticket When the scan is attempted while offline Then the app displays "Not found offline; available after sync" Given the device theme is light or dark When the QR is displayed Then it remains scannable by a standard smartphone camera at 30 cm distance
Link Signup/Deposit/Waiver to Ticket Offline
Given a signup is captured offline When a ticket is issued Then the ticket stores a link to the local signup record ID Given a deposit is recorded offline When it is attached to the ticket Then the ticket reflects the deposit amount and status and retains the link Given a waiver is signed offline When it is saved Then it is associated to the same ticket and marked complete on the ticket view Given multiple linked records exist for a ticket When queue order is displayed offline Then order is determined by ticket creation time and sequence, not by later edits Given any linked record fails to save When the ticket is created Then the ticket persists with a visible pending indicator and a retry action for the failed record
Fast Re-open and Lookup Offline
Given staff enters an exact ticket number When searching offline Then the ticket opens in ≤300 ms from local cache Given staff scans the ticket's QR on the same device while offline When the scan succeeds Then the ticket detail opens in ≤200 ms Given staff enters a partial number of at least 2 digits When searching offline Then the app shows up to 5 ranked matches from local cache Given the local cache contains 5,000 offline tickets When searching by exact number Then the response time remains ≤500 ms Given a non-existent number is searched offline When the lookup runs Then the app shows "No offline match" without crashing
Sync Reconciliation to Server IDs and SMS Dispatch
Given the device reconnects to the network When sync starts Then offline tickets are uploaded in creation order and receive server-issued canonical IDs Given server IDs are assigned When mapping is stored Then {local_ticket_id, ticket_number} resolve to {server_ticket_id} for future lookups Given customer SMS notifications are enabled When a ticket and its associated records sync Then configured SMS messages are sent within 30 seconds of sync completion Given a network interruption occurs mid-sync When sync resumes Then uploads are idempotent and no duplicate server tickets are created Given two devices generated offline tickets for the same location When both devices sync Then server-side queue ordering merges by created_at timestamp and, for ties, by device/session prefix without changing any device-visible sequential numbers Given sync completes When staff searches by the original local ticket number Then the app resolves the ticket via the stored mapping
Deferred SMS Queue & Auto Sync
"As a groomer, I want all confirmations and payment links collected offline to auto-send once I’m back online so that clients are informed without me following up manually."
Description

Queues outgoing SMS (confirmations, ticket notifications, payment links, reminders) locally with idempotency keys, send-by TTLs, and per-message templates. Upon reconnection, pushes records to the server and triggers SMS via standard gateways in correct sequence, with retries and exponential backoff. Surfaces delivery status to the operator and escalates failures with actionable errors. Prevents duplicate sends using idempotent server endpoints and message fingerprints.

Acceptance Criteria
Offline Queueing During Connectivity Loss
Given the device loses network connectivity while the operator triggers an SMS (confirmation, ticket notification, payment link, or reminder) When the operator completes the action Then the app persists a queued message locally with fields: idempotency_key (UUIDv4), recipient (E.164), template_id, template_params, created_at, send_by And the message status is "Queued" and visible in the operator UI within 1 second And no network request is attempted until connectivity is restored And the queued message remains intact after an app restart
Send-by TTL Prevents Stale Sends
Given a queued message has a send_by timestamp that is in the past or will be exceeded before the next scheduled attempt When a send attempt would otherwise occur (due to reconnection or retry timer) Then the message is marked "Expired" and will not be dispatched And no gateway invocation is made for the expired message And the operator sees an error "Send-by window expired" with an option to resend with a new TTL And an audit log entry records the expiration with timestamp and message identifiers
Ordered Dispatch After Reconnection
Given two or more queued messages exist for the same recipient created while offline When connectivity is restored Then the client syncs those messages to the server in ascending created_at order per recipient And the server triggers SMS in the same order (verified by gateway message timestamps) And for a batch of up to 10 messages, the time from first to last dispatch is less than 5 seconds, subject to TTL constraints And ticket confirmations are sent before related payment links for the same ticket
Idempotent Sync Prevents Duplicates
Given a message has been submitted to the server and the client did not receive a definitive outcome (timeout, retry) When the client resubmits the same payload with the identical idempotency_key and fingerprint Then the server processes the request idempotently and does not create a duplicate SMS And the client records a single final delivery status for that message And the recipient receives at most one SMS for that payload (validated by a single gateway message_id)
Exponential Backoff Retries for Transient Failures
Given a send attempt fails due to HTTP 5xx, network error, or HTTP 429 throttling When retrying the message Then the client performs up to 5 attempts with exponential backoff delays of approximately 2s, 4s, 8s, 16s, 32s with 120% jitter And retries cease immediately if the send_by TTL would be exceeded before the next attempt And upon exhausting attempts the message is marked "Failed" with the last error code and response captured in the audit log
Delivery Status Visibility and Failure Escalation
Given the server accepts a message for delivery and emits gateway events When delivery webhooks are received (accepted, sent, delivered, failed) Then the operator UI updates the message status within 3 seconds of webhook receipt to reflect the latest state And on a "Failed" event the UI displays the failure reason and presents actionable next steps (e.g., verify number or resend) And the message timeline shows all status transitions with timestamps and gateway message_id
Template Rendering and Message Fingerprinting Integrity
Given a message is prepared using a template When rendering the message and creating the queued record Then all required placeholders are resolved from ticket and pet profile data; otherwise the message is not queued and the operator is prompted to correct missing fields And the message fingerprint is computed as SHA-256 of normalized recipient + template_id + sorted template_params + ticket_id and stored with the record And the fingerprint and idempotency_key remain stable across app restarts and are used to deduplicate during sync
Sync Conflict Resolution & Idempotency
"As an operator, I want the system to reconcile offline entries with existing client records automatically so that I don’t create duplicates or lose data when the network returns."
Description

Implements deterministic merge rules when syncing offline records with existing customers, pets, and appointments (e.g., match by verified phone number and pet name). Uses record versioning and idempotency tokens so replay is safe and repeatable. Handles duplicates, partial failures, and race conditions with server-side upserts and compensating actions. Logs a full audit trail mapping offline IDs to server IDs for traceability and support.

Acceptance Criteria
Idempotent Appointment Creation on Retry
Given an offline-created appointment with request body B and Idempotency-Key T, When the client submits B with T multiple times due to retries, Then exactly one server appointment exists and subsequent responses return the same appointment_id and status without duplicate side effects. Given an offline-created appointment submitted with the same Idempotency-Key T but a different body B', When processed, Then the server returns 409 Idempotency Mismatch and applies zero state changes. Given an offline payment intent with Idempotency-Key P, When retried, Then exactly one payment capture occurs and the same charge_id is returned on all successful retries.
Deterministic Customer and Pet Merge by Phone and Pet Name
Given a syncing offline record with verified owner phone P (normalized E.164) and pet name N, When processed, Then the system upserts the customer matched by phone P; if none exists, a new customer is created. Given the matched/created customer and pet name N, When processed, Then if exactly one pet under that customer matches case-insensitive trimmed N, the pet is updated; if none match, a new pet is created. Given multiple existing pets under the customer match name N, When processed, Then no merge occurs; a new pet is created and a duplicate-review flag is set linking candidate pets and the new pet.
Versioned Upsert Conflict Resolution
Given a server record with version v and an offline mutation derived from version v, When another update increments the server record to version v+1 before sync, Then the client upsert returns 409 Conflict with latest_version=v+1 and no partial changes are applied. Given the client reapplies the mutation against latest_version with deterministic merge rules, When submitted, Then the upsert succeeds, the record version increments by 1, and only the intended fields in the mutation are changed. Given a stale update on a protected field (e.g., payment_status, deposit_amount), When submitted against a newer server version, Then the server rejects with 409 Conflict PROTECTED_FIELD and leaves the record unchanged.
Partial Batch Sync with Safe Retries
Given a batch of N offline operations (appointments, payments, waivers) during a reconnect, When sync executes, Then each operation is processed atomically and independently, returning per-item success or error with retryable/non-retryable classification. Given k successes and m failures in the batch, When sync completes, Then the k successes are committed and not re-enqueued; the m failures remain queued with error codes and can be retried without duplicating successful operations. Given a subsequent retry, When only failed operations are resubmitted, Then previously successful operations are skipped or acknowledged via idempotency without creating duplicates.
Duplicate Payments and SMS Prevention with Compensating Actions
Given two offline records that would create the same payment (same payment_intent_id or same natural key and Idempotency-Key), When both sync, Then the server records a single charge, emits a single receipt, and marks the duplicate as deduplicated with zero additional capture. Given a queued SMS action with de-dup key M, When the action is retried, Then exactly one outbound SMS is sent and subsequent retries return a de-duplicated response without sending. Given a duplicate appointment is detected post-sync, When a compensate duplicate API is invoked, Then the newer duplicate is cancelled, any fees are reversed, package balances restored, and all compensating actions are logged immutably.
Audit Trail and Offline-to-Server ID Mapping
Given offline entities with local_ids and idempotency keys, When sync completes, Then a mapping is persisted for each entity linking local_id -> server_id with entity_type, timestamp, device_id, user_id, request_hash, and outcome. Given an idempotent retry for the same local_id/key, When processed, Then the same server_id mapping is returned and no new mapping row is created except an appended retry event entry. Given a support query by local_id, server_id, or idempotency_key, When requested via the audit API, Then the complete mapping and event history are returned and entries are immutable (append-only).
Race Condition Handling Under Concurrent Edits
Given two offline devices A and B editing the same appointment, When both sync overlapping fields concurrently, Then the server accepts the first write and returns 409 Conflict MERGE_REQUIRED to the second, with no interleaved partial state. Given two edits modify disjoint fields, When synced, Then the server applies a deterministic field-level last-write-wins merge based on updated_at timestamps, excluding protected fields which must conflict. Given a conflict on a protected field, When detected, Then the server rejects the change with 409 and preserves current server state, requiring an explicit client merge and resubmit.
Secure Offline Storage & Compliance
"As a business owner, I want offline data to be protected and compliant so that customer information and payments remain secure even without internet."
Description

Encrypts all offline data at rest using OS keystore-backed keys, with schema-level PII minimization and configurable retention. Stores no raw card data; deposit capture offline records only intent details (amount, consent, last4/token reference) for later confirmation against saved payment methods or SMS pay links on reconnect. Includes inactivity locks, scrub-on-sync options, and audit logs to support PCI guidance and data protection requirements.

Acceptance Criteria
OS Keystore-Backed Encryption at Rest
Given the device is offline and an offline record (signup, deposit, waiver) is saved When the data is written to local storage Then it is encrypted using a non-exportable OS keystore-backed key (hardware-backed when available) with AES-256-GCM, a unique IV per record, and a 128-bit authentication tag Given the app database file is copied to another device or user profile When decryption is attempted Then the content remains unreadable because the key is bound to the original device keystore Given the app is uninstalled and reinstalled When attempting to read prior offline data Then decryption fails and no plaintext is recoverable Given any stored record is read When integrity is verified Then modified ciphertext is detected and rejected with an error
PII Minimization and Prohibited Data Safeguards
Rule: The offline schema stores only the minimal fields required to operate offline per record type. - Signup: first name, E.164 phone, ticket number, SMS consent flag, pet name(s) - Waiver: waiver template id, version, acceptance flag, signer first name/initials, signature blob reference - Deposit: amount, currency, consent timestamp, idempotency key, payment method token id or card last4 and brand Rule: The following are never stored offline: full card PAN, CVV, full expiry, magnetic track data, PIN, full billing address, government IDs, date of birth, vaccine file binaries or photos. Given an attempt is made to persist a prohibited field When the write is invoked Then the operation is blocked, an error is raised, and nothing is written Given the offline database is inspected When querying columns and sample rows Then only whitelisted fields are present and values comply with the rules
Configurable Retention and Secure Deletion
Given retention is configured between 24 hours and 30 days (default 7 days) When a record exceeds its retention window Then it is purged automatically within 15 minutes using cryptographic erasure and storage compaction Given a user triggers Manual Purge When the purge completes Then all offline records are deleted via cryptographic erasure (rotate/delete the data encryption key) and cannot be recovered via app APIs or forensic inspection of the database freelist Given retention is updated When the setting is saved Then the new policy is applied to existing records on the next maintenance cycle and all purge events are audit-logged
Inactivity Lock and Protected Unlock
Given the app has cached offline data When it is backgrounded Then the app locks immediately and hides sensitive UI Given the app is idle When the configured inactivity period elapses (default 3 minutes, configurable 1–15) Then the app locks and offline data is inaccessible until unlock Given a user attempts to unlock When biometric or app passcode is provided Then access is granted; after 5 consecutive failures a 5-minute cooldown is enforced Given the device receives notifications while locked When an offline cache exists Then notification content is redacted Rule: ADB/OS backups of app data are disabled and offline data cannot be accessed via unprivileged backups
Scrub-on-Sync and Partial Failure Handling
Given scrub-on-sync is enabled When a record is successfully synced to the server Then the local copy is securely deleted within 60 seconds and storage size decreases accordingly Given a batch sync where some records fail When the sync completes Then only the successfully synced records are scrubbed; failed records are retained for retry with exponential backoff Given scrub-on-sync is disabled When records are synced Then local copies remain until retention purges them Given scrub actions occur When audit logs are reviewed Then a per-record scrub entry exists with record id, timestamp, actor, and outcome
Tamper-Evident Audit Logging
Given any create, read, update, delete, lock, unlock, sync, purge, or key-rotation event occurs on offline data When the action completes Then an audit entry is recorded with actor id, device id, UTC timestamp, event type, record type/id, and success/failure Given an audit log file is altered When integrity verification runs Then the hash-chain check fails and raises a tamper-detected alert Given an admin requests an audit export When the export is generated Then it excludes sensitive data values, includes a verification signature, and is downloadable within 60 seconds
Tokenized Deposit Intent and SMS Pay Fallback
Given the device is offline and a deposit is captured When the record is saved Then only amount, currency, consent timestamp, ticket id, idempotency key, and payment method token id or card last4+brand are stored; no PAN, CVV, or expiry are stored Given the device reconnects When deposit intents are processed Then available saved payment method tokens are charged automatically; otherwise an SMS pay link is sent to the customer within 30 seconds Given a reconnection storm or double-submit When the same idempotency key is processed Then only a single charge or pay link is created and duplicates are prevented and logged
Offline Waiver & e‑Sign Capture
"As a trainer, I want clients to sign waivers offline so that check-in stays fast and compliant when connectivity drops."
Description

Caches service-specific waiver templates and collects signatures offline with time, device, and signer attribution. Supports multi-pet selections and attaches waivers to the corresponding offline tickets. On reconnect, renders a tamper-evident PDF, stores it in the document repository, and links it to the appointment and Pet Profile. Marks vaccine verification as pending if records can’t be fetched offline and resolves automatically after sync.

Acceptance Criteria
Offline Availability of Service-Specific Waiver Templates
Given the device has completed a template prefetch within the last 24 hours and the user is authenticated When the device is offline and a new ticket is created for a service requiring a waiver Then the correct latest-approved waiver template for that service is available offline with versionId and SHA-256 checksum And the waiver renders within 2 seconds on a mid-tier device And if a required template is not present, the UI displays a clear blocker state and provides a retry/prefetch action without allowing signature capture
Offline e‑Signature Capture with Attribution
Given the device is offline and a waiver template is loaded with owner and pet context When the signer draws a signature and enters their full legal name and relationship to the pet(s) Then the app records signature strokes, signer name, owner profile ID (or phone), deviceId, app version, and local timestamp with timezone offset And the signature capture enforces a minimum stroke length or 10-stroke threshold and requires all mandatory checkboxes/initials And the captured data is stored encrypted at rest and cannot be edited after save
Multi‑Pet Selection and Ticket Association
Given multiple pets are selected on a single offline ticket for services that share the same waiver When the owner completes one waiver signing session Then a single signed waiver instance is associated to each selected pet and its corresponding offline ticket And each association records ticketId, petId, ownerId, serviceId, and waiverVersionId And duplicate associations are prevented for the same ticketId/petId pair
Tamper‑Evident PDF Generation and Repository Linking on Reconnect
Given one or more offline-signed waivers are pending sync When connectivity is restored Then within 60 seconds the system generates a PDF per waiver including signature image, signer name, deviceId, timestamp, geotag (if available), waiverVersionId, and an embedded cryptographic hash And the PDF is stored in the document repository with immutable object key and server-verified hash/ETag And links to the stored PDF are created on the appointment and each Pet Profile involved And failures retry with exponential backoff up to 5 attempts before surfacing a sync error
Vaccine Verification Pending and Auto‑Resolution After Sync
Given vaccine records cannot be fetched while offline during ticket creation When the waiver is signed offline Then the pet’s vaccine status is marked Pending Verification on the ticket and Pet Profile and is visible in the UI And no Verified badge is shown while offline When connectivity is restored Then the system fetches vaccine records and automatically updates the status to Verified or Action Required, applying any gating rules accordingly
Resilience, Idempotency, and Conflict Handling During Sync
Given waivers were signed offline while a newer waiver template version was published on the server When sync occurs Then the server accepts the waiver bound to its original versionId without requiring re-sign for that visit And duplicate submissions (by client UUID) are treated as idempotent with no extra records created And partially synced items are retried with exponential backoff up to 15 minutes total then shown in a Sync Issues queue
Storage Limits, Security, and Cleanup for Offline Artifacts
Given many waivers are captured while offline When pending offline waiver data exceeds 200 items or 200 MB (whichever comes first) Then the app warns the user and blocks new captures until space is freed by sync or manual cleanup And all offline waiver data is encrypted using the OS keystore and is auto-deleted within 24 hours of successful sync And a Clear Offline Data action allows explicit removal of unsynced waivers with a confirmation that shows the count to be deleted
Connectivity Indicator & Queue Controls
"As a front-desk assistant, I want an obvious offline status and control over the sync queue so that I can manage the line confidently until we’re back online."
Description

Provides a clear offline/online status indicator with counts of queued records and messages, plus operator controls to retry, cancel, or prioritize items. Offers safe offline actions (mark no-show, refund intent, ticket reassign) with proper audit logging and deferred execution. Notifies the operator when sync starts/completes and surfaces any reconciliation issues requiring attention.

Acceptance Criteria
Offline/Online Status Indicator with Queue Counts
Given the device has active connectivity When the app is online Then the connectivity indicator displays "Online" with a green state and queue counts of 0 records and 0 messages Given the device loses connectivity for at least 2 seconds When the app detects no network transport Then the indicator switches to "Offline" with a red state within 2 seconds and displays the current counts of queued records and queued SMS messages Given at least 1 record or message is queued while offline When new items are added Then the indicator count increments in real time without requiring a screen refresh Given connectivity is restored When auto sync starts Then the indicator changes to "Syncing..." with a spinner and shows remaining queued counts decreasing until they reach 0 and returns to "Online" within 2 seconds of completion
Queue Item Retry, Cancel, and Prioritize Controls
Given there are queued items When the operator selects one or more items and taps "Retry" Then the system immediately attempts sync and updates each item's status to "In Progress" then "Synced" on success or "Failed" with an error code on failure, logging attempt count and timestamp Given there are queued items When the operator selects an item and taps "Cancel" Then the system prompts for a cancellation reason and, upon confirmation, marks the item "Canceled," removes it from the outbound queue, and writes an audit entry with reason, operator, timestamp, and prior state Given there are 3 or more queued items When the operator selects one and taps "Prioritize" Then that item moves to the top of the queue and is attempted before lower-priority items, preserving FIFO order among non-prioritized items Given batch controls are used When "Retry All" is tapped Then the system starts syncing all queued items in priority order and displays progress with success/failure counts
Safe Offline Actions (No-Show, Refund Intent, Ticket Reassign)
Given the device is offline When the operator marks an appointment as "No-Show" Then a queued no-show record is created locally with timestamp, operator, and associated ticket/appointment ID and no external SMS or payment call is executed until online Given the device is offline When the operator records a Refund Intent on a payment Then the system creates a pending refund instruction with amount, method, and reason, validates the amount does not exceed the captured amount, and defers gateway calls until connectivity is restored Given the device is offline When the operator reassigns a numbered ticket to a different pet/owner Then the reassignment is stored locally, updates the visible queue immediately, and enforces unique ticket numbering without collisions Given any deferred action exists When the operator views the item details Then the UI displays a "Pending" badge and the exact deferred side effects that will execute on sync
Audit Logging for Offline and Queue Operations
Given any queue operation occurs (add, retry, cancel, prioritize, resolve) When the operation completes Then an immutable audit entry is appended containing operation type, operator ID, device ID, timestamp (UTC), item ID, pre-state, post-state, and correlation ID Given audit entries exist When an admin exports logs for a date range Then the system produces a CSV including all the above fields and redacts PII as configured Given an audit entry is created while offline When sync completes Then the entry is preserved with the original offline timestamp and an additional syncedAt timestamp
Sync Start, Progress, and Completion Notifications
Given there are N queued items and connectivity is restored When auto sync begins Then the operator receives a non-blocking toast "Sync started: N items" and the status area shows a progress bar and X/Y items processed Given sync completes with all successes When the last item is processed Then the operator receives "Sync complete: N succeeded" and the indicator returns to "Online" with zero counts Given sync completes with failures When at least one item fails Then the system shows "Sync completed with issues" and a banner that links to the Reconciliation list filtered to failed items Given the operator has Do Not Disturb enabled When sync notifications fire Then they are delivered silently within the app without vibration or sound
Reconciliation Surface and Resolution Workflow
Given items failed during sync due to validation, conflicts, or gateway errors When the operator opens Reconciliation Then the list displays each failed item with error code, human-readable message, and recommended next actions Given a failed item has a fixable validation error When the operator edits the required fields and taps "Retry" Then the item status changes to "In Progress" and on success is removed from Reconciliation and logged as "Resolved" Given a failed item is a duplicate submission When the operator chooses "Merge" Then the system links the duplicate to the original, avoids double charging or double messaging, and records the merge reference in audit logs Given there are unresolved failed items older than 24 hours When the operator opens the dashboard Then a persistent badge shows the count and a reminder prompts review

Multi‑Station Sync

Distribute the queue across multiple groomers or tables automatically. The system balances workloads, assigns a station at booking, and updates signage/SMS if stations change—cutting bottlenecks and idle time.

Requirements

Smart Station Assignment at Booking
"As a shop manager, I want the system to auto-assign the best-fit station and groomer at booking so that workload is balanced and pets get the right setup without manual triage."
Description

Automatically assigns the optimal station and groomer at the moment of booking (dashboard or SMS) using availability, service duration templates, groomer skill tags, station equipment, and pet-specific attributes from Smart Cards (size, temperament, medical flags, vaccine status). Balances workload by distributing appointments across stations and start times while honoring buffers, clean-up, and prep time. Provides assignment rationale, predicted start/finish windows, and alternatives if the requested slot cannot meet constraints. Integrates with existing booking flows, packages, and payment holds so checkout and no-show policies remain intact. Exposes APIs and events for downstream systems to react to confirmed assignments.

Acceptance Criteria
Dashboard Booking: Auto-Assign Optimal Station and Groomer
Given a receptionist creates a new appointment in the dashboard with service, pet, and requested time And groomer skill tags, station equipment, and pet profile attributes are available When the booking is submitted Then the system assigns a station and groomer that satisfy skills, equipment, pet size, temperament, medical, and vaccine constraints And honors the service duration template including prep, clean-up, and buffer times with no overlaps And balances workload by minimizing the maximum projected station utilization over the service window deterministically And returns the assignment with predicted start and finish windows and a human-readable rationale And persists the booking and assignment atomically with no intermediate unassigned state And completes the assignment selection in 1500 ms or less under normal load
SMS Booking: Auto-Assignment and Confirmation via Text
Given a customer books via the SMS flow providing service, date, and time preferences And the pet’s Smart Card data is on file When the system confirms the booking Then it assigns a compliant station and groomer using the same rules as dashboard booking And replies via SMS with station name/ID, groomer name, and predicted start/finish windows And if the requested time cannot meet constraints, it replies with up to 3 viable alternatives including station/groomer and start windows And the SMS confirmation reflects any buffers included in the predicted window And the assignment decision completes within 2 seconds round-trip for the SMS interaction
Hard Constraint Enforcement: Skills, Equipment, and Pet Flags
Given groomer skills, station equipment, and pet attributes (size, temperament, medical, vaccine status) are defined When an assignment is calculated Then no assignment is produced that violates any hard constraint And if constraints cannot be satisfied for the requested time, the booking is not confirmed and the UI/SMS returns a list of violated constraints And compliance is logged with the set of constraints evaluated and the winning candidate’s satisfaction results
Alternatives and Rationale Presentation
Given the requested slot cannot meet all constraints or balancing goals When proposing alternatives Then the system returns at least 3 ranked alternatives when available And each alternative includes station ID, groomer ID, proposed start window, predicted finish window, and a short reason (e.g., equipment match, skill coverage) And alternatives are ordered by earliest feasible start, then lowest projected utilization impact And the booking UI/SMS displays the rationale for the chosen assignment and the primary factors considered (top 3) And the API returns a machine-readable rationale with scored candidates and chosen winner
Packages, Payment Holds, and Policy Compatibility
Given a business has package credits and no-show policies configured and payment holds are enabled When an assignment is confirmed at booking Then package application and payment authorization follow existing rules with no change to amounts or policy triggers And if a package fully covers the service, no payment hold is placed; otherwise the standard hold is placed successfully or the booking is declined with a clear error And no-show fee configuration remains unchanged and is not triggered by assignment alone And financial records show exactly one hold (if applicable) and zero captures at assignment time
APIs and Events for Downstream Systems
Given an assignment is confirmed When emitting integration outputs Then a StationAssignmentConfirmed event is published within 2 seconds containing bookingId, petId, serviceId, stationId, groomerId, startTime, endTime, rationale summary, and version And delivery is at-least-once with retry for 24 hours on failure And a REST/GraphQL endpoint returns the current assignment with the same fields and an ETag/version for concurrency control And the payloads are documented with examples and pass schema validation in contract tests
Concurrency and Conflict Resolution
Given two or more clients attempt to book overlapping times that would contend for the same groomer or station When submissions occur within 250 ms of each other Then only one booking is confirmed and assigned; others receive alternatives and a non-ambiguous conflict message And no double-booking occurs for any groomer or station considering buffers, prep, and clean-up And the operation is idempotent when the same client retries with the same idempotency key, resulting in the same assignment or the same conflict response
Real-Time Queue Rebalancing and Handoffs
"As a groomer, I want the queue to rebalance in real time when timings change so that I spend less time idle and we avoid bottlenecks."
Description

Continuously monitors check-ins, late arrivals, service overruns/underruns, cancellations, and no-shows to dynamically rebalance the queue and reassign stations with minimal disruption. Applies guardrails such as mid-service lock, grooming phase awareness, and pinned assignments before proposing swaps. Notifies affected staff and clients instantly, updating ETAs and pickup windows, and synchronizes changes to signage and SMS threads. Supports what-if suggestions to resolve bottlenecks and can auto-apply safe optimizations under configured rules. Maintains an audit trail of all reassignments for accountability and reporting.

Acceptance Criteria
Late Arrival Triggered Rebalancing with Instant Notifications
Given a booking is marked late beyond the configured grace period, when rebalancing runs, then a reassignment proposal is generated within 5 seconds that reduces predicted average wait time by at least 10% or records "no beneficial change". Given auto-apply is enabled for late-arrival triggers and guardrails are satisfied, when a valid proposal exists, then the reassignment is applied within 5 seconds and reflected consistently in dashboard, signage, and client SMS thread. Given a reassignment is applied, then the affected staff and client receive one consolidated notification within 5 seconds containing new station, updated ETA, updated pickup window, and reason code "late-arrival". Given the client arrives and is checked in before the reassignment is applied, when a proposal is pending, then the proposal is withdrawn and the audit trail records status "withdrawn: arrival superseded". Given skills, station capacity, and configuration constraints, when no compliant reassignment is possible, then the system records reason "guardrail/no-safe-option" and leaves assignments unchanged.
Guardrails: Mid-Service Lock, Phase Awareness, and Pinned Assignments
Given a job is in a locked phase (e.g., clipping/drying), when rebalancing runs, then the system does not propose moving that job or any dependent tasks. Given a booking is pinned by staff, when rebalancing runs, then the booking is excluded from move candidates until explicitly unpinned by a user with appropriate permissions. Given a job is in a transition-safe phase (e.g., post-bath/pre-finish) and handoff buffer time is available, when rebalancing runs, then the system may propose a move including calculated handoff start time and buffer >= configured minimum (e.g., 3 minutes). Given linked multi-pet bookings, when rebalancing runs, then the group is treated as a single unit unless an explicit split-with-consent flag is present, in which case each pet inherits guardrails independently. Given a user attempts a manual move that violates guardrails, when they confirm, then the system blocks the action, displays the violated rule(s), and writes an audit entry with reason "guardrail-violation".
No-Show Handling with Auto-Reassignment and Fee Application
Given a booking exceeds the no-show threshold, when it is auto-marked or staff-marked as no-show, then the system frees the station slot, applies the configured no-show fee to the client account, and sends a receipt SMS within 10 seconds. Given capacity is freed by a no-show, when rebalancing runs, then the system pulls forward eligible jobs to reduce idle time and sends updated ETAs/pickup windows to affected clients and stations within 5 seconds. Given payment capture for the no-show fee fails, when the system attempts to charge, then the fee is recorded as pending, staff are alerted, and rebalancing proceeds without blocking. Given a no-show event triggers reassignments, then a single audit bundle records the no-show, fee outcome, and each reassignment with before/after station and timestamps.
Overrun and Underrun Adjustments with ETA and Pickup Updates
Given a service is projected to overrun by at least the configured threshold (e.g., 8 minutes), when prediction is updated, then rebalancing executes within 5 seconds and proposes/executes moves that minimize maximum wait without violating guardrails. Given a service is projected to underrun by at least the configured threshold, when prediction is updated, then the system pulls forward the next compatible job(s) and updates ETAs/pickup windows within 5 seconds. Given multiple ETA changes for the same booking within a short window, when updates occur, then notifications are rate-limited to at most one per 10 minutes unless the net ETA shift is >= 5 minutes, in which case an immediate update is sent. Given ETAs are recalculated, then signage, dashboard, and SMS threads show the same ETA and pickup window values with no divergence across channels.
What-If Bottleneck Resolution Suggestions and Safe Auto-Apply
Given a manager opens What-If mode and selects goals and constraints, when they request suggestions, then the system returns at least three ranked suggestions within 3 seconds, each showing predicted impact (e.g., max-wait reduction, utilization balance) and constraints satisfied. Given a user accepts a What-If suggestion, when they confirm, then changes are applied atomically across assignments, ETAs, signage, and SMS, and an audit entry is created with suggestion ID and rationale. Given auto-apply-safe-suggestions is enabled, when a suggestion meets all guardrails and yields a predicted max-wait reduction of at least 5 minutes for affected jobs, then the system auto-applies it without manual approval and notifies staff. Given a suggestion violates any guardrail, when rendering results, then it is flagged as "requires override" and cannot be auto-applied.
Audit Trail Completeness and Reporting
Given any reassignment action (proposed, applied, withdrawn, failed), when it occurs, then an audit entry is written within 1 second including: timestamp, actor (system/user), trigger (late/no-show/overrun/what-if/manual), before/after station, affected booking IDs, reason code, guardrails consulted, and notification outcomes. Given audit entries are stored, when a user filters by date range, station, trigger, or actor, then results return within 2 seconds for up to 10,000 entries and can be exported to CSV. Given an audit record has been written, then it is immutable; any correction is added as a new entry referencing the original via parent ID.
Signage and SMS Thread Synchronization on Reassignment
Given a reassignment is applied, when propagation occurs, then station dashboards and lobby signage reflect the new assignment within 5 seconds. Given a reassignment changes a client's ETA or pickup window, when propagation occurs, then the client's existing SMS thread receives an update message within 5 seconds and two-way replies continue to route to the newly assigned station. Given a signage update fails, when retries run, then the system attempts up to three retries over 30 seconds and alerts staff if all retries fail, without rolling back the reassignment. Given multiple simultaneous reassignments, when updates are sent, then all channels present a consistent final state with no duplicate or contradictory messages.
Station and Groomer Capability Profiles
"As an owner, I want to define station equipment and groomer skills so that assignments match each pet’s needs and compliance requirements are met."
Description

Provides a structured model for stations (table size, dryers, bathing access, power constraints) and groomers (skills, certifications, service tags, speed factors, shift hours) to drive accurate routing. Defines service-to-capability compatibility, capacity limits, and mandatory buffers, including special handling for anxious or large breeds. Enforces compliance by checking vaccine status and required preferences from Pet Profile Smart Cards prior to assignment. Supports business rules such as preferred pairings, experience thresholds, and blackout periods. Integrates with scheduling to prevent over-allocation and to forecast realistic throughput by time of day.

Acceptance Criteria
Profile Data Model and Validation
Given an admin creates or updates a Station Capability Profile When they provide table_size (in), dryer_types, bathing_access, power_constraints, capacity_limit, and mandatory_buffer Then the system validates presence, types, ranges, and allowed values and saves successfully Given required station fields are missing or out of bounds When saving Then the system blocks save and returns field-level errors describing the violation Given an admin creates or updates a Groomer Capability Profile When they provide skills[], certifications[], service_tags[], speed_factor (0.5–2.0), shift_hours, and blackout_periods Then validation enforces formats, ranges, and no overlap between blackout and shift windows Given any profile change is saved Then the system writes an audit log entry capturing actor, timestamp, fields changed, and prior vs new values
Service-to-Capability Compatibility Mapping
Given a service definition has required capability tags and constraints (e.g., min_table_size, requires_bathing_access, required_dryer_types, handling_flags) When the auto-assignment engine evaluates a booking for pet P and service S Then only stations and groomers whose profiles satisfy all constraints are considered eligible Given no compatible station–groomer combination exists for service S and pet P When attempting assignment Then the system prevents assignment and returns specific incompatibility reasons and the soonest alternative time/combination if available Given a user attempts a manual override to an incompatible station or groomer without override permission When saving Then the system blocks the action and displays the exact constraints violated
Groomer Skills, Certifications, and Experience Thresholds
Given service S requires specific skills/certifications and a minimum experience in months When routing is executed Then only groomers meeting all required skills/certifications and min_experience are eligible Given multiple eligible groomers exist When ranking candidates Then the system ranks by preference score, speed_factor fit, and current load; ties are broken deterministically for repeatability Given a groomer loses a certification or a required skill is removed When the profile update is saved Then all future appointments depending on that requirement are flagged for re-routing within 5 minutes and owners are notified
Speed Factors and Duration Forecasting by Time of Day
Given a base duration for service S and groomer G's speed_factor and station setup_time/teardown_time When computing estimated start and end times Then duration = round_up(base_duration * speed_factor + mandatory_buffers + setup_time + teardown_time) and is stored on the appointment Given speed_factor or buffer rules change When the change is saved Then ETAs and end times for future appointments are recalculated and downstream reminders/notifications update within 5 minutes Given a date range is requested for capacity forecast When forecasting throughput Then the system returns per-hour capacity (appointments and minutes) per station and shop with a 95% utilization cap and excludes blackout periods
Shift Hours, Blackout Periods, and Capacity/Buffer Enforcement
Given groomer G's shift hours and station S availability When booking or reassigning an appointment Then the start and end times must fall fully within both availabilities Given mandatory buffers, including extra buffers for large-breed or anxious handling flags When scheduling sequential appointments involving the same groomer or station Then buffers are automatically inserted and overlapping is prevented Given an overlap or capacity exceedance is detected When attempting to save Then the system denies the action with conflict details and suggests the nearest feasible slots that satisfy all constraints
Vaccine Status and Required Preferences Check
Given service S has vaccine requirements and required preferences defined When booking or assigning a pet whose Pet Profile Smart Card is missing any required vaccine or preference Then the system blocks assignment, displays the missing items, and offers to send a vaccine request SMS to the owner Given a required vaccine will expire before the appointment start When attempting to finalize Then the system warns and requires updated proof or rescheduling before confirmation Given vaccine proof is uploaded and verified When validation completes Then the appointment becomes eligible for assignment immediately and prior blocks are cleared
Scheduling Integration: Over‑Allocation Prevention
Given current schedules across all stations and groomers When a new booking, reschedule, or reassignment is attempted Then the system ensures no station or groomer exceeds capacity_limit at any time slice and all mandatory buffers are respected Given an external change (e.g., time-off import or calendar sync) creates a conflict When synchronization completes Then all impacted appointments move to a "Needs Routing" queue and responsible users receive notifications with conflict details Given the public or partner API is used to create or update an appointment When the request would over-allocate resources Then the API responds with HTTP 409 and machine-readable conflicts plus alternative slot suggestions
Client Messaging and On-Prem Signage Sync
"As a client, I want a text when my pet’s station or timing changes so that I know where to go and when to return for pickup."
Description

Delivers templated SMS updates for initial station assignment, changes, delays, and pickup readiness, preserving the existing two-way conversation thread. Includes deep links to Smart Cards for arrival instructions, vaccine updates, and preferences when action is needed. Powers in-shop displays that show current queue and station mappings in real time via secure web clients and WebSocket updates, with role-based privacy controls and large-type accessibility. Ensures consistency between what staff see on the dashboard and what clients see on signage and SMS, with instant propagation on changes. Provides localization and branding controls for tone and terminology.

Acceptance Criteria
Initial Station Assignment SMS and Signage Sync
Given a new booking is created with an assigned station When the booking is saved Then an SMS using the Initial Assignment template is queued to the existing two-way thread (same sender ID) within 5 seconds, including station label and ETA window And the in-shop display reflects the pet-to-station mapping within 2 seconds of save via WebSocket And dashboard, signage, and SMS use the same station terminology/label And a deep link to arrival instructions is included only if that action is required for this client/pet
Station Reassignment Instant Update and Consistency
Given a pet is visible on signage and previously assigned to Station A When staff reassigns the pet to Station B Then the in-shop display removes the pet from Station A and shows Station B within 2 seconds And an SMS is queued to the same conversation thread within 5 seconds indicating the station change and updated ETA if applicable And all surfaces (dashboard, signage, SMS preview) show Station B consistently within 5 seconds And no conflicting prior station remains visible on any surface after 5 seconds
Delay Notification with Thread Preservation
Given a job status changes to Delayed with a new ready window When the delay is recorded Then the client receives a delay SMS queued to the same two-way thread within 5 seconds including the revised ready window And the in-shop display shows a delay indicator next to the pet without exposing PII beyond allowed fields And client replies to the SMS appear in the dashboard conversation within 3 seconds of receipt from the provider And only one delay SMS is sent per distinct delay change event
Pickup Ready Notification and Display Update
Given a job transitions to Ready for Pickup When staff marks the job Ready Then a pickup-ready SMS is queued to the existing thread within 5 seconds including pickup window and any configured pickup instructions And the in-shop display moves the pet to the Ready section within 2 seconds And dashboard, signage, and SMS content show the same readiness status and pickup terminology within 5 seconds
Smart Card Deep Links for Required Actions
Given a pet has missing or expiring vaccine records or incomplete preferences When any templated SMS (assignment, change, delay, pickup) is generated Then the SMS includes a deep link to the pet’s Smart Card prefilled with pet name and photo only if an action is required And opening the link routes to the specific action needed and, upon completion, updates the dashboard within 10 seconds And deep link tokens are single-use and expire after 24 hours; expired links prompt a fresh SMS link on next message And clients with no pending actions do not receive a Smart Card link
Role-Based Privacy and Accessibility on In‑Shop Displays
Given a secure display client is opened with a Public Display role When the queue is shown Then only pet name, owner first initial, station label, and status are visible; phone numbers and payment info are hidden And with a Staff role, full owner name and internal notes appear per permissions And the display renders large-type mode with minimum 18pt equivalent font and 4.5:1 contrast ratio And WebSocket connections use WSS and a signed, expiring access token; token refresh occurs without page reload
Localization and Branding Controls Application
Given the account localization is set to a non-default language and branding terms are customized (e.g., “Bay” instead of “Station”) When SMS and signage content is generated Then messages and labels render in the selected language and use customized terminology across dashboard, signage, and SMS And if a translation key is missing, the system falls back to the default language and logs a warning And dynamic variables (pet name, station label, times) appear correctly without placeholder text And template previews render localized, branded examples with sample data
Manual Override and Pinning
"As a front desk coordinator, I want to pin or override assignments so that special cases are honored without breaking the schedule."
Description

Allows staff to pin a pet to a specific groomer or station, block reassignments during critical phases, and apply temporary routing rules for VIPs, medical needs, or client requests. Presents clear indicators of pinned or locked assignments and explains how they impact balancing. Supports batch actions to move parts of the queue during rushes and gives suggested resolutions when overrides create conflicts. Records overrides in the audit log with reason codes for transparency. Respects permissions so only authorized roles can create or remove pins.

Acceptance Criteria
Pin Pet to Specific Groomer at Check‑In
Given an authorized staff member with role Manager or Lead is viewing a pet during check‑in When the staff pins the pet to Groomer X and selects a reason code from the controlled list Then the assignment to Groomer X is persisted and excluded from auto‑balancing until the pin is removed And a “Pinned to Groomer X” indicator appears on the queue list, pet card, and station board within 3 seconds And an audit entry is created capturing actor, role, timestamp, pet, prior assignment, new assignment, and reason code And an Associate attempting to pin receives a permission error and no changes are saved
Lock Assignment During In‑Progress Grooming
Given a pet’s service status transitions to In Progress When auto‑balancing runs or a batch move attempts to reassign the pet Then the reassignment is blocked due to lock, with a visible lock icon and tooltip explaining “Auto‑balancing disabled while in progress” And a “Blocked by Lock” event is recorded in the audit log And only Admin or Manager may override the lock by explicitly unlocking with a reason code
Apply Temporary Routing Rule for VIP or Medical Needs
Given a pet has VIP or Medical tags and an authorized staff applies a temporary routing rule (allowed stations: A,B; expiry: 2 hours) When auto‑balancing assigns or reassigns the pet during the rule window Then assignments are limited to stations A or B; other stations are excluded And upon expiry, the rule is removed automatically and standard balancing resumes And if no eligible stations are available, the system flags a conflict and proposes the earliest eligible slot/station
Batch Move a Subset of the Queue During Rush
Given an authorized user selects multiple pets via filters (e.g., arrival window or service type) When the user initiates a batch move to Station Group Y Then the system previews capacity and wait‑time impact and requires confirmation And pinned or locked pets are excluded by default; attempting to include them prompts to unpin/unlock with reason codes And upon confirmation, eligible moves complete within 5 seconds per 20 pets, station signage updates within 5 seconds, and audit entries are created per pet with a batch reference ID
Conflict Resolution Suggestions for Override‑Induced Imbalances
Given a new pin or routing rule causes over‑capacity or overlapping bookings on a station When the conflict is detected Then the system surfaces a conflict banner within 3 seconds and displays at least three resolution options ranked by predicted wait‑time reduction and fairness And selecting a resolution applies the necessary reassignments atomically and updates the queue, signage, and SMS as configured And the predicted versus actual wait‑time deltas are logged for the chosen option
Audit Logging and Transparency for Overrides
Given any manual override (pin, unlock, batch move, routing rule) is performed When the action is saved Then an immutable audit record is stored containing actor, role, timestamp (ISO 8601 with timezone), pet ID, prior assignment, new assignment or rule details, reason code from a controlled list, and optional note And audit records are viewable and filterable by date range, actor, pet, action type, and reason; exportable to CSV And audit records are retrievable via API with pagination and RBAC‑enforced access
Communications and Signage Updates After Manual Reassignment
Given a pet is manually reassigned or pinned to a different station When the change is saved Then station boards reflect the new assignment within 5 seconds And the client receives an SMS update within 30 seconds if Notify on Changes is enabled and the appointment is same‑day And duplicate SMS for the same change are suppressed within a 10‑minute window And no SMS is sent if the station did not actually change
Concurrency, Integrity, and Audit Safety
"As a tech lead, I want station changes to be atomic and auditable across devices so that we avoid double-bookings and can troubleshoot issues quickly."
Description

Implements atomic, idempotent operations for station assignments and reassignments across mobile and web, preventing double-booking under concurrent edits. Uses short-lived locks and optimistic concurrency with conflict detection, clear user prompts, and automatic retries where safe. Provides a durable event log of state changes with before/after snapshots and user attribution, supporting rollback for erroneous updates. Handles intermittent connectivity on mobile with queued sync and consistent conflict resolution. Exposes read-optimized projections so signage and SMS always reflect the latest committed state.

Acceptance Criteria
Prevent Double-Booking Under Concurrent Assignments
Given two users attempt to assign the same booking to different stations within 100 ms When both submit their assignment requests Then exactly one assignment commits and the other request is rejected with a conflict response including the current version and assigned station And no booking appears assigned to more than one station at any time And the committed assignment is atomic and visible in the read projection within 2 seconds (p95) And a client retry with the same idempotency key returns the original result without creating a duplicate assignment
Safe Reassignment With Conflict Prompt and Retry
Given a booking is assigned to Station A and two users attempt reassignment to different stations (B and C) When the first reassignment commits Then the second user is shown a conflict prompt that includes who changed it, when, and the new station And the second user may Cancel (discard) or Overwrite (apply new reassignment on the latest version) And if Overwrite is chosen, the reassignment commits on the latest version and Station A is freed and the final station reflects the overwrite And transient errors (e.g., timeouts) are retried automatically up to 3 times with exponential backoff; non-idempotent retries are prevented
Short-Lived Locks With Optimistic Fallback
Given a user begins editing a booking’s station When a lock is acquired Then the lock TTL is 3 seconds and is heartbeated while the user is actively editing And if the client disconnects or crashes, the lock expires and releases within 3 seconds And if a lock cannot be obtained within 300 ms, the client proceeds via optimistic concurrency and informs the user And no deadlocks occur under concurrent edits (verified by deterministic lock ordering) And all lock acquire/release events are recorded in the audit log
Offline Mobile Queue With Consistent Conflict Resolution
Given a mobile device is offline and a user assigns or reassigns a station When the action is taken Then the operation is queued locally with an idempotency key and is marked Pending in the UI within 300 ms And upon reconnect, queued operations are flushed in original order And conflicts detected on flush surface a prompt with latest state and resolution options; resolved outcomes match server rules And no queued operation is lost, duplicated, or applied out of order And the user receives success/failure feedback within 2 seconds of reconnect (p95)
Durable Event Log and Rollback Support
Given any station assignment or reassignment commits When the transaction completes Then an append-only event is persisted durably before acknowledging success, containing: eventId, bookingId, petId, previousStationId, newStationId, actorUserId, source (web/mobile), UTC timestamp, version, idempotencyKey, and before/after snapshots And events are immutable and ordered by version per booking, enabling full reconstruction of state And when an authorized user requests a rollback of an erroneous update Then a compensating event is written that restores the prior state and is applied to projections And audit queries can retrieve the exact sequence of changes for any booking
Read-Optimized Projections Drive Signage/SMS Consistency
Given a booking’s station assignment changes When the change commits Then the in-app signage/dashboard reflects the latest committed state within 2 seconds (p95) And any required SMS notification is enqueued with the correct station and time within 5 seconds (p95), with idempotent delivery preventing duplicates And signage/SMS never display an uncommitted or rolled-back station assignment
Idempotent Operations and Duplicate Submission Handling
Given a client does not receive a response and retries the same assignment or reassignment with the same idempotency key within 24 hours When the server receives the duplicate request Then it returns the original successful result without reapplying side effects And if a different payload is sent with the same key, the request is rejected with a clear error indicating key collision And idempotency keys are retained for 24 hours and included in the audit trail
Utilization and Bottleneck Analytics
"As an owner, I want to see station utilization and wait time trends so that I can adjust staffing and buffers to reduce bottlenecks."
Description

Captures metrics such as station utilization, groomer load, average wait times, on-time start rate, reassignments per appointment, and idle/downtime causes. Visualizes trends by daypart, service type, and staff member to identify imbalances and staffing gaps. Generates alerts when SLA thresholds are at risk and recommends schedule adjustments like staggered drop-offs or buffer increases. Includes export and API access for deeper analysis and ties insights back to revenue, no-show fees, and package usage to quantify impact. Preserves privacy by aggregating client-identifiable data where appropriate.

Acceptance Criteria
Real-Time Station Utilization and Groomer Load Dashboard
Given a location has staffed hours and scheduled appointments, When a user opens the Utilization view and selects a date range and location, Then utilization is displayed per station as busy_minutes_within_staffed_hours / staffed_minutes for the period, refreshed at least every 60 seconds and stamped with the last refresh time. Given groomers are assigned to time slots, When computing groomer load, Then load is displayed as (service_minutes + cleanup_buffer_minutes) / groomer_staffed_minutes for the selected period. Given overlapping appointments exist, When calculating metrics, Then overlapping time is not double-counted for a single station or groomer and time outside staffed hours is excluded. Given the user inspects a metric, When tapping or hovering, Then the tooltip shows numerator, denominator, and contributing appointment count. Given the business timezone is set, When the date range crosses DST boundaries, Then calculations use the business timezone and correctly handle DST transitions.
Trend Visualization by Daypart, Service Type, and Staff
Given filter controls for daypart, service type, and staff, When a filter is applied, Then all charts and tables update within 2 seconds for datasets ≤ 10,000 appointments and display the applied filters. Given the user selects a time grain (hour, day, week), When applied, Then trend lines and aggregates recompute to the selected grain with correct totals. Given a segment has no data in the period, When filtered, Then the UI shows an explicit No data state without rendering zeros. Given a user clicks a data point, When drilling down, Then a list of contributing appointments appears with non-PII columns (date, station, service type, staff, duration, status) and can be exported.
Wait Time, On-Time Start Rate, and SLA Risk Alerts
Given wait_time = max(0, actual_start - scheduled_start) and on_time_start if actual_start <= scheduled_start + 5 minutes, When the system computes daily metrics, Then it displays average wait time and on-time start rate per daypart. Given an SLA threshold is configured per location (default 15-minute avg wait, 90% on-time), When the forecast projects a breach for any daypart within the next 8 hours, Then an SLA Risk alert is created. Given an alert is created, When delivered, Then it appears as a dashboard banner and an SMS to the configured owner number within 60 seconds and includes metric, threshold, affected window, and a link to recommendations. Given the alert condition persists, When evaluating every 5 minutes, Then only one active alert per metric/daypart remains and it is not re-notified more than once every 30 minutes. Given the alert condition clears, When below threshold for 15 minutes, Then the alert auto-resolves and is archived with start/end timestamps and values.
Reassignments Tracking and Bottleneck Attribution
Given an appointment is moved to a different station or groomer after initial booking, When the change is saved, Then the appointment's reassignments_count increments by 1 and a required reason code is captured. Given signage/SMS updates are triggered by a reassignment, When sent, Then the system logs timestamp, from/to station, from/to groomer, and communication success/failure. Given the Reassignments report is viewed for a period, When loaded, Then it shows distribution of reassignments per appointment (0, 1, 2+), average per appointment, and top 5 reason codes. Given a multi-stage service runs within a single station/groomer, When stages change, Then no reassignment is counted unless the assigned station or groomer changes.
Privacy-Preserving Exports and API Access
Given analytics exports, When a user exports, Then a CSV is produced within 60 seconds for up to 100,000 rows with a data dictionary and all IDs hashed; pet/owner names, phone numbers, and message contents are excluded. Given the Analytics API, When a client requests GET /v1/analytics with a valid OAuth2 token, Then paginated JSON (cursor-based) is returned within P95 ≤ 1.5 seconds for cohorts ≤ 50,000 records, with fields matching the export schema and documented metric definitions. Given cohort size is fewer than 5 unique clients, When a query or export is requested, Then the system aggregates to the next level (e.g., staff to location) or returns insufficient cohort size without raw rows. Given role-based access control, When a user lacks PII privileges, Then the API/export cannot reveal unhashed IDs; reveal operations for privileged users are logged with user, time, and query scope. Given timezone and locale settings, When exporting or calling the API, Then timestamps are ISO 8601 with timezone offsets and currency fields include currency code.
Recommendations for Staggered Drop-Offs and Buffers
Given an SLA risk is detected for a future window, When the user opens Recommendations, Then the system proposes at least one adjustment (stagger drop-offs by X minutes, increase buffer Y minutes for service type Z, add temp staff N hours) with predicted impact on on-time start rate and average wait time. Given the user selects a recommendation, When applied, Then a draft schedule change is created showing affected appointments count and predicted impact and requires explicit confirmation to publish. Given a recommendation is published, When the window completes, Then the system computes realized impact vs. forecast (delta wait time, on-time start rate, revenue per hour) and stores it in the Recommendations log. Given the same recommendation was accepted for 3 consecutive comparable windows with <2% improvement each time, When generating new recommendations, Then the system suppresses that suggestion and explains why.
Revenue, No-Show Fees, and Package Usage Impact Attribution
Given payment records, no-show fees, and package redemptions, When viewing the Impact tab for a period, Then the dashboard shows revenue per staffed hour, no-show fee totals, and package redemption counts alongside utilization, wait time, and on-time start metrics. Given at least 13 weeks of history, When computing relationships, Then the system displays the Pearson correlation coefficient between utilization/wait time and revenue per hour with p-value, and flags non-significant results (p ≥ 0.05). Given a user selects a segment (daypart, service type, staff), When selecting a hypothetical 5-minute reduction in wait time, Then the system estimates revenue change and no-show rate change with 95% confidence intervals and a link to methodology. Given drill-down is requested, When opening the Impact detail, Then the list shows non-PII appointment-level fields and allows filtering by package vs. non-package to compare performance.

Add‑On Boost

Offer clear, tap-to-choose add‑ons (teeth, deodorizing, nail grind) during self-check-in. Prices and time impacts are shown up front, increasing average ticket while letting staff close out faster with preselected services.

Requirements

Add-On Catalog Management
"As a business owner, I want to configure standardized add-ons with prices and time impacts so that clients see clear options and we can reliably upsell during self-check-in."
Description

Provide an admin configuration module to create and manage add-ons (e.g., teeth brushing, deodorizing, nail grind) with fields for display name, short upsell copy, icon, price, estimated duration impact, tax code, service applicability (which base services the add-on can attach to), species/breed/weight rules, location/staff availability, visibility in self-check-in, max-per-appointment limits, and bundle/tiers. Include versioning, change audit logs, and API endpoints for retrieval so the check-in flow and POS can consume authoritative definitions. Ensures consistent pricing/time data across SMS check-in, dashboard, and payments, enabling standardized upsells at scale.

Acceptance Criteria
Admin creates a new add-on with full field configuration
- Given an authenticated admin with Manage Add-Ons permission, when they open Add New and input: display name (1–60 chars), short upsell copy (0–120 chars), icon (PNG or SVG, <=512KB), price >= 0.00 in store currency, estimated duration impact 0–240 minutes, optional tax code from configured list, at least one base service in service applicability, optional species/breed/weight rules, optional location/staff availability, visibility toggle (default ON), and max-per-appointment (integer 1–10, default 1), then the Save button is enabled. - Given any invalid value (e.g., negative price, missing base service, oversized/unsupported icon, duration >240), when Save is clicked, then the system blocks save, highlights fields, and shows inline validation messages specifying the issue. - Given valid inputs, when Save is clicked, then a new add-on is created as Version 1 (active=true) and appears in the catalog list within 5 seconds with all fields persisted exactly as entered. - Given the new add-on, when the admin refreshes or re-opens the record, then all saved values load accurately and match the last persisted state.
Admin edits an existing add-on with versioning and audit log capture
- Given an existing add-on at Version N, when the admin changes any field and clicks Publish, then the system creates Version N+1 and marks it as current; Version N remains immutable and viewable. - Given a publish action, when viewing the audit log, then it records actor, timestamp (UTC), previous value, new value, and reason (free-text up to 200 chars) for each changed field. - Given the audit log, when filtering by date or actor, then only matching entries are returned. - Given a need to revert, when the admin selects Roll back to Version K (< current), then Version K is duplicated as Version N+1 and marked current; the audit log records the rollback event with reference to K. - Given a prior appointment referencing an add-on, when a new version is published, then existing appointments retain their originally selected version’s price and duration, while new selections use the current version.
Add-on eligibility enforced by service, species/breed/weight, location, staff, and visibility
- Given an add-on applicable only to base service “Full Groom,” when starting check-in for a “Nail Trim” appointment, then the add-on is not offered in self-check-in or POS. - Given an add-on restricted to species=Dog and weight <= 50 lb, when checking in a 60 lb dog or any cat, then the add-on is not offered. - Given staff/location availability set to Location A and Staff X only, when the appointment is at Location B or assigned to Staff Y, then the add-on is not offered. - Given the add-on’s visibility toggle is OFF, when in self-check-in, then the add-on is hidden; when in back-office POS, then the add-on is visible if other eligibility conditions are met. - Given an API attempt to attach an ineligible add-on to an appointment, when the request is submitted, then the API responds 422 with error code ADDON_INELIGIBLE and a message listing the failing rule(s).
Max-per-appointment limits and tier exclusivity enforced
- Given an add-on with max-per-appointment=1, when the user tries to add it twice to the same appointment (via self-check-in or POS), then the second add attempt is blocked with a clear message and no duplicate line is created. - Given a tiered add-on family (e.g., Nail: Basic, Plus, Premium), when one tier is selected, then selecting another tier in the same family replaces the prior tier (single selection enforced) and price/duration update accordingly. - Given API calls adding multiple tiers from the same family, when the request is processed, then only the last specified tier remains attached and the response reflects the resolved single-tier selection. - Given a bundle that includes a member also present individually, when both are selected, then duplicates are prevented and the user is informed which line was retained.
Bundle and tier configuration applied to pricing and duration
- Given a bundle with a defined bundle price, when attached to an appointment, then the line item price equals the bundle price (not the sum of children) and estimated duration equals the sum of unique child duration impacts. - Given a bundle with price mode = sum-of-children, when attached, then the displayed and charged price equals the sum of included add-ons, and taxes apply per each child’s tax code or the bundle’s tax code according to configuration. - Given tiers with ascending prices/duration, when a tier is selected, then only that tier’s price and duration impact are applied; lower tiers are not additionally charged or counted. - Given a bundle is deactivated, when loading the catalog, then the bundle is hidden, and any attempt to add it via API returns 410 GONE with code ADDON_DEPRECATED.
API exposes authoritative add-on definitions with versioning and cache headers
- Given a client with valid OAuth token, when calling GET /addons?locationId=…&baseServiceId=…&species=…&weight=…&visibility=selfCheckIn, then the API returns 200 with eligible add-ons only, including fields: id, displayName, shortCopy, iconUrl, price, currency, durationMinutes, taxCode, serviceApplicability, species/breed/weight rules, location/staff availability, visibility, maxPerAppointment, bundle/tier metadata, active, currentVersion, updatedAt. - Given large catalogs, when calling GET /addons, then results are paginated with limit/offset (or cursor) and include total count; p95 latency <= 300 ms for 100 items. - Given versioned records, when calling GET /addons/{id}, then response includes currentVersion and ETag/Last-Modified headers; conditional requests with If-None-Match/If-Modified-Since return 304 when unchanged. - Given insufficient permissions, when calling any /addons endpoint, then the API returns 401 (unauthenticated) or 403 (unauthorized) as appropriate. - Given an id that does not exist or is inactive and not requested with includeInactive=true, when calling GET /addons/{id}, then the API returns 404.
System-wide consistency of pricing, duration, and taxes across check-in, dashboard, and payments
- Given an add-on price or duration change published to a new version, when users open self-check-in and POS, then both surfaces reflect the new values within 60 seconds (after cache invalidation) and match the API response. - Given the tax code on an add-on, when an appointment is checked out, then the payment processor receives the correct tax code mapping and the receipt shows accurate tax breakdown. - Given multiple add-ons on an appointment, when estimating total time, then the system aggregates duration impacts deterministically (sum of unique impacts, respecting bundles/tiers) and displays the same total across check-in, dashboard, and POS. - Given currency rounding rules, when charging for add-ons, then the final charged amount equals the sum of add-on prices after discounts and taxes and matches the amount shown during self-check-in and on the final receipt.
Self Check-In Add-On Selector UI
"As a pet parent, I want to easily select add-ons during check-in with clear prices and time changes so that I know what I’m agreeing to and can customize my pet’s service."
Description

Insert a mobile-first, tap-to-select add-on step in the self-check-in flow that displays available add-ons as cards with name, price, and time impact, with optional detail disclosure. Respect availability, applicability, and selection limits from the catalog. Provide clear selection states, instant feedback, and accessibility (WCAG AA, large touch targets, screen reader labels). Persist selections if the user navigates back, support localization, and log analytics events for impressions, taps, and step completion. This UI should be performant on low-end devices and resilient to intermittent connectivity (graceful retry/persist).

Acceptance Criteria
Card Display and Selection Feedback
Given the user reaches the Add-Ons step in self check-in When the app loads add-ons from the catalog Then each add-on appears as a card showing name, price formatted for the current locale and currency, and additional time impact in minutes And cards with extra details show a More info control that expands/collapses inline without leaving the flow And tapping a card toggles its selected state within 100 ms and updates visual state with a check indicator and high-contrast background And the running total price and total added time update immediately as selections change
Catalog Rules: Availability, Applicability, Limits
Given catalog data includes availability, applicability, and selection limits When the add-ons list is rendered Then unavailable add-ons are not displayed And inapplicable add-ons are shown disabled with an accessible reason and cannot be selected And per-add-on and per-step selection limits are enforced; attempts beyond limits show inline, non-blocking messaging
Persistence and Back Navigation
Given the user has selected one or more add-ons When the user navigates back to a previous step or backgrounds and returns within 30 minutes Then the previously selected add-ons are restored without requiring a network request And proceeding forward sends the same selections in the payload
Accessibility (WCAG AA) and Screen Readers
Given assistive technologies or keyboard navigation are used When interacting with the Add-Ons step Then all interactive targets are at least 44 by 44 points and have visible focus indicators And all cards and controls expose accessible names and states including name, price, time impact, and selected or not selected And text contrast is at least 4.5 to 1 and non-text contrast is at least 3 to 1 And the More info control announces expanded or collapsed state to screen readers And system font scaling up to 200 percent does not truncate labels or hide controls
Localization and Formatting
Given the device locale and currency are set When the Add-Ons step renders Then all static strings are localized using the active locale with fallback to en-US if a string is missing And prices display with the correct currency symbol, thousands grouping, and decimal precision for the locale And time impact strings use correct pluralization for the locale And right-to-left locales mirror layout and reading order correctly
Analytics Events: Impressions, Taps, Completion
Given analytics collection is enabled When the Add-Ons step is shown Then an add_ons_step_impression event is fired once per step view with business_id, location_id, appointment_id, pet_id, available_addon_ids, locale, and device_info And when a user taps an add-on, an add_on_tap event is fired with addon_id, selected_state, price, time_impact, and position_index, with rapid taps within 300 ms deduplicated And when the user completes the step, an add_ons_step_completed event is fired with selected_addon_ids, selection_count, total_addon_price, and total_time_impact And events are queued offline and flushed within 5 minutes of connectivity restoration without data loss
Performance and Connectivity Resilience
Given a low-end device with 2 GB RAM on a 3G or unstable network When the Add-Ons step loads and the user makes selections Then initial content renders within 1.5 seconds after data is available and time to interactive is under 2.0 seconds And tap-to-state-change latency remains under 100 ms And catalog requests use exponential backoff with up to 3 retries and show a non-blocking retry banner on failure And selections are saved locally immediately and synchronized when online; in case of conflict, the latest timestamp wins; the user does not lose selections
Real-Time Price and Time Calculator
"As a pet parent, I want the total price and estimated time to update instantly as I choose add-ons so that I can make informed choices and avoid surprises."
Description

Compute and display updated totals and estimated duration as add-ons are selected, showing incremental price and time deltas and an updated estimated pickup/finish time. The calculator must apply business rules: taxes, location pricing, packages/memberships, discounts/coupons, and rounding. Enforce scheduling constraints (e.g., max appointment duration window), warn when selection exceeds capacity, and suggest alternatives if needed. Provide a deterministic API for the UI and POS to ensure consistency across channels and store a snapshot of the calculation with the appointment for auditability.

Acceptance Criteria
Live Incremental Totals and Duration Update During Add‑On Selection
Given a base service with no add‑ons selected When the user selects an add‑on Then the UI displays that add‑on’s incremental price and time deltas And the total price and total estimated duration update within 300 ms of selection And the estimated finish/pickup time recalculates from the scheduled start time using the updated duration And when the user deselects an add‑on, the totals and deltas update accordingly within 300 ms And the values displayed in the UI exactly match the latest Calculator API response for price (to 2 decimals) and time (to the configured rounding granularity)
Business Rules Application: Taxes, Location Pricing, Packages/Memberships, Discounts/Coupons, and Rounding
Given a location with configured tax rules, location‑specific pricing, rounding rules, and a customer with possible package/membership entitlements and coupons/discounts When the user selects one or more add‑ons Then location pricing overrides global pricing where defined And package/membership entitlements are applied before coupons/discounts per configured priority And coupon/discount eligibility and stacking rules are enforced; ineligible promotions are rejected with machine‑readable reason codes And taxable amounts are calculated per jurisdiction and taxability flags and included in totals And final amounts are rounded per configured currency and rounding policy; time estimates are rounded per configured time granularity And the API returns a breakdown including: base, add‑ons, applied entitlements, discounts, taxes, rounding adjustments, subtotal, total, base time, add‑on time, total time
Scheduling Constraints Enforcement and Alternatives Suggestion
Given an appointment slot with a maximum duration window and current resource capacity When the selected add‑ons would exceed the allowed duration or available capacity Then the system prevents confirmation and shows a clear blocking message specifying the violated constraint And at least two viable alternatives are suggested (e.g., next available timeslot and/or reduced add‑on combinations) within the same location And if the user removes one or more add‑ons from the warning context, the calculator re‑evaluates and highlights the first valid combination And suggestions are generated and displayed within 500 ms of detection
Deterministic Calculator API Consistency Across UI and POS
Given identical inputs (customer, location, service, add‑ons, scheduled start time, configuration versions, packages, discounts) When the Calculator API is invoked from the mobile UI and from the POS within the same 10‑minute window Then both channels receive identical outputs and a shared deterministic calculation_id derived from the inputs and config versions And repeated calls with identical inputs return the same calculation_id and values And any change to inputs or configuration versions results in a different calculation_id and updated values And all numeric outputs use consistent precision (currency to 2 decimals; time in minutes) across channels
Persisted Calculation Snapshot for Auditability
Given an appointment is created or updated with selected add‑ons When the calculator runs as part of save/confirm Then a read‑only calculation snapshot is persisted with the appointment including: timestamp, channel, requester, input parameters, configuration/rules versions, detailed line‑item breakdown, totals, time estimates, and calculation_id And the snapshot is retrievable via API and admin UI within 1 second And snapshots are immutable; subsequent recalculations create new snapshots linked to the same appointment without altering prior snapshots
Calculator Performance and UI Responsiveness
Given a typical mobile network condition (≤150 ms RTT) and supported devices When the user selects or deselects any add‑on Then the total price, time deltas, and finish time render within 300 ms p50 and 600 ms p95 end‑to‑end And no more than one calculator request is issued per user change (requests within 100 ms are coalesced) And UI remains interactive during updates (no input blocked for >100 ms) And failures are surfaced within 1 second with a retry option without blocking base check‑in flow
Timezone, DST, and Finish‑Time Accuracy
Given the location’s timezone and any applicable daylight saving transitions When calculating the estimated finish/pickup time Then the result uses the location’s timezone and respects DST gaps/overlaps (no nonexistent or duplicated local times) And the displayed time includes local time with timezone abbreviation; the API returns ISO‑8601 with offset And rounding to the configured time granularity adjusts the displayed finish time without altering the underlying unrounded duration stored in the snapshot
Eligibility and Constraints Engine
"As a groomer, I want ineligible add-ons to be automatically blocked or flagged based on pet profile rules so that services stay safe and compliant without manual checks."
Description

Evaluate add-on eligibility at check-in using Pet Profile Smart Card data (vaccinations, age, weight, breed, behavior flags, medical notes) and service history. Support rule types such as required vaccines, minimum age/weight thresholds, cooldown since last service, contra-indications requiring consent, and staff-skill/resource dependencies. When data is missing, trigger targeted prompts to collect or update records; otherwise present a clear reason for ineligibility or a request-for-approval path to staff. Ensures safety, compliance, and reduces downstream manual intervention.

Acceptance Criteria
Required Vaccines Enforcement at Check-In
Given an add-on has a required vaccine rule defined with vaccine name(s) and validity period And the pet's Smart Card contains vaccine records with names and expiration dates When the user enters self-check-in and the engine evaluates eligibility for all displayed add-ons Then add-ons with valid, unexpired required vaccines are enabled for selection And add-ons with missing or expired vaccines are disabled and display a reason that includes the vaccine name and expiration date (if present) And a targeted prompt is shown to upload/update the missing vaccine record And upon successful upload/update, the engine re-evaluates the rule and updates the add-on state within 1 second
Age and Weight Thresholds Application
Given an add-on has min/max age or weight thresholds defined And the pet's Smart Card contains age (in months) and weight (in lbs or kg) When eligibility is evaluated during self-check-in Then if the pet's age and weight satisfy all thresholds, the add-on is enabled And if any threshold is not met, the add-on is disabled and displays a reason stating the violated threshold, the required value, and the pet's actual value And if age or weight data is missing, a targeted prompt requests only the missing attribute(s) And upon submission, the engine re-evaluates and updates the add-on state within 1 second
Service Cooldown Interval Enforcement
Given an add-on defines a cooldown period since the last time that add-on (or a mapped service group) was performed And the pet's service history is available with timestamps When eligibility is evaluated Then if the last service date is within the cooldown window, the add-on is disabled and displays the next eligible date And if the cooldown has elapsed or there is no prior service, the add-on is enabled And all date calculations use the business's timezone
Contraindications Requiring Consent and Staff Approval
Given an add-on has contraindication rules mapped to medical notes or behavior flags that require owner consent and staff approval And the pet's Smart Card contains matching contraindication tags or behavior flags When the user attempts to select the add-on at self-check-in Then the system blocks selection and displays a message that consent and staff approval are required And presents an in-line consent capture to the owner And routes an approval request to staff with the relevant contraindication details And only after both consent is recorded and staff approval is granted does the add-on become enabled And all actions are audit-logged with timestamps and actor identifiers
Staff Skill and Resource Dependency Check
Given an add-on requires specific staff skill tags and/or equipment resources And the appointment date/time and staffing roster/resource availability are known When eligibility is evaluated during check-in Then if at least one available staff member with the required skill and the required equipment resource is available for the booked slot, the add-on is enabled And if not available, the add-on is disabled and displays a reason indicating the missing skill or resource And a request-for-approval path is presented to staff to override by reallocating staff/resources or adjusting time And any staff override records the approver, reason, and the new allocation
Targeted Missing Data Prompts and Real-Time Re-evaluation
Given one or more eligibility rules cannot be evaluated due to missing Smart Card data (e.g., vaccine record, age, weight, breed) When the user reaches add-ons during self-check-in Then the system presents targeted prompts requesting only the specific missing data needed for the add-ons shown And each prompt clearly explains why the data is needed and how it impacts eligibility And after the user provides the data, the engine re-evaluates eligibility and updates all affected add-ons within 1 second And if the user skips data entry, the affected add-ons remain disabled with visible reasons
Decision Transparency, Error Handling, and Performance
Given the engine evaluates eligibility for all add-ons in a session When the evaluation completes Then each add-on surfaces a user-readable eligibility state: Enabled, Disabled with reason, or Requires consent/approval And a staff view exposes a rule-by-rule decision log for the session And the eligibility evaluation completes within 300 ms p95 for up to 10 add-ons on a mid-range mobile device over 4G And if an external data source is temporarily unavailable, the engine fails gracefully by marking impacted rules as undetermined, showing a retry option, and logging the error
Checkout Auto-Apply Selected Add-Ons
"As front-desk staff, I want preselected add-ons to appear automatically at checkout so that I can close out faster and avoid manual entry errors."
Description

Auto-populate the staff checkout screen with the customer’s preselected add-ons, carrying over calculated price, tax, and duration. Allow staff to add/remove items with permissioned overrides, record deviations with reasons, and ensure payment integration updates invoice totals and package balances accordingly. Preserve an audit trail linking selected add-ons from check-in to final charges, handle edge cases (partial completion, substitutions, refunds), and maintain compatibility with no-show/late-cancel policies and fees.

Acceptance Criteria
Auto-Populate Checkout with Preselected Add-Ons
Given an appointment with self check-in completed and add-ons selected with computed price, tax, and duration When a staff member opens the checkout screen for that appointment Then the checkout shows each selected add-on as a line item with the exact carried-over price, tax amount, and time impact from check-in And the appointment's estimated end time reflects the cumulative add-on duration And no repricing or retaxing occurs until a staff edit is made
Permissioned Staff Overrides with Reason Capture
Given role-based permissions are configured When a user with override permission adds, removes, or edits a preselected add-on at checkout Then the system requires a deviation reason (chosen from list or free text 3–200 chars) before saving And captures user ID, timestamp, previous value, new value, and reason in the audit log And updates totals immediately after save Given a user without override permission attempts the same Then the action is blocked and a clear permission error message is displayed, and no changes are persisted
Invoice Totals, Tax, Duration, and Payment Integration Updates
Given applicable tax rules, discounts, and price books are loaded When any add-on line is added, removed, or updated prior to payment capture Then subtotal, tax, discounts, and grand total recalculate and update the UI within 200 ms And appointment duration and resource capacity update accordingly And the pending payment intent/invoice in the payment provider is updated to match the new totals If the provider update fails Then the UI shows a non-dismissable error and prevents checkout until resolved or the change is reverted
Package Balance Application for Add-Ons
Given the customer has one or more active packages that include eligible add-ons or credits with defined application priority When opening checkout with preselected add-ons Then eligible add-ons auto-apply package credits per priority and coverage rules before charging the payment method And the package balance decrements accurately and is recorded with a usage reference to the add-on line item And any uncovered remainder is billed to the payment method And if an applied add-on is removed prior to payment capture, the reserved package credit is fully restored
Audit Trail from Check-In to Final Charge
Given an appointment with preselected add-ons moves through checkout When checkout is completed Then an immutable audit trail exists linking check-in selections to final charges, including item IDs, original prices/taxes/duration at check-in, deviations, user IDs, timestamps, and reasons And the audit trail is viewable in the appointment history and exportable via reporting And audit timestamps are stored in UTC with local offset metadata And audit entries are read-only to all staff
Partial Completion, Substitution, and Refund Handling
Given one or more preselected add-ons cannot be performed or must be substituted When staff marks an add-on as Not Performed or substitutes it during checkout Then the original line is removed from charges (or replaced), a deviation reason is required, and totals/duration update accordingly And any reserved package credits are released or reallocated to the substitute per coverage rules And the receipt clearly excludes not-performed items and lists substitutes with final pricing Given the add-on was already charged and later refunded When staff issues a partial or full refund for the add-on Then the payment provider refund is created for the correct amount including proportional tax And associated package credits are restored, and the audit log records the refund details and reason And the customer receives a refund receipt via the configured channel
No-Show/Late-Cancel Policy Compatibility
Given an appointment is marked as no-show or late-cancel per policy When staff proceeds to checkout or finalize the cancellation Then preselected add-ons are not billed by default And policy fees are applied as configured And package credits for add-ons are not consumed And the audit trail notes that add-ons were not performed due to policy status If the policy specifies billing a percentage of services Then the percentage applies only to the base service unless explicitly configured to include add-ons, and the calculation reflects that configuration
SMS Deep Link and Reminder Integration
"As a pet parent, I want to tap from an SMS reminder directly to select add-ons for my upcoming appointment so that I can quickly customize without calling or texting back and forth."
Description

Embed an add-on upsell prompt in two-way SMS reminders and confirmations with a secure deep link that lands the user on the add-on selection step for their specific appointment. Support fallback quick-reply keywords for basic selections when users cannot open links. Ensure link security (tokenized, expiring), carrier-safe formatting, quiet-hour rules, opt-out compliance, and attribution tagging to measure conversion from SMS to add-on attach. Handle expired/changed appointments gracefully with updated context or a helpful message.

Acceptance Criteria
Secure Deep Link to Add‑On Selection Step
Given a reminder or confirmation SMS is generated for an eligible appointment and recipient And a tokenized, signed deep link is created with a configured TTL of N hours and bound to the appointment ID and recipient phone hash When the recipient taps the link within the valid window Then they land directly on the add‑on selection step for that exact appointment And the page shows the pet name/photo, available add‑ons with price and time impact, and any existing selections And the URL is HTTPS on a branded domain and contains no PII (no names, emails, raw IDs) And attempts to use the link from a different account, appointment, or phone number are rejected with a safe error and no data leak And the link remains valid only until the earlier of TTL expiry or a configured cutoff relative to the appointment start time
Graceful Handling of Expired or Changed Appointments
Given a recipient taps a deep link after it has expired, or the underlying appointment was canceled or rescheduled When the link is opened Then the user sees a clear message explaining the situation without exposing internal details And if the appointment was rescheduled, the user is redirected to the updated appointment’s add‑on selection step and shown the new date/time And if the appointment was canceled, add‑on selection is disabled and the user is offered actions to rebook or text HELP And if the token is expired, the user is offered a one‑tap way to request a fresh link or to reply LINK via SMS to receive a new one And all such outcomes are logged with reason codes (expired, canceled, rescheduled) without double‑counting conversions
Fallback Quick‑Reply Keywords for Basic Add‑Ons
Given the recipient cannot or does not open the link and replies via SMS with a supported keyword (e.g., TEETH, DEO, NAILS, GRIND, NONE) When there is exactly one upcoming appointment within the configurable window (e.g., next M days) Then the keyword is applied to that appointment, the add‑on availability is validated, and the appointment’s price and duration are updated And a confirmation SMS is sent summarizing the added/removed add‑ons and incremental price/time When multiple upcoming appointments exist in the window Then the system requests disambiguation (e.g., reply 1 for Tue 2pm, 2 for Thu 9am) And no changes are applied unless the user confirms a specific appointment within a configurable timeout When an unsupported or ambiguous keyword is received Then a friendly help SMS is sent listing supported keywords and HELP/STOP options And all keyword selections and outcomes are attributed to the originating reminder/confirmation where available
Carrier‑Safe SMS Formatting
Given reminder/confirmation templates include an upsell prompt and deep link When messages are composed and queued for delivery Then each message includes the business name, upsell prompt, deep link, and “Reply STOP to opt out. HELP for help.” And message length stays within the configured segment limit (default 2 GSM‑7 segments); if longer, the upsell block is truncated before compliance text and link And the link uses a branded HTTPS domain (no public URL shorteners) and is not split across segments And content avoids forbidden patterns (e.g., excessive ALL CAPS, spammy phrasing) per carrier guidelines And delivery/send results are logged per carrier with message ID for attribution
Quiet‑Hour Sending Rules
Given business quiet hours are configured per location/timezone When an upsell reminder would be sent during quiet hours Then the upsell portion is deferred to the next allowable window before a configured cutoff prior to appointment start And if deferral would miss the cutoff, the upsell is suppressed (core reminder policy remains unchanged) and suppression is logged When a customer sends a keyword during quiet hours Then only necessary transactional confirmations are sent immediately if within a short, configurable response window; otherwise they are queued until quiet hours end
Opt‑Out and Consent Compliance
Given a recipient is opted out, or replies STOP to any message When the system attempts to send upsell content Then no upsell SMS is sent, the attempt is suppressed with reason=opted_out, and the contact’s opt‑out status is updated and auditable When the recipient sends START/UNSTOP Then the number is re‑subscribed and future upsell messages may be sent per policy, but no backfilled upsell is sent automatically When the recipient sends HELP Then an informational SMS is returned with business name, contact info, and opt‑out instructions; no upsell link is included And all compliance interactions (STOP/START/HELP) are logged with timestamps and source message IDs
Attribution Tagging and Conversion Measurement
Given an upsell reminder/confirmation is sent Then a unique message ID and campaign/source tag are generated and embedded in the deep link and stored with the outbound SMS record When the link is clicked Then a click event is recorded with message ID, appointment ID, timestamp, and device metadata (non‑PII) When an add‑on is attached via deep link or keyword Then a conversion event is recorded and attributed to the latest eligible message/keyword within a configurable attribution window, with revenue and time deltas And duplicate clicks or changes for the same appointment do not create duplicate conversions; updates adjust the single conversion record And aggregate metrics (send, deliver where available, click‑through rate, attach rate, revenue) are visible in dashboard reports and exportable
Add-On Performance Analytics
"As a business owner, I want to track how add-ons perform across locations and services so that I can optimize pricing and promotions to grow revenue."
Description

Provide reporting on add-on attach rate, incremental revenue, average ticket uplift, conversion by channel (SMS vs. in-flow), abandonment at add-on step, and impact on duration/capacity. Include filters by date range, location, staff, service type, breed/weight cohorts, and export to CSV. Surface alerts when attach rate or revenue dips below thresholds, and expose a lightweight API for BI tools. Data should be privacy-conscious and align with existing dashboard conventions to minimize training and maximize adoption.

Acceptance Criteria
Channel Conversion and Revenue Uplift Overview
Given I open Analytics > Add-Ons with the default date range Last 30 Days in the org business timezone When the overview loads Then I see metrics: Attach Rate, Incremental Add-On Revenue, Average Ticket Uplift, and Conversion by Channel (SMS, In-Flow) And metrics are calculated as: - Attach Rate = count(appointments with >=1 add-on) / count(eligible appointments) - Incremental Add-On Revenue = sum(revenue of add-on line items) in the period - Average Ticket Uplift = avg(total ticket amount for appointments with add-ons) - avg(total ticket amount for appointments without add-ons) - Channel Conversion = Attach Rate computed per channel using that channel's eligible appointments And values use org currency format, org business timezone for grouping, and dashboard label conventions And the overview query completes within 3 seconds at p95 for up to 100,000 eligible appointments And an empty state with guidance is shown when the period has 0 eligible appointments
Multi-Dimensional Filters and Segmentation
Given default filters are Last 30 Days, All Locations, All Staff, All Service Types, All Breeds/Weight Cohorts When I change any filter (date range, multi-select location, multi-select staff, service type, breed cohort, weight cohort) Then all widgets and tables update within 2 seconds at p95 And filters combine with AND semantics across dimensions And the applied filters are visible as chips and persist in the shareable URL And Clear All resets filters to defaults And totals after filtering match the sum of underlying records in CSV export within 0.5% And breed and weight cohorts use existing Pet Profile attributes and cohort definitions from org settings
Add-On Step Abandonment Funnel
Given add-on selection is enabled in self-check-in and via SMS flows When I view the Add-On Funnel report for a selected period and filters Then I see counts for Started Add-On Step, Viewed Add-Ons, Selected >=1 Add-On, Continued, Completed And Abandonment Rate is calculated as 1 - (Completed / Started) And step conversions are shown overall and segmented by channel (SMS, In-Flow) And sessions are de-duplicated per appointment; retries within 15 minutes are treated as one session And funnel excludes appointments not eligible for add-ons And the sum of channel counts equals the overall counts
Duration and Capacity Impact Reporting
Given add-ons have configured time impacts (minutes) in catalog When I view Duration & Capacity Impact for a selected period and filters Then I see metrics: Total Added Minutes (sum of configured add-on minutes), Avg Added Minutes per Appointment, Realized Overage Minutes (avg actual duration - scheduled duration) for appointments with add-ons, and Capacity Impact (slots) = floor(Total Added Minutes / default slot length minutes per location) And metrics are available overall and per location And only completed appointments are included in realized duration calculations And if scheduled duration is unavailable, realized overage is not shown and a tooltip explains why And values update when filters change and match underlying exported rows within 0.5%
CSV Export Completeness and Accuracy
Given filters are applied on the Add-Ons analytics dashboard When I click Export CSV Then the generated CSV reflects the same filters and timezone And includes columns: date, location_id, staff_id, service_type_id, channel, appointment_id, pet_id (pseudonymous), add_on_id, add_on_name, add_on_revenue, add_on_minutes, attached_flag, abandoned_flag And excludes PII (no owner name, phone, email, address) And the file name follows pattern add-ons-YYYYMMDD-HHMM-orgId.csv And exports up to 1,000,000 rows per request; if larger, the UI offers to narrow filters And the export completes within 10 seconds for <= 250,000 rows or provides an async download link And totals in the CSV match the dashboard totals for the same filters within 0.5%
Threshold-Based Alerts for Attach Rate and Revenue Dips
Given alert thresholds are configured (global defaults and optional per-location overrides) for Attach Rate (%) and Daily Add-On Revenue ($) When the last 24h aggregate falls below its threshold for any location or org-wide Then an alert is generated once per metric per scope within a 24h window And the alert appears as an in-dashboard banner and sends an email to analytics notification recipients And the alert links to the filtered analytics view that triggered it And authorized users can Acknowledge or Snooze (7 days) the alert And the alert auto-resolves after the metric has remained above threshold for 48 consecutive hours And all alerts are logged in an Alert History view
Lightweight BI API for Add-On Analytics
Given I have a valid org-scoped API key with analytics.read permissions When I call GET /api/v1/analytics/add-ons/aggregate with filters (date_range, location_ids[], staff_ids[], service_type_ids[], channel, breed_cohorts[], weight_cohorts[]) Then I receive JSON with fields: attach_rate, incremental_add_on_revenue, average_ticket_uplift, channel_breakdown[], abandonment_rate, added_minutes, capacity_impact_slots And the response excludes PII (no names, emails, phone numbers) and uses pseudonymous IDs (appointment_id, pet_id, owner_id hashed) And the API responds within 800 ms at p95 for aggregate queries and enforces 60 requests/minute per org And pagination with cursor is provided for record-level endpoint GET /api/v1/analytics/add-ons/records (max 5,000 rows per page) And invalid filters return 400 with error codes; unauthorized requests return 401; over-limit returns 429 And field names, timezone, and currency formatting align with dashboard conventions

Quick Waiver

A 10‑second consent and policy acknowledgement baked into the kiosk flow. Waivers attach to the pet’s Smart Card and are reusable for future pop-ups, keeping compliance tight without slowing the line.

Requirements

Kiosk Waiver Capture Flow
"As a pet owner checking in at a kiosk, I want to quickly acknowledge policies in under 10 seconds so that I can complete check‑in without slowing the line."
Description

Implement a 10‑second consent and policy acknowledgement step within the FetchFlow kiosk check‑in. Surface a concise policy summary with clear accept controls, optional signature toggle (tenant-configurable), and per‑pet handling for multi‑pet check‑ins. Auto-detect existing valid waivers and fast‑path with a brief confirmation banner; otherwise present the minimal, high‑contrast UI optimized for speed and accessibility. On completion, persist a consent record with policy version, timestamp, location, device identifier, staff override (if used) and reason, then advance the check‑in. Instrument time‑to‑complete and drop‑off to ensure the step stays within the 10‑second target.

Acceptance Criteria
Fast-Path for Existing Valid Waiver
Given a kiosk check-in for a pet with an existing consent whose policyVersion equals the tenant’s active policyVersion, is not expired, and is linked to this pet’s Smart Card When the flow reaches the waiver step Then display a confirmation banner only (no full waiver UI, no signature), with policy title and version, for no more than 2 seconds And auto-advance to the next step within 3 seconds of banner display without additional user input And emit waiver_reused analytics with tenantId, locationId, deviceId, kioskId, petId, policyVersion, elapsedMs And do not create a new consent record; instead log linkage of the existing consent to this check-in Given the policyVersion does not match or the prior consent is expired When the step is reached Then do not fast-path and present the full consent UI
First-Time Waiver Capture Without Signature
Given signatureRequired=false and the pet lacks a valid consent for the current policyVersion When the waiver step loads Then render a concise, high-contrast policy summary (<=600 characters) with a single primary Accept control and a secondary Decline/Back control And the Accept control is reachable with one tap and clearly labeled When the user taps Accept Then persist a consent record with fields: petId, ownerId (if available), policyVersion, timestamp (ISO 8601 UTC), locationId, deviceId, kioskId, staffOverride=false, overrideReason=null And attach the consent to the pet’s Smart Card for future reuse And advance to the next check-in step within 500 ms after persistence success If persistence fails Then show an inline retryable error and do not advance; on retry success, only one consent record exists (no duplicates)
Waiver Capture With Signature Required
Given signatureRequired=true and the pet lacks a valid consent for the current policyVersion When the waiver step loads Then display a signature pad (min canvas 300x150 px or device-density equivalent) with Clear and Undo controls, and disable Accept until a non-empty signature stroke is captured And the signature pad accepts touch and stylus input with input latency <50 ms on reference device When the user provides a signature and taps Accept Then persist a consent record including signatureImageUri or signatureHash, plus: petId, ownerId (if available), policyVersion, timestamp (ISO 8601 UTC), locationId, deviceId, kioskId, staffOverride=false, overrideReason=null And attach the consent to the pet’s Smart Card and advance to the next step within 700 ms after persistence success If signature save fails or is empty Then show a retry prompt, keep Accept disabled (for empty), and do not advance; ensure no duplicate consent records are created on retry
Multi-Pet Check-In with Mixed Waiver Status
Given a check-in includes multiple pets where some have valid consents for the current policyVersion and others do not When the waiver step loads Then fast-path pets with valid consents (confirmation banner only) and present consent UI only for pets lacking consent, clearly labeled with each pet’s name and photo And provide Accept per-pet and an Accept All option that applies to all pets lacking consent when the same policyVersion is in effect When Accept is confirmed Then create consent records only for the pets without valid consent, each attached to the respective Smart Card, and do not duplicate existing consents And advance the flow only after all required pets have consent recorded; do not re-prompt for pets already valid
Staff Override with Audit Trail
Given a staff member decides to bypass the waiver at the kiosk When the staff invokes Override from the waiver screen Then require staff authentication (PIN or role-based login) before proceeding And require selection of an override reason from a predefined list and allow an optional free-text note (minimum 10 characters if provided) Then do not create a consent record; instead log an override acknowledgment with fields: petId(s), policyVersion, timestamp (ISO 8601 UTC), locationId, deviceId, kioskId, staffOverride=true, staffId, overrideReason, note And clearly mark the check-in as Override in staff dashboards and exports And emit override_logged analytics with correlationId for audit
Accessibility and High-Contrast Compliance
Given the kiosk renders the waiver step When evaluated against accessibility standards Then all text and interactive elements meet a minimum 4.5:1 contrast ratio And all tap targets are at least 44x44 dp with at least 8 dp spacing And all controls and policy summary have accessible names/roles and proper focus order compatible with TalkBack/VoiceOver And the flow is fully operable without time-limited interactions; dynamic text scaling up to 200% preserves critical content (scroll enabled if needed) without truncation And the signature pad is labeled for assistive tech and does not capture unintended keystrokes; hardware keyboard navigation can reach Accept/Back
Telemetry, Performance, and Drop-Off Instrumentation
Given the waiver step is used When events occur Then emit analytics: waiver_step_started, waiver_step_completed, waiver_step_abandoned, waiver_reused with schema fields: tenantId, locationId, deviceId, kioskId, petId(s), policyVersion, signatureRequired, staffOverride, elapsedMs, outcome And ensure client- and server-side timers record elapsedMs; for non-signature flows on reference devices, p95 elapsedMs <= 10,000 ms And compute and surface drop-off as abandoned/started in the dashboard; events are delivered within 60 seconds of occurrence And persistence is idempotent: submissions with the same clientSubmissionId within 5 seconds result in at most one consent record And error events include machine-readable codes and correlationId for traceability
Smart Card Waiver Attachment
"As a groomer, I want waivers to live on each pet’s Smart Card so that I can verify compliance at a glance and avoid re‑asking for consent at every visit."
Description

Attach each completed waiver to the pet’s Smart Card as reusable metadata, including policy/version ID, consent date/time, consenting party, location, and scope (service type, event, or global). Expose waiver status on the Smart Card preview in the dashboard and SMS Smart Card link, and provide a quick validity check during booking, reminders, and checkout to skip redundant prompts. Support multiple waivers per pet when different policies apply, with clear status (valid/expired/re‑consent required).

Acceptance Criteria
Attach Waiver Metadata to Smart Card after Kiosk Consent
Given a pet with an existing Smart Card and a completed Quick Waiver submission at the kiosk When the kiosk submission is confirmed by the server Then a waiver record is attached to the pet’s Smart Card with fields: policyId, policyVersionId, consentedAt (ISO-8601 UTC), consentingParty (contactId or name+phone), locationId, scope (eventId|serviceType|global), source And the record status is "Valid" And the record is immutable; any modification creates a new waiver record And the recordId is returned in the API response And the Smart Card preview reflects the new status within 5 seconds of confirmation
Surface Waiver Status on Smart Card Preview (Dashboard and SMS)
Given a Smart Card is opened in the dashboard or via an SMS Smart Card link When the card has one or more waiver records Then the preview displays a Waiver status chip per relevant scope as Valid, Expired, or Re-consent Required And shows last consented date/time and policy title/version And provides a tap/click action to view waiver details And if no applicable waiver exists, shows Missing Waiver And UI updates within 5 seconds after a new consent is recorded
Quick Waiver Validity Check during Booking, Reminders, and Checkout
Given a booking, reminder scheduling, or checkout is initiated for a pet and service When the system performs the waiver validity check for the service scope Then the check completes within 300 ms p95 and 1 s p99 And if a valid waiver exists, no waiver prompt is shown And if missing, expired, or superseded by newer policy version, a Quick Waiver prompt is shown And upon successful consent mid-flow, the check re-runs and the flow continues without page reload And an audit log entry captures the check result and decision
Multiple Waivers per Pet and Scope Resolution
Given a pet may have multiple waiver records across policies and scopes When determining applicability for a service or event Then precedence is event-specific > service-type > global And among candidates of the same scope and policy, the most recent consentedAt is selected And waivers from different policyIds do not satisfy each other And the preview shows the highest-precedence status for the current context and indicates count of additional waivers And the system supports at least 50 waiver records per pet with p95 lookup < 200 ms
Policy Version Change Triggers Re-consent Status
Given a policy is published with a new versionId When evaluating a pet’s waiver for that policy Then any consent with a lower versionId is marked Re-consent Required And the Quick Waiver prompt uses the new versionId And the prior record remains retained and marked Superseded And status updates propagate to dashboard and SMS within 5 minutes of publish And APIs return consistent status values: Valid, Expired, Re-consent Required, Superseded
Auditability and Export of Waiver Records
Given an admin requests waiver audit data for a date range When exporting from the dashboard or API Then the export includes petId, policyId, policyVersionId, consentedAt (UTC), consentingParty, locationId, scope, status, source, recordId And records are immutable and include a checksum/hash for tamper-evidence And access requires Admin or Owner role and is logged And export completes within 30 seconds for up to 50,000 records And retention rules are enforced; records older than retention are excluded
Policy Versioning and Re‑Consent Rules
"As an owner-operator, I want to version and schedule policy updates so that clients are only prompted to re‑consent when it’s legally necessary and operationally convenient."
Description

Provide an admin interface for creating and managing waiver templates with semantic versioning, effective dates, and location/brand scoping. Define re‑consent triggers (e.g., material changes, legal updates, elapsed time thresholds) and mapping rules to services and events. When a new version becomes effective, the system should require re‑consent at next interaction and archive prior versions for reference. Include migration tools and preview/testing before publishing.

Acceptance Criteria
Create Waiver Template with Semantic Version and Effective Dates
Given I am an admin on the Waiver Templates screen, When I create a new template with name, body, semantic version in MAJOR.MINOR.PATCH, and an effective start datetime (and optional end datetime), Then the template saves and is assigned a unique ID and status Draft Given a semantic version not matching MAJOR.MINOR.PATCH, When I attempt to save, Then I see a validation error and the template is not saved Given another template exists in the same scope with the same semantic version, When I attempt to publish, Then the publish is blocked with a duplicate-version error Given I set an effective window, When I publish, Then the window is stored in UTC and must not overlap the effective window of another published version in the same scope Given a published template has an effective end datetime in the past, When I view active templates, Then it does not appear in the Active list
Scope Waiver to Brand and Location
Given I select Brand and Location scope during template setup, When I save, Then the scope is stored and displayed on the template detail Given I publish a Brand-scoped template and a Location-scoped override for the same location, When resolving at that location, Then the Location-scoped version takes precedence Given I publish a Brand-scoped template and there is no Location override, When resolving at any location in that brand, Then the Brand-scoped version applies Given I attempt to publish overlapping effective windows for the same scope (brand+location), When I confirm, Then the system blocks publish and indicates the conflicting versions
Configure and Apply Re‑Consent Triggers
Given I open Re‑Consent Rules, When I configure triggers for Material Change, Legal Update, and Elapsed Time (e.g., 365 days), Then the rules save per scope with last-updated metadata Given I mark a new version as Material Change on publish, When it becomes effective, Then all customers mapped to that scope are flagged requires re‑consent Given I set an Elapsed Time threshold, When a customer’s last consent age exceeds the threshold, Then the customer is flagged requires re‑consent before next interaction Given I publish a Legal Update trigger with an effective date, When the date arrives, Then all mapped customers are flagged requires re‑consent regardless of prior consent
Map Waivers to Services and Events
Given I open Service/Event Mapping, When I map a waiver template to selected services (e.g., Grooming, Training) and events (e.g., Pop‑up), Then the mappings save with scope and priority Given multiple mappings could apply (brand vs location), When resolving at runtime, Then the most specific mapping (location > brand) and the active effective version are selected Given a service has no explicit mapping, When resolving, Then the default brand waiver for that scope is used Given I attempt to delete a mapping in use by an active location, When I confirm, Then the system blocks deletion and suggests end‑dating the mapping instead
Require Re‑Consent at Next Interaction
Given a new waiver version becomes effective for a location/service mapping, When a customer checks in via kiosk or SMS for that mapping, Then the system interrupts the flow to collect consent before completing check‑in Given the customer provides consent, When submission succeeds, Then the consent record stores customer ID, pet ID, policy version ID, timestamp, channel (kiosk/SMS), and signature/acknowledgement artifacts Given a household with multiple pets where some have valid consent and others do not, When checking in, Then consent is requested only for pets lacking valid consent under the active version Given the customer already consented to the same active version, When checking in again, Then no re‑consent prompt is shown
Archive, Retrieve, and Audit Prior Versions
Given a template is published and later superseded, When a new version is published, Then the prior version is archived as read‑only with its effective window Given I view the template history, When I open the audit log, Then I see who created, edited, and published each version with timestamps and change notes Given I request a diff between two versions, When I compare, Then the system shows added/removed/changed sections and whether the change was marked Material or Legal Update Given I export archived versions, When I download, Then I receive a PDF/HTML copy of the exact published text and metadata for compliance
Preview, Test, and Migrate Before Publishing
Given I click Preview on a Draft template, When I select channel (kiosk or SMS) and locale, Then I see a rendered preview matching production formatting Given I run a Dry‑Run Impact Report before publishing, When executed, Then it lists impacted brands/locations/services and the count of customers/pets requiring re‑consent Given the Dry‑Run indicates N customers require re‑consent, When I publish, Then the actual queued re‑consent flags equal N ± 1% and discrepancies are reported Given I publish a new version with a future effective date, When I view status, Then it appears as Scheduled and does not prompt re‑consent until the effective datetime Given a Draft is published in error, When I attempt rollback, Then only unpublished changes can be reverted; published versions remain immutable and require a superseding version
Offline Pop‑Up Mode and Reliable Sync
"As a staff member running a pop‑up, I want waivers to work offline and sync later so that I can stay compliant even without reliable internet."
Description

Enable kiosks to operate in low/no‑connectivity environments (e.g., pop‑ups) by caching active waiver policies and queueing signed consent records locally with encryption at rest. Provide a resumable, ordered sync to the FetchFlow backend with de‑duplication and conflict resolution. Block check‑in only if no valid cached policy exists and rules require consent; otherwise allow provisional check‑in with flagged status until sync completes. Display sync health and counters to staff.

Acceptance Criteria
Kiosk Operates Offline with Cached Waiver Policies
Given the kiosk has completed at least one successful sync prior to losing connectivity When a staff member launches Quick Waiver while offline Then the kiosk loads the most recent active waiver policy set from local cache including version IDs and effective dates And the policy text and required acknowledgements render within 2 seconds And the UI clearly indicates "Offline - Using cached policies"
Local Queueing and Encryption of Signed Consents
Given the kiosk is offline and a client completes the waiver When the consent is saved Then the consent record is stored locally with encryption at rest and is not readable in plaintext if the application container is copied And the record includes pet identifier, owner contact, policy version ID, UTC timestamp, consent token/signature hash, device ID, and an idempotency key And the record status is marked "Pending Sync"
Resumable, Ordered Sync with De-duplication
Given there are N pending consent records queued locally and connectivity is restored When sync starts Then the device initiates sync within 10 seconds of detecting connectivity And records are transmitted to the backend in chronological creation order And any record whose idempotency key already exists server-side is acknowledged and marked "Synced" locally without creating a duplicate And if connectivity drops mid-sync, the next attempt resumes from the last acknowledged record without re-sending acknowledged records
Policy Update Conflict Resolution
Given a consent was captured offline for policy version V1 and, before sync, the backend requires policy version V2 When the queued V1 consent is uploaded Then the backend stores the V1 consent and marks it with an "Outdated Policy" flag And the pet profile and active visit are flagged "Re-consent required" for V2 And no duplicate waiver or visit records are created And an audit entry links the V1 consent to the superseding V2 policy
Provisional Check-In When Consent Rules Allow
Given organization rules allow check-in with pending consent sync And the kiosk is offline with a valid cached policy available When a client checks in and completes the cached waiver Then the check-in is permitted and marked "Provisional" And the visit appears to staff with a "Pending consent sync" badge And the provisional status auto-clears within 30 seconds after successful sync or persists with an error state if sync fails
Block Check-In When No Valid Cached Policy Exists
Given organization rules require consent prior to check-in And the kiosk is offline with no valid cached policy for the service/location When a client attempts to check in Then the check-in is blocked And the UI displays a clear message that consent is required and cannot be completed offline for this case And no visit record is created And staff are offered a retry option when connectivity is restored
Sync Health Indicators and Counters for Staff
Given the kiosk transitions between online and offline states When staff views the kiosk dashboard Then they can see current connectivity state, last successful sync timestamp, number of pending records, number of failed records with latest error, and current queue processing rate And these indicators update at least every 15 seconds while the dashboard is in focus And an accessible color and text indicator reflects healthy, degraded, or offline sync states
Compliance Logging and Audit Trail
"As a business owner, I want a complete audit trail of waivers so that I can prove compliance to insurers and resolve disputes quickly."
Description

Record an immutable audit trail for each consent: policy/version hash, consent artifact (checkboxes/signature image if enabled), signer name, device fingerprint, IP (when online), location, and staff override details. Generate a signed PDF summary per waiver and allow export by pet, date range, or event. Enforce retention and data privacy controls, and provide audit reports to demonstrate compliance to insurers and partners.

Acceptance Criteria
Audit Log Fields on Consent Submission
Given an active policy version and a Quick Waiver completed for a pet via kiosk or SMS link When the consent is submitted Then the system creates one audit entry containing: consent_id (UUIDv4), org_id, pet_id, smart_card_id, policy_version, policy_hash (SHA-256), checkbox_states[], signature_image_ref (if signatures enabled) or null, signer_name (as entered), device_fingerprint, ip_address (if online) or null, location (lat, lon, accuracy) or site_id, override=false, created_at (ISO-8601 with timezone) And the entry is linked to the pet’s Smart Card And the entry is retrievable by consent_id and by pet_id/date-range filters
Immutability and Tamper Evidence
Given an existing consent audit entry When any user or process attempts to update or delete the entry Then the operation is rejected (HTTP 405/409) and only an append-only new revision can be created that references the original And each entry stores entry_hash (SHA-256 of normalized payload) and prev_hash (or null for first) to form a verifiable chain And the verification endpoint reports "valid" for an intact chain and "invalid" if any payload byte is altered
Offline Capture and Deferred Enrichment
Given a kiosk or device is offline when a Quick Waiver is completed When the consent is submitted Then the audit payload is stored locally with ip_address=null and location=site_id and marked source="offline" And upon reconnection within 24 hours the server backfills ip_address and geolocation if available, marking source="inferred" And if reconnection does not occur within 24 hours, the audit entry remains with null fields and reason="offline" recorded
Staff Override Logging and Approval
Given staff accepts a waiver on behalf of a signer or overrides a missing/invalid element When the override is confirmed Then the audit entry records override=true, staff_user_id, staff_name, reason (min 10 characters), timestamp, and override_method And if two-person approval is enabled, approver_user_id and approval_timestamp are required And attempts to submit an override without a reason or approval (when required) are blocked
Signed PDF Summary Generation and Verification
Given a consent audit entry exists When a user or auditor requests the waiver summary PDF Then the system returns a PDF containing: policy title, policy_version, policy_hash, checkbox_states, signer_name, created_at, pet identifiers, location, device_fingerprint, signature_image (if enabled), consent_id, and a verification URL/QR And the PDF is digitally signed with the organization’s certificate And any modification causes signature verification to fail And the filename follows {consent_id}_{pet_name}_{YYYYMMDD}.pdf
Export and Audit Report for Insurers/Partners
Given an authorized role requests an export filtered by pet, date range, or event When the request parameters are valid Then the system exports matching audit entries as CSV plus a ZIP of signed PDFs (if requested) and logs the export with requester, timestamp, and filters And sensitive fields are redacted according to partner/export profile (e.g., hide signature images) And an audit report is generated showing counts, coverage rate, overrides, missing consents, and policy versions used And exports larger than 10,000 records stream progressively and complete within SLA
Retention and Data Privacy Controls
Given an organization’s retention policy (in years) and any active legal holds When a consent reaches end-of-retention without legal hold Then PII fields (e.g., signer_name, signature_image) are purged or irreversibly anonymized while retaining hashed metadata and consent_id And the purge action is itself logged in the audit trail And role-based access control and purpose-limitation flags restrict who can view/export PII And right-to-erasure requests create a tombstone entry that preserves chain integrity while removing PII within 30 days
Multilingual and Accessibility Support
"As a non‑English‑speaking client or someone using assistive tech, I want a clear, accessible waiver so that I can consent confidently without delaying check‑in."
Description

Localize waiver content for supported languages with tenant‑level defaults and kiosk language selection or auto‑detection. Ensure the waiver UI meets WCAG 2.1 AA: sufficient contrast, large text mode, keyboard and screen reader support, focus order, and clear error states. Keep content concise and scannable to preserve the 10‑second completion target across languages and accessibility modes.

Acceptance Criteria
Tenant-Level Default Language with Kiosk Override
Given the tenant default language is Spanish When the kiosk waiver loads Then all waiver UI strings (titles, body, buttons, checkboxes, tooltips, error messages, receipts) render in Spanish by default Given a user opens the language selector When they choose English Then the entire waiver UI switches to English within 300 ms without a full page reload and remains English for the session Given a session ends When a new session starts Then the kiosk reverts to the tenant default language Given an unsupported language is requested via URL or device header When the kiosk loads Then it falls back to the tenant default and displays a non-blocking notice of unsupported language Rule: No screen may display mixed-language fragments; all visible strings must match the currently selected language
Language Auto-Detection with Manual Override and Fallback
Given the device/browser sends Accept-Language preferences [fr-CA, en-US] When the kiosk supports French Then the waiver initializes in fr-CA if available, else fr, else tenant default Given Accept-Language has no match to supported set When the kiosk initializes Then it uses the tenant default language Given a lang URL parameter is present (e.g., ?lang=de) When the language is supported Then it overrides auto-detection for the session; else fall back to tenant default and surface a localized notice listing supported languages Rule: Manual selection always overrides auto-detection for the duration of the session Rule: Telemetry logs language source as one of [tenant-default, auto-detect, url-param, manual] without storing PII
WCAG 2.1 AA Contrast and Large Text Mode
Rule: Text and interactive elements meet contrast ≥ 4.5:1; large text (≥ 24px regular or ≥ 19px bold) meets ≥ 3:1, verified by automated audit plus manual spot-check Given a user enables Large Text mode When the waiver screen updates Then body text is ≥ 24px, controls scale 1.3x–1.5x, line-height ≥ 1.4, no truncation/overlap, and no horizontal scroll on a 375px viewport Given Large Text mode is enabled When the user progresses through steps Then the setting persists across all waiver steps and resets at session end Given standard and Large Text modes across supported languages When measured in a usability test (n ≥ 30 per mode/language) Then median completion time ≤ 10 seconds and 90th percentile ≤ 15 seconds
Keyboard-Only Navigation and Logical Focus Order
Given a user navigates with keyboard only When they tab through the waiver Then focus order follows the visual order, starting at the first actionable element and including language selector, consent checkbox, links, and Submit Rule: A visible focus indicator with contrast ≥ 3:1 is present on all focusable elements Rule: Space toggles checkboxes; Enter activates the primary action; Escape closes dialogs without data loss Rule: No keyboard traps exist; users can reverse-tab to exit modal dialogs Given a validation error occurs on submit When errors are rendered Then focus moves to the first invalid field and the error is programmatically associated and announced
Screen Reader Compatibility and Semantic Structure
Rule: The document lang attribute matches the current UI language and updates immediately on language change Rule: All controls expose correct roles, names, and states; checkboxes announce label and checked/required state; buttons announce action Rule: Headings follow a logical hierarchy; reading order matches visual order Rule: Error messages are linked via aria-describedby and announced via an assertive live region when they appear Rule: Decorative icons are aria-hidden; meaningful icons have accessible names Given VoiceOver (iOS) or TalkBack (Android) is active When a blind user completes the waiver Then all tasks can be completed without sight and median completion time ≤ 10 seconds across supported languages
Localized Error States and Recovery Without Slowing Flow
Rule: Validation and system error messages are localized to the active language, specific (actionable), and placed inline adjacent to the relevant control Rule: Error text meets contrast ≥ 4.5:1 and is paired with non-blocking iconography that is aria-hidden Given a user submits with missing consent When the error appears Then an error summary is announced via role="alert", focus moves to the consent control, and the user can correct and resubmit without re-entering other information Given a transient network error occurs on submit When the retry option is presented Then the user can retry within the same screen, state is preserved, and success path resumes without exceeding the 10-second median completion target overall
Waiver Receipt via SMS
"As a client, I want a texted copy of what I agreed to so that I can review it later and feel confident about the policies."
Description

After successful consent, send an SMS receipt to the client with a secure link to the waiver summary and the pet’s Smart Card. Track delivery status, retries, and opt‑out compliance. Allow tenants to brand the message and include policy links and revocation instructions. Store the receipt URL on the consent record for future reference from the dashboard.

Acceptance Criteria
Auto-Send SMS Receipt Post-Consent
Given Quick Waiver is enabled and a client successfully provides consent in the kiosk flow, And the client’s mobile number is validated as SMS-capable and not opted out, When the consent record is saved, Then exactly one SMS receipt is queued and sent within 10 seconds. And the SMS contains the tenant business name per configuration and a secure HTTPS link to the waiver summary and the pet’s Smart Card. And all template variables (e.g., {client_first_name}, {pet_name}, {business_name}, {waiver_link}) are resolved with no unreplaced tokens.
Delivery Status Tracking and Retries
Given an SMS receipt send is attempted, When delivery callbacks/events are received from the SMS provider, Then the final message status is recorded as Delivered, Failed, Undeliverable, or Unknown on the consent record and message log with timestamp and provider reason code. And for transient failures, the system retries up to 3 times over 15 minutes with exponential backoff before setting status to Failed. And for permanent failures (e.g., invalid/blocked number), no retries occur and status is set to Undeliverable. And all attempts, statuses, and timestamps are visible in the dashboard message log.
Opt-Out Compliance and Suppression
Given a recipient is opted out of SMS, When a waiver receipt would otherwise be sent, Then the message is suppressed, a “Suppressed — Opted Out” event is recorded on the consent record, and a non-blocking alert is displayed in the dashboard. And every waiver receipt SMS includes STOP and HELP instructions and links to tenant-configured terms/privacy. And if the recipient replies STOP, the opt-out is applied immediately and future sends are suppressed. And if the recipient replies HELP, a tenant-configured help message is returned within 10 seconds.
Tenant Branding and Policy Links
Given a tenant has configured SMS branding, policy links, and revocation wording, When a waiver receipt is generated, Then the message uses the tenant’s template, includes the business name, policy URL(s), and revocation instructions. And the resolved message length does not exceed 1000 characters; if it does, sending is blocked and staff see an actionable validation error. And unresolved template variables (e.g., {policy_link}, {revocation_link}) cause validation failure prior to send.
Secure Receipt Link Requirements
Given a waiver receipt link is generated, Then the link is HTTPS, contains no PII in path or query, and uses a signed, time-limited token valid for at most 30 days. And the link opens a mobile-responsive page showing the waiver summary (date/time, consent method, location, policy versions) and the pet’s Smart Card (name, photo, key attributes). And after expiry or revocation, the link displays an access-expired message and logs the attempt; it can be regenerated from the dashboard. And within the validity window, the link can be opened multiple times by the recipient without login.
Store Receipt URL on Consent Record
Given a consent record is created, When the receipt link is generated, Then the canonical receipt URL, created_at, and expires_at are stored on the consent record. And authorized users can view, open, and copy the receipt link from the dashboard consent details. And an audit trail records who accessed or copied the link and when. And if a new link is regenerated, the prior link is invalidated and marked superseded; both events are logged.
Resend and Failure Handling
Given a prior send attempt failed or was suppressed, When an authorized staff user clicks Resend from the dashboard, Then the system validates current opt-out status and phone number format and sends a new receipt SMS if allowed. And the resend action requires a reason note and is limited to 3 attempts per consent record per 24 hours. And the resend creates a new message log entry while preserving prior failure details and timestamps.

Tap‑To‑Vault

Send a secure, branded SMS link that lets clients save a card in seconds using Apple Pay/Google Pay or a quick camera scan. No app, no accounts—just a fast, trust‑building flow that gets a payment method on file before the visit and reduces last‑minute chasing.

Requirements

Secure Branded SMS Vault Link
"As a groomer running appointments by text, I want to send a secure, branded SMS link to collect a card on file so that clients can add a payment method quickly before their visit without downloading an app."
Description

Generate and send a unique, signed, single-use HTTPS link via SMS that is branded to the business and FetchFlow. The link deep-loads a lightweight, mobile-first web view where the client can save a payment method without logging in or installing an app. Include message templates with merge fields (client name, pet name, appointment date/time), configurable link expiration and retry rules, and a custom short-link domain to improve trust and deliverability. Track delivery and click events, handle carrier failures with automated fallbacks, and respect opt-in/opt-out preferences. Ensure the link is scoped to the intended client and appointment, rate-limited, and protected against replay. Integrate with existing messaging infrastructure and the appointment system to send automatically on configurable timelines (e.g., when appointment is confirmed).

Acceptance Criteria
Unique Signed Single‑Use HTTPS Link Generation
Given an existing appointment and a client phone number on file, When a vault link is requested, Then the system generates a unique HTTPS URL containing a signed, tamper‑evident token scoped to the client and appointment IDs. Given the generated link, When it is opened for the first time before expiration, Then the server validates the signature and scope and returns HTTP 200 with the vault page. Given the same link, When it is opened a second time or submitted after successful token vaulting, Then the server returns HTTP 410 Gone (or 403 Forbidden) and displays an "expired/used" message with a request‑new‑link option. Given an altered token or mismatched scope, When the link is requested, Then the server returns HTTP 403 and logs a security event. Given abuse conditions, When more than 5 vault links are generated for the same client within 60 minutes, Then the system returns HTTP 429 and logs a rate‑limit event. Given an expired token, When the link is requested after the configured TTL, Then the server returns HTTP 410 and no vault actions are possible.
Branded SMS Template with Merge Fields and Custom Domain
Given an enabled template containing merge fields {{client_name}}, {{pet_name}}, and {{appt_datetime}}, When a vault SMS is queued, Then the fields are populated with the correct values for that appointment. Given a configured custom short‑link domain, When the SMS is sent, Then the link uses the custom domain over HTTPS and resolves with HTTP 200 on HEAD/GET. Given the template after merge, When its length exceeds a single‑segment limit, Then the system calculates segments and displays the final segment count and cost estimate before sending. Given a misconfigured or invalid custom domain, When a send is attempted, Then sending is blocked with a clear error and falls back to the default trusted domain only if fallback is enabled in settings. Given preview mode, When the user previews the message, Then the preview shows the exact text and short‑link that will be sent.
Delivery and Click Tracking with Fallbacks
Given a sent SMS, When the carrier returns an accepted status, Then the message is stored with external message ID and status=Sent with timestamp. Given the message, When a delivery receipt indicates Delivered, Then status updates to Delivered with timestamp; if status indicates Failed or no delivery receipt is received within 4 hours, Then mark as Undelivered and trigger fallback route 1 within 5 minutes. Given fallback routes configured, When fallback route 1 fails, Then attempt route 2 (e.g., alternate sender ID or MMS) respecting quiet hours and opt‑out, and record all attempts. Given the client clicks the link, When the landing page loads, Then a Click event is recorded with timestamp, user agent, and message ID correlation; duplicate clicks within 5 minutes are de‑duplicated. Given reporting, When querying the appointment’s messaging log via API, Then sent, delivered, failed, and unique click counts are returned.
Consent and Opt‑Out Compliance
Given the client has not opted in, When an automated vault SMS would be sent, Then it is not sent and a suppression event is recorded with reason "no opt‑in". Given the client is opted in, When sending the SMS, Then include compliant opt‑out instructions and do not exceed per‑recipient send limits. Given the client replies STOP, When the platform receives the message, Then the client is opted out immediately, a confirmation SMS is sent, all scheduled retries are canceled, and no further messages are sent until START is received. Given a manual send attempt to an opted‑out number, When the user attempts to send, Then the UI/API blocks the send with an explicit suppression reason and suggests alternate contact methods. Given audit requirements, When reviewing the client record, Then opt‑in/out timestamps, source, and proof are available.
Mobile Web View Loads Without Login
Given a valid link, When opened on iOS or Android, Then the vault page loads without requiring login or app install and displays business branding, client first name, pet name, and appointment date/time. Given a 3G/slow network, When loading the page, Then Largest Contentful Paint <= 2.5s on a mid‑tier device and total transfer size <= 500 KB. Given accessibility needs, When navigating the page, Then the form is keyboard and screen‑reader accessible and meets WCAG 2.1 AA for labels, contrast, and focus styles. Given the link is opened on an unsupported or very old browser, When capabilities are insufficient, Then a graceful fallback message is shown with a tap‑to‑call option.
Payment Method Capture via Apple Pay/Google Pay and Camera Scan
Given a device with Apple Pay or Google Pay set up, When the user taps "Pay with Apple Pay/Google Pay", Then a vaulted payment token is created via the PCI provider and associated to the client without charging the card. Given a device without wallet support, When the user uses the camera scan, Then card number and expiry are extracted with >=95% accuracy on clear cards and pass Luhn and format validation; manual correction is allowed. Given successful tokenization, When it completes, Then the UI shows confirmation within 3 seconds, the appointment is marked "Payment Method on File", and the link is immediately marked used. Given tokenization fails or issuer challenges occur, When errors occur, Then clear error messages are shown and the user can retry; after 3 failed attempts within 15 minutes, lock the link and display a support option. Given data handling, When processing the payment method, Then PAN and CVV never traverse or persist on FetchFlow servers; only a token and last4/brand are stored.
Configurable Expiration, Retry, and Auto‑Send Timeline
Given business settings specify expiration in hours (24–168), When a link is generated, Then its expiry is set accordingly and the page indicates the expiry; after expiry, requests return HTTP 410 and events are recorded. Given auto‑send rules "on appointment confirmation" and "N days before appointment", When those triggers occur, Then the SMS is sent automatically at the configured times in the business’s timezone, excluding quiet hours. Given retry rules "resend after 24 hours if no click", When no click is recorded before the interval, Then a single resend is queued; if a click or a payment method on file is detected, Then no resend occurs. Given admin edits settings, When changes are saved, Then future sends use the new settings and an audit entry records the change; existing links retain their original expiry.
Wallet Pay Capture (Apple Pay/Google Pay)
"As a pet owner, I want to save my card using Apple Pay or Google Pay so that I can add a payment method in seconds from my phone with confidence it’s secure."
Description

Detect device capabilities to present Apple Pay or Google Pay as the primary capture method, enabling clients to save a payment method in one tap. On successful wallet authorization, store a processor-issued network token as the customer’s card-on-file. Support SCA/3DS where required, dynamic switching to card form if the device or browser does not support wallets, and clear, minimal-step UI to maximize conversion. Ensure compatibility with major processors, proper merchant validation for Apple Pay, and secure handling of payment tokens. Deliver a consistent, accessible experience across iOS and Android without requiring app install or account creation.

Acceptance Criteria
Apple Pay vault via SMS link on iOS Safari
Given a client opens the Tap‑To‑Vault SMS link in Safari on an Apple Pay–enabled iPhone or iPad When the page loads Then an Apple Pay button is rendered as the primary call to action within 2 seconds And no login, account creation, or app install is required When the client taps the Apple Pay button and authorizes with Face ID/Touch ID Then a processor‑issued network token is returned and stored as the customer’s card‑on‑file And a success confirmation is displayed within 1 second And no customer charge is captured during vaulting And the flow requires no more than 2 taps after page load and meets WCAG 2.1 AA for focus order, labels, and contrast
Google Pay vault via SMS link on Android Chrome
Given a client opens the Tap‑To‑Vault SMS link in Chrome on an Android device with Google Pay enabled When the page loads Then a Google Pay button is rendered as the primary call to action within 2 seconds And no login, account creation, or app install is required When the client taps the Google Pay button, selects a card, and authorizes Then a processor token suitable for card‑on‑file (network token where supported) is stored on the customer profile And a success confirmation is displayed within 1 second And no customer charge is captured during vaulting And the flow requires no more than 2 taps after page load and meets WCAG 2.1 AA for focus order, labels, and contrast
Dynamic fallback to card form when wallet unsupported or fails
Given the device/browser does not support Apple Pay or Google Pay, or wallet initialization returns capabilityUnavailable/NotSupported When the page loads or the wallet capability check fails Then a secure card entry form is displayed and wallet buttons are not shown And the form auto‑focuses the first field and supports camera card scan where available When the client enters valid card details and submits Then a processor token (or network token where available) is stored as card‑on‑file And a success confirmation is displayed within 1 second And the total steps from page load to success do not exceed 3 interactions
Apple Pay merchant validation is performed correctly
Given an Apple Pay session is initiated on a verified domain When merchant validation is requested Then the server performs validation with Apple Pay using the configured merchant ID and certificate and returns a valid session And the domain association file is accessible at /.well-known/apple-developer-merchantid-domain-association And validation completes within 1 second p95 in production When validation fails Then a clear, retryable error is displayed and vaulting is blocked And the failure is logged with non‑PII context
Strong Customer Authentication (SCA/3DS) is handled where required
Given the issuer or region requires SCA for saving a payment method When the client attempts to vault via wallet or fallback card form Then a 3DS/SCA flow is invoked as required by the processor/issuer And vaulting completes only after successful authentication When authentication fails or is abandoned Then no token is stored And the UI displays “Authentication failed—please try again” with a retry option And frictionless cases complete without additional steps
Secure handling and storage of payment tokens
Given any vault flow completes successfully Then only the processor‑issued token, last4, brand, and expiration are stored server‑side And PAN, CVV, and track data are never stored or logged And no payment token or PAN appears in client storage, URLs, or analytics payloads And all transport occurs over TLS 1.2+ with HSTS enabled And logs and error traces redact payment data And security tests confirm no sensitive data egress
Multi‑processor wallet tokenization compatibility
Given each supported payment processor integration is configured in test mode When Apple Pay and Google Pay vault flows are executed end‑to‑end for each processor Then a token suitable for card‑on‑file is successfully created and stored without code changes And processor‑specific parameters (e.g., merchant validation, gateway fields) are satisfied And the same SMS link and UI function on iOS Safari and Android Chrome for all processors
Camera Card Scan & Smart Form
"As a client without wallet pay available, I want to scan my card and have fields auto-filled so that I can save a payment method quickly without typing everything."
Description

Provide a fast fallback flow that lets clients scan their card with the device camera to auto-fill number and expiry, backed by a streamlined form with inline validation for CVV, ZIP/postal code, and name. Support brand detection, error messaging, international address formats, and accessibility (screen readers, proper labels, high-contrast). Optimize for small screens and low bandwidth with instant client-side validation and minimal network trips. Mask sensitive fields, prevent accidental data persistence, and localize copy for clarity and trust. Ensure performance budgets to keep time-to-interact under two seconds on 3G-equivalent networks.

Acceptance Criteria
Camera Scan Autofills Card Number and Expiry
Given a client opens the Tap‑To‑Vault link on a camera‑capable mobile device When the client taps "Scan card" and the card is successfully detected Then the card number and expiry month/year are auto‑filled into their fields And the card number is formatted with brand‑appropriate spacing And the CVV field remains empty And the auto‑filled values remain editable by the client Given camera permission is denied or a scan fails When the client returns to the form Then manual entry fields are available and focused on the card number And an inline, non‑blocking error message explains the failure with a "Try again" option
Inline Validation for CVV, ZIP/Postal Code, and Cardholder Name
Given the client is entering cardholder name When the field loses focus or 300 ms have elapsed after typing Then show an inline error if the name is fewer than 2 letters or contains only whitespace Given the client is entering CVV When the field loses focus or 300 ms have elapsed after typing Then show an inline error if the CVV length does not match the current brand’s requirement (3 or 4 digits) or contains non‑digits And prevent form submission until the CVV is valid Given the client is entering ZIP/Postal Code and a country is selected When the field loses focus or 300 ms have elapsed after typing Then validate against the country‑specific pattern and show a localized inline error on mismatch And focus the first invalid field on submit with a summary message at the top
Dynamic Card Brand Detection and Formatting
Given the client enters the first 6 digits of the card number When a brand can be inferred from the BIN Then display the brand icon and name And update number input spacing and CVV placeholder/expected length for that brand Given brand inference changes due to corrected digits When the number field is edited Then the formatting and CVV rules update immediately Given the full card number is entered When validation runs Then the Luhn check must pass before enabling the submit button And unsupported card brands show an inline error with guidance to use a different card
International Address Support and Localization
Given the page loads with a device locale When the locale is supported (en, es, fr) Then all labels, helper text, error messages, and buttons render in that language And if unsupported, fall back to English Given a country is selected (defaulted from locale and changeable) When entering postal code Then the label adapts (e.g., "ZIP code" vs "Postal code") And validation applies country‑specific formats (e.g., US 5 or 9 digits; CA A1A 1A1; UK valid outward/inward patterns) Given the client changes the country When validation re‑runs Then previously entered postal code is revalidated and errors update accordingly without clearing the input
Accessibility: Screen Readers, Labels, Focus, and Contrast
Given a screen reader user navigates the form When focusing each field Then the label, required status, and error state are announced via associated for/id and aria-describedby attributes Given the "Scan card" button is focused When activated via keyboard or switch control Then the scanner opens and returns focus back to the form on completion or cancellation Given any error appears When it is rendered Then it receives programmatic association to the field and is reachable within the logical focus order And color contrast for text, inputs, and error states is at least 4.5:1 And the form is operable at 200% zoom without horizontal scrolling at 320 px viewport width
Performance on 3G: Time-to-Interact Under Two Seconds
Given a Moto G4‑class device on Chrome with Fast 3G throttling When loading the Tap‑To‑Vault form Then Time to Interactive is ≤ 2.0 seconds at the 95th percentile across 20 test runs And total compressed JS ≤ 100 KB, CSS ≤ 30 KB, and images/fonts ≤ 30 KB for the initial route And no more than 2 network requests are made before first interaction (HTML plus one asset) And client‑side validation runs without network calls; only one tokenization request is made on submit Given a slow network condition causes asset retry When the page still loads Then the form controls are visible and usable with a lightweight skeleton within 1.2 seconds
Security and Privacy: Masking and No Data Persistence
Given the client enters card details When the number field loses focus Then only the last 4 digits remain visible and earlier digits are masked And the CVV is always masked And no card details are logged in the console or analytics Given the session is abandoned, canceled, navigated away, or idle for 2 minutes When the page is re‑opened Then no sensitive values persist in fields, browser storage, or URL And HTTP response headers include Cache‑Control: no‑store And browser prompts to save payment info are suppressed Given submission occurs When tokenization completes Then the form clears all sensitive data from memory and the DOM And network logs contain no PAN or CVV values
PCI-Compliant Tokenization & Security Controls
"As a business owner, I want payment details to be tokenized and kept out of scope so that we reduce PCI risk while keeping client trust high."
Description

Vault all payment methods using the payment processor’s tokenization so that raw PAN never touches FetchFlow systems. Use hosted fields or wallet token flows to maintain PCI SAQ A scope. Enforce TLS 1.2+, HSTS, CSP, and anti-replay nonces on the vault page. Store only the minimal card metadata (brand, last4, expiry) and necessary consent artifacts. Implement audit logging for link creation, delivery, opens, and tokenization events; rate limiting and bot mitigation; and encrypted at-rest storage for logs and consent records. Establish data retention policies and deletion workflows. Provide compliance hooks (e.g., 3DS indicators, SCA exemptions) and regular security scanning.

Acceptance Criteria
Maintain SAQ A Scope via Processor Tokenization
Given the Tap‑To‑Vault page is loaded When inspecting network requests in the browser Then all PAN/CVV inputs are rendered via processor-hosted fields or wallet SDKs and post directly to the processor domain, not FetchFlow Given a card is saved When the processor returns a payment method token Then FetchFlow stores only token ID, card brand, last4, and expiry month/year, and stores no PAN, CVV, or magnetic track data Given application, access, and error logs are produced during the flow Then no PAN/CVV appears in any logs and automated log redaction tests pass Given a PCI scope review When evaluating all FetchFlow-owned endpoints Then no endpoint accepts PAN/CVV and the implementation qualifies for PCI SAQ A
Secure Tap‑To‑Vault Link Lifecycle and Anti‑Replay
Given a Tap‑To‑Vault link is created When examining the link payload Then it includes a signed HMAC, a unique one-time nonce, a customer/appointment binding, and an expiration timestamp configurable with a default of 24 hours Given the link is opened after it expires or after a successful tokenization When the user visits the URL Then the server returns an expired/used message with 410 and does not render the vault form Given an attacker replays a previously used nonce or signature When the server validates the request Then the request is rejected with 409 and an audit log entry is recorded without revealing sensitive details Given the link is forwarded to a different customer When the link is opened Then the vault page blocks access because the link is bound to the original customer context
Enforce Transport and Page Security Controls
Given any request to the vault page When a client attempts TLS 1.0/1.1 Then the connection is refused; only TLS 1.2+ is accepted with modern ciphers and SSL Labs grade A or better Given the vault page is served Then the response includes HSTS with max-age >= 15552000, includeSubDomains, preload And includes a strict Content-Security-Policy that whitelists only required processor/wallet domains, uses script nonces, blocks inline/eval, sets frame-ancestors 'none', and sets object-src 'none' And includes anti-replay/CSRF nonces bound to the session and validated on submit Given the vault page is embedded or framed When a third party attempts to load it in an iframe Then the browser blocks it due to frame-ancestors 'none' and X-Frame-Options 'DENY'
Data Minimization, Encryption at Rest, Access Control, and Retention/Deletion
Given a successful tokenization When persisting payment method metadata and consent artifacts Then only card brand, last4, expiry, token ID, and consent details (timestamp, IP, user agent, consent text/version) are stored; no PAN, CVV, full name-on-card, or track data are stored Given data is written to storage Then all card metadata, consent artifacts, and logs-at-rest are encrypted with AES-256 keys managed by KMS/HSM, with access restricted by least-privilege IAM and key rotation at least every 90 days Given a staff user views payment method metadata When accessing the customer profile Then only authorized roles can view brand/last4/expiry, and each view is audited Given published retention policies Then security/audit logs are retained >= 1 year with >= 3 months immediately available, consent artifacts are retained per policy (default 3 years) or until revocation, and deletion workflows purge data within 30 days of a valid request with an auditable trail
Comprehensive Audit Logging with Tamper Evidence
Given Tap‑To‑Vault link lifecycle events occur When links are created, SMS is sent/delivered, links are opened, tokenization is attempted/succeeds/fails, consent is captured, or deletions occur Then an immutable audit log entry is recorded for each event with timestamp (UTC, ms precision), actor/system ID, customer ID, IP, user agent, event outcome, and correlation ID Given audit logs are stored Then logs are append-only with tamper-evident hashing or write-once storage; integrity checks detect any modification Given audit logs are queried or exported When a user with reporting permission requests them Then PII is minimized/redacted (no PAN/CVV), access is authorized and audited, and exports are available in standard formats (e.g., JSON/CSV)
Rate Limiting and Bot Mitigation on Vault Endpoints
Given traffic to the vault endpoints When a single IP exceeds 30 GET requests per minute or 10 POST tokenization attempts per minute Then subsequent requests receive HTTP 429 with a backoff header, and challenges are presented for legitimate users to proceed Given repeated failed tokenization attempts from the same customer When failures exceed 5 within 15 minutes Then further attempts require human verification (e.g., CAPTCHA/turnstile) and are logged with risk signals Given webhook endpoints for processor callbacks When requests originate from non-allowlisted source ranges or fail signature verification Then the requests are rejected with 401/403 and are rate-limited
Compliance Hooks (3DS/SCA) and Regular Security Scanning
Given a customer in an SCA jurisdiction saves a card When the processor indicates 3DS is required or an exemption applies Then FetchFlow passes the appropriate 3DS/SCA indicators, performs the challenge flow when needed, records the authentication result/LIABILITY SHIFT, and stores exemption outcomes in the audit log Given continuous integration builds When dependency and container scans run Then builds fail if any Critical/High vulnerabilities are present; Medium findings require documented risk acceptance or remediation within SLA Given the staging or production environment When DAST/ASV scans run monthly and before major releases Then there are zero Critical/High findings, CSP violation reports are monitored, and regressions block release until remediated
Auto-Link Payment Method to Client, Pet Profile, and Appointment
"As a groomer, I want the saved card to auto-attach to the client and appointment so that checkout, deposits, and no-show fees happen automatically without manual follow-up."
Description

On successful save, automatically associate the vaulted payment method with the client profile and the relevant Pet Profile Smart Card, marking it as default when applicable. Attach the method to the upcoming appointment for seamless checkout, and enable rules that use the card-on-file to apply deposits, packages, and no-show fees. Capture and store explicit consent for off-session charges with clear, compliant language. Support multiple payment methods per client, default selection, expiration monitoring, and graceful fallbacks. Provide webhooks/events so downstream workflows (e.g., auto-checkout, fee application) can react immediately.

Acceptance Criteria
Auto-Link Vaulted Method to Client and Pet Profile
Given an SMS Tap‑To‑Vault link encodes clientId, petId, and optionally appointmentId When the client saves a card successfully via Apple Pay, Google Pay, or camera scan Then the new payment method is associated to clientId and the Pet Profile Smart Card for petId And if the client has no existing methods, the new method is set as default; otherwise default remains unchanged unless "Make default" was selected And if appointmentId exists and is in Scheduled state, the appointment is updated to reference this method for checkout And the method metadata displayed is limited to brand, last4, expMonth, expYear, and network; full PAN is never stored or shown And the method appears in the dashboard within 2 seconds of save
Attach Default Method to Upcoming Appointment
Given a client with a default payment method and a scheduled appointment that lacks an attached payment method When the appointment is created, rescheduled, or a new default is set before the visit Then the appointment record attaches the current default payment method And the attached method is visible in the appointment view with masked details (brand, last4, exp) When auto-checkout or any off-session charge is triggered for that appointment Then the attached method is used for the charge first If no method can be attached, the appointment status is flagged "No card on file" and an SMS Tap‑To‑Vault request is sent to the client
Capture Explicit Off-Session Charge Consent
Given the Tap‑To‑Vault flow is opened When the client taps Save Then an explicit consent checkbox with compliant language for off-session charges, deposits, packages, and no-show fees must be checked And save is blocked until consent is provided And upon success, a consent record is stored with clientId, timestamp (UTC), phone, IP, userAgent, consentTextVersion, linkId, and a traceId And the consent artifact is retrievable from the client profile and appointment views in under 1 second and is included in related webhooks And any update to consent wording increments consentTextVersion, which is recorded with the vault event
Multiple Payment Methods and Default Selection
Given a client with one existing payment method When an additional method is vaulted Then both methods are stored and visible under the client profile; only one is default When "Make default" is chosen during vault or in the dashboard Then the selected method becomes default and the prior default is demoted And off-session charges use the default unless an appointment-specific override is set If the default method is expired, deleted, or returns a permanent decline, the system automatically attempts the next valid method by recency and logs the outcome
Expiration Monitoring and Graceful Fallbacks
Given the system runs daily card health checks Then cards expiring within 60 days and expired cards are flagged When a card is flagged Then an SMS reminder with a Tap‑To‑Vault link is sent every 14 days up to 3 times or until the card is updated At charge time, if the attached/default card is expired Then the next valid method is attempted; if none exist, the charge is deferred, the appointment is marked "Payment required," and SMS and dashboard alerts are issued And expiration state is visible in the dashboard and exposed via API
Real-Time Events and Webhooks on Vault and Linking
When a payment method is vaulted successfully Then a payment_method.vaulted event is emitted within 5 seconds including clientId, petId (when applicable), appointmentId (when applicable), last4, brand, expMonth, expYear, network, isDefault, consentTextVersion, and linkId When a method is linked to a pet/client, attached to an appointment, or default is changed, or consent is captured Then payment_method.linked, appointment.payment_method.attached, payment_method.default.changed, and consent.captured events are emitted respectively And all webhooks are HMAC-SHA256 signed, include an idempotency key, and are retried with exponential backoff for up to 24 hours until 200 OK And delivery is at-least-once; duplicate events share the same eventId for consumer deduplication And an event delivery log with status and last attempt time is accessible
Apply Deposits, Packages, and No-Show Fees Using Card-on-File
Given business rules require a deposit at booking and a client has a default method and consent on file When the booking is confirmed Then the deposit is authorized or captured per configuration automatically and an SMS receipt is sent Given a package with remaining balance is applied to an appointment Then the package balance is decremented first and any remainder is charged to the attached/default method When a no-show is recorded Then the configured no-show fee is charged to the attached/default method within 10 minutes and notification is sent If a charge fails with a retryable decline, up to 3 retries occur over 24 hours; on final failure, the next valid method is attempted; if none, an invoice is opened and an SMS payment link is sent And all auto-charges are logged in the ledger with reversal/refund support
Vault Request Tracking & Automation Dashboard
"As staff, I want to see who has completed saving a card and automate reminders so that I increase completion rates without spending time chasing clients."
Description

Expose end-to-end status for each Tap‑To‑Vault request—Not Sent, Sent, Delivered, Opened, Completed, Failed—with timestamps and error reasons. Provide conversion analytics by location, staff, and template, plus filters and CSV export. Enable automation: schedule sends a set number of days/hours before an appointment, auto-remind if not completed, and escalate with staff notifications. Support one-tap resend, bulk actions for a day’s schedule, and permissioned access controls. Integrate events into the main FetchFlow dashboard alongside appointments and messaging, and surface KPIs to show reduced chasing and faster checkout.

Acceptance Criteria
Status Lifecycle Visibility & Timestamps
Given a Tap‑To‑Vault request is created for an appointment Then its initial status is Not Sent and created_at is recorded in ISO‑8601 in the org’s timezone When the SMS handoff to the provider completes Then status updates to Sent within 5 seconds and sent_at is recorded When a delivery receipt is received Then status updates to Delivered within 5 seconds and delivered_at is recorded When the client opens the link Then status updates to Opened within 5 seconds and opened_at is recorded When the client successfully vaults a payment method Then status updates to Completed within 5 seconds and completed_at and payment_method_id are recorded When the message is undeliverable or the vault flow errors irrecoverably Then status updates to Failed with failed_at and an error_reason from the allowed set {CarrierUndeliverable, LinkExpired, PaymentDeclined, UserAbandoned, GatewayError, PermissionDenied} And the UI shows a chronological event timeline with all populated timestamps; statuses do not regress except via an Admin reset action that is logged
Automation Scheduling, Reminders, and Staff Escalation
Given automation is enabled with an offset of X hours before appointment start When an appointment is scheduled Then a send job is scheduled at appointment_start - X hours unless a valid card is already on file And no automated send occurs if the appointment is within 2 hours of start time (manual only) When the request remains not Completed after R hours from Sent Then an automated reminder SMS is sent and reminder_count increments And reminders stop immediately upon Completed and never exceed MaxReminders per request When MaxReminders is reached without completion Then a staff escalation notification is sent containing client name, pet(s), appointment time, last status, and a one‑tap resend link And all automated actions (scheduled, reminded, escalated, skipped) are logged with timestamps and actor=system
One‑Tap Resend and Bulk Actions for Daily Schedule
Given a user with permission views a request row When they tap Resend Then a new secure link is generated, the prior link is invalidated, a Sent event is emitted, and the client receives the SMS within provider SLA And the request timeline shows a Resent event with actor and timestamp Given a user selects Today’s schedule and chooses Bulk Send for requests not in Completed When they confirm Then up to 200 selected requests are queued, each respecting per‑client rate limits (>=5 minutes between sends) and duplication checks And the bulk job result shows counts of sent, skipped (already Completed or recently sent), and failed with reasons And all bulk actions are auditable per request
Filtering, Search, and CSV Export of Vault Requests
Given the dashboard is open When the user applies filters (date range, location, staff, template, status) and a text search (client, phone, pet, appointment ID) Then the list updates within 500 ms and the URL reflects the active filters for shareability And clearing filters returns the full result set for the selected date range When the user exports CSV Then the file includes headers and rows for visible scope with columns: request_id, client_name, phone_e164, pet_names, location, staff, template_name, appointment_id, appointment_start, status, created_at, sent_at, delivered_at, opened_at, completed_at, failed_at, error_reason, reminder_count, escalated And timestamps are ISO‑8601 with timezone offset; CSV is UTF‑8, comma‑delimited, values quoted as needed And exports up to 50,000 rows complete within 60 seconds; larger exports are blocked with a clear message to narrow filters
Conversion Analytics by Location, Staff, and Template
Given a date range and optional filters (location, staff, template) are selected Then analytics display: funnel counts and rates for Sent, Delivered, Opened, Completed; overall conversion = Completed/Sent And time‑to‑vault metrics show median and 90th percentile from Sent to Completed And breakdowns by location, staff, and template are visible and sortable And cancelled appointments and duplicate resends for the same appointment are excluded from denominators beyond the latest successful completion When the user changes filters or range (7/30/90 days or custom) Then charts and KPIs update within 2 seconds and totals reconcile with the filtered list within 0.1% variance And analytics can be downloaded as CSV with the same filters applied
Permissioned Access Controls and Audit Logging
Given roles are defined as Admin, Manager, and Staff Then Staff may view and act only on requests tied to appointments assigned to them and cannot configure automation or export CSV And Managers may view all requests in their location(s), perform bulk actions, resend, and export for their scope And Admins have full access across all locations and settings When a user attempts an unauthorized action (bulk send, export, automation edit) Then the action is blocked with a 403 and an in‑app message, and an audit log entry records actor, action, target, and timestamp And every send, reminder, escalation, resend, status change, and configuration change is captured in an immutable audit trail
Dashboard Integration and KPI Surfacing
Given the main FetchFlow dashboard is open Then each appointment card shows a vault status pill (Not Sent, Sent, Delivered, Opened, Completed, Failed) and updates in real time within 5 seconds of events And clicking the pill opens the request detail timeline within the dashboard And the messaging thread for the client displays the latest vault message status inline with a jump link to details And top‑level KPIs are visible: Vault Conversion (Completed/Sent), Median Hours to Vault, Checkout Time Reduction, and Chasing Reduction And KPI definitions are disclosed in a tooltip and values reconcile with analytics for the same filter range within 0.1%

Adaptive Hold

Automatically right‑size pre‑auth holds by service type, peak hours, client reliability, and travel distance. Holds place on claim, auto‑release after the window, or convert to a deposit on no‑show—protecting premium slots without over‑holding funds for good clients.

Requirements

Risk-Based Hold Sizing Engine
"As an owner-operator, I want holds to automatically fit the risk of each booking so that premium time slots are protected without penalizing my reliable clients."
Description

Compute a dynamic pre-authorization amount per booking by weighting service type, peak-hour surge, client reliability score (no-show/cancel history, payment success, tenure), and travel distance. Supports configurable weights, floors, and ceilings, with safe defaults for new clients and exemptions for known reliable clients. Pulls context from Pet Profile Smart Cards (package credits, vaccine compliance, pet temperament) and schedule metadata to avoid over-holding funds for good clients while protecting premium slots. Exposes a deterministic decision log for support, is idempotent, and returns the chosen amount, rationale, and confidence for downstream orchestration.

Acceptance Criteria
Peak-Hour Premium Service for New Client
Given serviceType="Grooming-Premium" and basePrice=12000 (cents) and scheduledAt=17:30 local (peakHour=true) And client has no reliability history (reliabilityScore=null, tenureMonths=0, paymentSuccessRate=null) And travelDistanceMiles=2.0 And config weights={serviceType:0.4, peakHour:0.3, reliability:0.2, travelDistance:0.1}, floor=5000, ceiling=15000, defaultReliabilityScore=0.5, currency="USD", rounding="nearestDollar" When the engine computes a hold for bookingId="B-1001" with idempotencyKey="K-abc" Then amount = roundToDollarCents(min(max(f(serviceType,peakHour,defaultReliabilityScore,travelDistance), floor), ceiling)) And response includes amount (int cents), currency, confidence in [0,1], rationale array with items {factor,input,normalized,weight,contribution}, and effectiveConfig {weights,floor,ceiling} And rationale shows reliabilityScore=0.5 (default) used with reason="no-history" And confidence <= 0.6 due to missing history flag present in rationale And a decision log is persisted with decisionId and deterministicInputHash And p95 latency for computation <= 150ms
Reliable Client Exemption with Package Credits
Given client reliabilityScore=0.95, tenureMonths=18, noShowRate=0.01, paymentSuccessRate=0.99 And serviceType="Walk-Standard" and basePrice=3000 (cents) And packageCreditsRemaining=2000 (cents) from the Pet Profile Smart Card And config reliableClientCeiling=2500 (cents), floor=1500 (cents), ceiling=8000 (cents) When the engine computes the hold Then if basePrice <= packageCreditsRemaining then amount=0 And otherwise amount = min(max(basePrice - packageCreditsRemaining, 0), reliableClientCeiling) And rationale includes reliableClientExemptionApplied=true and packageCreditsOffset=2000 And confidence >= 0.9 And decision log records the order of evaluations: packageCredits -> reliableClientExemption -> bounds
High-Risk Client at Peak with Long Travel
Given reliabilityScore=0.3 and noShowRate=0.2 over last 10 bookings and tenureMonths=2 And serviceType="Training-Private" and basePrice=9000 (cents) And scheduledAt=18:00 (peakHour=true) And travelDistanceMiles=10 And config weights={serviceType:0.35, peakHour:0.25, reliability:0.3, travelDistance:0.1}, floor=5000 (cents), ceiling=15000 (cents) When the engine computes the hold Then amount=15000 (cents) equals the configured ceiling And rationale shows ceilingApplied=true and sumOfContributionsBeforeCap >= 15000 And confidence >= 0.8 And response currency="USD" and amount is integer cents
Idempotent Re-Invocation Produces Same Decision
Given identical normalized inputs for bookingId="B-2002" and idempotencyKey="K-xyz" When the engine is called twice Then both responses have identical amount, confidence, rationale (same order), decisionId, and deterministicInputHash And only non-deterministic metadata (e.g., createdAt) may differ When the engine is called again with a different idempotencyKey but identical normalized inputs Then decisionId and amount remain identical And the decision log does not create duplicate records (single entry referenced by deterministicInputHash)
Configurable Weights, Floors, and Ceilings Enforcement
Given tenant config version="V2" with weights={serviceType:0.2, peakHour:0.2, reliability:0.5, travelDistance:0.1} (sum=1.0), floor=2000, ceiling=6000 And serviceTypeOverride for "Boarding" with floor=4000 and ceiling=12000 When the engine computes a hold for serviceType="Boarding" Then the engine applies the override floor/ceiling and V2 weights And the response echoes effectiveWeights and effectiveBounds used When provided weights do not sum to 1.0 Then the engine proportionally normalizes weights to sum=1.0 and records normalization in rationale When provided floor>ceiling or any bound is negative Then the engine returns a validation error with code="INVALID_CONFIG" and no amount is produced
Smart Card Context Adjustments
Given Pet Profile Smart Card shows vaccinesStatus="current", temperament="calm", and packageCreditsRemaining=5000 (cents) for pet "Luna" And serviceType="Grooming-Premium", basePrice=12000 (cents), peakHour=false And client reliabilityScore=0.88 And config reliableClientCeiling=3000 (cents) When the engine computes the hold Then package credits reduce the hold by 5000 and the hold does not exceed the uncovered portion (7000 cents) And vaccinesStatus="current" and temperament="calm" apply no additional risk surcharge And amount = min(7000, 3000) = 3000 (cents) And rationale lists smartCardContext fields consumed and adjustments applied
Decision Log Retrieval for Support
Given a prior decision with decisionId="D-555" exists for bookingId="B-3003" When support requests the decision log by decisionId or bookingId Then the system returns an immutable log containing inputsSnapshot, effectiveConfigVersion, normalizedInputs, factorContributions, appliedExemptionsAndCaps, finalAmount, confidence, idempotencyKey, decisionId, and timestamps And sensitive PII is redacted per policy and access is authorized And recomputing with identical normalized inputs yields the same decisionId and contents
Configurable Hold Policies & Caps
"As a business owner, I want to set clear rules for minimums, maximums, and exemptions so that holds align with my policies and local market norms."
Description

Provide an admin policy editor to define hold floors, ceilings, and exemptions per service, location, daypart, and travel radius, including holiday/peak overrides. Allow VIP/client-tier rules, promo exceptions, and package-based reductions. Support versioning, preview/simulate against historical bookings, and safe-guard rails (global cap, currency rounding). Policies are validated and stored server-side, roll out via staged releases, and are auditable.

Acceptance Criteria
Define Floors/Ceilings by Service, Location, and Travel Radius
Given an admin edits a policy, When they set floor and ceiling amounts for a specific service at a specific location and travel radius tier, Then the computed pre-auth hold for matching bookings is clamped to [floor, ceiling]. Given a service+location+radius combination without an explicit policy, When a booking is claimed, Then the fallback order is evaluated as: service+location+radius > service+location > service > global default, and the first match is applied. Given a policy where a floor exceeds its ceiling, When the admin attempts to save, Then the server rejects the change with a 422 validation error and the policy is not persisted. Given a booking with a travel distance that does not match any defined radius tier, When the booking is claimed, Then the policy uses the configured default radius tier for that location. Given policy amounts are defined as percentages of estimated service price, When computing a hold, Then the percentage is applied to the estimate before guardrail checks.
Daypart and Holiday/Premium Overrides
Given daypart overrides and a holiday calendar are configured for a location, When a booking start time (in the location's time zone) falls within an override window or on a holiday, Then the override policy supersedes the base policy for hold calculation. Given both a daypart override and a holiday override could apply, When computing the hold, Then precedence is Holiday > Daypart > Base policy. Given an override window ends, When a subsequent booking is claimed outside the window, Then the base (or next most specific) policy is applied. Given overlapping daypart overrides are configured, When saving the policy, Then the server rejects overlaps with a 422 error indicating the conflicting windows.
Client Tiers, Promos, and Package-Based Reductions
Given a VIP/client-tier rule defines a fixed or percentage reduction and a VIP minimum floor, When a VIP client books a matching service, Then the reduction is applied and the final hold is not less than the VIP minimum floor. Given a promo code flagged as hold-exempt or reduced-hold, When the promo is applied to a booking, Then the hold is adjusted per the promo rule and not below the global minimum floor. Given the client holds an applicable package with remaining credits, When the booking matches the package service, Then the hold is reduced per package rules (e.g., zero hold or reduced cap) without dropping below zero. Given multiple reductions (package, tier, promo) could apply, When computing the hold, Then reductions are applied in this order: Package > Client Tier > Promo, and the final result is clamped by global guardrails.
Global Cap and Currency Rounding Guardrails
Given a global hold cap is configured, When a computed hold exceeds the cap, Then the hold is limited to the cap amount. Given a location currency and rounding rule (e.g., to minor unit) are configured, When saving and applying hold amounts, Then all amounts are rounded according to the rule after percentage calculations and before comparisons to floors/ceilings. Given a policy contains negative amounts or non-numeric values, When saving, Then the server rejects the policy with a 422 validation error specifying the offending fields. Given a floor or ceiling is configured in a currency different from the location, When saving, Then the server rejects the configuration unless a supported conversion strategy is defined.
Versioning with Preview/Simulation on Historical Bookings
Given an admin creates a draft policy version V2, When saved, Then V2 is stored as Draft and does not affect runtime calculations. Given a Draft version is selected, When the admin runs a simulation over the last 90 days (or a chosen date range), Then the system returns: total bookings evaluated, count and percentage impacted, total/average hold deltas, exposure delta, and a list of top 50 impacted bookings with before/after holds and matched rules. Given simulation results are displayed, When the admin opens a booking detail, Then the system shows the rule match trace, floors/ceilings applied, overrides used, and final computed hold for both current and draft policies. Given V2 is scheduled for publication at timestamp T, When T is reached, Then bookings claimed at or after T use V2, and bookings claimed before T continue using the previously published version. Given a rollback is initiated, When confirmed, Then the prior published version becomes active and a new version entry records the rollback with linkage to the reverted version.
Server-Side Validation and Policy Storage
Given a policy payload is submitted, When validating server-side, Then the server verifies schema, referential integrity (services, locations, tiers, promos, packages), non-overlapping windows, numeric bounds, and precedence conflicts, returning detailed 422 errors on failure. Given a policy saves successfully, When persisted, Then it is stored with a version ID, immutable checksum, created/updated timestamps, and the actor ID, and is retrievable via API. Given concurrent edits occur, When a save is attempted with a stale ETag/version, Then the server responds 409 Conflict and no changes are applied. Given runtime hold computation, When a booking is claimed, Then the server computes the hold using the latest published policy (or targeted rollout policy) and ignores any client-supplied hold amounts.
Audit Trail and Staged Rollout
Given any create, update, publish, schedule, rollout, or rollback action, When completed, Then an audit record is written capturing actor, timestamp, action type, version IDs, environment/segment, and a field-level diff, and records are immutable and queryable by filters. Given a staged rollout is configured by percentage, location, or client cohort, When publishing a new version, Then only the targeted segment uses the new policy and non-targeted segments continue using the previous version. Given a rollout is paused or aborted, When the change is applied, Then new bookings immediately fall back to the prior published policy; previously computed holds remain as calculated and are not retroactively adjusted. Given a staged rollout reaches 100%, When completed, Then the new version becomes the default for all segments and the rollout state is recorded in the audit trail.
Payment Pre-Authorization Orchestration
"As a provider, I want holds to be placed and managed automatically with my payment processor so that I don’t have to chase payments or worry about failed authorizations."
Description

Integrate with supported processors to create, adjust, and release payment pre-authorizations at booking, modification, and completion events. Handle SCA/3DS where required, network timeouts, retries with backoff, and idempotency keys. Map holds to bookings and clients, store tokens securely (PCI-compliant), and support multi-merchant accounts. Normalize processor webhooks for authorization updates and failures, and expose a unified state machine that downstream workflows (release or conversion) consume.

Acceptance Criteria
Booking Creates Right-Sized Pre‑Auth Hold with SCA and Idempotency
Given a new booking with service_type, start_time, location_id, client_id, travel_distance_km, and reliability_score And an eligible payment method token for client_id When the orchestration service requests a pre-authorization using idempotency_key = "booking:{booking_id}:preauth:v1" Then it computes hold_amount according to Adaptive Hold policy and selects merchant_account_id based on location_id And sends a create-authorization to the configured processor with amount = hold_amount and idempotency_key And if the processor indicates 3DS/SCA is required, the flow initiates SCA and marks state = authorized only after successful challenge And the API responds within 3 seconds with status = authorized, authorization_id, amount_authorized = hold_amount, merchant_account_id, processor_name And the system persists mapping of booking_id -> authorization_id, client_id, merchant_account_id, processor_name And on transient errors (timeouts, 5xx), it retries up to 3 attempts with exponential backoff (approx 200ms, 800ms, 2s) using the same idempotency_key And on duplicate create calls with the same idempotency_key, it returns the original authorization details without creating a new hold
Booking Modification Adjusts Pre‑Auth Amount Safely
Given an existing booking with state = scheduled and an active authorization linked to booking_id When the booking is modified such that Adaptive Hold recalculates a greater hold_amount Then the service attempts an authorization increment when supported by the processor, else cancels the old authorization and creates a new one atomically so only one active authorization exists And if the new amount exceeds an SCA threshold, the client is prompted for SCA and the state remains pending_sca until completion And the response includes updated authorization_id (or the same if incremented), amount_authorized, and state = authorized upon success And when the booking is modified to reduce the hold_amount, a partial release is executed and amount_authorized reflects the reduced amount within 2 seconds And all adjust operations use idempotency_key = "booking:{booking_id}:adjust:{version}" and are idempotent across retries
Auto Release or No‑Show Conversion Executes per Policy
Given a booking with an active authorization When the booking is marked completed or canceled within policy and the release window elapses Then the orchestration releases the authorization within 5 minutes and sets state = released, emitting event = authorization.released with booking_id and authorization_id When the booking is marked no_show and policy deposit_amount = F is configured Then the orchestration captures min(F, amount_authorized) against the existing authorization and sets state = captured_deposit, emitting event = authorization.captured_deposit And if the authorization is already expired at processor, the state is set = expired and event = authorization.expired is emitted And transient release/capture failures are retried up to 3 times with exponential backoff; permanent failures set state = failed with error_code And all release/convert actions are idempotent using distinct idempotency keys per action
Unified Authorization State Machine Exposed to Downstream Workflows
Given downstream services query the orchestration API for a booking’s authorization state When GET /authorizations?booking_id={booking_id} is called Then the response contains a single source of truth with fields: state, previous_state, authorization_id, amount_authorized, merchant_account_id, processor_name, timestamps (created_at, updated_at), and latest_event And allowed states include: requested, pending_sca, authorized, adjusted, partially_released, released, capture_pending, captured_deposit, expired, failed And invalid transitions (e.g., released -> authorized) are rejected with 409 Conflict and no state change And each state transition emits a normalized event with correlation_id = booking_id and is durable (at-least-once delivery)
Webhook Normalization and Signature Verification Across Processors
Given processors send webhooks for authorization lifecycle events (authorized, incremented, partially_released, released, reversed, expired, failed) When a webhook is received Then the system verifies the provider signature (e.g., HMAC) using configured secrets and rejects unsigned/invalid requests with 401 And it normalizes payloads into an internal schema {booking_id, authorization_id, normalized_event, amount, processor_name, occurred_at} And processing is idempotent using a deduplication key derived from provider delivery_id And out-of-order webhooks do not regress state; only forward-valid transitions are applied And the endpoint responds within 2 seconds with 2xx on acceptance and persists the raw payload and verification result for at least 30 days
Secure Tokenization and PCI-Compliant Storage
Given a client provides a payment method during booking When creating pre-authorization Then the system uses processor tokenization; PAN/CVV are not stored or logged by FetchFlow services And only token, last4, brand, exp_month/year, and billing_zip are stored encrypted at rest (AES-256) with access restricted by service role And all logs and events redact PAN/CVV by policy, verified by automated log scans in CI And subsequent charges reference the stored token successfully in a test transaction And quarterly PCI ASV scans show no PAN data at rest and pass with zero High findings
Multi‑Merchant Account Routing and Settlement Mapping
Given a business has multiple merchant accounts mapped to locations and service categories When creating or adjusting an authorization for a booking Then the orchestration routes the request to the correct merchant_account_id based on booking.location_id and service configuration And the response and persisted record include merchant_account_id for reconciliation And if a booking’s location changes across merchants, the system cancels the prior authorization and re‑authorizes under the new merchant atomically so only one active authorization exists And settlement and capture events emitted include merchant_account_id to support downstream reporting
Auto-Release & Window Scheduler
"As a client, I want my funds released promptly after my appointment or if I cancel within policy so that I’m not over-held or surprised by lingering holds."
Description

Automatically release holds when the configured hold window expires or when the appointment completes, whichever comes first, with precise timezone/DST handling. Respects reschedules and cancellations by recalculating windows and releasing prior holds. Uses a fault-tolerant scheduler with exactly-once semantics, retry queues, and idempotent operations to prevent double releases or leaks. Sends confirmation to the client via SMS and updates the booking timeline and ledger.

Acceptance Criteria
Release on Appointment Completion
Given a pre-auth hold exists for an active appointment And the hold window has not yet expired When the provider marks the appointment as Completed Then the system releases the hold within 60 seconds And the booking timeline records a "Hold Released" event with the release timestamp and amount And the ledger records a single HOLD_RELEASED entry with the hold_id, amount, and payment_method last4 And the client receives an SMS confirmation including appointment date/time, amount released, and last4 within 2 minutes And the hold status becomes Released
Release on Hold Window Expiry
Given a pre-auth hold exists for a scheduled appointment And the hold window is configured (e.g., 120 minutes after placement or policy-defined) And the appointment has neither started nor been canceled When the current time reaches the computed expiry instant Then the system releases the hold within 60 seconds of expiry And no new holds or charges are created as part of the release And the booking timeline and ledger are updated as for a normal release And the client receives an SMS confirmation within 2 minutes
Expiry Across DST/Timezone Boundaries
Given a pre-auth hold for an appointment with tzid set (e.g., America/New_York) And the hold window crosses a DST change And the system has computed the expiry instant in UTC as T When local clocks shift due to DST between hold placement and expiry Then the release fires at UTC time T ± 60 seconds regardless of local clock changes And the timeline displays both local time (tzid) and UTC for the release event And no early, late, or duplicate releases occur
Reschedule Recalculates Window and Releases Prior Hold
Given a pre-auth hold exists for an appointment at time A with expiry Ea When the appointment is rescheduled to time B Then the system ensures only one active hold remains for the appointment And any hold associated with time A is released within 60 seconds and logged in the ledger/timeline And any previously scheduled release job for Ea is canceled or de-duplicated And a new release is scheduled for the recomputed expiry Eb based on time B and policy And no duplicate ledger entries are created
Cancellation Releases Hold
Given a pre-auth hold exists for a scheduled appointment When the appointment is canceled by either client or provider Then the system releases the hold within 60 seconds of cancellation And cancels any pending release jobs for that hold And records one HOLD_RELEASED entry in the ledger and a "Hold Released" event in the timeline And sends an SMS confirming cancellation and hold release to the client within 2 minutes And the hold status becomes Released
Exactly-Once Release and Idempotency
Given a pre-auth hold exists And the release job may be triggered concurrently or retried due to transient errors When multiple release attempts occur with the same idempotency key Then the payment gateway is instructed to release funds at most once And the ledger contains exactly one HOLD_RELEASED entry and zero refund entries And the timeline contains exactly one "Hold Released" event And subsequent duplicate attempts return idempotent success (no side effects)
Retry and Dead-Letter Handling
Given the payment gateway returns a 5xx or times out during a release attempt When the release job runs Then it retries with exponential backoff (e.g., 1m, 2m, 4m, 8m, 16m) up to 5 attempts using the same idempotency key And if all attempts fail, the job moves to a dead-letter queue and alerts on-call within 1 minute And when the DLQ job is replayed after recovery, the hold is released successfully with exactly-once semantics And the client SMS is sent only once, after a successful release
No-Show Conversion & Fee Application
"As a provider, I want no-shows to automatically trigger the right charge from the hold so that my premium time is compensated without manual follow-up."
Description

When a booking is marked no-show or late-cancel per policy, convert the existing hold to a deposit or fee according to configurable rules (flat or percentage, caps, grace periods). Support partial conversions, package credit application before charging, and immediate SMS receipts with a clear explanation. Handle disputes and reversals with audit trails, and sync outcomes to the client balance, QuickBooks export, and performance analytics.

Acceptance Criteria
No-Show: Flat Fee Conversion With Package Credit Applied First
Given a confirmed booking has a pre-authorization hold of $100 on the client card And the policy "No-Show Fee" is configured as a flat $25 And the client has $10 in eligible package credit And the setting "Apply package credits to penalties before capture" is enabled When staff marks the booking as No-Show Then the system captures $15 from the hold and releases $85 And the package credit of $10 is applied to the fee And an SMS receipt is sent to the client within 30 seconds showing: fee $25, package credit $10, captured $15, released $85, policy name, and dispute link And an immutable audit log entry is recorded with booking_id, policy_id, computed_fee, captured_amount, released_amount, package_credit_used, actor_id, timestamp, and sms_id And the client balance reflects no additional amount due And a QuickBooks export entry is queued within 2 minutes with line items: "No-Show Fee" $25 and "Package Credit Applied" -$10; payment capture $15; reference booking_id And analytics update within 2 minutes: no_show_count +1, penalties_collected $25, package_credit_on_penalties $10, captured_from_hold $15
Late Cancel Within Grace Period: Hold Released, No Fee
Given the late-cancel grace period is configured as 2 hours before service start And the client cancels 90 minutes before the scheduled start And a pre-authorization hold of $60 exists When staff marks the booking as Late Cancel Then the system charges a $0 fee and voids the authorization within 10 seconds And the hold status is updated to Released and $60 is released back to the client And an SMS confirmation is sent within 30 seconds stating "No fee due; hold released" And an immutable audit log entry is recorded with booking_id, policy_id, decision "grace_period", actor_id, timestamp And no QuickBooks fee entry is created And analytics update within 2 minutes: late_cancel_within_grace +1, penalties_collected $0
Late Cancel: Percentage Fee With Cap
Given the policy "Late Cancel Fee" is configured as 50% of service price with a $40 cap And the booking total service price is $120 And a pre-authorization hold of $120 exists And the cancellation occurs outside the grace window When staff marks the booking as Late Cancel Then the system computes the fee as min(50% of $120, $40) = $40 And captures $40 from the hold and releases $80 And sends an SMS receipt within 30 seconds showing: fee $40 (50% capped), captured $40, released $80, policy name, and dispute link And records an immutable audit log with booking_id, policy_id, inputs (price, percent, cap), computed_fee $40, captured_amount $40, released_amount $80, actor_id, timestamp And queues a QuickBooks export within 2 minutes: line "Late Cancel Fee" $40 with payment capture $40 referencing booking_id And updates analytics within 2 minutes: late_cancel_count +1, penalties_collected $40, captured_from_hold $40
No-Show: Partial Conversion by Policy Percentage
Given the policy "No-Show Fee" is configured as 30% of service total And the booking total is $80 And a pre-authorization hold of $80 exists When staff marks the booking as No-Show Then the system computes the fee as 30% of $80 = $24 And captures $24 from the hold and releases $56 And sends an SMS receipt within 30 seconds showing: fee $24 (30%), captured $24, released $56, policy name, and dispute link And records an immutable audit log with booking_id, policy_id, inputs (price, percent), computed_fee $24, captured_amount $24, released_amount $56, actor_id, timestamp And queues a QuickBooks export within 2 minutes: line "No-Show Fee" $24 with payment capture $24 referencing booking_id And updates analytics within 2 minutes: no_show_count +1, penalties_collected $24, captured_from_hold $24
Dispute Approved: Full Reversal With Audit Trail and Sync
Given a prior no-show fee of $25 was applied where $10 package credit was used and $15 was captured from the hold And a dispute record exists in status Open for the booking When an admin approves the dispute with reason "Arrived within grace" Then the system refunds $15 to the original payment method within 1 minute And restores $10 of package credit to the client account And sends an SMS to the client within 30 seconds confirming the reversal and restored credit And records an immutable audit log linking to the original charge with reversal_id, reason, refunded_amount $15, restored_credit $10, actor_id, timestamp And queues a QuickBooks export within 2 minutes: negative line "No-Show Fee Reversal" -$25 and "Package Credit Restored" $10 with reference to the original transaction And updates analytics within 2 minutes: penalties_collected -$25, refunds_issued $15, package_credit_on_penalties -$10, disputes_resolved +1
Multi-Pet Booking: Itemized Fee Calculation With Package Credits
Given a booking has two pets with services: Pet A $50 and Pet B $70 (total $120) And a pre-authorization hold of $120 exists And the policy "Late Cancel Fee" is 25% per line item And the setting "Apply package credits to base service before fee calculation" is enabled And the client has 1 package credit covering Pet A $50 When staff marks the booking as Late Cancel outside the grace window Then the system nets Pet A to $0 (credit applied) and Pet B to $70 before penalty calculation And computes fees: Pet A $0, Pet B 25% of $70 = $17.50 And captures $17.50 from the hold and releases $102.50 And sends an itemized SMS receipt within 30 seconds showing line-level fees and credits, totals, policy name, and dispute link And records an immutable audit log with booking_id, policy_id, per_line_inputs, per_line_fees, captured_amount $17.50, released_amount $102.50, actor_id, timestamp And queues a QuickBooks export within 2 minutes with itemized lines: Pet A "Credit Applied" -$50, Pet B "Late Cancel Fee" $17.50; payment capture $17.50 referencing booking_id And updates analytics within 2 minutes: late_cancel_count +1, penalties_collected $17.50, package_credit_applied_to_services $50, captured_from_hold $17.50
Client Transparency & Consent Messaging
"As a client, I want to know the hold amount and policy before confirming so that I can consent confidently and avoid surprises."
Description

Surface hold amount and policy early in the booking flow and via SMS reminders, including a one-tap consent flow embedded in Pet Profile Smart Cards. Localize text, support templates per service, and automatically suppress messaging for exempt clients. Store timestamped consent and policy version for compliance, include links to terms, and provide clear opt-out paths where legally required.

Acceptance Criteria
Booking Flow Early Hold Disclosure
Given a non-exempt client is booking a service that uses Adaptive Hold And the client’s locale and currency are known When the client reaches the confirmation step Then the UI displays the computed hold amount formatted to the client’s currency (e.g., $25.00) And the message includes the hold window duration and a summary that the hold may convert to a deposit upon no-show And a visible link to the full terms is present and opens in the client’s locale And the Confirm action is disabled until the client explicitly acknowledges the hold policy And the copy is rendered from the service’s template for the client’s locale And calculation and rendering complete within 500 ms at the 95th percentile
SMS Reminder Hold Messaging & Consent Link
Given a scheduled appointment for a non-exempt client When an SMS reminder is generated Then the SMS text is populated from the service- and locale-specific template And it includes the hold amount, hold window, and a short policy summary And it includes a unique, expiring link to the Pet Profile Smart Card consent screen And the message fits within 2 GSM-7 segments (or 2 UCS-2 segments for non-GSM characters) without truncation And if the client has already consented to the current policy version, the SMS omits the consent CTA and confirms consent on file
One-Tap Consent via Smart Card
Given a client opens the Smart Card consent screen from the SMS or booking flow When the client taps Agree or Decline Then the system records a consent event with clientId, bookingId, serviceId, policyVersionId, consentValue, ISO-8601 UTC timestamp, channel, IP, and user agent And the event is persisted within 1 second and retrievable via API and admin UI And repeat submissions for the same client/booking/policyVersion are idempotent (no duplicate records; last consentValue and timestamp reflect the latest action) And a success state is shown; on network failure a retry option is presented without creating partial records
Exempt Client Messaging Suppression
Given a client marked as exempt from hold messaging or a service flagged as no-hold When the booking flow renders or an SMS reminder is generated Then hold amount disclosures and consent prompts are not shown or sent And standard reminders are sent without hold-specific content And an internal audit log records the suppression with clientId/serviceId, rule matched, and timestamp
Localization and Per-Service Templates
Given per-service, per-locale templates exist with variables {hold_amount}, {hold_window_hours}, {service_name}, {terms_link}, {policy_version} When generating booking UI copy or SMS text Then the engine selects the exact service+locale template And if no exact match, it falls back by locale->default and service->default in that order And all variables are resolved; if any variable is missing, generation fails with a surfaced error and no message is sent And a preview shows the fully rendered text and estimated SMS segments before sending
Terms Link and Legally Required Opt-Out
Given a locale/jurisdiction that requires opt-out for policy/consent messaging When an SMS with hold policy content is generated Then the SMS includes a clear opt-out instruction or link appropriate to the locale And when the client opts out (e.g., replies STOP or uses the link), their preference is recorded with timestamp, channel, and locale And subsequent hold-specific messages are suppressed while transactional non-policy reminders continue where permitted And the terms link resolves to the locale-specific terms page with an HTTP 200 within 2 seconds
Consent Audit Log and Export
Given an admin with Compliance permission accesses Consent Logs When they filter by date range, client, service, or policy version and request export Then the system displays matching records within 3 seconds for up to 5,000 rows And exports a CSV within 30 seconds for up to 100,000 rows with columns: clientId, bookingId, serviceId, policyVersionId, consentValue, timestamp, channel, IP, userAgent, locale And the export includes a SHA-256 checksum file and is available for 7 days And access is logged with adminId and timestamp
Hold Analytics & Optimization
"As a business owner, I want to see how hold policies affect no-shows and client experience so that I can tune settings for maximum protection with minimal friction."
Description

Provide dashboards and exports showing hold amounts by segment, release vs. conversion rates, impact on no-shows and checkout speed, and client satisfaction signals (opt-outs, disputes). Enable A/B testing of weighting presets and policy variants to optimize for reduced no-shows with minimal hold friction. Include anomaly detection for stuck holds and per-processor success rates, and surface alerts in the operator dashboard.

Acceptance Criteria
Hold Amounts by Segment Dashboard & Export
Given I am an operator with Analytics permission and holds exist for the selected date range When I open Hold Analytics and select segments (service type, peak-hour flag, client reliability tier, travel distance bracket) and click Apply Then the dashboard lists per-segment metrics: total hold amount, count of holds, average, median, p90, p99 And totals across segments equal the grand total for the selection And all aggregates match backend truth within 0.5% And currency is displayed in the account currency and respects locale formatting And the Export control produces CSV and JSON files with identical aggregates for up to 100,000 rows within 60 seconds And exported files include column headers, ISO-8601 timestamps, and segment keys And users without Analytics permission cannot access the dashboard or exports
Release vs Conversion Rate Reporting
Given holds with final outcomes exist (released, converted, active, failed) When I filter by date range and any combination of segments Then release rate is calculated as released/placed and conversion rate as converted/placed, excluding active holds from the denominators And a 95% confidence interval (Wilson) is shown for each rate And counts are drillable to the underlying hold IDs And released + converted + active + failed equals placed for the selection And exported counts and rates match the on-screen values rounded to two decimals
No-Show and Checkout Speed Impact Comparison
Given a baseline (prior policy) and a comparison (current policy or variant) with sufficient data When I enable Compare mode and select the two periods or variants Then the dashboard shows no-show rate delta (relative %) and median checkout duration delta (seconds) with 95% confidence intervals And statistical significance is computed (two-proportion z-test for no-show rate, Mann–Whitney U for checkout duration) And results are marked Statistically Significant only when p < 0.05 and each group has ≥ 300 visits And canceled-by-provider and rescheduled visits are excluded from both metrics And exports include per-group metrics, deltas, p-values, and sample sizes
Client Satisfaction Signals Tracking
Given client opt-outs from holds and payment disputes are captured with timestamps and reason codes When I view the Client Signals panel and apply any segment filters Then the system shows opt-out rate (opt-outs/active clients) and dispute rate (disputes/holds) for the selection And segments exceeding thresholds (opt-out rate > 3% or dispute rate > 1%) are highlighted And top 5 reason codes are listed by frequency And the export includes de-identified client IDs, timestamps, reason codes, and segment attributes with PII masked per privacy settings
A/B Testing of Hold Weighting Presets
Given I have Admin permission When I create an experiment with two weighting presets or policy variants and set metrics (primary: no-show rate; guardrails: opt-out rate, average hold amount) Then traffic is randomized at the client level with a default 50/50 split and sticky assignment And the system enforces minimum sample size per arm of 300 unique clients and minimum duration of 14 days before calling a winner And results show lift, confidence intervals, p-values, and any guardrail breaches And I can pause, end, or roll out the winning preset from the results screen And an export of assignments and outcomes (arm, client ID hash, metrics) is downloadable and matches on-screen aggregates
Anomaly Detection for Stuck Holds and Processor Success Rates
Given hold lifecycle events are processed in near real-time When a hold remains authorized beyond its auto-release window by more than 10 minutes or a payment processor's auth/void success rate deviates by >3 standard deviations from its 7-day moving mean with ≥ 50 events in the last hour Then an anomaly is created with severity based on estimated impact (High if affected holds ≥ 50 or total value ≥ $2,000) And alert creation occurs within 2 minutes of detecting the condition And false positives are reduced by requiring the condition to persist for at least 5 minutes before alerting And the anomaly record links to affected hold IDs and the processor and auto-resolves when metrics return within 1 SD for 15 consecutive minutes
Operator Dashboard Alert Surfacing and Management
Given an anomaly or configured threshold breach exists When the condition triggers Then an alert card appears in the Operator Dashboard within 2 minutes and the in-app badge count increments And clicking the card opens a details view with remediation actions (e.g., bulk release, retry voids) and links to filtered analytics and exports And alerts can be acknowledged, snoozed, or resolved with reasons, and all actions are audit-logged with user, timestamp, and action And duplicate alerts are deduplicated within a 30-minute window And RBAC limits alert visibility to permitted locations, and resolved alerts remain searchable for 90 days

Auto‑Closeout

At “Visit Complete,” charges auto‑deduct from the vaulted card, with package credits applied first and add‑ons/taxes prefilled. Staff can adjust line items, prompt for tip, and send an itemized SMS receipt—speeding curbside checkout and cutting end‑of‑day reconciliation.

Requirements

Auto-Capture Payment on Visit Complete
"As a groomer, I want the visit to auto-charge the saved card when I tap Visit Complete so that checkout is fast and hands‑free at curbside."
Description

When staff marks a Visit as Complete, the system automatically assembles the charge by building the invoice, applying available package credits first, calculating add-ons, discounts, taxes, and fees, and capturing payment against the customer’s vaulted default card via the payment gateway. The flow must be idempotent using a per-visit closeout key to prevent duplicate charges, include configurable retry logic for transient failures, and fall back to an alternate path (select a different saved card, collect cash/check, or send an SMS pay link) on hard declines. The implementation stores only tokens (no PANs) and adheres to PCI requirements, supports SCA/3DS where required, and writes a complete audit record including authorization/capture IDs, last4, and timestamps. Success triggers post-payment events (receipt dispatch, ledger updates), while failure surfaces actionable errors to staff in the same SMS-first dashboard.

Acceptance Criteria
Idempotent Closeout with Per-Visit Key
Given a Visit in progress with a generated closeout key When staff taps "Visit Complete" once or multiple times rapidly Then exactly one invoice is created and at most one payment capture attempt is performed, associated to the closeout key Given concurrent closeout requests for the same Visit When processing completes Then subsequent requests return the same final result without duplicate charges and without duplicating post-payment events Given a successful closeout Then the audit record stores and displays the closeout key linked to the invoice and payment IDs
Apply Package Credits Before Vaulted Card Capture
Given the customer has eligible package credits When the invoice is assembled at Visit Complete Then credits are applied to eligible line items before charging any card and the remaining balance is recalculated Given credits fully cover eligible items and the total becomes zero Then no card authorization or capture occurs and the visit is marked paid with a zero-balance receipt Given a mixture of credited and non-credited items with taxes and fees Then taxes and fees are computed per configuration and any remaining amount, including applicable taxes and fees, is charged to the card
Line Item Adjustments and Tip Prompt
Given staff opens the closeout screen When they edit quantities, add or remove add-ons, or apply a discount Then the invoice total updates immediately and the pending capture amount reflects the adjusted total Given tipping is enabled When staff prompts for a tip and a tip is entered or selected Then the tip is added as a separate line, included in the captured amount, and shown on the receipt Given staff sends the itemized SMS receipt Then the receipt shows all line items, discounts, taxes, fees, tip, credits applied, and final total
Payment Capture with Fallback Paths
Given a default vaulted card token exists When Visit Complete is confirmed Then the system attempts authorization and capture against the default card and returns a clear success state on approval Given a hard decline or card error (e.g., insufficient funds, do not honor) Then the dashboard presents options to select another saved card, record cash or check, or send an SMS pay link, and allows completion via the chosen path Given an alternate path is taken When payment succeeds Then the visit is marked paid and the same post-payment events and receipt are triggered
Transient Failure Retry with Backoff
Given a transient failure occurs (e.g., gateway 5xx, network timeout) When Visit Complete triggers payment Then the system retries per configurable policy (e.g., max attempts and backoff), without duplicating charges, and surfaces retry status to staff Given all retry attempts fail Then the closeout status is set to "Payment Failed (Retry Exhausted)" with an actionable message and a "Retry Now" control
PCI Tokenization and SCA/3DS Support
Given the platform uses a payment gateway with tokenization Then only payment tokens and last4 or brand are stored; no PANs or sensitive authentication data are stored in the system Given a transaction requires SCA or 3DS When Visit Complete is initiated Then the system prompts to send a secure authentication link via SMS to the customer, resumes capture after successful challenge, and records the 3DS outcome in the audit Given the customer fails or abandons 3DS Then no capture occurs and staff see a clear failure reason with fallback options
Audit Trail and Post-Payment Events
Given a successful capture Then the audit record includes invoice ID, authorization ID, capture or charge ID, last4, brand, amount, currency, timestamps, idempotency key, staff user, and gateway response codes Given payment succeeds Then the system dispatches an itemized SMS receipt, updates the ledger and package balances, and emits events for downstream integrations exactly once Given a failure Then no ledger updates or receipt dispatch occur, and the audit contains failure codes and error messages
Package Credit Application Engine
"As a walker, I want package credits applied before charging so that clients use their prepaid visits automatically and see clear usage on their receipts."
Description

Before attempting any card charge, the system evaluates the client’s package balances and applies eligible credits to covered services on the invoice using configurable rules (e.g., match by service type, pet, and location; oldest-expiring first; max uses per visit). Partial coverage is supported, with any remainder flowing into the cash charge. The engine updates the package ledger atomically with the closeout, prevents over-application in concurrent sessions, and reflects credit consumption clearly on the invoice and receipt. Administrators can configure tax treatment of package usage, exclusion lists, and define how add-ons interact with credits. All applications are auditable with before/after balances and reason codes for manual adjustments.

Acceptance Criteria
Apply Eligible Credits Before Card Charge (Oldest-Expiring First, Partial Coverage)
Given a client has multiple active packages with varying expiration dates and eligibility scopes (service type, pet, location) and a vaulted card on file And an invoice contains a mix of eligible and ineligible line items And package rules require matching by service type, pet, and location and prioritize oldest-expiring first When the visit is closed out Then credits are applied only to eligible line items that match service type, pet, and location And credits are drawn from the oldest-expiring eligible package first; expired or ineligible packages are skipped And if a line item is only partially covered, the uncovered remainder stays on the invoice for cash charge And the net card charge equals invoice total minus credits applied (subject to tax configuration) And the package ledger decrements by the exact number of credits applied per package
Enforce Max Uses Per Visit Across Line Items and Pets
Given a package defines a max uses per visit limit of N And an invoice contains more than N eligible uses across one or more line items and pets When the visit is closed out Then no more than N total credits from that package are applied across the invoice And remaining eligible units beyond N are billed to cash And the invoice clearly indicates which units were limited by the max-per-visit rule
Atomic Ledger Update and Concurrency Control on Closeout
Given two users or processes attempt to close out the same visit concurrently And the client has K remaining eligible credits across applicable packages When both closeout operations run Then at most K credits in total are applied across both attempts And exactly one closeout succeeds with payment capture and ledger decrement committed atomically And the other attempt fails with a concurrency conflict error and no partial application or payment occurs And the final invoice, ledger balances, and payment state are consistent and reflect a single successful closeout
Clear Invoice and SMS Receipt Itemization of Credit Consumption
Given credits have been applied to one or more invoice line items When the invoice is finalized and an SMS receipt is sent Then each credited line item shows quantity credited, unit value or service reference, package name/ID, and extended credit value And the receipt displays before and after package balances per package used And partially covered line items show both the credited portion and the cash remainder And totals include: subtotal, tax, credits subtotal, tip, final amount charged to card
Configurable Tax Treatment for Package Usage (Exclude vs Include Tax)
Given an invoice contains taxable line items that will be covered fully or partially by package credits When tax mode is set to Exclude Tax From Credits Then tax is calculated on the full taxable price of the covered items and is charged to the card (credits do not offset tax) When tax mode is set to Include Tax In Credits Then tax is calculated only on the cash-payable portion of taxable items; the credit-covered portion incurs no additional tax charge And the invoice and receipt reflect tax amounts consistent with the selected mode
Exclusion Lists and Add-On Interaction with Credits
Given administrators have configured an exclusion list of services/add-ons and a setting controlling whether add-ons consume credits And an invoice contains excluded services, included services, and add-ons When the visit is closed out Then excluded items receive zero credits regardless of package availability And add-ons consume credits only when the setting is enabled; otherwise they are billed to cash And the invoice/receipt labels excluded and add-on handling accordingly
Audit Trail and Reason Codes for Auto and Manual Adjustments
Given credits are applied automatically during closeout or a staff member performs a manual override to add/remove credits When the closeout is completed or the adjustment is saved Then an audit entry is recorded with timestamp, actor, invoice ID, package ID, before and after balances, quantity delta, and reason code (manual adjustments require a non-empty reason) And audit entries are immutable and retrievable via the admin audit log with filtering by date, actor, invoice, and package And the invoice/receipt reflects any manual adjustments with an indicator of adjustment reason
Smart Prefill of Add‑ons, Discounts, and Taxes
"As staff, I want add‑ons, discounts, and taxes to prefill automatically so that I don’t have to rebuild the invoice at pickup."
Description

Line items are prefilled at closeout based on the booking details, Pet Profile Smart Card preferences (e.g., known add-ons, size-based pricing), staff-entered service outcomes, and configured business rules. Applicable taxes and surcharges are auto-calculated using location tax profiles and product tax codes, while discounts and no‑show/late‑cancel fees are auto-applied per policy. Rounding rules are consistent across UI and ledger. Prefill produces an editable cart, minimizing manual entry errors and speeding checkout while ensuring compliance with tax rules and promotions.

Acceptance Criteria
Profile-driven add-ons and size-based pricing prefill at closeout
Given a booking for a base service and a Pet Profile with size and preferred add-ons configured, When staff taps Visit Complete, Then the cart preloads the base service price for the pet’s size and adds each preferred add-on at the configured price. And Then each prefilled line item is labeled Auto with its source (Profile/Rule/Outcome) visible in the UI and audit log. And Then the subtotal equals the sum of prefilled base and add-ons without requiring manual entry. And Then staff can edit or remove any prefilled line item prior to charge, with recalculation of dependent amounts. And Then the effective business rule version at service start time is used for prices.
Outcome-driven add-ons prefill and update
Given outcomes are mapped to add-ons in business rules, When the staff records an outcome that is mapped to an add-on (e.g., Nail Grind = Performed), Then the corresponding add-on is added to the cart at the configured price. And When the staff changes the outcome to Not Performed before charge, Then the add-on is removed and totals recalculate. And Then each add-on added or removed via outcomes is tagged with source Outcome in audit history. And Then conflicting outcomes (e.g., mutually exclusive add-ons) resolve according to rule priority with a single correct line item remaining.
Location/product-code tax and surcharge auto-calculation
Given each product/add-on has a tax code and the booking has a location with a tax profile, When Visit Complete is initiated, Then tax and surcharge lines are calculated using the location’s tax profile and the product tax codes. And Then tax is computed on the taxable base after package credits and discounts, and excludes tips. And Then tax-exempt items produce $0.00 tax with the correct exemption reason code. And Then the configured rounding method (line-level or invoice-level) is applied to taxes to two decimals. And Then UI totals, itemized receipt, and ledger entries show identical tax amounts and components.
Discount and promotion application with exclusions and stack rules
Given active promotions, coupons, and client/loyalty discounts are configured, When Visit Complete occurs, Then eligible discounts auto-apply according to rule priority and stackability settings. And Then discounts do not apply to non-discountable items (e.g., surcharges, regulated fees) and respect per-item or per-invoice scopes. And Then discount amounts are calculated on the post-package-credit subtotal. And Then only one discount per scope applies when rules prohibit stacking, with deterministic tie-breaking by priority. And Then each discount line displays its code/label and appears in the audit log with rule ID and evaluator timestamp.
Package credits consumption precedence and partial coverage
Given the client has a package with remaining credits matching the booked service, When Visit Complete is triggered, Then the package credits are consumed first before any card charge or discount is applied. And Then the number of credits decremented equals the quantity of eligible services, without exceeding remaining credits. And Then if credits partially cover the service(s), the remaining balance proceeds to pricing, discounts, taxes, and payment. And Then ledger entries clearly record credit consumption with package ID, remaining balance, and valuation method. And Then credits respect restrictions (e.g., pet-specific, service-specific, location-specific) and are skipped when ineligible.
No-show and late-cancel fee auto-application per policy
Given a booking is marked No-Show or Late Cancel with timestamps and policy thresholds configured, When the appointment is closed out under that status, Then the correct fee auto-adds to the cart per policy (flat or percentage). And Then taxability of the fee follows its tax code and location profile. And Then policy caps, grace periods, and member exemptions are honored. And Then staff may waive or reduce the fee only if authorized by role, with a required waiver reason captured in audit. And Then the receipt and ledger explicitly itemize the fee and any waiver/adjustment.
Rounding, totals integrity, and editable cart recalculation
Given prices, package credits, discounts, taxes, and surcharges are present, When any prefilled line item is added, edited, or removed before charge, Then all dependent amounts (discounts, taxes, surcharges, totals) recalculate immediately and correctly. And Then the displayed subtotal, tax, and grand total match the ledger and the itemized SMS receipt to the cent. And Then monetary values use the configured rounding mode consistently across UI, receipt, and ledger. And Then the system prevents negative line totals and prevents totals drifting due to cumulative rounding. And Then edits to protected fields (tax code, discount rule) require proper permission and are logged with before/after values.
Inline Line Item Adjustments with Guardrails
"As a groomer, I want to tweak line items at pickup so that the bill reflects the actual services delivered without breaking policy limits."
Description

During closeout, staff can adjust quantities, prices, add/remove items, and override discounts directly in the cart with real-time totals and tax recalculation. Role-based permissions and policy guardrails (min/max price variance, required reasons for overrides) prevent misuse. All edits are captured in an immutable audit log with who/when/what before-and-after deltas. The UI is optimized for mobile with one-hand use and supports quick-add frequently used items. Concurrency controls prevent conflicting edits when multiple staff view the same visit.

Acceptance Criteria
Real-Time Totals and Taxes Recalculate on Inline Edits
Given a cart with taxable and non-taxable items and configured tax rules When staff changes quantity, unit price, discount, adds, or removes an item Then subtotal, tax, and grand total recalculate accurately and display within 300 ms of the last input And rounding follows the business’s currency and tax rounding rules And the recalculated totals persist after saving and reloading the visit
Role-Based Restrictions on Price and Discount Overrides
Given a staff user without OverridePrice permission When they attempt to edit unit price or discount fields Then the fields are disabled in the UI and API requests return HTTP 403 with an error code And no changes are applied to the cart Given a staff user with OverridePrice permission When they edit unit price or discount within allowed guardrails Then changes are accepted and reflected in totals
Guardrails: Min/Max Variance and Required Override Reason
Given business policy guardrails define allowable price/discount variance When a user proposes a price or discount outside the configured range Then the system blocks the change, shows an inline error explaining limits, and prevents save Given a price or discount override within the allowed range When the user saves the cart Then a reason selection (from list or free text minimum 5 characters) is required; otherwise save is blocked with an inline prompt
Immutable Audit Log of Cart Edits
Given any cart edit (add, remove, quantity, price, discount) When the change is saved Then an audit entry is appended capturing user ID, timestamp (UTC ISO-8601), item identifier, field changed, before and after values, and override reason if applicable And audit entries are append-only and cannot be edited or deleted via UI or API And the visit’s audit log is retrievable via API and visible in the UI in reverse chronological order
Mobile Quick-Add for Frequently Used Items
Given the quick-add panel on a mobile device (viewport width 360–430 px) When the user opens the closeout screen Then at least 6 frequently used items display with name, price, and one-tap add buttons And tapping an item adds it to the cart within 200 ms with default quantity 1 and correct tax category And a search field allows finding and adding any item in 3 taps or fewer
One-Hand Mobile Inline Edit Usability
Given a mobile device in portrait orientation When editing line items Then all primary controls (add/remove item, qty increment/decrement, price, discount, save) are accessible without horizontal scrolling And tap targets are at least 44x44 px, numeric inputs launch a numeric keypad, and the save/complete action is not obscured by the keyboard And the cart view loads in 1 second or less on a median device and remains responsive during edits
Concurrency Control for Simultaneous Closeout Edits
Given two staff open the same visit in the closeout screen When Staff A saves changes Then Staff B’s session is marked stale and receives a non-blocking banner within 1 second Given Staff B attempts to save without refreshing When the system detects a version mismatch Then the save is rejected with HTTP 409 Conflict and guidance to review changes And non-overlapping edits auto-merge on refresh, overlapping edits require explicit selection of which values to keep before save, and all attempts/resolutions are captured in the audit log
Configurable Tip Prompt and Capture
"As a trainer, I want customers to get a quick, branded tip prompt so that we capture more gratuities without slowing pickup."
Description

After the pre-tax/post-discount amount is determined, the system can prompt for a tip using configurable presets (e.g., 15/20/25% and custom) via SMS link or in-person quick buttons, with business-level settings for suggested rates and default behavior. Two capture modes are supported: (1) pre-charge tip collection that pauses auto-capture until the client responds within a short window, or (2) post-charge tip captured as a separate transaction within a configurable adjustment window, with cash tip entry supported for record-keeping. Tips are properly categorized for reporting and included in receipts, and staff never see full card data. Failure or timeout paths are clear and do not block checkout.

Acceptance Criteria
SMS Tip Prompt with Configurable Presets
Given a visit is marked Visit Complete and the pre-tax/post-discount subtotal is calculated and SMS tip prompting is enabled at the business, When the system sends the tip link to the client, Then the link displays the business-configured preset percentages (e.g., 15%, 20%, 25%) and a Custom option with the calculated amounts shown next to each. Given the business has configured a default suggested rate, When the client opens the SMS tip page, Then that rate is preselected. Given the client selects a preset or enters a custom tip amount, When they confirm, Then the tip amount is validated as a non-negative currency value and returned to Auto‑Closeout for inclusion according to the active capture mode.
In‑Person Quick Tip Buttons at Checkout
Given a staff member taps Visit Complete and in‑person tip prompting is enabled at the business, When the tip screen is shown, Then quick buttons for the configured preset percentages and a Custom amount entry are displayed with computed amounts based on the pre-tax/post-discount subtotal. Given the staff selects a preset or enters a custom amount on behalf of the client, When the amount is confirmed, Then the tip is applied according to the active capture mode and the UI reflects the updated total before finalizing.
Pre‑Charge Tip Collection Pauses Auto‑Capture
Given pre‑charge tip mode is enabled and a timeout window is configured, When Visit Complete is initiated, Then auto‑capture is paused and the tip prompt is sent/presented immediately via the configured channel (SMS or in‑person). Given the client submits a tip within the timeout window, When the payment is captured, Then a single card transaction is created for the visit total plus the tip amount, a confirmation SMS receipt is sent, and the visit is closed. Given the client does not respond or declines within the timeout, When the timeout elapses, Then the system captures the base charge without a tip, logs a Tip: None (Timed Out or Declined) outcome, sends the standard receipt, and the visit is closed.
Post‑Charge Tip Capture Within Adjustment Window
Given post‑charge tip mode is enabled and an adjustment window is configured, When Visit Complete is initiated, Then the base charge is immediately captured on the vaulted card and the tip prompt is sent to the client. Given the client submits a tip within the adjustment window, When the tip is processed, Then a separate card transaction is created for the tip amount linked to the visit, an updated itemized SMS receipt is sent, and reporting reflects the tip method as Card (Post‑Charge). Given the adjustment window expires or the client declines, When no tip is collected, Then no further charges are attempted and the visit remains closed with a Tip: None outcome noted.
Cash Tip Entry for Record‑Keeping
Given a visit has been charged or is being finalized, When a staff member records a cash tip amount, Then no card transaction is initiated, the cash tip is associated with the visit, appears on the itemized receipt as Tip (Cash), and is included in tip reporting. Given a cash tip is recorded, When the visit history is viewed, Then an audit entry shows the user, timestamp, and amount recorded.
Tip Categorization, Receipts, and Card Data Privacy
Given any tip is captured (pre‑charge, post‑charge, or cash), When tip and payout reports are generated, Then tips are categorized by method and source and totals are correctly aggregated by date range, staff, and visit. Given a receipt is sent, When the receipt is generated, Then it itemizes the tip line and shows updated totals, and the SMS receipt includes the tip amount. Given staff access checkout or receipt screens, When card details are displayed, Then the full PAN is never shown (masked last 4 only), no CVV is displayed, and PCI‑sensitive data is not logged.
Failure and Timeout Paths Do Not Block Checkout
Given the SMS tip link fails to deliver or the tip page fails to load, When Visit Complete is initiated, Then the base charge proceeds per the active capture mode and a non‑blocking alert is shown to staff with a retry option. Given a tip authorization fails in pre‑charge mode, When payment is attempted, Then the system falls back to capturing the base amount only, logs Tip: Failed, sends the standard receipt, and closes the visit. Given a tip authorization fails in post‑charge mode, When the tip transaction is attempted, Then the base capture remains settled, the tip is not captured, the client is notified via SMS with a retry link within the adjustment window, and staff see a non‑blocking alert.
Itemized SMS Receipt with Smart Links
"As a client, I want an itemized text receipt immediately after checkout so that I can verify charges and keep records on my phone."
Description

On successful payment (or manual completion), the system sends an itemized SMS receipt from the existing conversation thread, including services, add-ons, taxes/fees, package credits consumed, tip (if any), subtotals, total paid, masked card details (brand/last4), and transaction ID. A secure short link provides a hosted invoice view for downloads, card management, and support contact. Messages respect opt-in/opt-out status, local sending windows, and brand configuration (logo, business name). Delivery status is tracked; failures trigger a retry and surface to staff. Receipt content is consistent with ledger entries for auditability.

Acceptance Criteria
Auto-charge Success — Itemized SMS Receipt in Thread
Given a visit is set to Visit Complete and payment auto-charge succeeds using the vaulted card When the receipt is generated Then an SMS is sent from the existing conversation thread within 60 seconds And the SMS body lists: business name, each service with price, add-ons with price, taxes/fees, package credits consumed, tip (if any), subtotals, total paid, masked card brand and last4, transaction ID, and a secure short link to the hosted invoice And the short link is HTTPS and resolves successfully to the hosted invoice
Manual Completion — Receipt Without Payment Capture
Given a visit is manually marked Complete without a payment capture When the receipt is generated Then send an SMS in the existing conversation thread within 60 seconds And the itemization reflects services, add-ons, taxes/fees, and package credits consumed; total paid equals $0.00 if fully covered by credits or equals any collected amount And include a unique transaction/reference ID linked to the ledger entry And the hosted invoice via the link displays a clear payment status (e.g., Paid via Package Credits or No Charge) as applicable
Opt-in Compliance and Local Sending Windows
Given the client is opted in to SMS and local time is within the configured sending window When a payment succeeds or a manual completion occurs Then send the receipt SMS immediately and record a Sent timestamp on the visit timeline Given the client has opted out of SMS When a payment succeeds or a manual completion occurs Then do not send an SMS, record Not sent — client opted out on the visit timeline, and present the hosted invoice link to staff for manual sharing Given local time is outside the configured sending window When a payment succeeds or a manual completion occurs Then queue the receipt and send it at the start of the next sending window, displaying the scheduled send time and Queued status in the visit timeline
Delivery Status Tracking and Retries
Given a receipt SMS send is attempted When the provider reports Delivered Then record Delivered status with timestamp on the visit timeline Given the provider reports Failed or no delivery receipt is received within 60 seconds of send When automatic retry logic runs Then retry up to 3 total attempts with exponential backoff (1 min, 5 min, 15 min), respecting local sending windows and opt-in status Then if all retries fail When final attempt completes Then mark status as Failed, surface a visible alert on the visit and client's thread, and expose a Copy link action for staff
Hosted Invoice — Security and Actions
Given the recipient opens the secure short link within 30 days of issuance When the token is validated Then show a branded hosted invoice including: services, add-ons, taxes/fees, package credits consumed, tip (if any), subtotals, total paid, masked card brand and last4, transaction ID, business name and logo And provide actions: Download PDF, Manage Card (update vaulted card via PCI-compliant form), and Contact Support (tap-to-call or email per brand settings) Given the link token is expired or invalid When the link is opened Then show an Expired/Invalid page with no sensitive data and offer a way to request a fresh link via the business
Ledger Consistency and Auditability
Given a receipt has been sent When comparing receipt amounts to the ledger entries for that visit Then line items, taxes/fees, discounts/credits, tip, and total paid match exactly with at most $0.01 rounding variance And the receipt's transaction/reference ID equals the payment processor transaction ID (for captured payments) or the ledger entry ID (for manual completion) And an immutable, versioned representation of the receipt as sent is stored and retrievable for audit, with timestamp and checksum; any post-send edits create a new version linked to the visit
Branding, Masking, and Tip Display in SMS
Given an SMS receipt is generated When composing the message body Then include the configured business name and exclude images/logos from the SMS body And display the payment method as "Brand •••• last4" only; do not include full PAN, expiry, or tokens And include a Tip line item only if a non-zero tip was collected; otherwise omit the tip line And format currency, dates, and taxes per the business locale settings
Closeout Audit Trail and End‑of‑Day Reconciliation
"As an owner, I want daily closeout reports with a full audit trail so that I can reconcile deposits and understand revenue drivers without spreadsheets."
Description

Every closeout writes a detailed, immutable audit trail (edits, credit applications, authorizations/captures, tip actions, receipt sends) with timestamps and actor attribution. The system generates end‑of‑day reports summarizing gross, discounts, taxes, tips, package credits applied, net captured, failures/pending, and payout-mapped totals, filterable by staff, location, and service type. Exports are available as CSV and via API, aligning with processor payout batches where possible. Failed or pending payments surface in an actionable queue for follow-up. This reduces manual reconciliation time and improves financial accuracy.

Acceptance Criteria
Immutable Closeout Audit Trail
Given a visit is marked Visit Complete and closeout actions occur (authorization, capture, package credit application, line‑item edit, tip selection, receipt send) When the closeout is saved Then an audit event is appended capturing: event type, visit ID, client ID, pet ID(s), actor (user ID or system), timestamp (UTC), before/after values for edited fields, payment instrument brand and last4/token reference, amounts by line item, tax, tip, discounts, package credits consumed, and receipt delivery channel/status And Then the audit record is append‑only, non‑editable, and non‑deletable by any role And Then audit events are retrievable by visit ID and by date range, returning within 2 seconds for 95th percentile up to 200 events And Then timestamps are stored in UTC to the second and displayed in the location’s time zone in the UI
End‑of‑Day Summary Report Generation
Given a per‑location business day boundary is configured When an authorized user requests the EOD report for a specific date with optional filters (staff, location, service type) or the scheduled report is generated Then the report includes totals for: gross, discounts, taxes, tips, package credits applied, refunds, net captured, failures count/amount, and pending count/amount, plus payout‑mapped totals where available And Then totals equal the sum of underlying closeouts within the configured time zone boundaries and applied filters, with rounding to 2 decimals and zero discrepancy tolerance And Then the report generates within 10 seconds for up to 5,000 closeouts and renders within 3 seconds in the UI And Then the report can be exported as CSV and retrieved via API with identical columns, filters, and totals
Processor Payout Alignment
Given captured transactions exist for the report period When processor payout/batch IDs and settlement timestamps are available Then each captured transaction is mapped to a payout/batch ID and settlement date when provided by the processor And Then the EOD report groups and displays payout‑mapped subtotals by payout/batch ID and settlement date And Then the sum of payout‑mapped subtotals equals the net captured for mapped items; unmapped items are listed with reason (pending settlement or processor data unavailable) And Then payout mapping appears in CSV/API exports for both transaction‑level rows and payout summary rows
Failed and Pending Payments Action Queue
Given an authorization or capture fails or remains pending more than 5 minutes after Visit Complete When an authorized user opens the Payments Queue Then the item appears with visit ID, client name, amount, status (failed/pending), reason, attempt count, and last attempt timestamp And Then the user can: retry, choose a different vaulted card, send a payment request SMS link, or mark as write‑off with reason; each action writes an audit event And Then on successful capture the item auto‑resolves and is removed from the queue within 60 seconds, and the EOD report updates pending/failed totals within 2 minutes And Then the queue supports filters by staff, location, and service type and loads first page within 2 seconds
Receipts and Tip Actions Auditability
Given a closeout prompts for tip and an itemized SMS receipt is sent When the client selects a preset or custom tip amount Then the selected tip, calculation basis, actor attribution, and timestamp are recorded as an audit event And Then the receipt send event logs channel (SMS), masked destination, message ID, send status (queued/sent/delivered/failed), and receipt link URL And Then any receipt resend creates a distinct audit event with new message ID and status And Then the final captured amount equals subtotal + tax + tip − discounts − package credits; the equality check is enforced and logged
Access Control and Data Protection for Audit and Reports
Given role‑based permissions are configured When a user without Finance or Manager permissions attempts to view audit logs or EOD reports Then access is denied with an explanatory message and no data leakage And Then users scoped to a location only see audit and report data for their location across UI, API, and CSV exports And Then PII is masked in views/exports (card brand + last4 only; phone masked except last 2 digits; no full tokens) And Then CSV/API exports include generation timestamp, applied filters, and a checksum/hash; downloads are provided via expiring signed URLs valid for 15 minutes

Expiry Nudge

Continuously monitors expiring or failing cards and sends friendly SMS reminders with a one‑tap refresh link. Optional rules require a valid card before confirming new bookings, preventing day‑of payment surprises and keeping routes on time.

Requirements

Real-time Card Status Monitoring
"As an independent groomer, I want the system to automatically flag clients with expiring or failing cards so that I can address issues before appointments and avoid day‑of payment delays."
Description

Implements continuous detection of expiring or failing payment methods for each client, leveraging gateway webhooks (e.g., card update/failure notifications) and scheduled batch checks to identify cards expiring within configurable windows (e.g., 30/14/7 days) or recently declined. Persists a normalized card_on_file_status on the client profile and exposes events to downstream components (nudges, guardrails, analytics). Integrates with FetchFlow’s payments vault to avoid storing PAN data and respects PCI DSS constraints. Ensures data accuracy with idempotent processing, backfill on import, and reconciliation jobs, enabling the platform to proactively prevent day‑of payment surprises and keep provider routes on time.

Acceptance Criteria
Realtime Webhook Updates for Card Status
Given a client has a vaulted payment method token and the payments gateway sends a card_update webhook with a new expiration date When the webhook is received by FetchFlow Then client.card_on_file_status is updated to one of [valid, expiring_30, expiring_14, expiring_7, expired] based on the new expiry relative to today within 60 seconds of receipt Given the payments gateway sends a charge_failed webhook for the client’s token with a failure reason When the webhook is processed Then client.card_on_file_status is set to declined_recently and status_last_changed_at is set to the webhook event timestamp within 60 seconds Given a client is in declined_recently status and the payments gateway sends a successful authorization/charge webhook for the same token When the webhook is processed Then client.card_on_file_status transitions to valid within 60 seconds and status_reason is updated to cleared_by_successful_charge
Configurable Expiry Window Batch Scan
Given an organization has expiry windows configured for 30/14/7 days When the scheduled batch check executes Then clients with tokens expiring in ≤7 days are set to expiring_7, >7 and ≤14 days to expiring_14, >14 and ≤30 days to expiring_30, and ≤0 days to expired Given a client is currently expired or declined_recently When the scheduled batch check executes Then the job does not downgrade the status to an expiring_* state Given the batch job runs When it completes Then it emits metrics for processed_count, updated_count, duration_ms, and error_count and writes a job run record with start/end timestamps
Normalized Status Persistence and API Exposure
Given a client profile is requested via GET /clients/{id} When the response is returned Then it includes card_on_file_status in {valid, expiring_30, expiring_14, expiring_7, expired, declined_recently, no_card, invalid_token}, status_last_changed_at (ISO 8601), and status_reason (enum) and excludes PAN/CVV Given the dashboard client list is filtered by Card Status = Expiring (7 days) When results are shown Then only clients with card_on_file_status = expiring_7 are returned Given an API client attempts to set card_on_file_status via PATCH/PUT When the request is processed Then the request is rejected with 403 and a message indicating the field is system-managed
Idempotent and Ordered Processing of Notifications
Given a gateway webhook with event_id E is delivered multiple times When the handler processes the deliveries Then exactly one status update is persisted and exactly one status_changed event is published (subsequent deliveries are acknowledged with no-op) Given two webhooks for the same token arrive out of order where the older event has event_timestamp < client.status_last_changed_at When processing the older event Then the event is ignored and client.card_on_file_status remains unchanged Given a transient failure occurs while handling a webhook When the job is retried Then the resulting persisted state and emitted event count are identical to a single successful run and duplicate audit entries are not created
Backfill on Import and Periodic Reconciliation
Given clients are imported with existing vault token_ids When the backfill job runs Then for each token the system retrieves card metadata from the vault/gateway and sets client.card_on_file_status appropriately within 24 hours of import Given the daily reconciliation job executes When it compares local status to gateway token validity and expiration Then any mismatches are corrected, a status_changed event is emitted for corrections, and a reconciliation report is stored with counts of corrected and skipped records Given the reconciliation job is re-run for the same time window When records are already consistent Then no additional updates are made and the report reflects zero corrections
Downstream Event Emission for Status Changes
Given client.card_on_file_status changes from any value to another When the change is persisted Then an event card_on_file.status_changed is published with fields {event_id, occurred_at, client_id, org_id, token_id, status_before, status_after, status_reason, last4 (masked), brand, exp_month, exp_year} and without PAN or CVV Given card_on_file.status_changed is published When the Nudge, Guardrail, and Analytics subscribers are healthy Then they receive the event at-least-once within 60 seconds and can idempotently acknowledge using event_id Given an event payload fails schema validation at a subscriber When the subscriber rejects it Then the event remains available for retry with dead-letter routing and is visible in delivery metrics
PCI DSS Compliance and Vault-Only Card Handling
Given application databases and logs When inspected via automated scans and sampling Then no PAN or CVV data is stored or logged; only token_id, last4 (masked), brand, exp_month, and exp_year appear where necessary Given an API response or event payload containing card information When it is serialized Then PAN and CVV are absent and last4 is masked as ****1234 Given gateway request/response bodies are logged by infrastructure When logs are written Then PAN/CVV are redacted by logging middleware and data retention for these logs complies with configured retention policies
Personalized SMS Nudge Engine
"As a dog walker, I want automated, friendly SMS reminders sent to clients with soon‑to‑expire cards so that most clients fix issues themselves without me chasing them."
Description

Delivers friendly, branded SMS reminders to clients with expiring or failing cards, using merge fields (pet name, provider name, appointment date) and localized templates. Respects quiet hours, messaging frequency caps, and TCPA/opt‑out compliance (STOP/HELP). Generates a one‑tap secure refresh link per message and tracks delivery, clicks, and conversions for feedback loops. Supports A/B testing of copy and send times, multi-lingual content, and automatic re-nudging based on status changes from the monitoring service. Designed for an SMS‑first mobile experience to maximize response rates and minimize manual follow‑ups.

Acceptance Criteria
Localized, Personalized Nudge for Expiring Card
Given a client’s payment card is within the configured expiring threshold and the client has at least one upcoming appointment When the monitoring service flags the card as expiring Then the engine generates an SMS using the client’s locale-specific template And the SMS includes correctly populated merge fields for pet name, provider name, and the next appointment date And the SMS includes the provider’s brand name or sender ID And the message is queued for send with associated metadata (client_id, pet_id, appointment_id, template_id, locale, card_status)
Quiet Hours and Frequency Caps Enforcement
Given quiet hours are configured for the client’s local timezone When a nudge becomes eligible during quiet hours Then the send is deferred to the next permissible window Given per-client frequency caps are configured (per 24h and per 7d) When the cap for the period has been reached Then no additional nudges are sent in that period And a suppression event with reason "frequency_cap" is logged
TCPA Compliance and Opt-Out Handling (STOP/HELP)
Given a client replies STOP to any nudge Then the client is opted-out within 60 seconds And a single confirmation SMS is sent acknowledging opt-out and how to rejoin And future nudges are suppressed with reason "opt_out" and logged with timestamp Given a client sends HELP Then an SMS is sent within 60 seconds containing support contact info and opt-out instructions And the client’s subscription status remains unchanged Given a client is opted-out When a nudge would otherwise be sent Then no SMS is sent and a suppression event is recorded
Secure One‑Tap Refresh Link and Conversion Tracking
Given an SMS nudge is generated Then it contains a unique HTTPS link with a signed, single-recipient token And the token expires after the configured TTL or upon successful payment method update And the link resolves to a mobile-first refresh flow without requiring additional navigation Given the carrier returns a delivery receipt Then a delivery event is recorded with message_id, client_id, and timestamp Given the client taps the link Then a click event is recorded with message_id and timestamp Given the client completes a valid card update via the link Then a conversion event is recorded and tied to the originating message_id And subsequent pending nudges for that client’s card status are canceled
A/B Testing of Copy and Send Times
Given an A/B test with defined variants, allocation ratios, and success metrics is active When eligible nudges are created Then clients are randomly assigned to variants according to allocation ratios And the selected variant’s copy and scheduling rules are applied And the variant_id is persisted on the message for analytics Given outcomes accrue (deliveries, clicks, conversions) Then metrics are attributed to the correct variant and exportable for analysis And the test can be ended, after which the winning configuration is applied to new nudges
Multi‑Lingual Content with Fallback
Given a client has a preferred language that is supported When generating a nudge Then the engine uses the localized template for that language And characters are encoded correctly so the received SMS matches the template (including diacritics) Given the client’s preferred language is not supported When generating a nudge Then the engine falls back to the default language template And the selected language code is recorded on the message
Automatic Re‑Nudging Based on Status Changes
Given a client’s card status remains invalid after a sent nudge When the configured re-nudge interval elapses and frequency caps are not exceeded Then a new nudge is scheduled and sent Given the monitoring service reports the card status has changed to valid When there are pending or scheduled nudges for that client Then those nudges are canceled and no further nudges are sent for that issue Given a new failure event is received after a successful update Then the nudge cycle restarts for the new failure with fresh scheduling
Secure One‑Tap Card Update
"As a pet owner, I want a secure one‑tap link to update my card so that it’s fast and I don’t risk my appointment being delayed or canceled."
Description

Provides a mobile‑optimized, pre‑authenticated flow that lets clients refresh their card on file with one tap. Uses short‑lived, signed tokens tied to client identity and message to bypass login while enforcing strict expiry and single‑use. Integrates with the payment gateway to update the vaulted customer payment method, supports Apple Pay/Google Pay and network tokenization, and performs lightweight validation (e.g., $0/$1 auth) with clear success/failure states. On success, emits events to update card_on_file_status, unblocks bookings, and logs audit trails. The page renders brand elements and pet details to build trust and drive completion while maintaining PCI scope boundaries.

Acceptance Criteria
Tokenized SMS Link: Short‑Lived, Single‑Use Pre‑Auth
Given a client receives an Expiry Nudge SMS containing a signed one‑tap link bound to the client and message IDs When the client opens the link within the configured TTL (default 15 minutes) Then a pre‑authenticated session is established for that client without requiring login And the token is immediately marked consumed and cannot be reused And any subsequent attempt with the same token returns 410 Gone with an option to request a new link And attempts after TTL expiry return 401 Unauthorized with a regenerate‑link CTA And tokens with invalid signatures or mismatched IDs are rejected with 401 and a security event is logged And token TTL is configurable between 10 and 30 minutes
Apple Pay One‑Tap Card Refresh (iOS Safari)
Given an iOS device with Apple Pay available and a valid pre‑authenticated session from the SMS link When the client taps "Update with Apple Pay" and authorizes the payment Then the Apple Pay sheet launches within 2 seconds And the gateway updates the vaulted customer payment method using network tokenization And a $0/$1 authorization is performed and voided if required by the gateway And a success state is displayed within 5 seconds including masked brand, last4, and exp And no full PAN or CVV is processed or stored by FetchFlow systems And an audit log is recorded with actor, device, IP, token ID, gateway customer ID, and outcome And on wallet decline or cancel, an actionable error is shown with retry option
Google Pay One‑Tap Card Refresh (Android Chrome)
Given an Android device with Google Pay available and a valid pre‑authenticated session from the SMS link When the client taps "Update with Google Pay" and authorizes the payment Then the Google Pay sheet launches within 2 seconds And the gateway updates the vaulted customer payment method using network tokenization And a $0/$1 authorization is performed and voided if required by the gateway And a success state is displayed within 5 seconds including masked brand, last4, and exp And no full PAN or CVV is processed or stored by FetchFlow systems And on wallet decline or cancel, an actionable error is shown with retry option
Manual Card Entry Fallback with Lightweight Auth
Given a device without a supported wallet or a wallet attempt that fails When the client selects "Enter card manually" on the update page Then PCI‑compliant hosted fields render for card number, expiration, and CVC And submitted data is tokenized directly by the gateway; FetchFlow receives only tokens and masked data And lightweight validation ($0/$1 auth or gateway rules) is performed And on decline, a clear message (e.g., insufficient funds, expired card, do not honor) is shown and up to 3 retries are allowed And on success, the vaulted default payment method is updated and masked details are displayed And inputs meet WCAG 2.1 AA for labels, focus, and contrast
Success Events: card_on_file_status Update and Booking Unblock
Given a successful payment method update via wallet or manual entry When the gateway confirms the vaulted payment method update Then card_on_file_status is set to valid within 5 seconds for the client And pending bookings that require a valid card are unblocked and can be confirmed immediately And an event payment_method.updated is emitted with idempotency keys to prevent duplication And the provider dashboard reflects the new status within 10 seconds without refresh And the client timeline/audit trail records the change with timestamp and actor And on failure, payment_method.update_failed is emitted with error codes and no state change occurs
PCI Boundary, Branding, and Pet Trust Elements
Given the pre‑authenticated update page loads on mobile When the page renders Then the business name, logo, and the pet’s name and photo are displayed above the primary CTA And no PAN, CVV, or wallet tokens are logged or stored by FetchFlow; only gateway tokens and masked metadata persist And cross‑account access is prevented; tokens cannot reveal other clients’ data And Largest Contentful Paint is under 2.5 seconds at P95 on a 4G connection And analytics exclude card data and respect PCI scope boundaries
Booking Confirmation Guardrails
"As a trainer, I want the system to require a valid card before confirming sessions so that I avoid last‑minute payment failures and keep my schedule on track."
Description

Adds configurable rules that require a valid card on file before confirming new bookings. Rules can be enabled globally or per service/provider, with exceptions by client or scenario (e.g., first‑time promo). The scheduler surfaces clear statuses (valid/missing/expiring) and guided actions (send nudge, override with reason, take deposit). Staff can apply time‑boxed overrides with audit logging. If a booking is attempted without a valid card while rules are active, the system blocks confirmation and offers to send the refresh link automatically. Prevents day‑of payment surprises and supports on‑time routes by ensuring readiness at confirmation time.

Acceptance Criteria
Global Guardrail Blocks Confirmation Without Valid Card
Given the organization has enabled "Require valid card to confirm bookings" globally And a client has no card on file or the card on file is expired or has a recent authorization/charge failure When staff attempts to confirm a new booking for that client Then the system blocks confirmation and displays a clear blocking message indicating the missing/invalid payment method And the Confirm action remains disabled until the guardrail is satisfied or an authorized override/deposit is applied And the UI presents actionable options: Send Refresh Link, Override (if the user has permission), and Take Deposit (if configured)
Service/Provider-Level Rule Enforcement
Given the global guardrail is disabled And a service-level or provider-level rule "Require valid card to confirm" is enabled for Service A or Provider X And the client has a missing/invalid card When staff attempts to confirm a booking for Service A with Provider X Then the system blocks confirmation per the rule and offers Send Refresh Link, Override (if permitted), and Take Deposit (if configured) And when staff attempts to confirm a booking for a service/provider without such a rule Then the system allows confirmation without blocking for card status
Client or Scenario Exception Bypasses Guardrail
Given the organization has an active guardrail requiring a valid card And the client has an approved exception on file (e.g., First-time promo) with an optional expiration date When staff attempts to confirm a new booking for that client during the exception window Then the system allows confirmation without requiring a valid card And the booking details display an Exception badge with the exception reason and (if present) the expiration And an audit entry records the exception application and the confirmation event And when the exception expires, subsequent booking confirmations for that client are blocked again unless a valid card is present
Scheduler Displays Card Status and Guided Actions
Given staff views a booking or client in the scheduler When the system evaluates the client’s payment method Then it displays one of: Valid, Missing, Expiring (threshold per org setting), or Failing (last attempt declined) along with relevant metadata (e.g., last4, expiry month/year or last failure code) And each status provides context-appropriate actions: Send Refresh Link (pre-filled SMS with one-tap update link), Take Deposit (if enabled), Override (if user has permission), View Audit And selecting any action records an audit event with user, timestamp, booking/client reference, and action details
Time-Boxed Override With Required Reason and Audit Trail
Given a booking is blocked by the guardrail And the user has Override permissions When the user selects Override Then the system requires an override reason and an expiration timestamp not exceeding the org’s max override duration And upon confirmation, the booking becomes confirmable until the override expires And an audit record captures user, timestamp, booking ID, client ID, reason, expiration, and previous card status And if the override expires before the service start and card is still invalid, the booking returns to a Blocked state And users without permission cannot create overrides
Auto-Send Refresh Link on Blocked Confirmation
Given a booking is blocked due to missing/expiring/failing card When the user clicks Send Refresh Link Then the system sends an SMS with a secure one-tap card update link to the client’s preferred number, respecting org quiet hours and language settings And the system logs message ID, template used, recipient, timestamp, and delivery status And the UI confirms send success or shows an actionable error if sending fails And if a successful card update event is received for the client while the booking is open, the scheduler updates the status to Valid in real time and enables confirmation
Deposit Path Allows Confirmation Without Card on File
Given the organization has enabled "Allow deposit to confirm" with a configured minimum deposit amount or percentage And a booking is blocked due to missing/invalid card When staff selects Take Deposit and successfully collects a payment meeting or exceeding the configured minimum Then the booking status updates to Deposited and becomes confirmable without a card on file And the system records the deposit amount, method, transaction ID, user, timestamp in the audit log And if the deposit is refunded before the service start and the card remains invalid, the booking reverts to Blocked per guardrail rules
Nudge Retry and Escalation
"As a solo groomer, I want automatic retries and optional escalations if clients don’t update their cards so that I spend less time chasing payments and reduce no‑shows."
Description

Implements intelligent follow‑ups when initial nudges are ignored or fail. Schedules retries at optimized intervals, escalates to alternative channels (e.g., email) when available, and creates internal tasks for manual outreach on high‑risk accounts. Detects delivery failures, deduplicates messages across campaigns, and pauses retries upon user action or status resolution. Allows configuration of max attempts and escalation paths per provider. Feeds outcomes back to analytics to continually improve timing and copy.

Acceptance Criteria
Optimized Retry Scheduling for Ignored Nudges
Given a card-expiry nudge is sent and no user action occurs within 6 hours When the default schedule [6h, 24h, 72h] is active and quiet hours are 21:00–08:00 recipient local time Then the next retry is queued at +6h with 2–10% jitter, deferred to the next allowed window if inside quiet hours, and logged with attempt_number=2 And subsequent retries are queued at +24h and +72h after the prior attempt, respecting quiet hours and jitter And no more than the configured max_attempts are sent per recipient and campaign And per-provider configuration overrides the default schedule and is enforced per account
Delivery Failure Detection and Handling
Given the SMS provider returns a hard failure code for an attempt When delivery receipts are processed Then all future SMS retries for that campaign-recipient are cancelled and the escalation path is evaluated within 5 minutes And the attempt is recorded with provider_message_id and failure_code Given the provider returns a soft failure on the first retry When scheduling the next attempt Then one additional SMS retry is permitted after at least 24 hours unless configuration overrides Given no delivery receipt is received within 4 hours When evaluating attempt status Then the attempt is marked unknown and inclusion toward max_attempts follows configuration
Cross-Campaign Deduplication Window
Given multiple active campaigns could send a nudge to the same recipient for payment method issues When an attempt is due within the configured dedup_window (default 24h) Then only one message is delivered per channel and the others are suppressed with suppression_reason=deduplicated And deduplication keys include recipient_id, channel, and intent_type And suppressed attempts do not count against max_attempts and are rescheduled after the window if applicable
Escalation to Email After SMS Non-Response
Given a recipient has a verified email and SMS attempts >= the configured threshold without action When escalation rules are enabled Then an email is sent within 1 hour containing the one-tap refresh link and tracking parameters And the email template used is the configured escalation template for the provider And if the email hard-bounces, the next configured escalation step is evaluated or a manual task is created if none remain
High-Risk Account Manual Outreach Tasks
Given an account meets high-risk criteria (e.g., LTV threshold and 2+ failed/ignored attempts) When the escalation threshold is reached Then a manual outreach task is created with priority High, assigned to the default collections queue, with due_at within 24 hours And the task includes last three communication attempts and contact details And duplicate tasks for the same account and intent are not created within 7 days
Immediate Pause on User Action or Resolution
Given a recipient updates their payment method or the card status becomes valid via webhook When pending retries or escalations exist Then all scheduled attempts are cancelled within 2 minutes and the campaign state is set to Resolved And any open manual outreach tasks are closed as Not Needed with an audit trail entry And future campaign triggers for the same intent are suppressed for 24 hours
Analytics Feedback and A/B Performance Logging
Given any attempt is sent, suppressed, or escalated When recording analytics Then an event is logged with account_id, campaign_id, recipient_id, attempt_number, channel, template_id, scheduled_at, sent_at, delivery_status, and user_action_at (if any) And if A/B templates are configured with a 50/50 split, assignment is random and sticky per recipient within the campaign And aggregate metrics (send rate, delivery rate, action rate, time-to-action) are available per channel and template within 15 minutes
Nudge Insights Dashboard
"As a business owner, I want visibility into nudge performance and the impact on payment failures so that I can tune rules and messages to improve cash flow and on‑time routes."
Description

Provides a dashboard and exports that summarize expiring/failing card counts, nudge send/click/update conversion rates, time‑to‑update, blocked vs. confirmed bookings due to guardrails, and reduction in day‑of payment failures. Offers filters by date range, location/provider, service type, and client segment, with drill‑downs to client timelines and audit logs. Surfaces ROI estimates (time saved, cash flow impact) and highlights at‑risk upcoming appointments to prompt proactive action. Integrates with existing FetchFlow reporting and respects role‑based access.

Acceptance Criteria
KPI Summary Accuracy & Coverage
Given a selected date range and no filters applied, When the dashboard loads, Then it displays counts for expiring cards, failing cards, nudges sent, unique link clicks, and unique payment updates that fall within the date range. Given the displayed KPI tiles, When conversion rates are calculated, Then click-through rate = unique clients who clicked / unique clients nudged, update rate = unique clients who updated / unique clients nudged, and link-to-update rate = unique clients who updated / unique clients who clicked. Given the displayed KPI tiles, When time-to-update is calculated, Then it shows the median hours between the first nudge sent in the range and the successful payment update for each updated client in the range. Given guardrail-related KPIs, When counts are computed, Then “Blocked bookings” equals booking attempts prevented due to invalid/expiring cards within the range, and “Confirmed after card update” equals bookings confirmed within the range after a successful update by the same client. Given reduction metrics for day-of payment failures, When the current period is compared to an equivalent prior period, Then the dashboard shows absolute count change and percentage change, or N/A if a prior period is unavailable. Given any KPI value, When a user hovers or taps its info icon, Then a tooltip reveals its definition, denominator, and timezone used.
Filters & Segmentation Behavior
Given default view, When the dashboard first loads, Then the date range defaults to Last 30 Days and all filters are set to All. Given filters for date range, location/provider, service type, and client segment, When a user applies any combination, Then all widgets, charts, lists, and metrics update to reflect the intersection of selections. Given applied filters, When a user refreshes the page or returns within the same session, Then the last-used filters persist. Given a filter producing no results, When the dashboard renders, Then it shows a zero-state with no data messages and offers a Reset Filters action. Given filter changes, When applied, Then 95th percentile render time for summary KPIs is under 2 seconds for up to 100k events in range.
Drill-down to Client Timeline & Audit Logs
Given a KPI, chart segment, or table count, When the user clicks it, Then a drill-down list opens showing the underlying clients or events that compose that count, scoped to current filters. Given a selected client in the drill-down, When the user opens the client timeline, Then it displays nudge sends, link clicks, payment updates, booking blocks, and confirmations with timestamps and identifiers in chronological order. Given the client timeline, When the user opens Audit Log, Then it shows who performed relevant changes (e.g., guardrail setting updates, manual nudges) with time, actor, and before/after values. Given RBAC constraints, When a user lacks permission to view a client or audit entries, Then the drill-down and timeline omit restricted items and show an access-limited message without revealing hidden details.
CSV Exports Respect Filters & Timezone
Given the user has export permission, When Export is clicked, Then the user can download two files: a KPI Summary CSV (aggregated by day) and an Events CSV (row-per-event). Given active filters, When a CSV is generated, Then its contents reflect the same date range, location/provider, service type, and client segment filters. Given the Events CSV, When inspected, Then each row includes at minimum: event_id, event_type (nudge_sent, link_clicked, payment_updated, booking_blocked, booking_confirmed), client_id, client_name, phone_last4, location_id, provider_id (if applicable), service_type, booking_id (if applicable), event_timestamp_iso8601, timezone. Given a generated CSV, When opened, Then timestamps are in ISO 8601 with the dashboard’s selected timezone and numeric fields use a period decimal separator. Given row counts shown in the UI drill-down, When compared to the Events CSV under identical filters, Then the counts match within ±0 rows.
ROI Estimates Calculation & Display
Given ROI widgets enabled, When assumptions use their defaults, Then the dashboard shows Estimated Time Saved (minutes) and Estimated Cash Flow Impact (currency) for the selected date range. Given adjustable assumptions, When the user changes Average minutes avoided per update and Average invoice value, Then ROI values recompute immediately and match the formulas. Given the ROI formulas, When computed, Then Estimated Time Saved = (number of successful payment updates attributable to nudges) × (minutes avoided per update), and Estimated Cash Flow Impact = (successful updates that prevented day-of failure) × (average invoice value) × (collection probability uplift assumption). Given ROI details, When the user opens the tooltip, Then the formulas and the current assumption values are displayed. Given rounding rules, When values render, Then minutes are rounded to the nearest whole minute and currency to two decimal places in the account’s currency.
At-Risk Upcoming Appointments Highlighting & Action
Given upcoming appointments within the next 7 days by default, When a client has no valid card on file or a card that expires before the appointment start, Then the appointment appears in the At-Risk list. Given the At-Risk list, When the user applies filters or changes the lookahead window, Then the list updates accordingly and remains sorted by appointment start time ascending. Given one or more selected at-risk rows, When the user clicks Send Nudge, Then an SMS payment update nudge is sent and the action is logged with status feedback; rows update to reflect the new nudge state. Given an at-risk appointment that becomes safe after a payment update, When the dashboard refreshes, Then the appointment is removed from the At-Risk list.
Access Control & Reporting Integration
Given role-based access control, When an Owner/Admin logs in, Then they can view org-wide data and use Exports. Given role-based access control, When a Manager logs in, Then they see only assigned locations/providers and can export if their role includes export permission. Given role-based access control, When a Staff/Provider logs in, Then they see only their own clients’ data and cannot export; the Export control is hidden. Given a direct URL to a restricted view, When an unauthorized user attempts access, Then the system returns 403 and does not disclose whether the resource exists. Given FetchFlow Reporting, When navigating from Reporting > Payments > Expiry Nudge Insights, Then the dashboard opens within the reporting shell with correct breadcrumbs and back navigation to Reporting home.

Shared Wallet

Support multiple payers and cards per household, rescue, or property. Set a default per pet or service, choose the payer via quick reply, and fall back to a secondary card on decline—ideal for coordinators, foster networks, and split‑responsibility families.

Requirements

Household Payer Management
"As a coordinator managing multiple pets, I want to add and manage several payers under one account so that I can route charges and messages to the right person without juggling separate profiles."
Description

Enable creation and management of multiple payer profiles within a single account (household, rescue, or property), each with contact details, SMS-enabled phone, and one or more tokenized payment methods. Provide dashboard and mobile UI to add/remove payers, define relationships (owner, coordinator, foster, property manager), assign payers to pets, and toggle active status. Support an invite flow via SMS to securely collect card details and consent without exposing sensitive data to the business. Deduplicate by phone/email, normalize country codes, and store SMS opt-in state. Expose internal APIs to read/write payer data for booking, billing, and reporting. Enforce PCI-compliant storage through gateway tokens and maintain an audit trail of changes. This foundation enables Shared Wallet to route charges and communications to the right person reliably.

Acceptance Criteria
Dashboard Create Payer with Multiple Tokenized Cards
Given a groomer is on the household dashboard, when they add a payer with name, email, and an SMS-enabled phone, then the payer record is created and the phone is validated as SMS-capable. Given the groomer adds two cards via the gateway-hosted form, when the payer is saved, then only gateway tokens plus last4/brand/exp are stored and the first card is marked default. Given a required field is missing or invalid (e.g., phone), when save is attempted, then inline validation prevents creation and identifies the exact field(s). Given an existing payer profile, when contact fields are edited or cards are added/removed, then changes persist and reflect in UI and API responses within 2 seconds.
SMS Invite Flow for Secure Card Collection and Consent
Given a groomer selects Invite Payer via SMS and enters a phone number, when send is clicked, then the payer receives an SMS containing a unique, single-use link that expires in 24 hours. Given the payer opens the link, when they enter card details and check explicit consent for storing the card and receiving SMS, then a gateway token is created, SMS opt-in state is recorded with timestamp and source=invite, and no PAN/CVV is exposed to the business. Given the invite link is expired or already used, when accessed, then the page shows an expiration message and provides a Request new link option. Given the payer declines SMS consent, when submission completes, then the payer is created/updated without opt-in and outbound SMS to that number is blocked by the system.
Deduplicate Payers by Phone/Email with E.164 Normalization
Given a user enters a phone in any local format, when saving a payer, then the phone is normalized to E.164 using the selected country or business default. Given a payer exists with the same normalized phone or same email (case-insensitive), when attempting to create a new payer, then the system blocks creation and offers to open the existing record. Given two duplicate payers are selected in the merge tool, when merge is confirmed, then contact info is reconciled (most recent wins), payment methods and pet links are consolidated, and a merge audit entry is created.
Assign Roles and Link Payers to Pets with Defaults per Pet/Service
Given a payer profile is open, when a relationship is assigned, then allowed values are owner, coordinator, foster, or property manager and the role is saved and returned via API. Given a pet has multiple linked payers, when setting defaults, then a default payer can be selected per pet and per service type and is used by booking/billing unless overridden. Given a pet’s payer links are managed, when a payer is unlinked, then they are removed from that pet’s eligible list while remaining available for other pets/households.
Active Status Toggle and Workflow Filtering
Given a payer is marked inactive, when viewing payer pickers in booking, billing, or reporting, then the inactive payer is excluded by default and shown as Inactive on their profile. Given a payer is inactive, when a user attempts to charge or message them, then the action is blocked with an explanation and a link to reactivate. Given a payer is reactivated, when status is toggled back to active, then all prior pet links and default settings are immediately available again.
Internal APIs for Payer Read/Write with Auth and Validation
Given an authenticated internal client with read scope, when calling GET /internal/payers/{id}, then the response includes id, name, email, normalized_phone, sms_opt_in, roles, active, linked_pets, and payment_methods as token references only. Given a client with write scope, when calling POST/PUT to create/update a payer, then required fields, dedupe rules, and role enums are enforced and a 201/200 with the resource representation is returned. Given an unauthenticated or unauthorized caller, when any payer endpoint is called, then a 401/403 is returned and no sensitive data is leaked. Given request volume exceeds 100 requests per minute per client, when additional requests arrive, then the API returns 429 with a Retry-After header.
Audit Trail and PCI-Compliant Storage
Given any create/update/delete on a payer or payment method, when the operation completes, then an immutable audit record is written with actor, timestamp, action, and before/after values excluding sensitive fields. Given a payment method is stored, when inspecting storage and logs, then no PAN or CVV is present anywhere; only gateway token, brand, last4, and exp month/year are retained. Given a data export or report is generated, when it includes payer/payment fields, then sensitive fields are masked or excluded and access to the export is logged with actor and timestamp.
Per-Pet and Per-Service Default Payer
"As a groomer, I want a default payer per pet and service to auto-select at booking so that checkout is faster and charges go to the right person without manual steps."
Description

Allow configuration of default payer and default card at the pet level and per service type, with clear precedence rules: explicit booking selection overrides per-service default, which overrides per-pet default, which overrides account default. Provide UI in the dashboard and Pet Profile Smart Cards to set and view defaults, including a visible indicator in job details. On booking creation, reminders, and checkout, automatically select the default payer/card and expose a one-tap override. Persist the selected payer on the job for downstream billing, packages, and fees. Handle edge cases where a default payer is inactive or has no valid card by cascading to the next rule or prompting selection.

Acceptance Criteria
Set and View Defaults in Dashboard and Pet Profile
Given I am on the Pet Profile Smart Card for Pet X with Shared Wallet enabled When I open Payment Defaults Then I can select a default payer from the wallet and a default card for that payer and save successfully And the selected payer name and masked card last4 are displayed on the card Given Pet X and Service Type Y When I open Service Defaults Then I can set a per-service default payer and card distinct from the per-pet default and save successfully Given a booking for Pet X and Service Type Y When I view Job Details Then a payer/card indicator is visible
Precedence Resolution on Booking Creation
Given account default payer/card = A And per-pet default for Pet X = B And per-service default for Pet X, Service Y = C And an explicit selection D is made during booking When I create a booking for Pet X with Service Y Then the selected payer/card is D Given account default A, per-pet default B, per-service default C, and no explicit selection When I create a booking for Pet X with Service Y Then the selected payer/card is C Given account default A, per-pet default B, no per-service default, and no explicit selection When I create a booking for Pet X with Service Y Then the selected payer/card is B Given account default A, no per-pet default, no per-service default, and no explicit selection When I create a booking for Pet X with Service Y Then the selected payer/card is A
Automatic Selection and One-Tap Override Across Flows
Given booking creation, reminder, and checkout screens When each screen loads for a job for Pet X and Service Y Then the payer/card is auto-selected according to precedence rules Given a job with an auto-selected payer/card When I tap the one-tap override control Then I can switch to any eligible payer and card in one action And the job's selected payer/card is updated immediately Given I override the payer/card during booking When I proceed to reminders and checkout for the same job Then the overridden payer/card remains selected
Inactive or Missing Default Payer/Card Handling
Given the per-service default payer for Pet X, Service Y is inactive or removed When I create a booking for Pet X with Service Y Then the system cascades to the per-pet default Or to the account default if per-pet default is unavailable Or prompts me to select a payer/card before I can save Given the resolved default payer has no valid card on file When the selection occurs Then the system cascades to the next available payer with a valid card Or prompts me to select/add a card Given all candidate payers lack a valid card When I attempt checkout Then the payment is blocked And I am prompted to select or add a valid card
Fallback to Secondary Card on Payment Decline
Given a selected payer with a primary card C1 and a secondary card C2 When a charge at checkout is declined on C1 due to processor error or insufficient funds Then the system retries the same charge one time on C2 automatically And records that a fallback occurred on the job timeline Given the retry on C2 succeeds Then the job is marked paid using C2 And the receipt reflects C2 Given both C1 and C2 decline Then I am prompted to choose another card or payer And no charge is captured
Persisted Payer Drives Billing, Packages, and Fees
Given a job with selected payer P and card C When package redemptions, no-show fees, and invoices are applied Then they are applied to payer P and charged to card C Given defaults are changed after the job is created When I view or update the job Then the selected payer/card on the job remains unchanged unless I explicitly override it Given I run end-of-day billing for multiple jobs When charges are processed Then each job uses its own persisted payer/card selection
SMS Quick-Reply Payer Selection
"As a rescue coordinator on the go, I want to choose who pays via a quick SMS reply so that I can finalize bookings without logging into the dashboard."
Description

Enable payer selection and switching via SMS quick replies within booking, reminder, and checkout threads. Present numbered options using payer display names or initials to protect privacy, and accept short numeric replies or explicit commands to select the payer. Scope reply tokens to the conversation and time window to prevent spoofing, and restrict selection to authorized phone numbers. Confirm the selection via SMS and update the associated job and invoice context. Provide graceful fallbacks to defaults when no selection is made. Log selections for auditability and expose the final payer in the dashboard in real time.

Acceptance Criteria
Numbered Payer Options in SMS Threads
Given a booking, reminder, or checkout SMS thread with multiple eligible payers When the system prompts the client to choose a payer Then it sends a single SMS listing 2–9 numbered options (1..N) using payer display initials or masked names, never full legal names or card details And marks the current default payer with “(default)” And instructs the client to reply with a number 1..N or an explicit command (e.g., "PAYER 2") And the selection options are uniquely bound to this thread for validation
Numeric Quick-Reply Selection (Authorized Sender, Valid Window)
Given payer options were sent for a specific job/invoice and the sender’s phone is authorized for the household/rescue/property And the reply is received within 30 minutes of the options message When the sender replies with a single digit N corresponding to a listed option Then the system validates the thread-scoped reply And sets the selected payer on the job and associated invoice And sends an SMS confirmation acknowledging the selection without exposing full names or card details And the dashboard reflects the selected payer within 5 seconds
Explicit Command Selection Variants
Given payer options were sent and are still valid within 30 minutes When the sender replies with one of the accepted command formats: "PAYER N", "PAY N", or "SET PAYER N" (case-insensitive, optional spaces/colon) Then the system parses the command, validates the thread-scoped reply, and applies the payer selection to the job and invoice And sends an SMS confirmation of the selection And updates the dashboard within 5 seconds
Scoped Token and Anti-Spoofing Validation
Given an incoming payer selection reply When the reply does not match a valid, unexpired selection scope for the specific SMS thread (e.g., expired after 30 minutes or mismatched thread) Then the system rejects the selection with a non-disclosing error SMS And makes no changes to the job or invoice And logs the attempt with timestamp, sender phone (hashed), thread identifier, and reason (invalid/expired scope)
Authorization Enforcement for Payer Selection
Given an incoming payer selection reply from an unauthorized phone number When the sender attempts to select a payer Then the system denies the request with a generic SMS that reveals no payer identities And makes no changes to the job or invoice And logs the attempt with timestamp, sender phone (hashed), and reason (unauthorized)
Fallback to Default and Secondary Card Handling
Given payer options were sent but no valid selection is received within 30 minutes or before the checkout trigger When the booking or checkout proceeds Then the system uses the configured default payer for the pet/service And sends an informational SMS noting the default was applied And if the default payer’s primary card declines at checkout, the system automatically retries with the configured secondary card once and records both attempts And the dashboard shows which fallback was applied
Audit Logging and Real-Time Dashboard Exposure
Given any successful payer selection via numeric reply or explicit command Then the system writes an immutable audit entry with timestamp, job/invoice ID, thread type, previous payer, new payer, sender phone (hashed), and outcome And the final selected payer is displayed in the dashboard’s job/invoice panel in real time with a "Selected via SMS" indicator And audit entries are retrievable via admin API filtered by date range and pet
Automatic Payment Fallback on Decline
"As a walker handling contactless checkout, I want failed charges to automatically try a backup card so that I can complete payment without chasing the client."
Description

When a payment attempt fails for the selected payer, automatically retry using the next available card according to a configurable priority order per payer or household wallet. Define retry strategy (immediate next card, limited scheduled retries), ensure idempotency, and prevent double-charging. Notify the groomer and designated payer via SMS of the fallback and outcome, with a link to update cards if needed. Respect payment method constraints (e.g., ACH vs card) and merchant settings for maximum attempts. Update invoice status, attach failure and retry events to the job timeline, and emit webhook events for external reconciliation.

Acceptance Criteria
Immediate Fallback to Next Card on Decline
Given an open invoice with selected payer P whose wallet has a prioritized list of eligible cards [C1, C2, C3] And merchant settings allow immediate fallback When a capture attempt on C1 is declined by the processor Then the system initiates a capture on the next eligible card (C2) within 15 seconds And if C2 succeeds, the system stops further retries in this cycle And the system never retries C1 again within the same fallback cycle And the payment record reflects fallback_used=true and attempt_count=2 And only one successful capture exists for the invoice amount
Scheduled Retry Strategy with Merchant Attempt Limits
Given merchant retry settings max_attempts=4 and retry_delays=[15m, 2h, 24h] And a payment attempt has failed across all immediate eligible methods When the failure reason is in the soft_decline list Then the system schedules up to the remaining attempts using the configured delays without exceeding max_attempts And no retries are scheduled for hard_decline reasons in the do_not_retry list And scheduled retries are canceled if a successful payment posts or the invoice is canceled And each scheduled retry records next_retry_at and the method priority it will use
Idempotency and Double-Charge Prevention Across Retries
Given an invoice payment attempt group with a stable idempotency_key derived from invoice_id + attempt_window When a gateway timeout or ambiguous response occurs and the system retries Then downstream capture requests reuse the same idempotency_key per method And if any method later confirms a successful capture, subsequent retries are suppressed across all methods for that invoice And at most one successful transaction exists for the invoice amount across all methods And duplicate gateway responses are recorded as idempotent_replay without creating new charges
SMS Notifications with Update Link for Fallback Outcome
Given a decline on the selected payer’s method and an automatic fallback is initiated When the fallback attempt starts and when it completes (success or failure) Then the designated payer receives SMS messages that include business name, amount, pet name(s), last4 + brand of original and fallback method, outcome, and a secure link to update cards And the groomer/operator receives an SMS summarizing the outcome and next steps And messages are delivered within 60 seconds of each event and respect SMS opt-out settings And links are deep links with single-use tokens that expire in 24 hours And no full PAN or sensitive data is included And delivery status (sent, delivered, failed) is recorded per message
Respect Payment Method Constraints and Eligibility
Given the wallet contains a mix of cards and ACH with method- and merchant-level constraints When the original attempt fails Then fallback considers only eligible methods: not expired/disabled, meets merchant rules (e.g., immediate-authorization services require card), excludes ACH pending verification, and respects household sharing settings And the system does not switch to a payer-owned method that is not share-enabled in the household wallet And ACH methods with pending debits or R0x return codes are not retried within the fallback window And if no eligible methods remain, the system stops and prompts payer via SMS link to update methods, recording the constraint reason
Invoice, Timeline, and Webhook Event Updates for Failures and Retries
Given any payment failure, retry attempt, or success during fallback When the system processes each event Then invoice status transitions are applied: Pending -> Paid on success; Pending -> Past Due when max attempts exhausted; Pending during scheduled retries And the job timeline records an entry per attempt with timestamp, amount, currency, method type, last4, attempt_index, decline_code/reason (if any), and next_retry_at (if scheduled) And webhooks are emitted: payment.retry.scheduled, payment.retry.attempted, payment.failed, payment.succeeded with signed payload containing attempt_id, invoice_id, job_id, payer_id, wallet_id, method_id, amount, currency, outcome, idempotency_key, and created_at And webhooks are delivered at-least-once with deterministic ordering via created_at and retried on failure with exponential backoff
Split Payment Allocation
"As a family sharing pet care costs, I want to split a service charge between two payers so that we each pay our agreed share automatically."
Description

Support splitting a single job’s charges across multiple payers by percentage or fixed amounts, with validation that totals equal the charge amount after taxes and discounts. Allow setting a split at booking or checkout, or saving a default split per pet or service. Ensure each payer receives an SMS invoice for their portion and that partial authorizations are captured independently. Define rounding rules, handle partial failures by continuing collection from other payers and prompting resolution for the remainder, and reflect allocations in receipts. Integrate with credits and packages by applying them before charging cash and clearly indicating consumption per payer.

Acceptance Criteria
Split by Percentage and Fixed Amounts with Validation and Rounding
Given a job total after taxes and discounts exists When the user selects a percentage-based split across N payers Then the system calculates per-payer amounts to 2 decimal places using round half up and assigns any 1-cent remainder to the last edited payer, and the sum equals the job total Given the user selects a fixed-amount split When the entered amounts do not sum to the job total Then the system displays an error "Totals must equal $X.XX; $Y.YY remaining" and disables Save until resolved Given an existing split When the user switches between percentage and fixed modes Then allocations convert accurately, remain within a 1-cent tolerance, and continue to equal the job total Given a saved split When the job total changes due to taxes or discounts Then percentage splits auto-scale to preserve ratios, fixed splits require explicit user confirmation to re-balance, and Save is blocked until confirmation
Set and Edit Split at Booking, Checkout, or via Default per Pet/Service
Given a new booking is being created When the user opens Payments > Split Then they can add multiple payers from the Shared Wallet, set percentages or amounts, and Save the split Given a default split exists for the pet or service When a new job is created for that pet/service Then the default split auto-applies and can be overridden prior to Save Given a job is at checkout with a saved split When the user edits allocations before capture Then changes are allowed, validations re-run, and an audit entry records who changed what and when Given only one payer exists in the Shared Wallet When the user attempts to enable split payments Then the system prompts to add additional payers and prevents enabling split until at least two payers are present
Independent Authorization and Fallback per Payer with Partial Failure Handling
Given a job with a saved split is being charged When payment is captured Then the system creates separate authorizations/charges per payer equal to their allocations and records unique transaction IDs Given a payer's primary card is declined When a secondary card exists for that payer Then the system automatically retries once on the secondary card within the same checkout and records the retry result Given at least one payer charge fails after all fallbacks When other payers' charges succeed Then the successful payments complete, the job status is Partially Paid with the exact outstanding balance, and the failing payer and coordinator receive an SMS with a secure pay link for the remainder Given a job is Partially Paid due to a failed payer When the failing payer completes payment via the SMS link Then the job status updates to Paid without recharging other payers, and all records reflect the completion
SMS Invoicing per Payer with Accurate Allocation Details
Given a split allocation has been saved When invoices are issued at checkout or upon booking with payment requested Then each payer receives an SMS including pet name, service, date/time, allocated amount due, applied credits/packages, due date, and a secure payment link Given a payer completes payment When the transaction succeeds Then the payer receives an SMS receipt and the coordinator receives a summary SMS indicating which payer paid, amount, and timestamp Given an unpaid balance remains past the due date When reminder automations run Then only the payers with outstanding balances receive reminder SMS messages for their remaining amounts
Integration with Credits and Packages Prior to Cash Charges
Given a payer has available credits or applicable package units When the split allocation is applied Then credits/packages are consumed first per payer up to their allocation before any card charge is attempted Given a payer's allocation is fully covered by credits/packages When charging occurs Then no card authorization is created for that payer, and the invoice/receipt shows $0 due with itemized credit/package consumption Given credits/packages only partially cover a payer's allocation When charging occurs Then the remaining amount is authorized/charged to that payer's card, and the receipt itemizes both consumption and cash charged Given multiple payers have packages for the same service When consumption is calculated Then decrements are applied to the correct payer's package balances without cross-applying units
Receipts and Audit Trail Reflecting Payer Allocations and Consumption
Given all payer charges and credit/package applications are finalized When receipts are generated Then each payer's receipt shows the total job amount, their allocation, proportional taxes/discounts, credits/packages consumed, card brand and last4, authorization/charge IDs, and timestamps Given a coordinator views the job in the dashboard When the payment section is opened Then a consolidated view shows per-payer amounts, payment methods, statuses, any 1-cent rounding assignment, and the grand total equals the job total with no discrepancy > $0.01 Given any change to payers or allocations is saved When the change is committed Then the audit log records the previous values, new values, user ID, timestamp, and reason (if provided), and the change is visible in the job history
Payer Consent and Role-Based Permissions
"As a property manager coordinating services, I want to grant pay permissions to certain payers and keep others view-only so that billing stays compliant and controlled."
Description

Capture and store explicit consent to charge from each payer via SMS e-sign links and secure web views, with reusable mandates tied to specific pets and service types if needed. Define roles (owner, coordinator, payer-only) to control who can add/remove cards, select payers via SMS, approve charges, and view balances. Enforce permissions across dashboard and SMS interactions, and require re-consent when roles change. Maintain a comprehensive audit trail of consents, role assignments, and charge authorizations. Comply with PCI and relevant e-sign regulations, and provide revocation workflows and notifications.

Acceptance Criteria
SMS E‑Sign Consent Capture and Storage
Given a payer receives an SMS with a secure e‑sign link for a specific pet and service type When the payer completes the e‑sign flow in the secure web view Then the system records a reusable consent mandate with payer ID, pet ID, service type, masked funding source token, signature evidence (hash), timestamp, and IP/device metadata And Then the consent appears in the dashboard and is retrievable via API within 10 seconds And Then subsequent charges for that pet/service initiated by permitted roles do not prompt for approval until the mandate expires or is revoked And Given the payer abandons or declines the e‑sign Then no mandate is created and the dashboard shows consent status = Pending for that payer/pet/service
Role Enforcement: Owner vs Coordinator vs Payer‑Only
Given a user is assigned Owner role for a household wallet When they attempt to add/remove a card, set per‑pet/service defaults, select a payer via SMS, or view balances Then all actions succeed and full wallet balances/transactions are visible without exposing full PAN Given a user is assigned Coordinator role When they attempt to add/remove a card Then the action is blocked with a permission error and no card changes occur And When they select a payer via SMS or set per‑booking payer Then the selection is allowed only among payers with active mandates for the pet/service And When viewing balances Then only aggregated balances and masked funding sources are shown Given a user is assigned Payer‑Only role When they add/remove their own cards or approve/revoke charges Then those actions succeed And When they attempt to select other payers, change defaults, or view other payers’ balances Then the action is denied with an appropriate SMS/dashboard message
SMS Payer Selection and Charge Approval Flow
Given a service is being scheduled via SMS with a default payer set for the pet/service When the default payer has an active consent mandate Then the system confirms the payer selection and prepares the charge without prompting the payer Given no active mandate exists for the selected payer When the coordinator/owner chooses that payer via quick reply Then the payer receives an approval SMS link and the booking is marked Awaiting Approval until consent is provided Given the primary card declines during charge capture When a secondary card is configured for the same payer or alternate payer Then the system retries only against funding sources with active consent mandates; otherwise no retry occurs and notifications are sent to the coordinator and payer
Re‑Consent on Role or Scope Change
Given a payer’s role changes (e.g., Payer‑Only to Coordinator) or their mandate scope (pets/services covered) is modified When the change is saved Then existing mandates outside the new scope are invalidated and marked Expired in the audit trail And Then the payer is sent an SMS to re‑consent for the new role/scope via secure web view And Then charges requiring the updated scope are blocked until re‑consent is completed Given re‑consent is not completed within the configured timeout Then pending authorizations lapse and affected bookings show status = Awaiting Consent
Audit Trail for Consents, Roles, and Authorizations
Given any consent capture, role assignment/change, approval/decline, charge authorization, or revocation event occurs When the event is committed Then an immutable audit record is stored with actor (user/payer/system), role at time of action, channel (SMS/Dashboard/API), timestamp (UTC), affected pet(s)/service(s), funding source token (masked), and outcome And Then audit records are filterable by household, payer, pet, service type, date range, and event type, and exportable as CSV And Then only staff or roles with Audit permission can view/download audit records And Given an attempt is made to edit or delete an audit record Then the system prevents modification and logs the attempt
Consent Revocation Workflow and Notifications
Given a payer sends an SMS command to revoke consent or toggles revoke in the secure web view When the revocation is confirmed Then all active mandates for the selected pet/service (or entire wallet if chosen) are set to Revoked immediately And Then future charges using those mandates are blocked, pending holds are voided where possible, and affected bookings show status = Payment Method Needed And Then notifications are sent to the coordinator/owner and the payer confirming revocation and listing affected pets/services Given the payer re‑consents later When a new mandate is signed Then prior Revoked records remain in the audit trail and a new mandate record is created
Compliance Safeguards: PCI and E‑Sign Evidence
Given a user or system attempts to view card data via SMS, dashboard, or API When the request is processed Then only tokenized identifiers and masked PAN (last4) are returned; full PAN/CVV are never accessible or logged And Then all consent artifacts (signed text, IP/device, timestamp, mandate scope, explicit e‑signature) are stored tamper‑evidently and are downloadable as a PDF/JSON evidence package And Then the e‑sign flow captures electronic records consent and provides a copy to the payer Given application logs and exports are inspected When reviewing for sensitive data Then no PAN, CVV, or full cardholder data is present; secrets are redacted and data at rest is encrypted (AES‑256 or equivalent) with access logs available
Billing Rules and Credits Application
"As a trainer working with fosters, I want package credits and no-show fees to apply to the right payer automatically so that billing is fair and consistent without manual adjustments."
Description

Apply packages, credits, discounts, taxes, and no-show fees to the correct payer according to configurable rule priorities. Allow configuration at account, pet, and service levels for which payer’s package credits should be consumed first, how to handle mixed responsibility (rescue vs foster), and who bears no-show fees by default. Ensure rules execute consistently across booking, reminders, and checkout, with transparent line-item breakdowns in SMS receipts and the dashboard. Handle edge cases such as insufficient credits, expiring packages, and prepaid services, and ensure charges and credit consumption are posted to the appropriate payer’s history.

Acceptance Criteria
Pet-Level Override Applies Package Credits Before Account Default
Given account-level priority for Grooming packages is Payer A then Payer B And pet "Rex" has a pet-level priority set to Payer B then Payer A And Payer B has 3 active Grooming credits and Payer A has 5 When a Grooming booking for Rex is checked out from a reminder reply Then 1 credit is deducted from Payer B's Grooming package And 0 credits are deducted from Payer A And the SMS receipt and dashboard show a line item "Grooming — 1 credit (Payer B)" And the deduction is posted to Payer B’s history with booking ID and timestamp
Service-Level Split for Rescue vs Foster Responsibilities
Given service "Vaccination Check" is configured so Rescue covers package credits and Foster pays remaining service fees and taxes, and the default no-show fee payer is Rescue And Rescue has 2 active Vaccination Check credits and Foster has a valid card on file When the appointment is completed and checked out Then 1 credit is deducted from Rescue’s package And the remaining service subtotal, taxes, and tip (if any) are charged to Foster’s default card And if the appointment is marked No-Show, the configured no-show fee is charged to Rescue And line items attribute each component to its payer in SMS and dashboard receipts And all postings appear in the respective payer histories
Partial Consumption on Insufficient Credits with Card Fallback
Given service "Daycare" requires 2 credits And Payer C has 1 active Daycare credit and both a default and secondary card on file When checkout is processed Then 1 credit is deducted from Payer C’s package And the remaining equivalent of 1 credit is charged to Payer C’s default card And if the default card declines, the charge automatically retries on the secondary card And the SMS receipt and dashboard show "1 credit (Payer C) + remaining amount charged (Payer C)" And both the credit deduction and the card transaction are posted to Payer C’s history
Soonest-Expiry Package Consumed First
Given Payer D holds two "Walk" packages: Package A expiring on 2025-09-01 with 2 credits and Package B expiring on 2025-12-01 with 5 credits When a Walk service is checked out on 2025-08-11 Then 1 credit is deducted from the package with the nearest future expiry (Package A) And any package past its expiry at checkout time is not consumable And the receipt shows the package identifier and expiry date used And the deduction is recorded against that specific package in the payer history
No-Show Fee Default Payer with Pet-Level Override
Given account default no-show fee payer is Coordinator And pet "Milo" has a pet-level override setting no-show fee payer to Owner And an appointment for Milo is auto-marked No-Show from the reminder flow When the No-Show is finalized Then the configured no-show fee is charged to Owner’s card And no charge is applied to the Coordinator And SMS and dashboard receipts show "No-Show Fee — amount (Owner)" And the charge is posted to Owner’s payer history with the related booking ID
Consistent Rule Execution and Transparent Line Items Across Flows
Given a booking for pet "Luna" where rule priority is Payer E’s package credits first, then Payer F’s card for any remainder When the booking is created via SMS, reminders are processed, and checkout is completed in the dashboard Then the same payer selection and credit consumption are reflected at estimate, reminder preview, and final checkout And SMS receipts and dashboard invoices display identical line items with payer attribution, quantities, amounts, taxes, and tips And the audit log records a single rule set version applied across all steps without conflicts
Prepaid Service Respects Original Payer and Skips New Charges
Given service "Training" was prepaid at booking by Payer G using 100% package credits And the appointment is rescheduled within allowed rules When the appointment is checked out Then no new credits are consumed and no cards are charged for the base Training service And the receipt references the original prepayment and shows $0 due for the base service And any add-ons added at checkout follow the current rule priority for payer selection and are posted to the appropriate payer history

Charge Shield

Attach proof‑of‑service artifacts to each transaction—GPS check‑in/out, time stamps, before/after photos, signed waivers, and vaccine confirmations from the Smart Card. Clear descriptors and a one‑tap “dispute pack” export help prevent and contest chargebacks.

Requirements

GPS Check-in/Out Proof
"As a provider, I want automatic GPS check-in/out attached to each job so that I can prove I was on-site at the scheduled time if a client disputes."
Description

Capture provider GPS check-in at service start and check-out at service end, geofenced to the appointment location and automatically linked to the associated booking and payment. Support offline collection with secure local queueing and signature-based, tamper-evident payloads that sync when connectivity returns. Display location pins and accuracy radius on the Charge Shield timeline, and flag anomalies (late arrival, off-geo, missing checkout). Respect user permissions and client opt-ins; store only necessary coordinates with configurable retention. Provide admin controls to disable per service type and export coordinates in dispute evidence bundles.

Acceptance Criteria
On-Geo GPS Check-In/Out Linking to Booking and Payment
Given a scheduled appointment with a defined location, configured geofence radius R, and GPS accuracy threshold A When the provider taps Check In Then the system records a check-in event with lat, lon, accuracy, timestamp, provider ID, booking ID, and links it to the booking and pending payment And sets event.status = "on-geo" if distance to appointment location <= R and accuracy <= A; otherwise sets event.status = "off-geo" and displays an inline warning to the provider When the provider taps Check Out Then the system records a check-out event with the same fields, links it to the same booking and pending/posted payment, and computes service duration = checkout_ts - checkin_ts And both events appear in the booking detail and Charge Shield timeline within 2 seconds when online And event IDs are referenced on the payment record for auditability And events persist if the app is backgrounded or terminated during capture
Offline Capture and Deferred Sync with Tamper-Evident Queue
Given the device has no network connectivity at the moment of Check In or Check Out When the provider performs the action Then the app captures GPS coordinates and constructs a payload containing event_id, booking_id, action_type (checkin/checkout), timestamp (UTC), lat, lon, accuracy, device_id, and app_version And computes a cryptographic signature over the payload using a device-held key and stores payload+signature in an append-only, encrypted local queue And the UI marks the event as "Pending sync" When connectivity is restored Then queued events are uploaded in FIFO order within 60 seconds And the server verifies the signature and rejects any event with an invalid signature, marking it as "Rejected – signature invalid" with an anomaly flag And on successful verification, events are written to the booking, linked to payment, and shown on the timeline in the original chronological order
Charge Shield Timeline Visualization of Pins and Accuracy Radius
Given a booking with recorded check-in and/or check-out events When a user opens the Charge Shield timeline for that booking Then a map renders check-in and check-out pins, each with an accuracy radius circle equal to the recorded accuracy value And each pin displays timestamp (in client’s local time), distance to appointment location in meters, and on-geo/off-geo label And events are ordered chronologically and visually distinct (check-in green, check-out blue) And the timeline, after data is loaded, renders within 500 ms on a median device And if an event has accuracy > A, a "Low accuracy" badge appears on the event
Anomaly Detection: Late Arrival, Off-Geo, Missing Checkout
Given a booking with scheduled start Ts, configured grace period G minutes, geofence radius R, and missing-checkout window W minutes When a check-in timestamp > Ts + G Then the event is flagged "Late arrival" with the minutes late recorded When an event’s distance to appointment location > R Then the event is flagged "Off-geo" with the distance recorded When a booking has a check-in but no checkout by max(Ts_end + W, checkin_ts + W) Then the booking is flagged "Missing checkout" And all anomaly flags are visible on the Charge Shield timeline and in the booking list with filterable counts And anomalies are included in audit/export data
Permissions and Client Opt-In Enforcement with Data Minimization
Given client location tracking opt-in = false or the viewer lacks the "View GPS Proof" permission When a provider attempts to capture location or a user attempts to view coordinates Then coordinates are not stored or displayed; only a null-location marker with timestamp and reason = "Client opt-out" or "Insufficient permission" is recorded And UI shows "Location hidden" indicator instead of map pins When client opt-in = true and the viewer has permission Then only necessary fields (lat, lon, accuracy, timestamp, provider ID, booking ID, event_id) are stored and viewable And all access to GPS events is authorized and logged with user ID and timestamp
Configurable Data Retention and Purge of Coordinates
Given GPS data retention is configured to N days When an event’s age exceeds N days Then lat, lon, accuracy, and raw signatures are purged by an automated job And event metadata (timestamps, event type, on-geo/off-geo flag, distance bucket) is retained And purge operations are logged with counts and timestamps And dispute exports exclude coordinates for purged events and include a note "Coordinates purged per retention policy"
Admin Controls per Service Type and Dispute Pack Export
Given an admin has disabled GPS Proof for service type S When a booking of type S is created or performed Then Check In/Out GPS capture UI is hidden/disabled and no GPS events are stored Given a booking with GPS events and an authorized user selects "Export Dispute Pack" When the export is generated Then a ZIP is produced within 10 seconds containing: a JSON file of event data (timestamps, lat, lon, accuracy, on/off-geo), a signature verification report, map snapshots for each event, and a manifest with SHA-256 checksums for all files And the export link is available to authorized users only and expires within 24 hours
Cryptographic Timestamps & Audit Trail
"As an owner, I want an immutable timeline of service events so that I can demonstrate the sequence and integrity of what happened."
Description

Generate server-authoritative timestamps for key events (booking created, reminder sent, GPS check-in/out, photo uploads, waiver signed, payment authorized/captured/refunded) and persist them in an append-only, hash-chained audit log per transaction. Each artifact receives a unique ID with SHA-256 content digest to detect tampering. Surface a human-readable timeline in the dashboard and include a machine-readable event log in exports. Handle device clock drift by reconciling client times with server time and annotate any adjustments. Ensure logs are immutable post-settlement while allowing redactions of PII via reversible tokens referenced in the dispute pack.

Acceptance Criteria
Authoritative Timestamping of Key Events
Given any of the following events occurs for a transaction: booking_created, reminder_sent, gps_check_in, gps_check_out, photo_uploaded, waiver_signed, payment_authorized, payment_captured, payment_refunded When the event is received by the server Then the system persists the event with server_received_at set by the server in UTC ISO 8601 with millisecond precision And client_reported_at (if provided) is stored without overwriting server_received_at And server_received_at is the canonical sort key for timelines and exports And clock_offset_ms = client_reported_at - server_received_at is recorded (null if client time absent)
Append-Only Hash-Chained Audit Log per Transaction
Given a transaction exists with zero or more prior events When a new event is persisted Then event.event_id is unique per event and prev_hash equals the event_hash of the immediately preceding event in that transaction (or null for the first event) And event_hash = SHA-256 over the canonicalized event payload including prev_hash And any attempt to update or delete an existing event is rejected with HTTP 409 and no mutation occurs And a chain verification API returns Verified for the transaction when all hashes recompute and link without gaps
Artifact Identity and Content Integrity
Given an artifact (GPS snapshot, photo, waiver PDF, vaccine confirmation) is associated to a transaction event When the artifact is stored Then artifact_id is assigned as a UUIDv4 and content_sha256 is computed over the exact stored bytes And subsequent retrieval returns content_sha256 and byte-for-byte content such that SHA-256(content) equals content_sha256 And on any integrity mismatch the system flags the artifact as integrity_error and blocks substitution of content while preserving the audit trail
Client Clock Drift Reconciliation
Given a client submits an event with a device timestamp When the server processes the event Then the system computes clock_offset_ms = client_reported_at - server_received_at And if abs(clock_offset_ms) > 120000 the event is annotated with clock_adjusted=true and includes the offset value And timeline and exports include both client_reported_at and server_received_at as well as clock_offset_ms And event ordering within a transaction is determined by server_received_at; out-of-order client times do not reorder the timeline
Dashboard Timeline Rendering
Given an authenticated user views a transaction in the dashboard When the timeline loads Then all events for the transaction render in descending order by server_received_at with human-readable labels And each event shows absolute UTC time, local time, and links to associated artifacts (photos, waiver, GPS) using their artifact_id And events with clock_adjusted=true display an "Adjusted" badge with the offset in minutes And any chain verification failure displays a non-dismissable warning on the timeline
Machine-Readable Event Log and Dispute Pack Export
Given a user taps One-Tap Dispute Pack Export for a transaction When the export is generated Then the export includes a machine-readable event log file (JSON, schema versioned) containing for each event: event_id, event_type, server_received_at, client_reported_at, clock_offset_ms, prev_hash, event_hash, artifact references with content_sha256 And the export includes all linked artifacts or signed URLs plus their content_sha256 And the export is downloadable within 10 seconds and the event log passes chain verification when validated offline
Post-Settlement Immutability with PII Redaction Tokens
Given a transaction is marked Settled When a user attempts to modify or delete any existing event or artifact content Then the system rejects the operation with HTTP 409 and logs a denied_change event without altering prior records And when a PII redaction is requested on allowed fields (e.g., client_name, client_phone, client_email), the system replaces values with reversible tokens and appends a pii_redacted event referencing the token ids And token-to-PII mappings are stored in a secure vault and are only rehydrated during authorized dispute pack exports; rehydration events are logged in the audit trail
Before/After Photo Capture
"As a groomer, I want to add before/after photos to a transaction so that I can show the quality and completeness of the service during a chargeback."
Description

Provide an in-app, mobile-first camera flow to capture before/after photos tied to a specific pet, appointment, and transaction with embedded metadata (server timestamp, uploader ID, optional coarse location). Support SMS link capture for staff using basic devices, with authenticated one-time links. Offer lightweight annotation (arrows, notes), automatic background face/license plate blurring, and size optimization for fast upload on cellular networks. Store originals securely with derivations for preview and export, enforce consent flags for client-visible sharing, and include thumbnails and hashes in the Charge Shield timeline and dispute exports.

Acceptance Criteria
In-App Before/After Capture Flow
Given a signed-in staff member opens an appointment with an associated pet and pending transaction When they capture one "Before" photo and one "After" photo via the in-app camera and tap Save Then Save is accepted only if both tags are present, and both photos are linked to petId, appointmentId, and transactionId Given photos are saved When viewing the appointment gallery Then the "Before" photo appears before the "After" photo with their tags visible Given the upload completes When the server responds Then the UI shows success and the photos are available for preview within the appointment
Server Metadata and Linking Integrity
Given any photo upload (in-app or via SMS link) When it is ingested by the server Then the system writes immutable metadata: serverTimestamp (UTC), uploaderId (from auth context), and optional coarseLocation (>=100m accuracy if provided), ignoring client-supplied timestamps for audit fields Given a stored photo When metadata is retrieved via API or export Then serverTimestamp and uploaderId are present and non-null; coarseLocation is present only when capture permission was granted Given the original and its derivatives When downloaded Then EXIF/XMP metadata includes serverTimestamp and uploaderId; the original file’s SHA-256 matches the stored hash
SMS One-Time Link Capture for Basic Devices
Given a staff member requests an SMS capture link for an appointment When the link is generated Then it is single-use, expires 15 minutes after issuance, is bound to the appointmentId and uploaderId, and is accessible only over TLS Given the link is opened When the token is valid and unexpired Then a mobile web camera flow loads without requiring app installation and allows capturing one "Before" and one "After" photo Given the link is expired or already used When opened Then access is denied with a clear error and no capture is allowed Given photos are submitted When ingestion completes Then photos are linked to appointmentId, petId, and transactionId with origin=smsLink and appear in the appointment gallery
Annotation Tools and Non-Destructive Storage
Given a captured photo When a user adds arrows and text notes and saves Then an annotated derivative is created and versioned; the original image is preserved unmodified Given an annotated photo When previewed in the appointment or timeline Then the annotated derivative is shown by default with an internal-only toggle to view the original Given a dispute export is generated When it includes this photo Then both the original and the annotated derivative are included and labeled
Automatic Background Blurring for Faces and License Plates
Given a photo is uploaded When background face and license plate detection runs Then detected human faces and license plates are blurred with a minimum 12px blur radius and 10px margin, rendering them unreadable at 200% zoom Given a blur is applied When staff reviews the photo Then staff can approve the blur or adjust by adding/removing blur regions before client-visible sharing Given pet faces or the primary subject area is erroneously blurred When staff removes the blur region Then the saved derivative reflects the correction while preserving the original
Network-Efficient Upload and Derivations
Given a photo is captured on a cellular connection When uploading begins Then a thumbnail (<=50KB, 320px longest edge) and a preview (<=500KB, 1280px longest edge) are generated and uploaded first for immediate UI feedback Given the original file When uploaded Then it is transferred via resumable, chunked upload with automatic retry; upon completion the server-stored SHA-256 equals the client-calculated SHA-256 Given connectivity drops mid-upload When connectivity returns within 15 minutes Then the upload resumes from the last confirmed chunk without data loss
Consent, Visibility, Timeline, and Dispute Export
Given client photo-sharing consent is false When staff attempts to mark a photo client-visible or share via SMS/portal Then the action is blocked with an explanation and the photo remains internal-only Given client consent is true When staff marks a photo client-visible Then the shared view uses the blurred/annotated derivative; the original is never exposed to the client Given photos exist for an appointment When viewing the Charge Shield timeline Then each entry shows a thumbnail, Before/After tag, serverTimestamp, uploaderId, and the photo’s SHA-256 hash Given one-tap Dispute Pack export is requested When generated Then the ZIP includes originals, annotated/blurred derivatives, a manifest with filenames, sizes, and hashes, and embedded metadata; thumbnails are included for quick review
Smart Card Vaccine Snapshot
"As a trainer, I want vaccine confirmations attached to the visit so that I can prove compliance with safety requirements if questioned."
Description

At service start, snapshot the pet’s vaccine confirmations from the Pet Profile Smart Card, capturing vaccine types, dates, issuer, and document references with a server timestamp, and attach the snapshot to the transaction. Preserve the snapshot even if the underlying Smart Card data later changes, and show a comparison indicator if discrepancies arise post-service. Expose a concise vaccine summary in the payment detail view and include the full snapshot in dispute exports. Provide validation rules to flag expired or missing vaccines before check-in and log override reasons when staff proceed.

Acceptance Criteria
Snapshot Capture and Attachment at Service Start
Given a staff member initiates check-in for a scheduled service on a pet with a Smart Card When the check-in is confirmed Then the system creates a read-only vaccine snapshot and attaches it to the transaction within 2 seconds And the snapshot includes, for each vaccine on the Smart Card at that moment: vaccine type/name, administered_on (if present), expires_on (if present), issuer name, and document reference(s)/ID(s) And the snapshot includes a server-side UTC timestamp (ISO 8601) and the pet and transaction identifiers And the snapshot is immutable via UI and API And the same capture behavior occurs when check-in is initiated from dashboard, mobile web link, or SMS flow
Post-Service Discrepancy Indicator and Compare View
Given a transaction has a stored vaccine snapshot And the pet’s Smart Card is later updated such that any captured vaccine field differs When a user views the transaction or payment detail Then a discrepancy indicator is shown with the label "Vaccine data updated after service" And a compare view shows side-by-side values for fields that changed (snapshot vs current), with differences highlighted And if no differences exist, no discrepancy indicator is shown
Payment Detail Vaccine Summary Display
Given a transaction with a vaccine snapshot When a user opens the payment detail view Then a "Vaccines" summary is displayed showing, for required vaccines configured for the service, each vaccine status as Current, Expired, or Missing and the snapshot timestamp And a "View snapshot" action is available to open the full snapshot in one click And the summary is visible on both mobile and desktop payment detail views
Dispute Pack Export Contains Full Vaccine Snapshot
Given a transaction with a vaccine snapshot When the one-tap dispute pack export is generated Then the export bundle includes the full, unaltered vaccine snapshot captured at service start And the snapshot is included in both a human-readable section (PDF) and a machine-readable file (JSON) And the export references the transaction ID and snapshot timestamp
Pre-Check-In Vaccine Validation and Override Logging
Given validation rules define required vaccines per service type and species When a staff member attempts to check in a pet Then the system evaluates current Smart Card data and flags any missing or expired vaccines before check-in And if a rule severity is Block, the system prevents check-in until the staff selects an override reason from a configurable list or enters a note and confirms And all overrides are logged on the transaction with user ID, timestamp, triggered rule(s), override reason, and optional note And if severity is Warn, the system displays a warning but allows check-in; the warning is logged
Performance and Reliability of Snapshot Creation
Given normal network conditions When creating a vaccine snapshot at check-in Then the 95th percentile snapshot creation time is ≤ 1.0 seconds and the 99th percentile is ≤ 2.0 seconds And on transient failure, the system retries up to 2 times and surfaces a clear error if unsuccessful without duplicating the transaction And successful snapshot creation is required before the service is marked as Checked In
Multi-Pet Transaction Snapshot Handling
Given a booking contains multiple pets under one transaction When check-in is confirmed Then the system creates and attaches a distinct vaccine snapshot per pet and associates each snapshot with the correct pet line item And the payment detail view shows per-pet vaccine summaries with drill-down to each pet’s full snapshot And discrepancy indicators and dispute exports include each pet’s snapshot separately
E-signature Waiver Attachment
"As a business owner, I want signed waivers linked to payments so that I can show consent to terms when contesting a dispute."
Description

Enable SMS-delivered, mobile-friendly e-signature collection for liability and service-specific waivers prior to or at check-in, with template management, versioning, and merge fields (pet name, service, date/time). Record signer name, capture method, server timestamp, IP/device fingerprint, and signature image, then generate a sealed PDF linked to the payment. Support multilingual templates, per-service auto-selection, and conditional clauses (e.g., matted coat consent). Display waiver status in the timeline and include the signed PDF and signature audit data in dispute exports.

Acceptance Criteria
SMS Waiver Request and Signature Capture Before Check-In
Given an appointment for a service that requires a waiver and no valid waiver on file for the current template version When the appointment is created or confirmed Then the system sends an SMS containing a secure, single-use waiver link to the client within 30 seconds Given the client opens the link on a mobile device When the waiver page loads Then the page renders responsively and loads in under 2 seconds on a 4G connection and prompts for required fields (name, consent checkboxes, signature) Given the client completes all required fields and provides a signature (typed or drawn) When they tap Submit Then the submission is accepted only if all required fields are valid and the server records completion successfully, returning a confirmation screen within 2 seconds Given a required waiver is still unsigned at check-in When staff attempt to check the client in Then the system blocks check-in with a prompt to collect the signature on-device and allows completion after successful signature Given a signed waiver exists for the current template version within its validity window When a new appointment for the same service is created Then the system does not request a new signature
Per-Service Template Auto-Selection and Merge Fields
Given an appointment with service = X and pet = Y When generating a waiver Then the system auto-selects the active template version mapped to service X and locale, and records template_id and version_id Given merge fields {pet_name, service_name, start_datetime, business_name} When rendering the waiver Then all placeholders are replaced with correct values using the business timezone, with no unresolved placeholders remaining Given a template mapping is missing When generating a waiver Then the system falls back to the default waiver template and logs a warning event Given a template version is superseded When its end date/time passes Then new waivers use the new version while previously signed PDFs remain linked to their original version
Conditional Clauses Rendering (Matted Coat Consent)
Given the pet has matted_coat = true or matting_severity >= configured threshold at intake When rendering the waiver Then the "Matted Coat Consent" clause is included and requires client initials in a separate field Given the condition is not met When rendering the waiver Then the clause is omitted and no initials field is shown Given the clause is included When the client submits without providing initials Then validation fails with a clear error and prevents submission Given an admin updates the condition rule When a new waiver is generated Then the latest rule is applied and the rule_id and evaluation result are stored in the audit trail
Multilingual Waiver Delivery
Given a client preference language = L and a template translation exists for L When sending the SMS and rendering the waiver Then the SMS copy and waiver UI render in language L and the audit trail records language_code = L Given a client preference language = L and no translation exists for L When sending the SMS and rendering the waiver Then the system falls back to the business default language and notes the fallback in the audit trail Given the client switches language from the waiver UI When selecting an available language Then the content re-renders without losing entered data and the selected language is stored
Signature Audit Trail and Sealed PDF Generation
Given a client submits a signed waiver When the server accepts the submission Then the system records signer_full_name, capture_method (typed|drawn), server_timestamp (ISO 8601, UTC), client_ip, device_fingerprint, user_agent_hash, and a base64 signature image Given the audit data is recorded When generating the PDF Then the PDF embeds the signature image, audit metadata, template_id, version_id, rule evaluations, and includes a SHA-256 hash and cryptographic seal visible on the last page Given the PDF is generated When persisting Then the file is stored immutably with content_hash, size, and storage_uri, and is linked to the appointment_id and payment_id Given a previously generated PDF When retrieved Then the content hash verifies against the stored hash and the file downloads within 3 seconds
Timeline Status and Check-In Gate
Given an appointment with a required waiver When viewing the appointment timeline Then the waiver status displays as one of {Not Requested, Sent, Viewed, Signed, Expired} with corresponding icon and timestamp Given the waiver status = Signed When viewing the timeline Then the timeline shows signer name, sign time, and a link to view the PDF Given the waiver status != Signed and the waiver is required When staff attempt to check in Then check-in is blocked unless an authorized staff member overrides by providing a reason, which is recorded in the audit trail Given staff resend a waiver When tapping Resend Then a new secure link is sent, previous links are invalidated, and the timeline logs the resend event
Dispute Pack Export Includes Waiver and Audit
Given a transaction linked to an appointment with a signed waiver When exporting the dispute pack Then the export includes the signed waiver PDF and a machine-readable audit file (JSON) containing signer_full_name, capture_method, server_timestamp, client_ip, device_fingerprint, user_agent_hash, template_id, version_id, clause_evaluations, and content_hash Given the export is initiated When processing Then the export completes in under 10 seconds for the 95th percentile and provides a single ZIP with clear filenames including pet_name, service_name, appointment_start_datetime Given no signed waiver exists When exporting Then the export includes the waiver status, evidence of attempted collection (SMS send and view events), and a note that no signature was obtained
Friendly Statement Descriptor
"As a client, I want recognizable charges on my statement so that I don’t accidentally dispute a legitimate service."
Description

Generate clear, client-friendly soft descriptors for card statements and receipts that include business name, pet name, service type, city, and visit date within processor and network length/character constraints. Apply per-transaction descriptors via the payment processor when supported; otherwise, add a prominent descriptor on receipts and SMS confirmations. Localize formatting, handle fallback rules, and store the descriptor used alongside the transaction for reference in disputes. Provide a preview in the charge screen and a validation check to avoid truncation or prohibited characters.

Acceptance Criteria
Descriptor Generation Meets Processor Constraints
Given a configured processor rule set RP-22 with max_length=22 and allowed charset [A–Z a–z 0–9 space - /] And inputs BusinessName="Happy Paws SF", PetName="Milo", ServiceType="Grooming", City="San Francisco", VisitDate=2025-09-12, Locale="en-US" When the friendly statement descriptor is generated Then the descriptor is constructed in token order [BusinessName, PetName, ServiceAbbrev, CityAbbrev, DateFormatted] And ServiceAbbrev is applied per {Grooming->Groom, Walking->Walk, Training->Train} And CityAbbrev uses "SF" for "San Francisco" if needed to meet length And DateFormatted uses MM/DD for en-US (e.g., 09/12) And the resulting descriptor length <= 22 and contains only characters in the allowed charset And no token is truncated mid-word; only approved abbreviations may be applied And if the descriptor still exceeds 22 after all approved abbreviations, validation returns Valid=false with error "Exceeds processor limit (22) for RP-22" and the Charge action is disabled
Per-Transaction Descriptor Applied to Processor
Given the payment processor capability flag supports_descriptor=true And an authorization request is submitted for Transaction T with generated descriptor D When the charge is created Then the processor API request includes D in the designated per-transaction descriptor field And the processor response or subsequent retrieval confirms the applied descriptor equals D And the transaction record stores descriptor_used=D and descriptor_source="processor" with timestamp And if the processor rejects the field as unsupported, the system retries without the descriptor and switches to fallback (descriptor_source="receipt_sms"), logging the reason
Prominent Descriptor on Receipts and SMS (Fallback)
Given supports_descriptor=false for the processor OR the per-transaction descriptor send fails When the receipt and SMS confirmation are generated for Transaction T with descriptor D Then D appears as the top line on the receipt and as the first line of the SMS body And D is identical across receipt and SMS And T stores descriptor_used=D and descriptor_source="receipt_sms" And automated checks verify D is visible without scrolling on a 375x667 viewport and within the first 140 characters of the SMS
Localization and Character Compliance
Given Locale="fr-FR", City="Málaga", PetName="Chloé", VisitDate=2025-09-12, and processor rule set RP-25 with max_length=25 and allowed charset ASCII When generating the descriptor Then diacritics are transliterated to ASCII (Málaga->Malaga, Chloé->Chloe) And the date is formatted per locale policy using DD/MM (e.g., 12/09) And the descriptor uses only ASCII characters and length <= 25 And if a locale-specific format would violate the allowed charset, transliteration/format fallback is applied automatically and shown in validation details
Charge Screen Preview and Live Validation
Given the charge screen with inputs that produce candidate descriptor D When the user edits BusinessName, PetName, ServiceType, City, VisitDate, or Locale Then the on-screen descriptor preview updates within 250 ms of the last keystroke And a live counter shows current length and max (e.g., 18/22) And prohibited characters (e.g., emoji) are highlighted inline with actionable messages And the Charge/Confirm action is disabled while validation is invalid and enabled when valid
Fallback Rules and Missing Data Handling
Given one or more tokens are missing or empty When generating the descriptor under rule set RP-22 Then missing PetName is omitted (no extra spaces or dangling delimiters) And missing City falls back to BusinessProfile.City if available; otherwise City is omitted And missing ServiceType is replaced with "Svc" And missing VisitDate falls back to the transaction creation date formatted per locale And prohibited characters in BusinessName are transliterated or removed to meet the allowed charset And the final descriptor is non-empty, within max length, and passes validation; otherwise a specific validation error identifies which token(s) caused failure
Descriptor Storage and Dispute Pack Export
Given a completed transaction T with descriptor_used=D and descriptor_source=S When generating a Charge Shield dispute pack export for T Then the export includes fields descriptor_used, descriptor_source, processor_rule_set_id, and a rendered descriptor preview on the first page And the values exactly match those stored with T And the descriptor data is immutable once the transaction is captured
One-tap Dispute Pack Export
"As an owner, I want a one-tap export of all proof so that I can respond to chargebacks quickly and consistently."
Description

Provide a single-action export from the payment detail view that compiles all available evidence—GPS check-in/out, cryptographic event timeline, photos, signed waivers, vaccine snapshot, itemized invoice, service notes, and relevant SMS message history—into processor-ready formats. Produce a concise PDF summary plus a ZIP containing originals and a JSON manifest, and map evidence to card network requirements (e.g., descriptors, delivery/attendance proof, policy acceptance) to satisfy dispute reason codes. Offer a secure, expiring share link and direct API submission to supported processors, track submission status, and log outcomes for follow-up.

Acceptance Criteria
One-Tap Export Produces PDF, ZIP, and Manifest
Given I am on the payment detail view for a completed transaction with available artifacts When I tap "Export Dispute Pack" Then the system generates within 15 seconds a PDF summary, a ZIP of original artifacts, and a JSON manifest And the PDF includes: merchant and customer identifiers, transaction reference, service date/time, GPS check-in/out coordinates and timestamps, before/after photo thumbnails with IDs, signed waiver summary (name, version, signedAt), vaccine snapshot (vaccine names, status, expiry), itemized invoice with line items and totals, service notes, SMS conversation summary, and a mapping section to dispute requirements And the ZIP contains original files in logical folders (gps/, photos/, waivers/, vaccines/, invoice/, notes/, sms/) preserving original formats and metadata (e.g., EXIF) And the JSON manifest enumerates each artifact with fields: id, type, source, relativePath, sha256, capturedAt (ISO 8601 with timezone), relatedRequirementTags, and links it to the dispute requirement(s) it supports And file names follow pattern TXN-{transactionId}-{artifactType}-{artifactId}.{ext} And the pack is saved to the transaction record and available for sharing and submission
Reason-Code Requirements Mapping Checklist
Given a card-network dispute reason code is selected or retrieved from the processor for the transaction When the dispute pack is generated Then the PDF includes a checklist mapped to that reason code with items marked Met, Not Met, or N/A And required descriptors are populated (merchant descriptor, service descriptors, delivery/attendance proof, authorization/customer identity, and policy/waiver acceptance) And each checklist item links to specific artifacts (by manifest IDs) that satisfy it And unmet items are explicitly listed with recommended alternate evidence types if available And the manifest records reasonCode, mapping version, and per-item status And if the reason code is unknown, a default services mapping is used and the PDF flags the need to confirm the code
Secure Expiring Share Link
Given a dispute pack exists for a transaction When I create a share link Then a HTTPS URL with a cryptographically random token (>=128 bits entropy) is generated And the link expires by default in 14 days, configurable between 1 hour and 60 days And the link grants read-only access to view the PDF and download the ZIP and manifest And all accesses are logged with timestamp, IP, and user agent And I can revoke the link at any time; revoked or expired links return HTTP 410 and cannot be accessed And an optional passcode can be set and is required to view/download when enabled
Direct API Submission and Status Tracking
Given my account is connected to a supported payment processor and a dispute pack exists When I choose "Submit to Processor" and select the processor Then the system submits the pack using the processor’s disputes API in the required format (e.g., multipart with PDF, ZIP, and manifest or equivalent) And includes transaction reference, reason code, and metadata as required And stores the processor acknowledgment ID on success And displays submission status transitions: Submitted -> Pending -> Delivered or Failed based on webhooks or polling And retries transient failures up to 3 times with exponential backoff and jitter And surfaces failure reasons and allows manual resubmission And logs all submission events and outcomes on the transaction timeline
Relevant SMS History Inclusion and Redaction
Given SMS message history exists between the business and the customer When the dispute pack is generated Then only messages from 7 days before booking creation through 14 days after service completion are included And messages with other contacts are excluded And phone numbers are masked except the last 4 digits, and sensitive tokens/links are redacted according to policy And timestamps are shown in both local service timezone and UTC in the PDF summary And the manifest includes for each message: id, direction, timestamp, redactionsApplied, and linkage to the scenario it supports
Performance, Resilience, and Offline Queueing
Given the user initiates an export When the pack generation starts Then 95th percentile generation time is <= 15 seconds for packs up to 200 MB and 500 artifacts And a progress indicator displays compile stages and percent complete And if the app is backgrounded, the job continues and the user receives a completion notification And if the device is offline, the request queues and runs automatically when connectivity is restored And if some artifacts fail to fetch, the pack still generates with missing items clearly labeled in the PDF and manifest
Integrity and Tamper-Evidence
Given artifacts are sourced from FetchFlow or verified integrations When the pack is generated Then each artifact’s SHA-256 checksum is computed and recorded in the manifest And the PDF summary includes the ZIP checksum and the manifest checksum And the event timeline section presents a cryptographic hash chain across key events (check-in/out, photo capture, waiver signed) with monotonic timestamps And immediate post-generation verification of checksums passes for all items And on any checksum mismatch, generation halts, the user sees an error explaining which artifact failed, and an audit log entry is created

Smart Retry

If a charge declines, automatically try the best path: alternate card on file, scheduled bank‑friendly retries, or capturing the original hold as a partial deposit when allowed. Supports automatic card updates via network account updater where available to keep cash flow smooth.

Requirements

Decline Reason Analysis & Retry Orchestration
"As an independent pet pro, I want failed payments automatically routed to the best recovery path so that I spend less time chasing clients and keep my cash flow stable."
Description

Build a rules-driven engine that interprets gateway/issuer decline codes and selects the optimal recovery path (alternate card, scheduled retry, partial deposit capture) per invoice. The engine ingests context such as prior attempts, card type, merchant policy, client preferences, and time zone to produce a deterministic next action. It records attempt history and outcomes, exposes configurable rules in the FetchFlow dashboard, and emits audit logs and metrics. Integrates with existing payment gateway abstractions and Booking/Invoices to update status in real time, maximizing recovery while minimizing fees and customer friction.

Acceptance Criteria
Deterministic Decline Code Mapping to Recovery Action
Given a declined payment with gateway decline_code and full context (prior_attempts, card_type, client_prefs, merchant_policy, timezone) When the rules engine evaluates the next action Then it returns a single deterministic action (alternate_card | schedule_retry | partial_capture | do_not_retry) with rule_id and rationale And repeated evaluations with identical inputs produce the same action And unknown or unmapped codes fall back to the configured default rule And rule precedence is resolved by explicit priority values and rule status (enabled/disabled) And the selected action respects merchant hard limits (e.g., max_total_retries_per_invoice)
Alternate Card Retry Orchestration
Given a declined payment eligible for alternate card retry per rules and client_prefs allow_alt_card=true And the customer has ≥1 other active payment methods on file When the engine selects alternate_card Then it chooses the next method per configured strategy (last_successful_then_newest) excluding the last attempted fingerprint And attempts a single authorization/capture for the full outstanding amount using an idempotency key unique to invoice+attempt And updates the invoice status to Payment Pending during processing and Paid on success or Retry Failed on failure And enforces max_alt_card_attempts_per_invoice and cool_down_minutes between attempts And records the attempt with method_fingerprint, amount, currency, result, rule_id, decline_code (if any)
Bank-Friendly Scheduled Retry Plan with Time Zone Awareness
Given a declined payment and rules select schedule_retry And the contact timezone is known or a merchant default exists When the engine computes the next attempt Then it schedules within the configured bank_friendly_window and outside quiet_hours for that timezone And never schedules in the past and handles DST transitions correctly And persists a job with run_at, rule_id, attempt_number and marks the invoice Payment Scheduled And executing the job performs one attempt with a fresh idempotency key and updates the outcome in real time And the schedule respects max_scheduled_retries and min_retry_interval_minutes
Partial Deposit Capture of Original Authorization
Given an authorization exists and the gateway supports partial capture And merchant_policy allows partial_deposit with min_deposit_amount or percent When rules select partial_capture for the invoice Then the engine captures the lesser of (configured percent/amount) and remaining authorized amount within auth_validity_window And updates the invoice to Partially Paid with remaining_balance computed And schedules follow-up per rules for the remaining balance or marks Do Not Retry if disallowed And logs gateway references (auth_id, capture_id) and captured_amount And does not attempt partial capture if gateway capability check fails
Configurable Rules Management with Validation and Preview
Given a user with role permission manage_retry_rules in the FetchFlow dashboard When they create or edit a rule (conditions, action, priority, limits) Then the UI validates schema and condition fields and prevents saving invalid configurations And a preview tool returns the predicted action for provided sample inputs before save And saves create a new version with audit entry (who, when, before→after) and effective_from timestamp And enabling/disabling or reprioritizing updates the evaluation order immediately for new decisions without altering recorded past outcomes
Attempt History, Audit Logs, and Metrics Emission
Given any retry decision or payment attempt occurs When the engine processes the event Then it appends an immutable history record with timestamp, action, rule_id, attempt_number, payment_method_fingerprint (hashed), amount, currency, gateway_ids, result, decline_code, latency_ms And emits metrics: recovery_rate, retries_per_invoice, time_to_recovery, fee_cost_per_recovery, distribution by decline_family And redacts PAN/PII per policy and encrypts sensitive fields at rest And history is queryable by invoice_id and exportable as CSV/JSON within retention_window days
Account Updater Trigger for Expired/Changed Card Declines
Given a decline with reason in {expired_card, card_changed, contact_issuer_for_update} and network account updater is available for the BIN When the rules engine evaluates the next action Then it triggers an account_updater request with the necessary tokens and pauses retries until updater_result or timeout_ms And on updater success, it updates the payment method credentials and immediately retries once using a new idempotency key And on updater failure or timeout, it follows the configured fallback action (alternate_card or schedule_retry) And all updater events are logged with updater_request_id and outcome
Alternate Payment Method Fallback
"As a groomer, I want the system to try another saved card on file when one fails so that I can collect payment without manual follow-up."
Description

On a decline, automatically attempt other cards on file in a merchant-configured priority order per client, honoring explicit consent and card eligibility (e.g., not expired, not previously hard-declined). Retrieve tokenized payment methods securely, run risk checks, and ensure idempotency to avoid duplicate charges. Update the invoice and activity log with each attempt and outcome, and stop after configurable max attempts per invoice. Integrates with Client Profiles and the Pet Profile Smart Card for adding or reordering cards without leaving SMS flows.

Acceptance Criteria
Auto-Fallback to Next Eligible Card After Decline
Given a client has at least two payment methods on file with a configured priority order And the primary method charge for an open invoice declines And at least one alternate payment method is eligible When Smart Retry processes the decline Then the system attempts to charge the next eligible payment method in the configured priority order And stops attempting alternates as soon as one attempt is authorized or captured And updates the invoice balance and status to reflect the successful payment
Eligibility and Consent Filtering for Fallback Candidates
Given a client's profile includes multiple payment methods with metadata for expiration, consent, and decline history When Smart Retry evaluates fallback candidates for an invoice Then it excludes any method that is expired, deleted, lacking explicit consent, or flagged as hard-declined And it includes only methods meeting eligibility rules and within the merchant-configured method scope for fallback And the list of attempted methods appears in the activity log in the same order as the filtered priority
Risk Check Gating Prior to Alternate Charge
Given risk checks are enabled for card-not-present transactions When Smart Retry is about to attempt a charge on an alternate method Then it executes the risk check policy for the invoice and method And if the decision is "block" the attempt is not sent to the processor and the outcome is logged as "blocked by risk" with reason And if the decision is "allow" the processor is called once with an idempotency key
Idempotent Fallback and Concurrency Control
Given multiple triggers may invoke fallback for the same invoice within a short window When two or more fallback flows run concurrently Then a deterministic idempotency key scoped to invoice and attempt sequence is used for the processor call And at most one successful authorization/capture occurs for that attempt sequence And the invoice reflects a single payment record without duplicates
Invoice and Activity Log Updates per Attempt
Given any fallback attempt is executed or skipped When the outcome is recorded Then the invoice activity log entry includes: timestamp, attempt_number, payment_method_brand, last4, masked token, decision (attempted|skipped|blocked), gateway response code, response text, idempotency key, and actor "system:smart-retry" And the invoice payment status and amounts are updated consistently with the outcome And the processor call uses a tokenized method reference; no raw PAN/CVV is handled or logged
Enforcement of Max Attempts per Invoice
Given merchant configuration maxAttemptsPerInvoice = N And an invoice has already recorded K attempts (including the initial and all alternates) When K >= N and a new decline trigger occurs Then Smart Retry performs no further automatic attempts And logs "max attempts reached" on the invoice with K and N values And subsequent triggers for that invoice do not attempt charges unless a user resets attempts or increases N
SMS Smart Card Integration Updates Fallback Priority
Given a client uses the Pet Profile Smart Card link in SMS to add or reorder payment methods and provides explicit consent When the client saves the changes Then the client profile reflects the new priority order immediately And subsequent fallback uses the updated order for that client without leaving the SMS flow And the consent and change metadata (actor, timestamp, changeset) are stored and visible in the activity log
Issuer-Friendly Retry Scheduler
"As a walker, I want failed charges retried automatically at bank-friendly times so that they are more likely to go through without angering clients."
Description

Schedule automatic retries using issuer- and network-friendly backoff rules based on decline reason (e.g., insufficient funds vs. do not honor), with configurable windows, attempt caps, and quiet hours per merchant and client time zone. Skip retries for non-retriable declines, avoid same-day thrashing, and dynamically reschedule when account updates occur. Provide a calendar view in the dashboard, emit reminders to the timeline, and expose controls to pause/resume per invoice. Optimize for higher success rates and lower fees while preserving customer trust.

Acceptance Criteria
Decline-Reason Backoff Rules Applied
Given a merchant retry policy mapping decline categories to offset sequences (e.g., insufficient_funds -> [1d, 3d, 7d]) with attempt_cap=3 and retry_window=08:00–18:00 client local time When an invoice payment declines with reason category "insufficient_funds" Then exactly 3 retries are scheduled at offsets of ≥1d, ≥3d, and ≥7d from the decline, each within 08:00–18:00 client local time, and no additional retries are created beyond the cap And each scheduled attempt is persisted with decline_category, planned_at (ISO-8601 with timezone), and plan_version
Configurable Windows, Attempt Caps, and Quiet Hours by Time Zone
Given merchant quiet hours 20:00–08:00 in America/Los_Angeles, client quiet hours 21:00–07:00 in America/Denver, a retry_window of 08:00–18:00 client local time, attempt_cap=4, and min_interval=24h When the scheduler generates a plan across the next 14 days Then no retry is scheduled within merchant or client quiet hours, all retries fall within 08:00–18:00 client local time, no more than 4 retries are created, and the interval between attempts is ≥24h And all times displayed in the dashboard reflect the merchant display timezone while the execution timezone respects the client timezone
Skip Non-Retriable Declines
Given a decline code mapped to hard_non_retriable by policy (e.g., stolen_card, do_not_try_again) When a payment attempt returns that code Then zero retries are scheduled for the invoice And the invoice retry_plan.status is set to "skipped_non_retriable" with reason=decline_code and recorded_at timestamp And a timeline event "retry_skipped_non_retriable" is emitted and available via API and dashboard
Prevent Same-Day Thrashing
Given an invoice has already had a decline today and min_interval is configured to 24h When the scheduler evaluates additional retries for the same invoice on the same calendar day Then at most one retry is scheduled for that day and any further retries are shifted to the next eligible day within the configured window and outside quiet hours And the time between consecutive attempts is ≥24h (or the configured minimum)
Dynamic Reschedule on Account Updater Event
Given an invoice has a pending retry scheduled and the network account updater posts an update at 10:00 client local time with au_retry_lead_time=2h When the update is ingested Then the next retry is rescheduled to the first eligible slot ≥2h after 10:00 that is within the configured retry window, outside quiet hours, and within the attempt cap And the prior scheduled attempt is canceled with an audit record linking old_next_retry_at and new_next_retry_at And a timeline event "retry_rescheduled_account_update" is emitted
Dashboard Calendar View with Pause/Resume Controls
Given scheduled retries exist for a selected date range When a user opens the Calendar view Then each scheduled retry is displayed with invoice_id, client name, decline category, scheduled local time (client TZ), and status (scheduled/paused/exhausted/succeeded) And day/week/month views show the same counts for the range, and filtering by status and client reduces the visible items accordingly When the user clicks "Pause retries" for an invoice from the calendar Then all future retries for that invoice are canceled within 60 seconds and the status updates to paused When the user clicks "Resume retries" for the paused invoice Then a new schedule is computed to the next eligible slot and appears on the calendar
Timeline Reminders and Audit Events
Given an invoice with a retry plan When a retry is scheduled, attempted, succeeds, fails, is skipped, exhausted, paused, or resumed Then a corresponding timeline entry is created with event_type, invoice_id, actor (system/user), reason (if applicable), previous_next_retry_at, new_next_retry_at (if applicable), and UTC timestamp And the entry is visible in the dashboard and via API within 10 seconds of the event and contains no PII beyond configured fields And duplicate events for the same event_id are not created
Partial Deposit Capture on Authorization Hold
"As a trainer, I want the system to capture a partial deposit when the full amount can’t be collected so that my appointment is secured and I’m protected from no-shows."
Description

When an initial authorization exists and card network/processor rules allow partial capture, capture a configurable deposit percentage or flat amount to secure the appointment, then adjust the remaining balance for later collection. Release unused authorized amounts per gateway requirements, update booking status and invoice, and send confirmations. Enforce guardrails (single/multiple partial captures, time limits) and handle fallbacks when partial capture is not permitted. Integrates with no-show fee logic and checkout flows to settle the remainder seamlessly.

Acceptance Criteria
Capture configurable deposit from existing authorization
Given an approved card authorization exists for the booking and a business-defined deposit rule (percentage or flat amount) is active When Smart Retry determines the processor/network permits partial capture within the authorization’s valid window Then the system calculates the deposit per configuration, caps it at the authorized amount, rounds to the currency minor unit, and submits a partial capture And the gateway returns a successful capture response with a capture_id linked to the original authorization And the captured amount is stored on the invoice as a deposit payment line with reference to capture_id and auth_id
Release unused authorized amount after partial capture
Given a partial capture against an existing authorization has succeeded When there is remaining authorized amount after the deposit is captured Then if business rules or the processor prohibit multiple partial captures, the system voids/releases the unused remainder per gateway requirements within the same operation window and records the release confirmation And if multiple partial captures are allowed, the remaining authorization stays open until expiry or until configured time limit, with remaining_amount updated and visible in the booking/payment details
Adjust invoice and booking status after deposit capture
Given a deposit has been partially captured from an authorization When the capture is confirmed by the gateway Then the invoice remaining balance is recalculated as total - deposit and persisted And the booking status changes to "Deposit secured" and is shown in the dashboard and booking timeline And the checkout flow is configured to automatically apply the deposit to the final settlement without double-charging
Enforce guardrails and fallback when partial capture is not permitted
Given an authorization exists but the processor/network or business rules do not permit partial capture (e.g., expired auth, auth type not eligible, time window exceeded, single-capture policy) When Smart Retry attempts to secure funds Then the system does not submit a partial capture and records the disallowed reason And it executes the fallback path in priority order: attempt alternate card on file for the deposit amount; if unavailable or declined, schedule a bank-friendly retry; if permitted, re-authorize the deposit amount on the same card And if all fallback steps fail, the booking remains in "Awaiting deposit" with no funds captured and customer/provider notifications are queued per messaging rules
Idempotent retries and audit trail for partial capture workflow
Given network timeouts, webhooks, or internal retries may trigger duplicate requests When the same partial capture operation is retried with the same idempotency key or matching auth_id + booking_id + deposit_amount fingerprint Then the system returns the original capture outcome without creating an additional capture And an immutable audit log entry records each attempt with timestamps, actor (system/user), request identifiers, gateway responses, and resulting state transitions
Apply deposit to checkout and no-show fee logic
Given a deposit was captured for a booking When the customer checks out for completed services Then the remaining amount charged equals invoice_total - deposit (minus any other credits) and receipts show the deposit application And if the booking is marked as a no-show, the system retains the deposit up to the configured no-show fee, charges any remainder via Smart Retry, or refunds/credits any excess per business policy, with an itemized receipt
Confirmations and receipts after deposit capture
Given a partial capture (deposit) has succeeded When the capture is confirmed Then the customer receives SMS and email confirmations including deposit amount, remaining balance, appointment details, and a receipt link And the business receives a dashboard alert and email summarizing the capture and updated balance And delivery status is tracked with automatic retries on transient failures and respect for user notification preferences
Network Account Updater Integration
"As a business owner, I want cards on file to update automatically when customers get new cards so that fewer payments fail and I don’t have to chase new details."
Description

Integrate with card network account updater services to automatically refresh expired or replaced card tokens for customers who have consented. Trigger updates proactively (e.g., nightly batch) and reactively upon update-eligible declines, then retry using the refreshed token. Store update events, outcomes, and costs; notify clients only when updates fail or additional authorization is required. Provide merchant-level controls to enable/disable, set budgets, and view impact metrics (decline reduction, recovered revenue).

Acceptance Criteria
Nightly Proactive Card Updates via Network Account Updater
Given a merchant has Network Account Updater (NAU) enabled and budget remaining And customer payment tokens have valid consent and are eligible for update When the nightly update batch runs within the configured window Then the system submits NAU inquiries for eligible tokens up to the remaining budget And updates local tokens with any returned expiry/PAN token updates And skips tokens without consent or marked ineligible And writes a batch summary including total requests, updates found, no-matches, errors, and total fees And records one event per token with request/response details and costs
Reactive Update and Auto-Retry on Update-Eligible Declines
Given a payment attempt is declined with an updater-eligible reason (e.g., expired/replaced card) And the merchant has NAU enabled and budget remaining When the decline is received Then the system queries NAU for the specific token within 1 minute And if an update is returned, the token is replaced and the original charge is retried once within 5 minutes And if the retry succeeds, the payment is marked Recovered via Updater with linkage to the decline And if no update or the retry fails, Smart Retry proceeds to the next path (e.g., alternate card) per configuration And all actions and outcomes are logged with correlation IDs
Consent Enforcement for Account Updater Queries
Given customer-level consent is stored with timestamp, source, and policy version When preparing a proactive or reactive NAU request Then the system only submits the request if merchant-level NAU is enabled and customer consent = true And revocations are honored within 24 hours and prevent any queued requests from being sent And requests include no PAN data; only tokenized identifiers are transmitted And an audit record captures consent state used for the decision
Event, Outcome, and Cost Logging for Update Operations
Given any NAU request or response occurs Then an immutable log entry is created containing merchant ID, customer reference, token fingerprint, request ID, provider, timestamps, request type, response type (updated/no match/error), updated fields mask (masked), per-request fee, and retry result if applicable And entries are retained for at least 24 months and are exportable by date range And logs store only masked card data (last4/expiry) and no PAN or CVV And log counts reconcile with dashboard metrics within 1% for the same date range
Merchant Controls: Enable/Disable and Budget Caps
Given a merchant can configure NAU settings in the dashboard When the merchant toggles NAU to Disabled Then all future proactive and reactive NAU requests are prevented within 5 minutes When the merchant sets a monthly budget cap and per-request max fee Then the system blocks additional NAU requests once the cap is reached or if the fee exceeds the per-request max And the dashboard shows remaining budget, projected spend, and pause state with effective timestamps
Notifications on Failures or Additional Authorization Required
Given an NAU attempt results in failure or additional authorization is required When the outcome is finalized Then the merchant is notified within 10 minutes via the configured channel(s) And the notification includes event type, customer reference, card label (masked), reason, next steps, and correlation ID, excluding full card data And successful updates that lead to successful payment generate no notification And notifications are deduplicated per event and visible in the dashboard activity feed
Impact Metrics Dashboard: Decline Reduction and Recovered Revenue
Given NAU activity and payment outcomes exist within a selected date range When a merchant views the Account Updater metrics screen Then the dashboard displays decline reduction rate, recovered revenue, number of updates requested, update success rate, average fee per update, total fees, and ROI And metrics are filterable by location and staff and exportable to CSV And all metric totals reconcile to the underlying event log within 1% for the same filters
SMS Notifications & Self-Serve Update Links
"As a client, I want to be notified when a payment fails with a quick way to fix it so that my pet’s appointments aren’t interrupted."
Description

Send clear, event-based SMS messages on declines, scheduled retries, alternate card attempts, and deposit captures, including secure deep links for clients to choose a card, add a new card via the Pet Profile Smart Card, or pay now. Support two-way replies, opt-out handling, rate limiting, and localized templates. Log all messages and resolutions in the appointment timeline. Provide merchant customization for tone, timing, and triggers to balance transparency with minimal noise.

Acceptance Criteria
Decline SMS With Secure Self‑Serve Link
Given a payment attempt for an appointment declines And the client has SMS consent When the decline is recorded Then send an SMS within 10 seconds using the client's locale template And the SMS includes: business name, appointment identifier, amount due, decline context, and a single-use deep link And the deep link is a signed token scoped to the client and appointment and expires after 24 hours And the deep link opens a mobile-friendly page allowing: choose a card on file, add a new card via Pet Profile Smart Card, or pay now And upon successful payment or card update, the outstanding charge is retried immediately And all message, delivery, link-click, and resolution events are logged on the appointment timeline
Scheduled Retry And Alternate Card Attempt Notifications
Given a declined charge enters Smart Retry with a configured schedule When a scheduled bank-friendly retry is planned for future time T Then send a pre-attempt SMS 30 minutes before T if the "pre-retry notice" trigger is enabled And the SMS includes a deep link to update card or pay now When the retry is executed Then send a post-attempt SMS indicating success or failure if the "post-retry" trigger is enabled Given multiple cards are on file and "auto alternate card" is enabled When the system attempts an alternate card Then send an SMS indicating the masked card attempted and the result, with a deep link to manage cards And no more than one notification is sent per attempt
Partial Deposit Capture Notification
Given a platform-supported hold exists for the appointment and partial deposit capture is allowed by the processor When Smart Retry elects to capture a partial deposit Then send an SMS within 10 seconds stating the deposit amount, remaining balance, and next steps And include a deep link to settle the remainder or update the payment method And if the remaining balance is settled via the link, mark the appointment as paid and stop further retry notices
Two‑Way Replies And Opt‑Out Handling
Given a client receives any Smart Retry SMS When the client replies STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, or QUIT Then cease further messages except a single opt-out confirmation And record the opt-out on the client profile and appointment timeline When the client replies START or UNSTOP after opting out Then restore messaging consent and send a confirmation When the client replies HELP Then send a help SMS containing business name and support contact And ignore case and leading/trailing whitespace in keywords
Rate Limiting And Consolidation
Given multiple payment events occur within a short window for the same appointment When generating SMS notifications Then send no more than 3 Smart Retry SMS per client per 24 hours by default And consolidate multiple events occurring within 5 minutes into a single SMS where possible And these limits are configurable per merchant within allowed bounds (1–5 messages/day, 1–15 minute consolidation window) And attempts to send beyond the limit are suppressed and logged
Localization And Template Integrity
Given a client has a preferred language and region stored When any Smart Retry SMS is sent Then select the matching localized template or fall back to English if unavailable And render variables (client first name, pet name, amount, time) correctly, with currency and date formatting per locale And do not include full PAN or sensitive data; mask cards as brand •••• last4 And templates pass automated linting for required placeholders before activation
Merchant Customization Of Tone, Timing, And Triggers
Given a merchant opens Messaging Settings for Smart Retry When they configure tone (formal or informal), timing offsets (pre- and post-attempt notices), and select which event triggers are active (decline, scheduled retry, alternate card, deposit capture, success receipt) Then changes can be saved, previewed with sample data, and applied within 5 minutes And a test SMS can be sent to a verified number And an audit record of changes is stored with user, timestamp, and diff And disabling a trigger immediately suppresses future messages of that type
Idempotency, Limits & Compliance Safeguards
"As an operator, I want safeguards that prevent duplicate charges and ensure compliance so that retries are safe and trustworthy."
Description

Enforce idempotency across all retry attempts to prevent duplicate captures, apply per-invoice and per-card attempt limits, and block retries on hard declines. Maintain PCI-DSS scope by using tokenized payments only, and trigger SCA/3DS workflows when required via secure SMS links. Provide complete audit trails, monitoring, and alerts for anomalies. Ensure TCPA-compliant messaging behavior and configurable merchant policies to meet regional regulations, keeping Smart Retry safe and trustworthy by design.

Acceptance Criteria
Idempotent Retry Across Concurrent Paths
Given an invoice has a single outstanding payment authorization and Smart Retry is invoked twice concurrently with the same idempotency_key And alternate path evaluation (alternate card, scheduled retry, partial deposit) is enabled When both invocations evaluate and attempt to capture or re-authorize Then exactly one downstream financial operation is sent to the PSP And the successful response returns a stable transaction_id and idempotency_replay=false And the duplicate invocation returns HTTP 200 with the same transaction_id and idempotency_replay=true And the invoice ledger reflects exactly one capture for the amount attempted And no more than one customer notification is sent for this payment event
Per-Invoice and Per-Card Attempt Limits Enforced
Given merchant policy defaults of max_attempts_total_per_invoice=4, max_attempts_per_card_per_invoice=2, and min_retry_interval_hours=12 And an invoice has already recorded 2 failed attempts on Card A and 1 failed attempt on Card B When Smart Retry evaluates the next retry Then Card A is excluded due to per-card limit And only one eligible path is scheduled within policy windows And when total recorded attempts reach 4, no further retries are scheduled or executed for the invoice And the invoice state becomes retry_exhausted and an event retry.limit_reached is emitted And any API call to enqueue another retry returns HTTP 409 with code attempt_limit_reached
Hard Decline Codes Block Same-Card Retries
Given a configured hard_decline_codes list from the PSP (e.g., invalid_account, lost_stolen, pickup_card, do_not_try_again) And the most recent attempt on Card A ended with a hard decline code When Smart Retry runs Then Card A is permanently excluded from further retries for this invoice And alternate eligible payment methods (e.g., Card B on file) may be attempted within limits And no scheduled or immediate retries are created for Card A And an action_required event is emitted requesting a new payment method
Tokenized Payments Only and PAN Redaction
Given any Smart Retry payment operation When building requests to the PSP Then only tokenized payment identifiers (e.g., payment_method_token) are transmitted; raw PAN/CVV data is never present And any API call attempting to pass raw PAN/CVV is rejected with HTTP 400 code token_required and no PSP call occurs And all logs, events, and error messages redact PAN-like patterns to last4 only And automated scans of application and log storage detect 0 occurrences of unredacted PAN/CVV in Smart Retry components
SCA/3DS Challenge via Secure SMS Link
Given an issuer requires SCA/3DS for the next retry attempt When Smart Retry prepares the challenge flow Then a single secure SMS is sent containing a short-lived link (TTL 15 minutes, one-time use) to a hosted 3DS page over TLS 1.2+ And no additional retry attempts are executed until the challenge is completed And upon successful challenge, the retry executes within 60 seconds using the authenticated payment method and records liability shift metadata And if the link expires or the challenge fails, the invoice state is set to action_required and no additional SMS is sent automatically
Immutable Audit Trail and Anomaly Alerts
Given Smart Retry creates, schedules, executes, or cancels a retry attempt When the event occurs Then an immutable audit record is written with fields: invoice_id, attempt_id, idempotency_key, actor, path_selected, PSP_response_code, amount, timestamp, and hash_chain_prev And authorized users can query and export audit records for a given invoice within 5 seconds for the last 90 days And an alert is triggered within 5 minutes if any duplicate capture is detected for the same invoice within 24 hours, or if any retry is executed after a hard decline on the same card
TCPA-Compliant Messaging and Regional Policy Enforcement
Given a recipient's consent status, timezone, and merchant policy settings When Smart Retry sends any SMS related to retries, SCA, or payment updates Then messages are only sent to numbers with recorded consent and include business name and STOP/HELP instructions And quiet hours are enforced per recipient timezone (default 8am–8pm local unless configured) with 0 messages sent outside the window And STOP opt-outs are honored within 60 seconds and suppress further messaging And per-invoice messaging is rate-limited to 3 messages/day and 6 messages/week by default, configurable per region And all consent events (opt-in, opt-out, timestamp, source) are logged and exportable

Auto Pet Match

Automatically identifies each pet’s name and key details from the uploaded vaccine record and attaches the shots to the correct Pet Profile Smart Card—even in multi‑pet households. Eliminates misfiled records and saves time by reducing back‑and‑forth corrections.

Requirements

Universal Vaccine Record OCR
"As a busy groomer using FetchFlow, I want the system to read vaccine records from any format so that I don’t have to manually transcribe shot details."
Description

Build a robust ingestion pipeline that accepts photos, PDFs, and scans of vaccine records, auto-enhances image quality, and performs OCR to extract pet identifiers (name, species, breed, DOB/age, sex), vaccine details (name, abbreviation, lot number, dose, clinic, veterinarian), administered and expiration dates, and owner contact info. Support multi-page documents, common abbreviations and handwritten notes, multi-language detection (EN/ES), and return field-level confidence scores. Store originals securely, process asynchronously with progress states, and produce structured outputs consumable by Pet Profile Smart Cards.

Acceptance Criteria
Ingest Photos/PDFs (Multi-Page) with Auto-Enhancement
Given a user uploads a JPEG, PNG, HEIC, or PDF vaccine record up to 20 pages; When the ingestion pipeline starts; Then the file is accepted, the original is stored unchanged, and a processing job ID is returned. Given an image page with skew ≤ 15°, shadows, or low contrast; When auto-enhancement runs; Then deskewing (≤ 15°), denoising, and contrast normalization are applied and recorded; And the enhanced image is used for OCR. Given the standard image test set; When OCR runs on enhanced images; Then average character-level accuracy is ≥ 95%.
Field Extraction and Confidence Scoring (Printed Records)
Given a legible printed vaccine record in EN or ES; When OCR and parsing complete; Then the output includes pet identifiers (name, species, breed, DOB or age, sex), vaccine details (vaccine name and/or abbreviation, lot number, dose, clinic, veterinarian), administered_date, expiration_date, and owner contact (owner name, phone, email) when present. And each returned field includes a confidence value in [0.00,1.00] with two-decimal precision; Missing or unreadable fields are null or omitted with confidence 0.00. And dates are normalized to ISO 8601 (YYYY-MM-DD); Phone numbers are normalized to E.164 when country can be inferred; Species/breed names are normalized against system taxonomy. And on the printed-records test set, macro F1 for field correctness is ≥ 0.92.
Multi-Language (EN/ES) and Abbreviation Canonicalization
Given a vaccine record page in English or Spanish; When processed; Then page language is detected with confidence; If confidence ≥ 0.80, the corresponding language model is used for labels and vocabulary. And common vaccine abbreviations (e.g., DHPP/DAPP, RV, Bord, Lepto, FVRCP) are mapped to canonical vaccine names and codes; Ambiguous abbreviations are flagged with alternatives and confidence ≤ 0.60. And on the EN+ES mixed-language test set, page-level language detection accuracy is ≥ 97%; Abbreviation-to-canonical mapping accuracy on the known abbreviation corpus is ≥ 95%.
Handwritten Notes and Dates Extraction
Given a vaccine record containing handwritten entries for dates, lot numbers, doses, or checkmarks; When processed; Then handwriting OCR is applied, fields are extracted with confidence, and machine-printed text remains unaffected. And on the handwriting test set (legible hand-print/cursive within form regions), administered_date extraction accuracy is ≥ 90% and lot number extraction accuracy is ≥ 85%; Fields below per-type thresholds are flagged needs_review=true.
Asynchronous Processing and Progress States
Given a file has been uploaded; When processing begins; Then job status transitions through: received -> enhancing -> ocr -> parsing -> validating -> completed or failed, with monotonic progress_percent updates (0–100). And job status is queryable via API and via webhook callback; 95th percentile completion time is ≤ 30s for 1-page images and ≤ 120s for 10-page PDFs under nominal load; Failure reasons are returned with error codes.
Secure Storage of Originals and Access Controls
Given any uploaded document; When stored; Then the original binary is retained encrypted at rest (AES-256 or FIPS-compliant equivalent) and transmitted only over TLS 1.2+. And access to originals and enhanced derivatives is restricted to authorized service accounts and users with Document.Read permission; All access is audit-logged with user/service ID, timestamp, action, and IP. And logs and telemetry do not contain raw PII or image contents; Access violations are blocked and alerted.
Structured Output Compatible with Pet Profile Smart Cards
Given processing completes; When the structured output is produced; Then it conforms to the Universal Vaccine Record JSON schema v1 (including document metadata, per-page sections, and extracted field objects with value and confidence). And each vaccine entry includes canonical_name, source_name_or_abbreviation, lot_number, dose, clinic, veterinarian, administered_date, expiration_date with ISO 8601 dates; Each field carries confidence in [0.00,1.00]. And the pet identifier block includes name, species, breed, DOB or age, sex, and owner contact (name, phone, email) when present. And 100% of outputs in the integration test ingest successfully into Pet Profile Smart Cards without transformation errors.
Multi‑Pet Record Splitter
"As a groomer handling families with multiple pets, I want the system to split a single document into per-pet records so that each pet’s shots attach to the correct profile."
Description

Automatically detect and separate sections within a single vaccine document that pertain to different pets in the same household by using pet-name anchors, species markers, section headers, and layout cues. Produce per-pet payloads that can be independently matched and attached, while preserving the original document and a mapping of which pages/lines correspond to each pet. Handle edge cases like repeated names, missing headers, and shared clinic pages.

Acceptance Criteria
Split Two-Pet PDF by Section Headers and Pet Names
Given a 10-page PDF containing vaccine records for two pets with distinct section headers and pet names When the Multi‑Pet Record Splitter processes the document Then it returns exactly two per‑pet payloads And each payload contains only lines assigned to that pet And the original PDF binary is preserved unmodified in storage And a mapping is stored that lists for each pet the page and line-number ranges assigned And line-assignment precision >= 99% and recall >= 98% measured against a labeled fixture And total processing time <= 10 seconds on the reference environment
Disambiguate Repeated Pet Names Using Species and Metadata
Given a document where two pets share the same name but have different species and/or DOB/microchip When the splitter assigns sections Then it uses species markers, breed keywords, DOB, microchip/ID anchors, and clinic row associations to disambiguate And no line is assigned to both pets And if confidence for any pet falls below 0.90, the payloads are created with status=needs_review and are not auto-attached And an ambiguity_reason and per-pet confidence score are included in the mapping
Split Without Headers Using Layout Cues and Anchors
Given a scanned PDF without explicit section headers but containing pet-name mentions and species markers When the splitter processes the document with OCR Then it groups contiguous lines into per-pet sections using name anchors, species tokens, font/indent/whitespace cues, and a proximity window <= 50 lines And OCR confidence per token must be >= 0.85 for anchors to be considered And lines with anchor confidence < 0.85 are excluded from auto-assignment and flagged in the mapping And final per-pet recall >= 95% on the provided headerless fixture
Shared Clinic Summary Pages Allocation
Given a clinic summary page that lists multiple pets in a table on the same page When the splitter processes the document Then it parses the table rows and assigns only the rows matching each pet to that pet’s payload And the shared page binary is stored once and referenced by per-pet line ranges; it is not duplicated And no pet’s payload contains rows for another pet And row-assignment precision >= 99% on the shared-page fixture
Per-Pet Payload Schema and Attachment Readiness
Given per-pet payloads have been created When payloads are emitted Then each payload conforms to schema v1.2 with fields: pet_id (nullable), pet_name, species, source_document_id, assigned_segments [page, start_line, end_line], vaccines [name, date_administered, lot, manufacturer, due_date, clinic] And required fields pet_name, species, source_document_id, assigned_segments, vaccines are present and non-empty And payload size <= 2 MB and valid JSON And when pet_id is known in FetchFlow, the payload includes pet_id and is marked attachable=true; otherwise attachable=false
Original Document Preservation and Versioned Re-Runs
Given any splitting operation When the splitter completes Then the original document file is preserved with checksum and immutable retention >= 90 days And the mapping includes algorithm_version, features_used [name anchors, species markers, section headers, layout cues], and a deterministic run_id And re-running the splitter on the same input with the same algorithm_version produces identical mappings (idempotent) And any subsequent re-run with a higher algorithm_version creates a new mapping version linked to the same source_document_id without altering prior mappings
Smart Pet Matcher (Entity Resolution)
"As an independent walker, I want incoming vaccine records to auto-attach to the right Pet Profile even when names are similar so that I avoid misfiled shots."
Description

Match extracted pet payloads to existing Pet Profile Smart Cards using multi-signal scoring across name similarity (fuzzy/nickname handling), owner phone/email match, species/breed alignment, age/DOB proximity, weight/color when available, and household context. Produce a ranked match with a confidence score, auto-attach above a configurable threshold, and avoid false positives via tie-breaker rules and negative signals. Log decisions with explainability data and ensure idempotent matching to prevent repeated attachments.

Acceptance Criteria
Auto-attach on high-confidence single-pet household
Given a single-pet household with an existing Pet Profile Smart Card named "Max" with species Dog and a stored owner phone number And an extracted vaccine payload with name "Max", species Dog, owner phone matching exactly, and DOB within ±30 days of the profile DOB When the Smart Pet Matcher processes the payload with an auto-attach threshold of 0.85 Then it computes a confidence score ≥ 0.90 and ranks the "Max" profile as Top-1 And it auto-attaches the vaccine record to that profile And it persists a decision log containing payload ID, matched profile ID, final score, and threshold used And it does not create a manual review task for this payload
No auto-attach on ambiguous tie with negative signals
Given a household with two dog profiles "Bella" (25 lb) and "Stella" (70 lb) sharing the same owner contact And an extracted payload with name "Bella", species Dog, and weight 65 lb When the Smart Pet Matcher computes scores and Top-1 and Top-2 scores differ by < 0.02 or a negative signal (weight mismatch ≥ 10 lb) is present Then the final score for Top-1 is set below the auto-attach threshold (0.85) and no auto-attach occurs And the system returns a ranked list with at least the Top-3 candidates and their scores And the record is marked "Needs Review" And the decision log records tie-breaker evaluation and the negative signal(s) applied
Nickname and fuzzy name handling leads to correct match
Given a Cat profile named "Theodore" with a nickname alias "Theo" and the owner email on file And an extracted payload with name "Theo", species Cat, and the same owner email; DOB missing When the Smart Pet Matcher processes the payload Then the name signal uses nickname aliasing and fuzzy matching to consider "Theo" equivalent to "Theodore" And the combined score meets or exceeds the auto-attach threshold (≥ 0.85) And the system auto-attaches the vaccine record to "Theodore"'s profile And the decision log includes a "nickname_match" signal entry with its contribution to the final score
Multi-pet household disambiguation via color and weight
Given a household with two Labrador profiles: "Coco" (Chocolate, 70 lb) and "Koko" (Black, 55 lb) and the same owner phone And an extracted payload listing species Dog, breed Labrador, color Chocolate, and weight 69–72 lb When the Smart Pet Matcher evaluates signals including color and weight proximity Then "Coco" is ranked Top-1 with a score ≥ 0.88 and is auto-attached And "Koko" receives a score ≤ 0.75 And the decision log lists contributing signals for color match and weight proximity with their normalized values
Configurable auto-attach threshold respected at runtime
Given a tenant-level configuration setting auto-attach threshold = 0.92 And an extracted payload that yields a computed score of 0.90 for the Top-1 candidate When the Smart Pet Matcher decides attachment Then it does not auto-attach and marks the record for manual review When the threshold is updated to 0.88 and the same payload is re-evaluated Then the system auto-attaches the record to the same Top-1 candidate without code changes or redeploy And all decision logs record the threshold value used at decision time
Idempotent reprocessing prevents duplicate attachments
Given a vaccine record that was successfully attached to a Pet Profile and assigned a decision ID and payload hash When the same document or identical payload (same normalized payload hash) is re-uploaded or retried within 30 days Then the Smart Pet Matcher performs an idempotency check and does not create a duplicate attachment And it records a no-op decision referencing the original decision ID And the total number of attachments for that document remains exactly one And only one outbound notification related to this attachment exists
Negative signals block cross-household false positive
Given an extracted payload with name "Luna", species Cat, and owner phone/email not matching any existing household And an existing profile named "Luna" with species Dog in a different household When the Smart Pet Matcher scores candidates Then contact mismatch and species mismatch apply negative signals reducing the dog profile score below 0.50 And no auto-attach occurs and the record remains unassigned in a "No Confident Match" state And the decision log includes negative signal entries for "contact_mismatch" and "species_mismatch" with their contributions
Vaccine Normalization & Auto‑Attach
"As a trainer, I want vaccines to be standardized and attached to each pet’s card with correct expiration so that I can quickly verify compliance."
Description

Normalize vaccine names and abbreviations (e.g., Rabies, DHPP/DAPP, Bordetella, Lepto, CIV) to a canonical schema, parse administered/expiration dates across formats, and compute expirations per jurisdictional rules where applicable. Attach normalized shots to the matched Pet Profile Smart Card, update current status, and maintain a chronological immunization history with source provenance. Enforce idempotency to prevent duplicates and trigger existing compliance indicators and reminders in FetchFlow.

Acceptance Criteria
Normalize Common Vaccine Names and Abbreviations
Given vaccine labels Rabies, RAB, RV, DAPP, DHPP, Bordetella, Kennel Cough, Lepto, Leptospirosis, CIV, H3N2, H3N8 in an uploaded record When the record is processed Then each label is normalized to a single canonical vaccine code and display name in the schema Given any label with mixed case, punctuation, or extra whitespace When normalized Then normalization ignores case, punctuation, and superfluous whitespace Given a combo vaccine label (e.g., DHPP, DAPP) When normalized Then it maps to the same canonical entity and its component antigens are recorded per schema Given an unrecognized vaccine label When processed Then the shot is not attached, the event is flagged "Unknown Vaccine," and the raw label is stored for review Given a recognized label that maps to multiple canonical options by jurisdiction When processed Then the mapping uses the practice’s configured jurisdiction profile
Parse Dates and Compute Expirations by Jurisdiction
Given administered and expiration dates in formats such as MM/DD/YYYY, YYYY-MM-DD, DD Mon YYYY, or MM-DD-YY When parsed Then dates are stored in ISO-8601 (YYYY-MM-DD) using the practice’s configured timezone Given an administered date without an explicit expiration date When jurisdictional rules apply Then expiration is computed per the configured jurisdiction/vaccine policy and stored Given both administered and expiration dates are present When processed Then the provided expiration is used unless it violates jurisdictional constraints, in which case the record is flagged for review Given ambiguous or invalid dates (e.g., 13/40/2025 or 01/02/03 without disambiguation) When processed Then the event is not attached, and the record is flagged "Date Parsing Error" with the raw value preserved Given jurisdictions with multiple allowable durations (e.g., Rabies 1-year vs 3-year) When the record explicitly indicates duration (e.g., tag or note) Then the computed expiration respects the indicated duration; otherwise default jurisdictional policy is applied
Auto-Attach to Correct Pet Profile in Multi-Pet Household
Given an uploaded vaccine record that references a pet name matching exactly one Pet Profile under the client When processed Then normalized shots are attached to that Pet Profile Smart Card Given multiple Pet Profiles share a similar name (e.g., Milo vs Millie) When the record also contains disambiguators (e.g., species, breed, DOB) Then the system uses those attributes to select the correct profile; if still ambiguous, the record is flagged for manual review and no attachment occurs Given a document that includes shots for multiple pets with distinct name sections When processed Then each shot is attached to the corresponding Pet Profile and no shot is cross-assigned Given a record uploaded under Client A that mentions a pet name identical to a pet under Client B When processed Then no attachment is made outside Client A’s household context Given a successful attachment When complete Then the attachment includes a link to the source document and the ingestion batch ID
Idempotent Ingest and De-duplication
Given the same vaccine document is uploaded multiple times for the same client When processed Then no duplicate immunization events are created and the system records a single attachment with aggregated provenance Given an immunization event already exists for pet+canonical_vaccine+administered_date When the same event is re-ingested Then the existing event is preserved (no new row) and provenance reflects multiple sources/attempts Given a re-ingested event provides a corrected expiration date When processed Then the existing event is updated with the new expiration, the change is audited, and idempotency ensures no duplicate reminders are triggered Given two distinct administered dates for the same canonical vaccine When processed Then both events are retained in chronological history Given an ingestion request is retried with the same idempotency key after a transient failure When processed Then the final persisted state is identical to a single successful run and side-effects (attachments, reminders) are not duplicated
Update Current Status, Chronological History, and Provenance
Given new normalized shots are attached to a Pet Profile When processed Then the pet’s vaccine status for each canonical vaccine is recalculated as Up-to-date, Expiring Soon, or Expired per configured thresholds Given multiple shots for the same vaccine across time When viewed in the Pet Profile Then immunization history is displayed in chronological order (most recent first) with administered and expiration dates Given any attached shot When stored Then provenance fields include source_document_id, source_type (upload/manual/integration), uploader_user_id, ingested_at, and jurisdiction_profile_id Given a newer shot supersedes an older one for status calculations When recalculated Then the older shot remains in history, and the current status reflects the newest valid shot and its expiration Given timezone-aware date storage When rendered in the UI Then dates display in the practice’s local timezone with no off-by-one errors at midnight boundaries
Trigger Compliance Indicators and Reminder Workflows
Given a pet transitions from Expired or Expiring Soon to Up-to-date after attachment When recalculated Then any pending overdue or upcoming reminder jobs for that vaccine are cancelled or updated to reflect the new expiration Given a pet’s vaccine becomes Expiring Soon or Expired after normalization and computation When recalculated Then compliance indicators on the dashboard update and reminder workflows are scheduled according to existing FetchFlow policies without creating duplicate jobs Given compliance status changes from an ingestion When processed Then the pet’s compliance badge and counts update in the dashboard within 60 seconds Given duplicate ingestion of the same event When processed Then no additional reminders or notifications are created beyond the initial trigger, and the audit log records the deduped trigger
Ambiguity Handling & Review Queue
"As a shop owner, I want low-confidence matches routed to a simple review queue so that I can resolve them quickly without hunting through records."
Description

Route low-confidence matches and parsing uncertainties to an in-dashboard review queue with side-by-side document preview, highlighted extracted fields, ranked match suggestions, and one-click accept/reassign controls. Provide keyboard shortcuts, bulk actions, and an audit trail of reviewer decisions. Display confidence and SLA indicators, and notify staff when items require attention to keep workflows moving without leaving the SMS-first dashboard context.

Acceptance Criteria
Low-Confidence Auto Match Routed to Review Queue
Given an uploaded vaccine record yields a pet match confidence below the configurable threshold of 0.85 When auto-processing completes Then the item appears in the Review Queue within 5 seconds with status "Needs Review" And the queue row displays numeric confidence (0–100%), confidence label (Low/Medium/High), and up to 5 ranked pet candidates including pet name, photo, household, and candidate score And the review panel presents a side-by-side view with the original document preview and extracted fields highlighted with bounding boxes mapped to source text And selecting the queue item opens the review panel in under 1 second
One-Click Accept/Reassign Applies Correctly
Given a reviewer opens a queue item with suggested pet candidates When the reviewer clicks Accept on a candidate Then the vaccine records are attached to that Pet Profile Smart Card and the queue item is removed within 2 seconds And a success toast appears within 1 second and the Pet Profile timeline reflects the new records within 5 seconds When the reviewer clicks Reassign and selects a different pet or household Then the prior linkage (if any) is replaced and the new assignment is saved with the same timing guarantees And if multiple pets share the same name in the household, the UI requires disambiguation by photo or birthdate before enabling Accept
Keyboard Shortcuts Enable Efficient Review
Given focus is within the Review Queue or review panel When the reviewer presses A (Accept), R (Reassign), N (Next), P (Previous), O (Open/Toggle document), and ? (Show shortcuts) Then the corresponding actions execute within 150 ms without mouse interaction And unavailable actions are disabled and produce non-destructive feedback And the shortcuts are listed in an in-app overlay (triggered by ?) and do not conflict with common browser defaults on Chrome, Safari, and Edge (latest two versions) on macOS and Windows
Bulk Actions Process Multiple Items Reliably
Given the reviewer multi-selects items via checkboxes or Shift+Click in the Review Queue When the reviewer applies Accept to the same suggested pet or applies Reassign to a selected pet Then up to 50 items are processed within 10 seconds with a progress indicator And per-item results are surfaced; successful items are removed, failed items remain with an inline error reason and are retriable And bulk actions are atomic per item (one failure does not roll back others)
Audit Trail Captures Reviewer Decisions
Given any Accept or Reassign action is performed Then the system logs an immutable audit entry containing item ID, previous assignment, new assignment, reviewer user ID, timestamp (UTC), original confidence score, selected candidate rank, and optional reviewer note And audit entries are queryable by date range, reviewer, household, and pet, and exportable as CSV When viewing an item’s history from the review panel Then all prior actions are displayed in chronological order within 1 second
SLA Indicators and Notifications Drive Timely Review
Given each review item is assigned an SLA countdown per policy (default 24 hours) When the remaining time is >8h, between 2–8h, <2h, or overdue Then the item displays a green, yellow, red, or red-overdue badge respectively with the exact time remaining/overdue When an item breaches SLA Then the assigned staff receive a notification (per preference: SMS, email, or push) within 2 minutes including a deep link to the item And breached items are auto-prioritized to the top of the queue until resolved
In-Dashboard Slide-Over Preserves SMS Context
Given a user is in an SMS conversation thread When they open a review item from the header badge or inline notification Then a slide-over review panel opens without a full navigation, preserving the message thread state and scroll position And closing the panel returns focus to the SMS input within 200 ms And all accept/reassign actions are available in the slide-over and complete without a page reload
Client SMS Disambiguation Flow
"As a client, I want a quick text asking me to confirm which Bella the record belongs to so that my pets’ profiles stay accurate without phone calls."
Description

When household-level ambiguity remains, automatically send a concise, compliant SMS to the client with tappable options (e.g., pet names with photos) to confirm the correct pet. Ingest the reply, update the match decision, and complete attachment without staff intervention. Respect consent and rate limits, localize templates, handle timeouts/failures with fallbacks to the review queue, and log the conversation to the pet and client timelines.

Acceptance Criteria
Ambiguity Triggers SMS Disambiguation
Given a vaccine record is uploaded and auto-match yields multiple candidate pets in the household with match confidence below 0.90 And the client has SMS consent = true And the per-client disambiguation rate limit has not been exceeded When the system detects household-level ambiguity after auto-match Then the system sends a single SMS thread to the client's phone within 60 seconds containing a numbered list of candidate pet names (max 6) and a secure short link to a page with each pet's photo thumbnail And the SMS includes compliant text (e.g., "Reply STOP to opt out") and a unique correlation code for this request And the message is recorded with provider message ID and delivery status
Client Confirms Correct Pet via Tappable Option
Given the client receives the disambiguation SMS containing options 1..N and the correlation code When the client replies with a valid option number or taps the link and selects a pet Then the system validates the reply against the open request and marks the selected pet as the match And attaches the uploaded vaccine record to the selected Pet Profile Smart Card And updates the pet's vaccination records and last-verified date And marks the ingestion task Completed without staff intervention And sends a confirmation SMS to the client including the pet name And the total processing time from valid reply to attachment completion is <= 10 seconds And subsequent duplicate replies for the same request receive a non-blocking "Already confirmed" response and are ignored for matching
Freeform Reply Parsing and Error Recovery
Given the client replies with freeform text that is not a numeric option (e.g., "Bella") When the text exactly or fuzzily matches one of the presented candidate pet names (Levenshtein distance <= 2) Then the system maps the reply to that pet and proceeds as a valid confirmation When the text matches multiple candidates or matches none Then the system asks once for clarification, re-displaying the numbered options And if no valid confirmation is received after 2 clarification attempts Then the request is closed and the vaccine record is routed to the staff review queue with reason = "SMS Disambiguation Unresolved"
Consent and Rate Limit Enforcement
Given the client's SMS consent status is Opted-out or a STOP/UNSUBSCRIBE was received within the last 24 hours When ambiguity is detected Then no disambiguation SMS is sent and the record is routed to the staff review queue with reason = "No SMS Consent" Given the client is Opted-in And the client has received >= 3 disambiguation messages in the past 24 hours or a message was sent < 60 minutes ago When ambiguity is detected Then no new SMS is sent; the disambiguation is queued for 60 minutes or until limits reset, whichever is sooner And if the queue expires without sending, the record is routed to the staff review queue with reason = "Rate Limited"
Localization and Template Selection
Given the client has a locale preference stored When the disambiguation SMS is generated Then the system selects the locale-specific template and renders pet names, numbers, and compliance text accordingly And currently supported locales include en-US and es-US with parity of content and placeholders And if the locale is missing or unsupported, the en-US template is used by default And all dynamic values (pet names, links) are correctly inserted without placeholder artifacts And the link page for pet photos uses the same locale as the SMS
Timeout, Reminder, and Fallback Handling
Given a disambiguation SMS has been sent When no valid confirmation is received within 4 hours Then the system sends exactly one reminder in the same thread And if no valid confirmation is received within 24 hours of the initial message Then the request is closed and the record is routed to the staff review queue with reason = "SMS Timeout" And if a valid reply arrives after fallback, the system acknowledges with "Handled by team" and does not alter the staff queue item without manual action And all state transitions are idempotent across retries
Conversation and Outcome Logging to Timelines
Given a disambiguation flow is initiated When outbound and inbound SMS events occur Then all messages, delivery receipts, and actions are logged to the client's timeline with timestamps, sender/receiver, and correlation code And upon successful pet confirmation, the conversation and final decision are also logged to the selected pet's timeline And prior to confirmation, the conversation is associated with all candidate pets as "Pending Match" without duplicating content And logs include staff-visible reason codes for any fallback to review queue And all entries are exportable via the existing timeline export and filterable by "Auto Pet Match"
Duplicate & Conflict Resolution
"As a groomer, I want the system to detect duplicate or conflicting vaccine entries so that pet records remain clean and trustworthy."
Description

Detect and merge duplicate vaccine entries across multiple uploads by comparing normalized vaccine type, date, lot, and clinic. Prefer the most complete or most recent verified record, flag conflicting dates or vaccine types for review, and prevent double-counting toward compliance. Preserve the original source documents and provide versioned change history on the Pet Profile Smart Card.

Acceptance Criteria
Duplicate Merge by Normalized Attributes
Given a single pet has two vaccine entries from separate uploads where vaccine type normalizes to the same family, administration date matches (YYYY-MM-DD), and either lot or clinic matches after normalization When the deduplication job runs Then the two records are merged into one canonical vaccine entry And the canonical entry contains the union of non-conflicting fields (date, type, lot, clinic, expiration) with normalized values And the non-canonical records are marked as superseded, hidden from compliance, and linked as sources to the canonical And the canonical lists all originating upload IDs and timestamps
Canonical Selection: Most Complete or Most Recent Verified
Given multiple duplicate candidates exist for the same pet and vaccine family When selecting the canonical record Then the system computes a completeness score (presence of date, type, lot, clinic) and selects the highest score And if tied, prefer a record with verified = true over unverified And if multiple verified, prefer the record with the most recent administration date; if still tied, prefer the latest upload timestamp And the canonical is augmented with missing, non-conflicting fields from other candidates And a reason code is stored with the selection decision
Conflict Detection for Mismatched Types or Dates
Given two records for the same pet where lot and clinic normalize to the same values but administration dates differ When processed Then the system flags a Date Conflict and does not auto-merge Given two records for the same pet on the same administration date where vaccine types normalize to different families When processed Then the system flags a Type Conflict and does not auto-merge And both records remain visible in a Needs Review state with references to both sources and the conflict reason And conflicts are surfaced on the Pet Profile Smart Card and in the dashboard queue within 5 seconds of detection
Compliance Double-Counting Prevention
Given duplicate records have been merged for a pet When computing compliance status and counts Then only the canonical record contributes to compliance and counts reflect a single administration event Given duplicate candidates are flagged as a conflict and remain unmerged When computing compliance Then none of the conflicting records contribute until resolved or a verified canonical is selected by a user, and the UI displays a Pending Review tag Given a conflict is resolved and a canonical is selected When compliance recalculates Then the compliance state updates within 10 seconds and an audit entry is added
Source Preservation and Versioned History
Given any merge, conflict flag, or resolution occurs on a vaccine record When viewing the Pet Profile Smart Card Then original source documents remain retrievable via View Source links for each contributing record And the vaccine entry shows a versioned history including: created, merged, superseded, conflict flagged, conflict resolved — each with timestamp, actor (system/user), reason, and source references And selecting a history entry displays the exact prior field values; history entries cannot be deleted And exporting the pet's vaccine history includes the version events and source references
Household Integrity: No Cross-Pet Merges
Given two pets in the same household share the same name or similar vaccine details When deduplication runs Then no cross-pet merge occurs; only records associated with the same Pet Profile Smart Card ID are considered for merging Given a single uploaded document contains vaccine entries for multiple pets When Auto Pet Match assigns records to pets Then deduplication runs per pet independently and does not reduce the number of records across different pets And any potential cross-pet duplicate candidates are flagged as Pet Mismatch for review rather than merged

Confidence Nudge

If OCR certainty is low, FetchFlow sends a quick SMS link showing the extracted vaccine, date, and expiry. Clients tap to confirm or fix in seconds, so you get accurate, compliant records without slowing down booking or adding admin work.

Requirements

Low-Confidence OCR Trigger Thresholds
"As a business owner using FetchFlow, I want low-confidence OCR results to automatically trigger a client confirmation step so that vaccine records are accurate without me manually inspecting every upload."
Description

Define and implement confidence scoring thresholds that determine when OCR-extracted vaccine fields (vaccine name, administered date, expiry) are considered low certainty and should trigger a client nudge. Support per-field and aggregate thresholds, vaccine-name normalization/alias mapping, and configurable sensitivity by document type/quality. Integrate into the existing OCR pipeline so that nudges fire in real time during record ingestion without blocking the booking flow. Log confidence metrics for observability and allow runtime configuration via admin controls to fine-tune false positives/negatives.

Acceptance Criteria
Per-Field Low-Confidence Thresholds Trigger Nudge
Given OCR returns per-field confidence scores for vaccine_name, administered_date, and expiry_date and per-field thresholds are configured When any field’s confidence is below its configured threshold Then the system flags only those fields as low-confidence and queues a Confidence Nudge including just the flagged fields And Then no nudge is sent if all fields meet or exceed their thresholds And Then the decision outcome and the confidence values and thresholds used are recorded with a trace id
Aggregate Confidence Thresholds Trigger Nudge
Given an aggregate confidence rule (e.g., mean or minimum across fields) and an aggregate threshold are configured When the computed aggregate confidence is below the aggregate threshold regardless of individual fields Then a Confidence Nudge is queued with reason "aggregate-low" and includes all fields for verification And When the aggregate is at or above threshold and all per-field confidences meet their thresholds, no nudge is queued And Then the system ensures only one nudge is emitted per record ingestion (idempotent)
Vaccine Name Normalization and Alias Mapping
Given a maintained alias mapping to canonical vaccine names (e.g., DHPP ↔ DA2PP, RV ↔ Rabies) and a normalized-name threshold When OCR extracts a vaccine name that matches a known alias and the normalized match confidence meets or exceeds the normalized threshold Then the canonical vaccine_name is stored and no nudge is triggered for vaccine_name due to name uncertainty And Then the system logs the canonical name, original text, alias used, and pre/post-normalization confidence And When no alias resolves the OCR text, the original value and its raw confidence are evaluated against the per-field threshold as-is
Sensitivity by Document Type and Quality Profile
Given document metadata (type: PDF/scanned/handwritten) and quality metrics (e.g., DPI, blur score, skew) are available And threshold profiles are configured per document type and quality band When an OCR record is evaluated Then the system selects and applies the matching threshold profile based on type and quality band And Then the applied profile id and thresholds are logged with the decision And When no profile matches, the default profile is applied and logged
Real-Time Nudge Triggering Without Blocking Booking
Given vaccine records are ingested during an active booking flow When low-confidence is detected for any field or by aggregate rule Then nudge creation and SMS link generation completes within 3 seconds at p95 from ingestion event And Then added latency to booking APIs due to confidence evaluation is ≤ 200 ms at p95 And Then the booking flow proceeds without waiting for client confirmation of the nudge
Confidence Metrics Logging and Observability
Given any ingestion and confidence evaluation When the decision step completes Then per-field confidences, aggregate confidence, thresholds applied, profile id, decision (nudge/no nudge), and reason codes are emitted to structured logs with a trace id And Then metrics counters/gauges (e.g., nudge_rate, avg_confidence_by_field, decisions_by_doc_type) are exported to monitoring at ≥ 1-minute intervals And Then dashboards display distributions by document type and quality band over 24h and 7d windows
Runtime Admin Configuration of Thresholds and Mappings
Given an authorized admin edits thresholds, profiles, or alias mappings via the admin UI/API When the admin submits changes that pass validation (range checks, required fields, conflict detection) Then the new configuration is applied to the evaluation service within 2 minutes without service restart And Then an audit log is recorded with admin id, timestamp, environment, and configuration diff And When validation fails, the change is rejected with clear error messages and no partial application And Then admins can rollback to the previous version in a single action
Secure SMS Nudge Delivery
"As a client, I want to receive a secure, easy-to-understand text with a link to verify my pet’s vaccine info so that I can fix mistakes quickly from my phone."
Description

Generate and send a personalized SMS containing the pet’s name and a short, single-tap link to review extracted vaccine details. Use signed, single-use tokens with configurable TTL, HTTPS-only, and device-bound checks to protect PHI/PII. Comply with 10DLC/opt-out rules, include STOP/HELP handling, and apply rate limiting, retries, and quiet-hour windows. Provide failover to email where available and capture delivery status webhooks for analytics and re-send logic.

Acceptance Criteria
Personalized Secure SMS Content and Link
Given OCR certainty for a pet’s vaccine record is below the configured threshold and the client has SMS opt-in and a valid mobile number When the nudge is triggered Then an SMS is sent within 5 seconds that includes the pet’s name and a single HTTPS short link And the SMS body contains "Reply STOP to opt out, HELP for help" And the link length is <= 40 characters and contains no PHI/PII And tapping the link loads the review page showing the extracted vaccine name, administration date, and expiry for that pet
Signed Single-Use Token with Configurable TTL and Device Binding
Given a nudge link is generated When a client opens the link from the same device that received the SMS within the configured TTL Then the token signature validates, the token has not expired nor been used, and the page renders the review without additional login And the token is marked used upon successful review submission When the same link is opened again or after TTL expiry or from a different device Then no PHI/PII is displayed and the page shows an invalid/expired link message with an option to request a new link
HTTPS-Only Link and No PII in URL or Logs
Given the system generates a nudge link When building the URL Then the scheme is HTTPS only And no PHI/PII (pet name, vaccine details, client identifiers) appears in the path or query And only a signed opaque token identifies the session And any HTTP attempt to token routes is blocked with a non-redirecting 403 response And tokens and phone numbers are redacted in application logs
10DLC Compliance and STOP/HELP Handling
Given a client replies STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, or QUIT to any FetchFlow SMS When the message processor receives the reply Then the client’s SMS opt-out status is updated within 5 seconds and a confirmation is sent once And all future SMS nudges to that number are blocked Given a client replies HELP When the message processor receives the reply Then an SMS with support contact, brand identification, and opt-out instructions is sent within 10 seconds And initial outbound messages include brand identification and opt-out instructions in the footer
Rate Limiting, Retries, and Quiet-Hour Enforcement
Given multiple nudge events are queued for the same recipient or account When sending is evaluated Then per-recipient and per-account rate limits enforce the configured thresholds and excess messages are queued or dropped with reason recorded Given the current time falls within the configured quiet hours for the recipient’s time zone When a nudge is scheduled to send Then the message is deferred to the next allowed window and the planned send time is recorded Given a transient carrier error occurs When sending fails Then retries are attempted with exponential backoff up to the configured maximum and no retries occur for hard failures or opt-outs
Email Failover on SMS Failure or Opt-Out
Given SMS cannot be sent or is marked failed/undelivered or the recipient is SMS opted-out and an email address with consent is on file When failover is enabled Then an email with an equivalent secure, single-use HTTPS link is sent within 60 seconds And email quiet hours and rate limits are respected And SMS and email sends are idempotent so the client receives at most one active link per nudge event
Delivery Status Webhooks, Analytics, and Re-send Logic
Given the SMS provider posts delivery status webhooks When webhooks are received Then events are validated, deduplicated, and persisted with message ID, carrier code, and timestamps within 60 seconds And the message record state transitions to Sent, Delivered, Undelivered, or Failed accordingly And analytics counters and dashboards are updated within 5 minutes Given a status of Undelivered with a retryable reason and policy allows When outside quiet hours and within rate limits Then a re-send is scheduled via SMS or failover email per configuration And webhook payloads and logs do not include tokens, phone numbers, or PHI/PII
One-Tap Confirmation & Inline Edit Microform
"As a client on the go, I want a fast, prefilled form where I can confirm or correct my pet’s vaccine details in seconds so that booking isn’t delayed."
Description

Present a mobile-first microform that pre-fills OCR results (vaccine name, administered date, expiry) and allows one-tap Confirm or quick edits with input masks, vaccine suggestions, and date pickers. Show the uploaded document preview for context, validate formats in real time, and clearly indicate what will be saved. Optimize for sub-10-second completion, fast load over cellular, and accessibility (screen reader labels, sufficient contrast). Persist partial edits and handle expired tokens by gracefully re-issuing links.

Acceptance Criteria
Mobile Load Performance over Cellular
Given a user opens the SMS link on a modern smartphone under Slow 4G conditions (≈400 ms RTT, ≈400 kbps throughput) When the microform page loads Then First Contentful Paint occurs within 2.0 seconds And Time to Interactive is within 3.5 seconds And total transfer size for initial load is ≤ 250 KB And input latency for form controls is < 100 ms
Pre-Filled Fields and Document Preview
Given OCR results exist for vaccine name, administered date, expiry, and the uploaded document URL When the microform renders Then the vaccine name, administered date, and expiry inputs are pre-populated with the OCR values And a thumbnail preview of the uploaded document is displayed above the form And tapping the preview opens a full-screen view with pinch-to-zoom enabled And if an OCR field is missing, the corresponding input is empty and marked Required without blocking page render
One-Tap Confirmation Flow
Given the pre-filled values are unchanged and valid When the user taps Confirm Then the form submits in a single interaction And the API persists vaccine name, administered date, expiry, confirmation_source=client, method=one_tap, and timestamp And the user sees a success state within 800 ms And a 2xx response and audit log entry (including link token id) are recorded And analytics capture time-to-confirm with p90 ≤ 10 seconds from page load under Slow 4G
Inline Edits: Input Masks, Date Picker, Vaccine Suggestions
Given the user edits any field When typing dates Then an input mask enforces locale-appropriate date format and blocks invalid characters And a native/mobile date picker opens on focus When typing vaccine name Then typeahead suggestions appear after 2 characters from a curated vaccine list And selecting a suggestion populates the field And all interactive elements have accessible names, visible focus, and touch targets ≥ 44×44 pt
Real-Time Validation with Accessible Errors
Given any field value is invalid (e.g., impossible date, expiry before administered date, date in the future, vaccine name < 2 characters) When focus leaves the field or 300 ms after typing stops Then an inline error message appears under the field describing the issue And the field shows an error state with color contrast ≥ 4.5:1 And a screen reader announces the error via an ARIA live region And the Confirm button is disabled until all errors are resolved And validation occurs client-side without extra network calls
Review Summary of Saved Data
Given all fields are valid When the confirmation area is visible Then a read-only Will be saved summary displays vaccine, administered date, and expiry exactly as they will be stored (respecting locale) And the summary matches the payload sent to the API And a concise consent note is displayed: By confirming, you consent to update this pet's vaccine record And no hidden or additional fields are persisted
Partial Save and Expired Token Recovery
Given the user has modified any field When the session is interrupted (navigate away, app backgrounded, or connection lost) Then the draft is saved to secure local storage within 200 ms And returning via the same link within 7 days with a valid token restores draft values and scroll position When the link token is expired on open Then an Expired link screen appears with a one-tap Send me a new link action And tapping it issues a fresh SMS link within 30 seconds and invalidates the old token
Real-Time Record Update with Audit Trail
"As a business owner, I want corrected vaccine data to update immediately with a traceable history so that I remain compliant and can resolve disputes."
Description

On submission, validate and immediately write confirmed/corrected vaccine data to the Pet Profile Smart Card, marking the record as client-verified. Maintain full version history with diffs (OCR output, user corrections, timestamp, user identity, origin link), and emit internal events to update bookings, reminders, and compliance checks. Provide rollback, conflict detection, and idempotent writes with retries to prevent duplication. Ensure data retention and privacy compliance with export/delete on request.

Acceptance Criteria
Immediate Client-Verified Vaccine Update to Smart Card
Given a client confirms or corrects vaccine data via the SMS link When the backend receives the submission Then the system validates the payload and writes the vaccine fields to the Pet Profile Smart Card within 2 seconds And the record is marked client-verified with the client's user ID and UTC timestamp And all read surfaces (API and dashboard) return the updated values within 5 seconds of write completion
Audit Trail Versioning and Diffs
Given any create, update, or rollback of a vaccine record When the write succeeds Then a new immutable version with an incremented version number is appended And the version stores OCR output, user corrections, field-level diff from the prior version, UTC timestamp, actor identity (client or staff), and origin link And authorized users can retrieve the full version history and diffs via API and UI
Internal Events Drive Downstream Updates
Given a vaccine record is persisted after confirmation or correction When the write completes Then events VaccineRecord.Updated and Compliance.StatusEvaluated are published within 2 seconds with an idempotency key and version ID And bookings, reminders, and compliance checks consume the event and reflect the new status within 10 seconds And no duplicate charges, blocks, or notifications occur if the same event is delivered multiple times
Rollback to Previous Version
Given an authorized staff user selects a previous vaccine record version When they initiate a rollback with a reason Then the system creates a new version that restores the selected values and metadata as applicable And logs the rollback actor, reason, and UTC timestamp in the audit trail And publishes the same downstream events as an update And preserves all prior versions without deletion
Conflict Detection and Safe Resolution
Given two or more updates target the same vaccine record within overlapping time frames When the second update is processed against a stale version Then the system detects the version mismatch via optimistic concurrency (etag/version) And rejects the stale write with a conflict status without overwriting the latest version And records the conflicting payload and diff for review And exposes an API endpoint to retrieve conflicts for manual merge or retry
Idempotent Writes and Retry Behavior
Given multiple identical submissions with the same idempotency key arrive within 15 minutes When the backend processes the requests Then only one persistent version is created and subsequent identical requests return the original version ID and a 200 response And only one set of internal events is published And transient failures trigger automatic retries with exponential backoff up to 3 attempts without creating duplicates
Data Export and Deletion Compliance
Given a verified data subject export request for a pet's vaccine records When the request is submitted Then a secure, machine-readable export containing all versions, diffs, timestamps, actor identities, and origin links is delivered within 72 hours And given a verified deletion request When deletion is processed Then PII and vaccine record content are deleted from primary systems within 30 days and from backups within 35 days or irreversibly anonymized where legally required And analytics datasets exclude the subject's data within 7 days And a completion log and confirmation are issued
Reminder & Escalation Workflow
"As a business owner, I want automated reminders and a fallback workflow for missing confirmations so that bookings stay on track without manual follow-up."
Description

If the client does not respond within a configurable window, send up to N automated reminders spaced by policy, respecting quiet hours and time zones. After final reminder, route the case to a staff review queue and optionally request a fresh upload. Do not block in-progress booking unless the business’s compliance rules require it; instead, annotate the booking with a pending-verification flag. Stop reminders upon confirmation or staff resolution and record all steps in the audit log.

Acceptance Criteria
Automated Reminder Scheduling with Quiet Hours and Time Zones
Given a pending verification case created at T0 with policy window W, reminder count N, spacing S, quiet hours [Qstart, Qend], and client time zone tzClient When no client response is received by T0 + W Then the system schedules up to N reminders in tzClient spaced by S between sends And no reminder is sent during [Qstart, Qend] in tzClient; reminders falling in quiet hours are deferred to the next allowed time and spacing is recalculated from the actual send time And reminders honor daylight saving time transitions in tzClient And if N = 0, no reminders are sent And the first scheduled reminder time is visible in the case details
Immediate Stop of Reminders on Client Confirmation or Staff Resolution
Given scheduled reminders exist for a case When the client confirms or corrects the record via the SMS link OR a staff member marks the case resolved Then all queued reminders for that case are canceled within 60 seconds And no further reminders are sent for that case And the case status is updated to Verified or Resolved accordingly And an audit entry records the stop action and the reason
Final Reminder Escalation to Staff Queue with Optional Fresh Upload Request
Given the final (Nth) reminder has been sent and no response is received within the configured post-final grace period G When G elapses Then the case is added to the staff review queue with metadata (client, pet, vaccine, attempts=N, lastAttemptAt) And duplicate escalations are prevented for the same case And if “Request Fresh Upload on Escalation” is enabled, a one-time upload request message is sent to the client at the next allowed send time respecting quiet hours and tzClient And an audit entry records the escalation and any upload request
Non-Blocking Booking with Pending-Verification Flag Unless Compliance Requires Blocking
Given a client starts or continues a booking while their vaccine verification case is pending When the business’s compliance rule for bookings is Non-Blocking Then the booking flow proceeds without blocking And the booking is annotated with a visible “Pending Verification” flag for staff And no checkout or payment step is delayed by the pending verification When the business’s compliance rule for bookings is Blocking Then the booking cannot be finalized until verification is complete And the user sees a clear message indicating the blocking reason and required action
End-to-End Audit Logging of Reminder and Escalation Lifecycle
Given reminders are scheduled, sent, deferred, canceled, or escalated When any of these events occur Then an immutable audit log entry is created capturing caseId, eventType, timestamp (UTC), actor/system, channel, destination, templateId, outcome (success/failure), and error details if applicable And audit entries are viewable in case details in order and exportable And audit entries exist for client confirmation, staff resolution, and escalation creation
Configurable Policy Validation and Application
Given an admin configures the reminder policy (window W, count N, spacing S, quiet hours [Qstart, Qend], time zone basis) When the policy is saved Then inputs are validated: W ≥ 0, N is an integer ≥ 0, S > 0, Qstart ≠ Qend and defines a valid interval, and time zone basis is selected And the saved policy is applied to new cases created after the change And existing cases keep their original policy unless explicitly re-applied by staff And the effective policy is displayed in the case details
Admin Review Queue & Resolution Tools
"As staff, I want a centralized queue to resolve vaccine confirmation exceptions so that I can keep records compliant with minimal time."
Description

Provide a dashboard queue listing nudges that are unresolved, expired, or conflicting. Display OCR values, confidence scores, client edits, and document previews. Allow staff to accept, amend, request re-upload, or mark verified, with bulk actions and filters (status, location, vaccine type, age). Enforce role-based permissions, notify clients of outcomes, and update the Pet Profile and audit trail in real time. Include SLA indicators to highlight items risking booking delays.

Acceptance Criteria
Review Queue Shows Unresolved, Expired, and Conflicting Nudges
Given at least one nudge exists with status unresolved, expired, or conflicting across any location When a user with queue access opens the Admin Review Queue Then the queue displays only nudges with statuses unresolved, expired, or conflicting And each item displays status, vaccine type, pet identifier, received timestamp, and location And items not matching these statuses are excluded from the queue view And the queue supports pagination or infinite scroll to view all items
Nudge Detail View Shows OCR, Confidence, Edits, and Document Preview
Given a nudge is selected from the queue When the detail panel/view loads Then the OCR-extracted vaccine name, vaccination date, expiry date, and confidence score are displayed And the confidence score shows as a numeric percentage with one decimal place And any client edits from the Confidence Nudge flow are listed with timestamp and field-level before/after values And a preview of the uploaded document is rendered with zoom and page navigation for multi-page files And if the document cannot be previewed, a download link is provided with a descriptive error state
Single-Item Resolution Actions: Accept, Amend, Request Re-upload, Mark Verified
Given a user with appropriate permissions views a nudge detail When the user selects Accept Then the system saves the OCR values as the current record and marks the nudge as resolved And the item is removed from the queue within 5 seconds When the user selects Amend and updates fields Then field validation enforces valid dates (no future vaccination date, expiry after vaccination date) and allowed vaccine types And on save, the amended values become the current record and the nudge status updates to resolved When the user selects Request Re-upload Then the system sets the nudge status to awaiting re-upload and triggers a client re-upload request And the item remains in the queue with a pending badge until a new document is received When the user selects Mark Verified Then the vaccine record is flagged as verified with user, timestamp, and source noted
Bulk Actions and Queue Filters
Given multiple nudges are selected in the queue When the user initiates a bulk action (Accept, Request Re-upload, Mark Verified) Then a confirmation dialog shows the count of items to be affected And upon confirmation, the action is applied to all eligible items And a results summary shows counts of successes and failures, with reasons for any failures Given the queue has items across different attributes When the user applies filters for status, location, vaccine type, and age (days since receipt) Then the queue updates to show only items matching all selected filters And the applied filters are visible as removable chips And clearing filters restores the full queue
Role-Based Permissions Enforcement
Given an unauthenticated user attempts to access the Admin Review Queue When the page loads Then access is denied and no queue data is returned Given an authenticated user without VaccineReview permission attempts a resolution action When they click any of Accept, Amend, Request Re-upload, or Mark Verified Then the action controls are disabled or hidden, and any direct API call returns 403 Forbidden Given a user role with Reviewer permission When viewing the queue Then they can Accept, Amend, and Request Re-upload but cannot Mark Verified unless they also have Manager permission
Client Notifications and Real-Time Profile Updates with Audit Trail
Given a resolution action is completed on a nudge When the action is saved Then the Pet Profile vaccine record is updated in real time (within 5 seconds) with the new values and status And an audit log entry is created capturing user, timestamp, action, and field-level before/after values Given an outcome requires client messaging (Accept with confirmation, Request Re-upload, or Amend impacting client data) When the action is saved Then an SMS notification is sent using the correct template for the outcome including pet name, vaccine, and next steps And notification delivery status is recorded (queued, sent, failed) and visible on the nudge detail And on failure, a retry option is available
SLA Indicators for Items Risking Booking Delays
Given a pet with an unresolved, expired, or conflicting vaccine nudge has a scheduled booking within the next 24 hours When the queue renders Then the item displays an SLA indicator labeled At Risk with time-to-booking countdown Given the time-to-booking is less than 2 hours and the nudge remains unresolved When the queue refreshes Then the item displays a Breach indicator and is visually prioritized at the top Given an item no longer meets At Risk or Breach conditions When the underlying data changes (resolution or booking canceled) Then the SLA indicator is removed within 5 seconds and the item returns to normal ordering
Analytics & Threshold Tuning Controls
"As an owner, I want to see how the nudge performs and tune thresholds so that I balance client friction with data accuracy."
Description

Capture end-to-end metrics for the Confidence Nudge funnel: trigger rate, delivery success, open/click, confirm vs. correct split, time-to-confirm, reminder effectiveness, and impact on booking speed and compliance. Provide an admin dashboard to visualize trends, export data, and safely adjust confidence thresholds, message templates, and reminder cadence with guardrails. Support A/B testing of SMS copy and link UX to optimize completion while minimizing client friction.

Acceptance Criteria
Funnel Event Capture & Attribution
- Given a low OCR confidence result triggers a Confidence Nudge, When the nudge is generated, Then an analytics event is persisted with fields: nudge_id, business_id, client_id_hashed, pet_id, booking_id_nullable, ocr_model_version, confidence_score, trigger_reason, template_variant_id, link_variant_id, timestamp_ms. - Given an SMS send is attempted, When the carrier returns a status, Then delivery_success (true/false) and failure_reason_code are recorded and ingestion latency is <= 60 seconds for 99.5% of events. - Given the client receives the SMS, When the link is opened and the flow is completed, Then open, click, confirm, correct events are recorded with time_to_open_ms and time_to_confirm_ms, and confirm_vs_correct_ratio is computable per day. - Given reminders are sent, When a client completes after a reminder, Then completion is attributed to reminder_n and reminder_effectiveness is computable by n across the selected date range. - Given duplicate event deliveries, When the same event_id is received more than once, Then only one record is stored (idempotency enforced). - Given analytics data storage, When events are written, Then PII is not stored in analytics fact tables (client_id_hashed only) and data retention is 18 months (configurable).
Impact Metrics: Booking Speed & Compliance
- Given a booking was initiated for a pet requiring vaccine verification, When a Confidence Nudge is used, Then time_to_booking_completion_ms is recorded and an impact metric delta_vs_baseline is computed daily. - Given vaccine compliance outcomes, When a client confirms or corrects to a valid record, Then the booking's compliance_status becomes compliant and is attributed to the nudge in compliance_uplift metrics. - Given a control cohort is enabled, When X% of eligible nudges are suppressed by feature flag, Then booking speed and compliance metrics are reported separately for treatment vs control. - Given a date range and segment filters, When impact metrics are queried, Then KPIs (booking_speed_delta, compliance_uplift, no_show_rate_change) are returned with 95% CI when sample size >= 200 per segment; otherwise an insufficient_sample flag is shown.
Admin Dashboard Visualization & Filtering
- Given an admin with permission views the Analytics dashboard, When a date range and filters (location, service type, provider, template variant, link variant) are applied, Then all funnel metrics render within 2 seconds (P50) and 5 seconds (P95). - Given the dashboard loads, When no data matches filters, Then an empty state appears with an option to reset filters. - Given a metric card is clicked, When drilldown is requested, Then a trend chart (daily) and a table of recent nudges (last 100) display with columns: nudge_id, status, variant, time_to_confirm_ms. - Given accessibility requirements, When the dashboard is navigated via keyboard and screen reader, Then it meets WCAG 2.1 AA for focus order, labels, and contrast. - Given real-time needs, When new events arrive, Then the dashboard reflects data with <= 5-minute freshness and a visible freshness indicator.
Data Export & Scheduling
- Given an admin selects Export, When CSV or JSON format is chosen with filters and timezone, Then an export is generated with a documented schema and delivered within 2 minutes for files up to 100k rows. - Given large datasets, When export exceeds 100k rows, Then files are chunked into parts of 100k rows with deterministic naming and an accompanying manifest file. - Given security requirements, When exports are prepared, Then files are encrypted at rest; scheduled exports can be delivered via time-limited secure link or SFTP with SSH key. - Given auditing needs, When an export is created or downloaded, Then an audit log entry is recorded with user_id, timestamp, filter_params, row_count, and destination. - Given data integrity, When timestamps are exported, Then they are ISO 8601 with timezone offset, and numeric IDs are strings to avoid precision loss.
Confidence Threshold Guardrails & Rollout Controls
- Given threshold tuning, When an admin sets the OCR confidence threshold, Then allowed values are 0.50–0.95 in 0.01 increments and out-of-range inputs are blocked with validation messaging. - Given a proposed threshold change, When the admin requests preview, Then projected trigger_rate_delta based on the last 30 days displays with 95% CI. - Given safety requirements, When saving a threshold change, Then a change reason is required, a two-step confirmation is shown, and the change is logged with before/after values, user_id, and timestamp. - Given rollout controls, When a change is published, Then the admin can select rollout scope by location and percentage (10–100%) and can rollback to the previous version in one click. - Given permissions, When a non-admin accesses tuning controls, Then controls are hidden or disabled and API returns an authorization error.
Template & Reminder Cadence Editor with Safeguards
- Given template editing, When an admin edits SMS copy, Then variables validate (required placeholders present), character and segment counts display, and saving is blocked if > 2 segments unless an explicit override is confirmed. - Given compliance, When a template is saved, Then opt-out text and brand name placeholders are required and a tracked short link is auto-inserted. - Given preview needs, When a template is previewed, Then a live example renders with sample pet name, vaccine, date, expiry, and short link. - Given versioning, When changes are saved, Then a new version is created, previous versions are retained, and rollback is available. - Given reminder cadence configuration, When admin sets reminders, Then guardrails enforce 0–2 reminders, minimum 6 hours between reminders, quiet hours 20:00–08:00 local, and stop conditions (completion, opt-out, hard bounce).
A/B Testing Framework for SMS Copy & Link UX
- Given experiment creation, When an admin defines 2–3 variants and a traffic split, Then eligible nudges are randomly bucketed with consistent assignment per client_id_hashed for the test duration. - Given metrics, When the experiment runs, Then primary (completion_rate) and secondary metrics (time_to_confirm_ms, correction_rate, open_rate) are computed per variant daily. - Given statistical reporting, When sample size per variant >= 200, Then the dashboard shows lift, p-value, and 95% confidence intervals; otherwise it shows collecting data. - Given governance, When a test is active, Then a maximum duration of 28 days or 10,000 nudges per arm is enforced; early stop is allowed when p-value < 0.05 and minimum sample is met. - Given test management, When a winner is selected, Then the winning variant can be promoted to default in one click, the test is archived, and all results are exportable.

Booster Guard

Applies species‑ and region‑aware rules to validate dose intervals, rabies validity, and kennel requirements. Flags gaps or early/invalid doses, proposes grace windows, and pre‑schedules renewals—preventing day‑of surprises and keeping policies consistent.

Requirements

Species- and Region-Aware Rule Engine
"As a groomer using FetchFlow, I want vaccine compliance automatically determined by species and my region so that my bookings and reminders always follow the right policies without me memorizing local rules."
Description

Implement a centralized, versioned rules service that evaluates vaccination compliance by species (e.g., dog, cat) and region (state/province/country), with support for facility context (grooming, boarding, daycare). The engine must interpret rabies validity windows, minimum/maximum dose intervals for multi-dose series, early/invalid dose detection, and kennel policy nuances. It should ingest vaccine records from Pet Profile Smart Cards and manual uploads, normalize lot/date data, and return structured decisions (compliant/non-compliant), reasons, next-due calculations, and effective grace windows. Expose the engine via an internal API for use by booking flows, reminders, and payment gating, ensuring consistent decisions across SMS, mobile app, and dashboard.

Acceptance Criteria
Region-TX Dog Grooming: Rabies Expired Within Configured Grace Window
Given species=dog, region_code="TX", facility_type="grooming", rules.rabies.validity_years=3 and rules.rabies.grace_window_days=14 And vaccine_history contains rabies dose with administered_at = today - (3 years + 5 days) When the engine evaluates compliance for evaluation_date = today Then compliant = false and reason_codes includes "RABIES_EXPIRED_WITHIN_GRACE" And grace_window.start = (administered_at + 3 years) + 1 day and grace_window.end = (administered_at + 3 years) + 14 days And next_due.date = administered_at + 3 years and next_due.vaccine_code = "RABIES" And rule_version is present and non-empty And identical requests from booking, reminders, and payment gating return byte-identical decision JSON
Puppy DAPP Series: Early Second Dose Detection and Next-Due Recalculation
Given species=dog, vaccine_series="DAPP", rules.series.min_interval_days=21, rules.series.max_interval_days=35 And vaccine_history contains dose_1 at day 0 and dose_2 at day 14 When the engine evaluates series compliance Then compliant = false and reason_codes includes "EARLY_DOSE" And normalized_history marks dose_2.invalid = true with invalid_reason = "BEFORE_MIN_INTERVAL" And next_due.earliest_date = dose_1 + 21 days and next_due.latest_date = dose_1 + 35 days And decision includes actionable.recommended_repeat = "DAPP dose_2" with recommended_on = dose_1 + 21 days
Facility Context Split: Boarding vs Grooming Bordetella Interval
Given species=dog, region_code="NY", rules.bordetella.boarding.max_age_days=180 and rules.bordetella.grooming.max_age_days=365 And vaccine_history contains bordetella administered_at = today - 200 days When the engine evaluates with facility_type="grooming" Then compliant = true and reason_codes does not contain "BORDETELLA_OUT_OF_DATE" When the engine evaluates with facility_type="boarding" Then compliant = false and reason_codes includes "BORDETELLA_OUT_OF_DATE" and next_due.date = administered_at + 180 days
Multi-Source Ingestion: Normalize and Deduplicate Smart Card and Manual Records
Given Smart Card provides vaccine label "Rabies 1YR" with administered_at="2025-01-10" and manual upload provides "Rabies (1-year)" with administered_at="01/10/2025" and lot numbers differ And normalization rules map both labels to vaccine_code="RABIES_1YR" and parse dates in UTC When the engine ingests both sources for the same pet Then normalized_history collapses these into one dose with administered_at="2025-01-10T00:00:00Z" and source=["smart_card","manual"] And the compliance decision is identical whether evaluated with only Smart Card data, only manual data, or combined And duplicate detection window tolerance is <= 1 day and is reported as normalization.duplicates_resolved = 1
Internal Evaluate API: Contract, Determinism, and Performance
Given a POST to /internal/rules/v1/compliance/evaluate with valid body {pet_id, species, region_code, facility_type, vaccine_history, evaluation_date} When the request is processed under 100 requests/second for 5 minutes Then 200 OK responses have schema containing [compliant:boolean, reasons:array, next_due:object, grace_window:object|null, rule_version:string, evaluated_at:timestamp] with 99.9% conformance And P95 latency <= 200 ms and P99 latency <= 400 ms And responses are deterministic: the same input (including evaluation_date and rule_version, if provided) yields a stable SHA256 hash across SMS, app, and dashboard invocations And invalid input returns appropriate errors: 400 INVALID_REGION for unknown region, 422 VALIDATION_ERROR for missing required fields
Rules Versioning and Time-Bound Rollout by Region
Given region_code="CA" has rule_set v1 effective_until=2025-09-30 and v2 effective_from=2025-10-01 And two evaluations are made with evaluation_date="2025-09-30" and evaluation_date="2025-10-01" When the engine evaluates without explicit rule_version Then decisions include rule_version="v1" for 2025-09-30 and rule_version="v2" for 2025-10-01 And re-evaluating the same input with as_of_rule_version="v1" yields the same decision as the original v1 decision (byte-identical) And a signed decision_trace_id is returned that can be used to reproduce the decision with the same rule_version and inputs
Renewal Pre-Scheduling Suggestion Within Lead Window
Given rules.renewal_lead_days=30 and a rabies vaccination due_date="2025-09-15" When the engine evaluates on "2025-08-25" Then actionable.renewal_suggestion.proposed_date is between "2025-08-16" and "2025-09-15" (inclusive) And actionable.renewal_suggestion.window = {start: due_date - 30 days, end: due_date} And the suggestion includes vaccine_code="RABIES", reason="DUE_SOON", and respects facility_type-specific constraints if any
Regional Policy Data Sync & Versioning
"As an operator managing multiple locations, I want regional policy updates applied automatically and safely versioned so that each location stays compliant without manual reconfiguration."
Description

Provide a background synchronization service and admin console to source, maintain, and version regional vaccination and kennel requirements (e.g., rabies validity, county-specific boosters, boarding rules). Support effective-dated rule bundles, change logs, and rollback. Include manual overrides for jurisdictions lacking structured feeds, with approvals and publish workflows. Cache active policies per region and alert staff when updates impact current bookings. Ensure fail-safe defaults and clear fallbacks when policy data is unavailable, and log all decisions for auditability.

Acceptance Criteria
Background Sync of Regional Policy Bundles with Versioning
Given the sync scheduler runs at the configured interval and valid credentials, when a sync cycle starts, then the system fetches policy feeds for all configured regions within 5 minutes and stores each as a versioned draft bundle with source timestamps, checksums, and provenance. Given a feed response is unchanged (HTTP 304 or matching checksum), when processing completes, then no new version is created for that region. Given a feed item fails schema validation, when processing the region, then the item is skipped, an error is logged with region and item identifiers, a notification is raised within 2 minutes, and other regions continue processing. Given a transient feed error occurs, when retry policy executes, then the system retries with exponential backoff up to 3 attempts; on final failure marks the region as sync-degraded and retains the last active bundle. Given the same sync cycle is retriggered, when it runs again, then processing is idempotent and results in the same set of bundle versions.
Effective-Dated Rule Bundles and Activation
Given an approved versioned bundle with effective_start T and optional effective_end U, when the bundle is published, then it becomes Active at T for its region and the previously active bundle is set to Superseded without data loss. Given a booking with service_time S in region R, when policies are resolved, then the bundle whose effective window contains S is applied. Given no bundle covers S for region R, when policies are resolved, then the system applies the configured fail-safe default bundle, records fallback_reason "no_active_bundle", and returns policy_source "default". Given a bundle is future-dated, when admins preview policies for a date within its window, then the API returns the previewed bundle without changing the active bundle.
Change Log, Diff, and Rollback
Given two bundle versions A and B for the same region, when viewing changes, then the UI/API returns a field-level diff including added/removed/modified rules, thresholds, and effective dates. Given a bundle is edited or published, when saved, then the change log records actor, timestamp, action (create/edit/approve/publish/rollback), reason, source (feed/manual), and previous_version_id. Given an authorized admin selects a prior version V and requests rollback with a new effective window, when approved, then the system creates a new bundle copying V’s content, assigns a new version_id, and publishes it per the requested effective dates. Given any rollback, when history is queried, then all intermediate versions remain intact and discoverable.
Manual Overrides with Approval and Publish Workflow
Given a jurisdiction without a structured feed, when an editor drafts or edits a regional policy bundle, then the bundle is saved in Draft state with full validation and inline errors. Given a Draft is submitted for review, when routed, then at least one Approver (not the author) must approve before it can be published; self-approval is blocked. Given a bundle with validation errors is submitted, when validation runs, then submission is rejected with specific error messages and no state change. Given a review request is sent, when notifications are processed, then approvers receive in-app and email notifications within 2 minutes with deep links to the review screen. Given an Approver rejects the submission with comments, when the author views the bundle, then status is Rejected with comments preserved and the bundle returns to Draft.
Caching Active Policies and Fallback Defaults on Outage
Given an active bundle exists per region, when the policy service starts or redeploys, then it loads all active bundles into cache within 3 seconds. Given a publish event occurs for region R, when caches refresh, then all application instances invalidate and refresh R’s cached bundle within 30 seconds. Given a cache miss or policy service outage, when a policy lookup occurs, then the API responds within 500 ms using the last-known-good cache or the fail-safe default and includes policy_source ("last-known-good" or "default") and a fallback_reason. Given an admin triggers cache purge for region R, when executed, then the cache for R is cleared and repopulated on next read without downtime.
Impact Alerts for Current Bookings on Policy Update
Given a new bundle is published for region R, when activation is scheduled, then the system computes impacted future bookings in R whose previously evaluated policies would now fail or change and generates an impact list with reasons per booking. Given impact analysis completes, when alerts are dispatched, then staff sees a dashboard alert within 5 minutes summarizing counts by severity and can drill down to affected bookings. Given staff marks a booking as addressed, when impact alerts refresh, then the booking is suppressed from future alerts for that change set. Given no bookings are impacted, when alerts are dispatched, then the system logs a zero-impact event and does not notify staff.
Decision Logging for Policy Evaluations
Given a policy evaluation occurs during booking creation, modification, or check-in, when rules are applied, then the system logs: request_id, actor, region, bundle_version_id, rule outcomes (pass/fail/reason), fallbacks used, and timestamps. Given logs are generated, when queried by authorized roles, then logs are searchable by date range, region, booking_id, and bundle_version_id and exportable to CSV. Given logs exceed a retention window of 365 days, when cleanup runs, then logs older than 365 days are archived or purged per policy with an audit record of the action. Given an unauthorized user attempts to access decision logs, when access control checks run, then access is denied and the attempt is recorded in the audit log.
Dose Interval Validator & Gap Flagging
"As a scheduler, I want the system to flag early or missing doses and show when a pet is next eligible so that I can avoid last-minute cancellations and guide clients to fix issues ahead of time."
Description

Create a validation module that evaluates each vaccine series against species-specific schedules, checking minimum/maximum intervals, valid dose sequencing, and recognition of early/invalid administrations. Detect expired rabies, missing core vaccines, and incomplete series; compute and display human-readable flags and next-eligible dates. Integrate flags into booking, check-in, and SMS threads, with configurable behavior (warn, require upload, block service) and prominent dashboard banners to prevent day-of surprises.

Acceptance Criteria
Interval Validation for Core Canine Series at Booking
Given a dog with DA2PP dose 1 recorded on 2025-07-01 and species rules requiring dose 2 after a minimum of 21 days and within 28 days When a user attempts to book dose 2 for 2025-07-15 Then the system flags the dose as Early and displays: "Earliest eligible date: 2025-07-22; grace window: 2025-07-22–2025-07-29" And the next-eligible date is stored and exposed via API and UI And the booking action respects current enforcement mode (no block/warn handled separately) Given a user attempts to book dose 2 for 2025-07-25 (inside the window) When validation runs Then no interval flag is shown and the dose is considered valid Given a previously uploaded dose 2 dated 2025-07-10 When validation runs Then the dose is marked Invalid - Early and excluded from series completion calculations And the next-eligible date remains 2025-07-22
Region-Aware Rabies Expiry Block at Check-In
Given a cat in region NY with a rabies vaccination expiring on 2025-08-05 and a service with enforcement mode set to Block When the staff initiates check-in on 2025-08-11 Then the system blocks check-in with a banner: "Rabies expired 6 days ago (NY rules). Upload valid record or reschedule." And the flag severity is Critical and visible on the appointment card and pet profile And the check-in decision (allow/block) is computed in under 200 ms on the device Given a pet with a valid 3-year rabies per NY rules and label When validation runs at check-in Then no rabies expiry flag is shown and remaining validity days are displayed Given the enforcement mode is Warn instead of Block When the same expired case is checked in Then check-in is allowed but a dismissible warning is logged to the audit trail with timestamp and user
Missing Vaccine Prompt in SMS Thread Before First Service
Given a new client books a boarding service 3 days in the future and the pet lacks Bordetella per species/service rules When the booking is confirmed Then within 1 minute the SMS thread receives an automated message listing missing vaccines and a secure upload link And the message includes next-eligible dates where applicable and a short human-readable explanation And the link click is tracked and correlated to the pet record Given the client uploads a valid Bordetella record via the link When validation completes Then the SMS thread posts a confirmation: "Bordetella received and valid through <date>" And the booking flag is cleared automatically Given no upload occurs When 24 hours have passed since the first SMS Then a single reminder SMS is sent; no more than 2 total reminders are sent before the appointment
Enforcement Mode Configuration Per Service Applies at Booking and Check-In
Given business-level enforcement is Warn and the Boarding service override is Block When a Boarding appointment is booked with an expired rabies Then the booking flow is blocked with a clear banner and resolution options (Upload/Reschedule) Given the same pet books a Grooming service that inherits Warn When validation runs Then booking proceeds with a visible warning and an action to request upload Given an admin changes Boarding enforcement from Block to Require Upload When a pet with missing core vaccines reaches check-in Then the check-in flow requires a successful upload before allowing start, and logs the enforcement decision with user, time, and rule source
Dashboard Banner Highlights Vaccine Gaps Across Today’s Appointments
Given there are appointments today with Critical (expired rabies) and High (missing core series) flags When the dashboard loads Then a top banner displays aggregated counts by severity and a "View All Flags" action And clicking the banner opens a filtered list of affected pets/appointments with reasons and next-eligible dates And counts in the banner match the filtered list exactly And the banner does not display if there are zero Critical/High flags And initial banner render completes within 500 ms on a dataset of 200 appointments
Early Dose Marked Invalid and Excluded from Series Completion
Given a dog has DA2PP dose 1 on 2025-06-01 and an uploaded dose 2 on 2025-06-10 (min interval 21 days) When validation runs Then dose 2 is labeled Invalid - Early with a tooltip explaining the rule breach And series completion status remains Incomplete And the next-eligible date for dose 2 is computed as 2025-06-22 and displayed Given a replacement valid dose 2 is uploaded for 2025-06-25 When re-validation runs Then the invalid early dose is ignored in sequencing And the series status updates to Complete (dose 2) And the flag is cleared and the history retains both records with statuses for audit
Auto Pre-Scheduling of Rabies Renewal with Grace Window
Given a dog’s rabies expires on 2025-09-10 and the region rule allows a 30-day renewal window When the system detects T-30 days on 2025-08-11 Then it creates a tentative renewal appointment within the grace window and sends an SMS with Confirm/Reschedule options And the proposed slots respect staff availability and service duration And if the client confirms via SMS, the appointment is marked Confirmed and the flag changes to Scheduled And if the client declines, reminders are snoozed for 7 days and no more than 3 prompts are sent before expiry And if a new rabies record is uploaded, the tentative appointment is auto-cancelled and the flag is cleared
Grace Window Configuration & Controlled Overrides
"As a location manager, I want configurable grace windows and a tracked override process so that I can accommodate edge cases without breaking policy consistency or losing audit trails."
Description

Enable business-level configuration of grace periods per vaccine/region and facility type, including default and maximum allowed values. Provide a controlled override workflow for staff: capture reason, duration, approver, and scope (single visit vs. package), with automatic expiry and audit logging. Reflect active grace windows and overrides in eligibility checks, receipts, and SMS messages, ensuring transparency to clients and consistency across booking, payment, and reminder flows.

Acceptance Criteria
Configure Grace Windows by Vaccine/Region/Facility
Given a user with Manage Policies permission When they set default grace = 14 days and max grace = 30 days for Rabies in CA, Facility = Boarding Then the system saves the values, returns 200, and persists the configuration with a unique ID Given default > max or any value is negative or non-integer days When saving the configuration Then the system rejects the change with 422, field-level errors, and no update is persisted Given configurations exist for multiple vaccine/region/facility combinations When retrieving via Settings API or UI Then the exact values and effective precedence are returned, including version, editor ID, and timestamp
Eligibility Check Applies Configured Grace Rules
Given a pet’s Rabies expired 5 days ago and business default grace for Rabies in CA Boarding is 14 days When staff attempts to book a Boarding service Then eligibility = "Within Grace", booking is allowed, and UI shows banner with exact grace expiry date Given a pet’s Rabies expired 45 days ago and max grace is 30 days When staff attempts to book the same service Then eligibility = "Not Eligible", booking is blocked with reason "Rabies beyond max grace (30 days)" Given a dose is recorded early/invalid per rules When eligibility is evaluated Then the dose is not considered valid and grace does not apply; rationale is surfaced in the result payload
Create and Approve Single-Visit Override
Given a staff member initiates an override for a specific appointment When they enter a non-empty reason (>= 10 characters), duration = 3 days, select an approver, and submit Then an override request is created in Pending status and the approver is notified Given the approver opens the pending request When they approve it Then the override activates immediately, captures approver ID, sets an exact expiry timestamp, and eligibility for that appointment becomes "Overridden" Given the requested duration exceeds the configured max grace for the vaccine/region/facility When submitting or approving the override Then the system rejects with 422 and message "Duration exceeds configured maximum"
Package-Level Override Scope
Given a client holds a 5‑session grooming package When an override with scope = Package and duration = 10 days is approved Then all sessions scheduled on or before the expiry timestamp are marked "Overridden", and sessions after expiry are not affected Given additional package sessions are scheduled after the override expiry When the schedule is updated Then no new sessions inherit the override and standard eligibility rules apply
Automatic Expiry and Audit Logging
Given an active override or grace window with expiry at T When the current time passes T Then the system automatically deactivates the override/grace without manual action and recalculates eligibility on next check Given any create/update/approve/revoke/expire action on grace configurations or overrides When viewing the audit log Then each entry includes action type, before/after values, actor user ID, target (vaccine/region/facility or visit/package ID), timestamp (UTC), and reason (if provided)
Client Transparency via Receipts and SMS
Given a visit is completed under a grace window When the receipt is generated Then it includes a line item note "Serviced under vaccine grace until <date>" and excludes internal staff notes/reasons Given an appointment is booked within grace When the booking confirmation SMS is sent Then the SMS contains a friendly reminder of the renewal date and a link to update vaccine records Given an override is active with expiry in ≤ 72 hours When reminder messages are generated Then the client receives an SMS stating the override expiry date and steps to update the vaccine record
Consistent Eligibility Across Booking, Checkout, and Reminders
Given a pet is within grace or has an active override When booking, checkout, and reminder services query eligibility Then all services return the same status and expiry from a single shared service/API, with matching rationale codes Given an override expires between booking and checkout When staff attempts to collect payment Then checkout enforces the same eligibility rule as booking, blocking or warning with the identical message and code until the record is updated or the visit is rescheduled
Renewal Pre-Scheduler & SMS Reminders
"As a pet owner, I want timely SMS reminders with an easy link to update vaccine records so that my pet stays eligible and I don’t lose appointments."
Description

Automatically generate renewal tasks and tentative appointments based on upcoming expirations and lead times per vaccine and region. Orchestrate multi-touch SMS reminders with Smart Card links for clients to upload new records or book vet visits, escalating cadence as expiry nears. Hold calendar slots when configured, release them if documents arrive, and update compliance status in real time. Provide analytics on renewal conversion and lead time effectiveness to reduce day-of failures.

Acceptance Criteria
Renewal Task and Tentative Appointment Generation
Given a pet profile with vaccine expiration dates and region- and species-specific lead-time rules configured And the business timezone and pre-scheduler job are configured and enabled When the pre-scheduler runs at its scheduled time Then it creates one renewal task per vaccine that is within its lead time window and not already renewed And sets the task due date to the expiration date minus the configured lead time And if hold-calendar is enabled, it creates a tentative appointment within the allowed window and marks it as Hold And it avoids conflicts with confirmed bookings and respects staff availability constraints And it is idempotent so reruns do not create duplicate tasks or holds for the same pet-vaccine pair And an audit log entry is recorded with timestamp, rule version, and actions taken
Multi-Touch SMS Reminder Cadence and Content
Given a client with SMS consent and a pending renewal task with due date D And a reminder schedule configured as T-14, T-7, T-3, T-1, and T0 days relative to expiration And business quiet hours are configured When each reminder trigger time occurs Then the system queues exactly one SMS per trigger outside quiet hours, delaying to the next allowed send time if needed And each SMS contains the pet name, vaccine name, expiration date, and a unique Smart Card link And delivery status (queued, sent, delivered, failed) is recorded per message And if the client completes renewal via Smart Card upload or books a vet visit, all future reminders for that vaccine are canceled within 5 minutes
Hold Calendar Slot and Auto-Release on Document Receipt
Given a tentative Hold appointment created by the pre-scheduler for a vaccine renewal And a grace window in days is configured When a valid vaccine document is uploaded via Smart Card and passes Booster Guard validation Then the system releases the held slot within 30 seconds, cancels the renewal task, and marks the pet compliant And a confirmation SMS is sent to the client and a notification is shown to the business, respecting quiet hours And if the grace window elapses without a valid document, the hold converts per business setting (confirm or auto-cancel) and the task status updates accordingly And calendar, task list, and compliance indicators reflect the change in real time and an audit log entry is recorded
Real-Time Compliance Status Update and Flagging
Given a new vaccine record is received via upload, email import, or manual entry When Booster Guard validates dose dates, intervals, rabies validity, and kennel requirements per species and region Then the pet compliance status updates within 30 seconds across Pet Profile, Booking, and Day-Of views And services requiring vaccination become eligible or ineligible immediately per rule outcomes And any gaps or invalid or early doses are flagged with reason codes and next-action suggestions And reminders and holds for satisfied vaccines are canceled while invalid records keep the cadence active
Renewal Analytics and Lead-Time Effectiveness
Given at least 30 days of renewal activity exists When the renewal analytics dashboard is opened Then it displays conversion rate, average days-to-renew, median touches-to-conversion, and percent renewed before expiration, each filterable by vaccine, species, and region And it shows cohort comparisons across different lead-time configurations over time And counts reconcile to underlying renewal tasks within plus or minus 1 percent or plus or minus 5 records, whichever is greater And CSV exports mirror on-screen filters and totals And the dashboard data refreshes daily by 02:00 local business time
Regional Rule Application and Grace Window Proposal
Given region- and species-specific rules for rabies validity, booster minimum intervals, and kennel requirements are configured When computing renewal due dates and scheduling holds Then the system prevents scheduling before the earliest valid booster date and after the legal validity lapse And it proposes a grace window per region policy and displays it in the task and client SMS And if multiple pets for a client have overlapping grace windows within 3 days, reminders are consolidated into a single thread per send time And all decisions persist rule version identifiers for traceability
SMS Compliance, Opt-Out, and Quiet Hours Enforcement
Given a client consent state and business quiet hours are configured When the system attempts to send a renewal reminder SMS Then messages are blocked if consent is revoked, the number is on do-not-contact, or carrier lookup indicates a landline And STOP, START, and HELP keywords are processed in real time, updating consent state and sending appropriate replies And no reminders are sent during quiet hours; triggers in quiet hours are deferred to the next allowed time And all messages include the business name, opt-out instructions, and functional Smart Card links with click-through tracked
Compliance Profiles & Eligibility Gates
"As an admin, I want to attach compliance profiles to services so that eligibility checks happen automatically and clients see exactly what’s required for their appointment."
Description

Offer reusable compliance profile templates (e.g., Boarding, Daycare, Grooming) that map required vaccines, intervals, grace rules, and regional variations by species. Allow assigning profiles to services, staff calendars, and locations, then enforce them as eligibility gates during booking, reminders, and check-in. Surface clear, client-friendly explanations via Smart Cards and SMS, and support A/B configurations to evaluate policy adjustments without disrupting operations.

Acceptance Criteria
Create and Save Species- and Region-Aware Compliance Profile Template
Given I am an admin user with permission to manage compliance profiles When I create a new template named "Boarding" scoped to Species=Canine and Region=CA with required vaccines (Rabies 1yr or 3yr, DHPP, Bordetella), dose intervals, and grace rules Then the system validates that each vaccine has a validity period, minimum dose interval, and grace-window definition and prevents save with field-level errors if any are missing Given the template passes validation When I save it Then the template is versioned (v1), assigned a unique ID, and appears in the profile library within 5 seconds Given template v1 is saved When I create template v2 by editing rules Then v1 remains immutable and v2 becomes the default for new assignments, with change log capturing editor, timestamp, and diff Given the template is scoped to Species=Canine and Region=CA When I view available profiles for Feline or Region=NY Then this template is not listed for assignment
Assign Compliance Profiles to Services, Calendars, and Locations
Given a saved profile template When I assign it to Services=[Boarding, Daycare], Staff Calendars=[Team A], and Locations=[Oakland] Then the assignment is saved and visible on each target within 10 seconds with inheritance and override indicators Given a service-level override for grace-window days When I set the override and save Then the effective rule set for that service displays the override value and all other rules from the base profile Given an assignment is removed When I unassign the profile from a location Then future bookings at that location no longer evaluate against the profile and the unassignment is recorded in the audit log
Eligibility Gate at Booking (SMS and Dashboard)
Given a client initiates a booking for Boarding via SMS and their pet's Rabies expires before the service start date When the system evaluates eligibility Then the booking flow blocks confirmation and returns a single SMS explaining the failing items, required actions, and earliest eligible date based on grace rules Given the same evaluation in the dashboard When a staff member attempts to confirm the booking Then the Confirm button is disabled with inline reasons and links to view the pet's Smart Card Given a failing vaccine has an allowed grace window covering the service dates When eligibility is re-evaluated Then the booking may proceed and the system pre-schedules a renewal reminder task to the client 14 days before grace end Given a client uploads a new vaccine record during the SMS flow When the document is verified as valid and within dose interval rules Then eligibility updates in real time and the booking can be completed without restarting the flow
Eligibility Gate at Check-In with Controlled Override
Given a client arrives for check-in with a pet failing Bordetella interval rules When staff opens the check-in screen Then the system blocks check-in and presents actionable reasons and a Smart Card link Given a manager has override permissions When the manager enters an override reason, selects an expiration date, and attaches supporting documentation Then the check-in is allowed, the override is logged with user, timestamp, reason, and expiry, and the pet remains flagged for follow-up after expiry Given an override reaches its expiry When the pet is scheduled again Then eligibility reverts to standard rules and the system no longer treats the expired override as valid
Client-Facing Smart Card and SMS Explanations
Given a pet has compliance gaps for a service When the client opens the Smart Card link Then the card shows the pet’s name and photo, a checklist of required items with pass/fail status, per-item due/expiry dates, grace status, and clear next-step actions Given an eligibility failure during booking When the system sends an SMS explanation Then the message uses client-friendly language within 320 characters, includes the failing items and earliest eligible date, and contains a shortened Smart Card URL Given a client resolves an item by uploading a record When the record is verified Then the Smart Card and SMS status updates reflect compliance within 10 seconds
A/B Policy Configuration and Measurement
Given two variants of a Boarding compliance profile (A: grace 7 days, B: grace 14 days) When I start an experiment with 50/50 random assignment for new bookings over a defined date range Then bookings are evaluated against their assigned variant without leakage between groups Given the experiment is running When I view experiment metrics Then I can see at minimum: blocked-booking rate, override rate, reschedule rate, no-show rate, and average time-to-check-in per variant Given the experiment reaches the minimum sample size When I end the experiment Then the system preserves assignments for existing bookings, stops assigning new ones, and exports results as CSV with per-variant outcomes and confidence intervals

Vet Ping Verify

One tap sends a verification request to the listed clinic via secure link, email, or fax. Clinics confirm dates or upload a stamped record; the Smart Card is marked “Clinic Verified” with a timestamp—ideal for compliance‑first workflows and adoption events.

Requirements

Multi-channel Clinic Outreach
"As a groomer using FetchFlow, I want to send a verification request to a clinic in one tap via the clinic’s preferred channel so that I can quickly obtain verified vaccine status without leaving my SMS workflow."
Description

Enable one-tap dispatch of verification requests from the pet’s Smart Card and job thread, supporting secure tokenized web links, email, and fax with automatic channel fallback. Each request includes the pet’s name, photo, owner consent attestation, target vaccines/records needed, and a unique request ID. Provide branded email/fax templates with clinic-friendly instructions, attach a QR code and short URL, and track delivery, open, and bounce events. Implement per-clinic contact preferences, rate limiting, retries with exponential backoff, and localized content. Expose send status in the FetchFlow dashboard and notify the business via SMS on key milestones (opened, confirmed, uploaded).

Acceptance Criteria
One-Tap Dispatch from Smart Card and Job Thread
Given a user is viewing a pet’s Smart Card with a linked clinic and owner consent is available or collected inline When the user taps “Verify via Clinic” Then the system sends a verification request using the clinic’s preferred channel, displays a confirmation with the unique request ID, and logs the action in the audit trail And the same one-tap flow is available and functional from the job thread And if no valid clinic contact method exists, the send action is blocked and the user sees an error with guidance to add contact details
Payload Completeness and Consent Attestation
Given a verification request is dispatched When it is generated Then the outbound payload includes: pet name, pet photo (or placeholder if unavailable), clinic name, owner consent attestation text with owner name and timestamp, the specific vaccines/records requested, and a unique request ID And the secure link preloads the pet identifiers and requested items for the clinic And an immutable audit log stores the full payload snapshot associated to the request ID
Multi-Channel Fallback with Secure Tokenized Link
Given the clinic’s preferred channel fails (e.g., email bounce, fax busy or no answer) When the system detects a non-success delivery status Then it retries the failing channel per configured attempts and backoff and then falls back to the next channel in the clinic’s preference order without creating a new request ID And all outbound channels contain the same tokenized, request-scoped link that is revocable and expires per org policy; expired or invalid tokens show a secure error page And once any channel is opened or the clinic submits, the request is marked accordingly and further fallback attempts stop
Branded Templates with QR Code and Short URL
Given email or fax is selected as the channel When the request is sent Then the message uses the branded template (business name + FetchFlow branding) with clinic-friendly, step-by-step instructions and support contact And the message includes both a QR code and a short URL that resolve to the same secure verification page And the email renders correctly in major clients (Gmail, Outlook, iOS Mail, Android) and the fax QR code remains scannable from a standard 200–300 dpi printout
Event Tracking, Dashboard Status, and Business SMS Notifications
Given a verification request has been sent When delivery, open, bounce, confirmation, or record upload events occur Then each event is captured with request ID, channel, and UTC timestamp and updates the request timeline in the FetchFlow dashboard within 60 seconds of receipt And duplicate events from multiple channels are de-duplicated at the request ID level And the business receives SMS notifications for opened, confirmed, and uploaded milestones, respecting configured do-not-disturb hours and user notification preferences
Per‑Clinic Contact Preferences and Localization
Given a clinic profile defines contact methods, channel preferences, language, and timezone When a verification request is created Then the system selects the default channel and content language according to the clinic profile, falling back to a sensible order (email → fax → link) if unspecified And all timestamps shown to the clinic are localized to the clinic’s timezone, while storage remains in UTC And localized templates (subject lines, body text, instructions) render with correct placeholders and grammar for the selected locale
Rate Limiting, Retries with Exponential Backoff, and Idempotency
Given multiple verification requests are initiated to the same clinic within a short interval When the cumulative send rate exceeds the configured per-clinic threshold Then the system blocks additional sends, surfaces a non-blocking warning with next-allowed time, and records the rate-limit event And transient delivery failures trigger automatic retries with exponential backoff and jitter up to the configured maximum attempts per channel And repeated user taps within a short window produce a single request due to an idempotency key tied to the pet, clinic, and vaccine set
Clinic Directory & Contact Resolver
"As a business owner, I want FetchFlow to automatically find and validate a clinic’s best contact method so that my verification requests reach the right inbox or fax the first time."
Description

Provide a searchable, de-duplicated directory of veterinary clinics linked to each pet’s listed provider, resolving validated email and fax endpoints plus phone and hours. Auto-suggest clinics based on name, city, and phone, verify email deliverability and fax capability, and store a preferred verification channel per clinic. Allow manual overrides and per-request edits with inline validation. Maintain change history and consent records. Expose an API-backed resolver service used by Outreach to select the best contact, with graceful fallback and logging for auditing and support.

Acceptance Criteria
Clinic Search and De-duplication
Given the directory contains duplicate clinic records with matching normalized phone number and address, When the de-duplication job runs, Then only one canonical clinic record is retained and all duplicates reference the canonicalClinicId. And When a user searches for that clinic, Then only the canonical record appears in results and it includes phone and hours. And Then the canonical record has a stable clinicId and a merge log entry listing source recordIds and timestamps.
Pet Provider Linking to Clinic Directory
Given a pet profile with no linked provider, When a user selects a clinic from the directory, Then the pet is linked to the clinicId and the clinic’s name appears on the profile. And When the selected clinic is later merged, Then the pet linkage is updated to the new canonicalClinicId without data loss. And Then multiple pets can link to the same clinic without conflicts.
Auto-suggest by Name, City, or Phone
Given a user types at least 3 characters in the clinic search field, When matches exist by name or city, Then the top 5 suggestions appear within 300 ms (p95) and are ranked by relevance. And Given a 10-digit numeric input, When a phone number matches exactly, Then that clinic is ranked first and highlighted as “Phone match”. And Then no duplicate clinics appear in suggestions for the same canonicalClinicId.
Email Deliverability and Fax Capability Verification
Given a clinic email is added or changed, When verification runs, Then the email status is set to Deliverable, Unknown, or Undeliverable with a checkedAt timestamp and TTL of 30 days. And Given a clinic fax number exists, When capability check runs, Then fax status is set to Capable or Not Capable with lastChecked timestamp. And Then endpoints with status Undeliverable/Not Capable are excluded from selection by the resolver and flagged in the UI.
Preferred Verification Channel Persistence and Enforcement
Given a clinic has at least one validated endpoint, When a user sets a Preferred Verification Channel (email, fax, phone), Then the preference is saved to the clinic record with updatedBy and updatedAt. And When the resolver is called, Then it selects the preferred channel if its status is valid; otherwise it falls back according to policy. And Then removing the preference reverts selection to policy order without errors.
Manual Overrides and Per-request Edits with Inline Validation
Given a verification request is being prepared, When a user edits the email or fax endpoint inline, Then invalid formats are rejected client-side with error messages and the Save button remains disabled until valid. And When the user applies an override “for this request only”, Then the override is used for dispatch but does not persist to the clinic record. And When the user chooses to persist the override, Then the clinic record is updated and a change history entry is created with before/after, actor, timestamp, and reason. And Then capturing consent is required when sending to a newly added endpoint, and the consent record is stored with scope, actor, timestamp, and retention of at least 12 months.
Resolver API: Best Contact Selection, Fallback, and Auditing
Given a clinicId and request context are provided, When Outreach calls POST /resolver, Then the response includes selectedChannel, endpoint, confidence score, and a reasons array, and returns within 200 ms (p95). And When the preferred channel is invalid, Then the resolver falls back in order: validated preferred -> validated email -> validated fax -> phone (manual follow-up) -> none. And When no valid endpoints exist, Then the API responds 422 no_valid_contact with a correlationId and guidance message. And Then every resolution attempt writes an audit log with clinicId, inputs, decision path, outcome, and correlationId accessible to support; logs are retained for 12 months.
Clinic Verification Link Portal
"As a clinic staff member, I want a secure, one-click link to confirm dates or upload a stamped record so that I can complete verification quickly without creating an account."
Description

Deliver a mobile-friendly, no-login portal for clinic staff to complete requests securely via a signed, expiring link. Pre-fill pet details and requested records, allow staff to confirm vaccine types and dates, add notes, and optionally upload a stamped record (PDF/JPG/PNG). Capture staff name, clinic role, and consent to attest accuracy, then apply a cryptographic signature to the response. Validate file types and size, provide drag-and-drop and camera capture, support reCAPTCHA, and ensure links expire or invalidate after use. On submission, return a confirmation page and push results to FetchFlow via a webhook with redundancy and idempotency keys.

Acceptance Criteria
Signed, Expiring, Single‑Use Link Controls
Given a verification request has a signed, time‑boxed link When clinic staff opens the link before its expiration and it has not been used Then the portal validates the signature and renders the request details And the request state is marked In Progress for audit Given the link is accessed after expiration or after a successful submission When the link is opened Then the portal shows an Expired or Already Used message with no pet PII And all input controls are disabled And an audit event is recorded with timestamp, IP, and user agent Given the link signature is missing, malformed, or does not match the payload When the link is accessed Then the portal returns HTTP 403 and renders no request data
Mobile‑Friendly, No‑Login Clinic Portal Rendering
Given a clinic staff member opens the link on a modern mobile browser When the portal loads Then no login or account creation is required to view and complete the request And the layout adapts to common mobile and desktop breakpoints without horizontal scrolling And all interactive controls (date pickers, file inputs, checkboxes, buttons) are touch‑friendly and accessible via keyboard And the page loads over HTTPS with a valid certificate and HSTS enabled
Pre‑Filled Pet and Requested Records Display
Given a valid signed link maps to a specific pet and requested vaccine records When the portal renders Then it displays the pet name, photo (if available), and species/breed And it lists each requested vaccine type exactly as requested by FetchFlow And non‑editable request metadata (clinic name, requester, request ID) is shown for context And no unspecified vaccines are pre‑selected or displayed as requested
Vaccine Confirmation and Date Entry Validation
Given the portal lists requested vaccine types When staff confirms vaccine details Then each vaccine row allows entry of an administered date using a date control And dates must be valid calendar dates, not in the future, and formatted per locale And leaving a vaccine unconfirmed requires either a Not Available/Unknown selection or a note And per‑field validation errors are shown inline and prevent submission until resolved
Stamped Record Upload with File Validation and Camera Capture
Given staff chooses to attach a stamped medical record When a file is selected via drag‑and‑drop, file picker, or camera capture Then only PDF, JPG, or PNG types are accepted And files exceeding the configured size limit are rejected with a clear error And exactly one file may be attached; selecting a new file replaces the previous one And a thumbnail or filename with size is shown prior to submission with a remove option
Staff Identity, Role, Attestation, and Cryptographic Signature
Given staff is completing the verification When they enter their full name and clinic role and check the attestation of accuracy Then the Submit button becomes enabled And submission is blocked if name, role, or attestation is missing And upon submit, the response payload is signed server‑side with the platform private key, including link ID, timestamp, pet ID, vaccines, notes, staff name, role, and attestation flag And the stored signature verifies successfully with the corresponding public key identifier
Submission Confirmation, reCAPTCHA, and Webhook Delivery with Idempotency
Given all required fields are valid and any reCAPTCHA challenge is presented When staff completes the reCAPTCHA and submits the form Then the portal displays a confirmation page with a success message, request ID, and timestamp And the link is invalidated for further edits or resubmission And a webhook is sent to FetchFlow containing the full response, a unique idempotency key, and signature metadata And on transient delivery failures (network/5xx), the system retries with exponential backoff within the configured retry window using the same idempotency key And duplicate deliveries to the same endpoint are not processed more than once as confirmed by a single accepted response
Smart Card Status & Audit Trail
"As a groomer, I want the Smart Card to show a timestamped "Clinic Verified" status with an audit log so that I can prove compliance at check-in and resolve disputes."
Description

Upon clinic confirmation, update the pet’s Smart Card with a "Clinic Verified" badge showing clinic name, method (link/email/fax), verifier identity if provided, and an immutable timestamp. Display verified vaccine types and expirations, and surface warnings for upcoming expirations. Store an audit trail including request ID, channel, message metadata, file hashes for uploaded documents, IP and user agent of the clinic session, and all status transitions (Sent, Opened, Confirmed, Rejected, Expired). Enable export of a verification certificate (PDF) and provide role-based access controls for viewing or revoking verifications. Sync status to booking and check-in flows for compliance gating at adoption events.

Acceptance Criteria
Clinic Verified Badge Rendering on Smart Card
Given a pending verification linked to a pet’s Smart Card When a clinic confirms via secure link, verified email reply with attachment, or processed fax upload Then the Smart Card displays a “Clinic Verified” badge within 5 seconds of server confirmation And the badge shows clinic legal name, channel in {link,email,fax}, verifier full name if provided, and a server-generated immutable timestamp in ISO 8601 UTC And any attempt via UI or API to edit the timestamp, channel, or clinic name is rejected with HTTP 403 and no data change And when verifier identity is not provided, the UI displays “Clinic Staff” in place of a name And the badge appears on the pet profile header and list cards consistently across iOS, Android, and web
Verified Vaccine Types, Expirations, and Warning States
Given a clinic confirmation includes vaccine records with type and expiration date When the records are persisted Then the Smart Card displays each vaccine type with formatted expiration (MMM DD, YYYY) And a yellow warning appears when a vaccine will expire within 30 days; a red “Expired” state appears on or after the expiration date And warnings clear within 60 seconds after a newer expiration date is saved And if no vaccines were confirmed, the UI shows “No verified vaccines” with no warnings And date comparison is timezone-independent and performed at UTC midnight
Immutable Verification Audit Trail Capture
Given a verification request is created When any verification event occurs (create, send, open, confirm, reject, expire, revoke) Then an audit entry is appended with requestId (UUIDv4), petId, clinicId, channel, message metadata (to/from, delivery ids), status, event timestamp (UTC), IP and user agent for link sessions, file hashes (SHA-256) for any uploaded documents, and actor id when applicable And audit entries are append-only and cryptographically chained via previousHash (SHA-256) for tamper evidence And read APIs return entries in chronological order and require roles {Admin, Staff} with permission “Compliance:View” And any modify or delete attempt on audit entries returns HTTP 405 and is logged
Status Lifecycle and Transitions (Sent, Opened, Confirmed, Rejected, Expired)
Given a verification request is issued Then its initial status is Sent When the clinic opens the secure link Then status becomes Opened once; repeated opens do not create duplicate transitions When the clinic confirms Then status becomes Confirmed and captures verifier identity if supplied When the clinic rejects Then status becomes Rejected and a rejection reason (min 10 characters) is required When no action occurs for the configured TTL (default 7 days) Then status becomes Expired automatically And only transitions allowed are Sent->Opened, Sent->Expired, Opened->Confirmed, Opened->Rejected, Opened->Expired; all others are blocked and logged And all transitions are recorded in the audit trail with timestamps within 200 ms of server event time
Verification Certificate PDF Export
Given a Smart Card with status Confirmed When a user with role {Admin, Staff, Owner} and permission “Compliance:Export” requests a certificate Then a PDF is generated within 5 seconds at the 95th percentile And the PDF includes pet name and photo, owner name, clinic name and address, channel, verifier (if provided), confirmation timestamp (UTC), verified vaccines and expirations, requestId, and a document SHA-256 hash And the PDF embeds a QR code that resolves to a read-only verification details page showing the same requestId and hash And the file name follows PetName_ClinicVerified_YYYYMMDD_requestId.pdf And generation failures return HTTP 503 with a correlationId and do not alter verification state
Role-Based Access for Viewing and Revoking Verifications
Given RBAC permissions are configured When a user accesses verification details Then permissions are enforced as Admin=View/Revoke/Export, Staff=View/Export, Owner=View, Clinic=No access after session end And users without required permissions receive HTTP 403 with no sensitive fields in the payload When an Admin performs a revoke with a required reason Then the Smart Card badge is removed, status changes to Revoked with timestamp, and an audit entry is appended And revoked verifications are blocked from export and booking compliance; existing certificate QR resolves to a page marked “Revoked” And all access and revoke actions are logged with actor id and timestamp
Compliance Gating in Booking and Check-in Flows
Given an adoption event or compliance-first workflow with required vaccines configured When a booking or check-in is attempted Then progression is blocked if any required vaccine is missing or expired or the Smart Card is not Clinic Verified And the block message lists unmet requirements and offers “Send Vet Ping” if the user has permission And upon successful confirmation during the session, the gate clears in real time without reloading And Admins can override with a mandatory reason; overrides are recorded in the audit trail and expire at event end And decisions use the latest verification state and vaccines at decision time to avoid stale passes
Reminders & Escalation Workflow
"As a busy operator, I want automated reminders and channel escalation so that I don’t have to manually chase clinics and can keep schedules compliant."
Description

Automate follow-ups for unanswered requests with configurable schedules (e.g., 24h, 72h, 7d), switching to alternate channels on non-delivery or non-open. Provide a dashboard queue of pending verifications with bulk nudge, pause/resume, and retry controls. Notify the business via SMS when clinics view or respond. Track SLA timers and highlight at-risk pets for upcoming appointments. Support per-clinic quiet hours, holiday calendars, and throttle windows to reduce spam. Expose request states (Draft, Sent, Opened, Bounced, Confirmed, Rejected, Expired) and reasons, with filters and exports for operations.

Acceptance Criteria
Auto Follow‑up Schedule and Channel Escalation
Given a clinic has at least two delivery channels configured with a defined follow-up schedule (e.g., 24h, 72h, 7d) When a request is Sent and remains neither Opened, Confirmed, nor Rejected at each threshold Then a follow-up is queued within 10 minutes of the threshold and logged with the attempt number and timestamp And if the previous attempt was Delivered but not Opened, the system sends the follow-up on the next available channel in the clinic’s escalation order And if the clinic Opens or Responds before a scheduled follow-up, all future follow-ups for that request are canceled and the schedule is marked Completed And all automated follow-ups respect per-clinic quiet hours, holidays, and throttle limits
Bounce Detection and Alternate Channel Failover
Given a request attempt via email or fax returns a permanent failure (bounce) with a provider error code When the bounce is received Then the request’s state updates to Bounced with the captured reason code and timestamp And the system triggers a failover attempt on the next available channel within 5 minutes, respecting quiet hours, holidays, and throttles And an audit log entry records the bounce and failover channel And if no alternate channel exists, the system records reason NoAlternateChannel and halts further sends until manually retried or the request expires per schedule
Verification Queue Bulk Controls
Given the dashboard queue lists pending verifications with states and reasons When a user selects multiple requests and clicks Nudge Then the system enqueues a follow-up for each selected request that is eligible, and displays counts of Enqueued and Skipped with skip reasons When a user clicks Pause on a request Then automated follow-ups for that request stop scheduling and the request displays Paused, while SLA timers continue tracking When a user clicks Resume on a paused request Then the next follow-up is scheduled according to the remaining schedule and constraints When a user clicks Retry on a failed attempt Then the system immediately attempts the next eligible channel and logs the retry outcome
Real‑time Business SMS Notifications on Clinic Activity
Given the business has a verified notification phone number and notifications enabled When a clinic opens the verification link Then an SMS is sent to the business within 2 minutes indicating the clinic viewed the request, including pet name and clinic name When a clinic confirms, rejects, or uploads a stamped record Then an SMS is sent to the business within 2 minutes with the outcome and a link to the request And duplicate notifications for the same event within 5 minutes are suppressed And notifications are rate‑limited to a maximum of 5 per business per 15 minutes
SLA Timer Tracking and At‑Risk Highlighting
Given a request is associated to an upcoming appointment with an SLA threshold configured (e.g., 48h prior) When the request is Sent Then an SLA countdown timer starts and is visible on the request row and detail view When the remaining time falls below the SLA threshold and the request is not Confirmed Then the pet is flagged At‑Risk in the queue and appointment view and sorted to the top of the list When the request becomes Confirmed or Rejected Then the SLA timer stops and the At‑Risk flag is cleared And SLA breach events are logged and available for export
Per‑Clinic Quiet Hours, Holidays, and Throttle Windows
Given a clinic has quiet hours (e.g., 20:00–08:00 local time), a holiday calendar, and a throttle rule (e.g., max 2 messages per 30 minutes) When a follow-up would occur during quiet hours or on a holiday Then the send is deferred to the next permissible window and the queue displays the scheduled send time When multiple messages are eligible within a throttle window Then only up to the throttle limit are sent and the remainder are deferred with reason Throttled And both automated and bulk actions respect these constraints consistently
Request States, Reasons, Filtering, and Export
Given requests may transition through states Draft, Sent, Opened, Bounced, Confirmed, Rejected, Expired with associated reason codes where applicable When state changes occur Then each transition is recorded with previous state, new state, reason (if any), actor/system, and timestamp When a user filters the queue by state, reason, clinic, date range, and at‑risk flag Then the results update within 1 second for up to 5,000 records and reflect accurate counts When the user exports the current view Then a CSV is generated within 30 seconds containing all visible columns plus state, reason, and SLA fields, and the row count matches the filtered total
Record OCR & Data Extraction
"As a groomer, I want uploaded clinic records automatically parsed into structured vaccine dates so that the Smart Card stays accurate without manual data entry."
Description

When a clinic uploads a stamped record, run OCR and structured data extraction to identify vaccine types (e.g., Rabies, DHPP, Bordetella), administration and expiration dates, and lot numbers. Normalize results against FetchFlow’s vaccine taxonomy, detect duplicates, and compare to existing Smart Card data. Use confidence thresholds with a human-in-the-loop review queue for low-confidence fields, and store the original document alongside parsed fields. Support multi-page, multi-pet documents, common clinic templates, and handwritten entries where feasible. On acceptance, automatically update the Smart Card and trigger alerts for discrepancies or missing vaccines.

Acceptance Criteria
OCR Field Extraction with Confidence Scoring
Given a clinic-uploaded stamped vaccine record (PDF/JPG/TIFF) with typed entries for a single pet When OCR and data extraction run Then the system extracts per vaccine: vaccine type, administration date, expiration date, and lot number And assigns a confidence score (0–1) for each extracted field And normalizes all dates to ISO 8601 format (YYYY-MM-DD) And records the source page number and bounding box for each field
Vaccine Taxonomy Normalization
Given a raw extracted vaccine label (e.g., Rabies, DA2PP, DAPP, DHPP, Bordetella) When normalization runs Then the label is mapped to the canonical FetchFlow vaccine type per taxonomy (e.g., DA2PP/DAPP/DHPP -> DHPP) And unknown or unmapped labels are set to UNKNOWN and routed to review And the canonical vaccine type is stored with the extracted record
Duplicate Detection and Merge
Given the Smart Card already contains vaccine entries for the pet When a newly extracted vaccine entry matches an existing entry by canonical type and administration date within ±1 day or by matching lot number and expiration date Then the entry is marked as a duplicate candidate and no new duplicate record is created And field-level merge occurs, preferring higher-confidence values and preserving both sources in the audit log And a deduplication note is added to the vaccine record with criteria used for the match
Human-in-the-Loop Review Queue and Thresholds
Given the default field confidence threshold is 0.85 When any extracted field has confidence < 0.85 or conflicts with existing Smart Card data Then a review task is created in the Vet Ping Verify queue containing: field name, extracted value, confidence score, conflicting value (if any), and a cropped image of the source region And the affected fields are blocked from updating the Smart Card until reviewer approval When a reviewer approves or corrects values Then the Smart Card and parsed fields are updated, and an audit entry is recorded with reviewer ID, timestamp, and before/after values
Smart Card Update, Verification Stamp, and Alerts
Given all extracted fields for the pet meet confidence thresholds or have been approved in review When the record is accepted Then the Smart Card is updated with normalized vaccine entries And the Smart Card is marked Clinic Verified with a timestamp and clinic identifier And discrepancy flags are shown for missing or conflicting vaccines And an alert is generated to the dashboard for missing or expired vaccines and for vaccines expiring within 30 days
Multi-Page and Multi-Pet Association
Given a multi-page document that may include multiple pets from the same owner When OCR and extraction run Then entries from all pages are processed And each vaccine entry is associated to the correct pet by exact name match on the document to a pet in the account; if ambiguous or not found, the entry is routed to review with candidate matches And handwritten fields are accepted when confidence ≥ 0.85; otherwise they are routed to review with the cropped image
Original Document Storage and Audit Trail
Given a stamped record is uploaded and processed When parsing completes Then the original document file and a viewable preview are stored and linked to the pet’s Smart Card And versioned parsed data is stored alongside the original with a reference to source page and bounding boxes And all automated merges, reviewer actions, and Smart Card updates are captured in an immutable audit log accessible from the vaccine record
Compliance, Consent & Privacy Controls
"As a business owner, I want clear consent capture and privacy safeguards so that my clinic verifications meet event and insurer requirements and protect client data."
Description

Capture and store owner consent to contact the listed clinic, including purpose and scope, and embed consent attestations in each request. Enforce encryption in transit and at rest, signed link tokens, and scoped access by role. Provide configurable retention and purge policies for verification artifacts and documents, along with export and deletion workflows. Maintain system audit logs for administrative access and changes. Offer event-mode settings for adoption events that require stricter verification freshness windows and printable certificates. Publish a data processing notice and clinic terms on the portal, and support organization-level configuration for jurisdictions with additional requirements.

Acceptance Criteria
Owner Consent Capture and Attestation Embedding
Given a user initiates a Vet Ping Verify request for a pet When the consent dialog is presented Then the dialog displays purpose of contact, scope of data requested, clinic identity, and retention summary And requires explicit opt-in (unchecked checkbox + confirm action) before enabling Send Given consent is granted When the request is transmitted via link, email, or fax Then the request includes a signed consent attestation containing owner name, pet name, clinic, purpose, scope, request ID, and consent timestamp And the consent record is stored and linked to the request, pet, owner, organization, and clinic Given consent is not granted When the user attempts to send a request Then the system blocks sending and shows an error indicating consent is required
Encryption, Signed Links, and Role-Scoped Access
Given any data in the Vet Ping Verify workflow Then all data is encrypted in transit (TLS 1.2+) And encrypted at rest (AES-256 or equivalent) with managed key rotation per policy Given a clinic portal link is generated When the clinic opens the link Then the token is signed, single-use, scoped to the specific request and clinic, and expires after a configurable duration (default 7 days) And expired or already-used tokens are rejected with a non-disclosing message Given a staff member attempts to view a verification artifact Then access is authorized only for users with roles that include the relevant org/location scope And unauthorized access is denied and logged
Configurable Retention and Automated Purge
Given an organization sets retention policies for verification artifacts and uploaded documents (e.g., 180 and 365 days) When items exceed their retention windows Then a daily purge job removes them irreversibly And the purge operation is logged with counts, item types, and anonymized references Given a jurisdiction-specific retention override is configured When the org is mapped to that jurisdiction Then the override values are applied instead of the default org policy Given a retention policy change is saved Then the change is versioned with effective date, author, old/new values, and appears in the audit log
Export and Deletion Workflows
Given an authorized admin requests an export for a date range and clinic/owner filter When the export is generated Then the system produces a machine-readable dataset (JSON or CSV) with linked documents in a ZIP, includes a checksum, and records an audit entry Given an authorized admin submits a deletion request for a specific verification request ID Then the system deletes the associated artifacts and documents within a configurable SLA window And updates indexes and caches to remove references And records a non-content audit pointer that the deletion occurred Given a deletion request includes items currently under active review Then the system blocks deletion and explains the reason, offering to retry when review is cleared
Administrative Access and Configuration Audit Logs
Given an admin views or changes compliance-related settings Then the system records user ID, role, IP, timestamp, action, resource, and before/after values (excluding secrets) Given any access to verification artifacts or documents Then a read-access audit entry is recorded with user, resource, optional reason, and outcome (success/denied) Given audit logs are stored Then logs are immutable, tamper-evident, searchable by filters (user, date, resource), retained per org policy (minimum 2 years), and exportable by authorized admins
Event Mode for Adoption Events
Given event mode is enabled for an adoption event When a clinic verification is evaluated Then the system enforces a freshness window configurable by the org (e.g., verification within the last 72 hours) And requests outside the window are blocked until a new verification is obtained Given a verification passes in event mode Then the Smart Card displays Clinic Verified with event tag and timestamp And the system can generate a printable certificate with pet photo, owner, clinic, verification timestamp, freshness window, and a scannable code linking to verification status Given event mode is disabled Then standard verification rules and displays apply
Published Notices and Jurisdictional Terms
Given a clinic accesses the portal Then the portal displays the current Data Processing Notice and Clinic Terms with version number and effective date And requires acknowledgment before submission of records Given the org is configured for a jurisdiction with additional requirements Then the portal and request templates automatically include the required disclosures, consent language, and data fields for that jurisdiction Given notices or terms are updated Then new versions are published without altering prior acknowledgments, with version history visible and downloadable

Batch Intake

Text multiple photos or a PDF via a secure SMS link and let FetchFlow auto‑split pages by pet name and vaccine type, routing each entry to the right Smart Card. Bulk review and fixups happen in one screen—perfect for rescues and multi‑pet families.

Requirements

Secure SMS Upload Link
"As a rescue coordinator, I want to send a secure link to upload multiple vaccine records at once so that I can quickly share documents without logging into a portal."
Description

Generate a one-time, tokenized SMS link that allows clients to upload multiple photos and PDFs in a single batch. Enforce link TTL, device-bound tokens, file-type/size validation, client-side compression for images, resumable/chunked uploads, and malware scanning. Associate each batch to the requesting business and contact, store files encrypted at rest, and capture metadata (filename, timestamp, orientation). Provide a mobile-friendly upload flow with progress indicators and error recovery, supporting up to 50 files or 100MB per batch.

Acceptance Criteria
One-Time Tokenized SMS Link Generation and Redemption
Given a business user requests a batch intake upload link for a specific contact, When the system generates the SMS, Then the URL contains a cryptographically secure, opaque, single-use token associated with that business and contact Given the link is delivered via SMS to the contact’s phone number, When the recipient taps the link, Then the mobile upload flow opens over HTTPS and displays the batch intake screen Given the token has not yet been consumed, When the first upload session is initiated, Then the token state transitions to consumed and cannot be reused Given a consumed token, When any subsequent request is made to the link, Then the user sees an invalid/used link message with an option to request a new link and no uploads are accepted Given a link is generated, When viewed in system logs/analytics, Then the token value is never logged in plaintext and only a redacted form is stored Given a link is generated, When auditing events, Then events are recorded for link_created, link_sent, link_opened, upload_started, and upload_completed with timestamps
Link TTL Expiry and Invalid Token Handling
Given a link is created with a configured TTL, When the current server time exceeds the TTL, Then link redemption returns an expired state and no upload actions are enabled Given an expired or revoked token, When the user opens the link, Then an expired link page is displayed with a CTA to request a new link and a support contact option Given a malformed or unknown token, When the link is requested, Then the system returns 404/410 without revealing whether a valid link ever existed Given the client attempts to alter TTL or token in the URL, When the server validates the request, Then the original TTL and token integrity are enforced and the request is rejected Given a business revokes an active link before TTL, When the user opens it, Then the link is blocked and marked revoked in audit logs
Device-Bound Token Enforcement
Given a tokenized link is first opened, When the upload flow loads, Then the token is bound to the initial device context (e.g., fingerprint/session) for the remainder of the TTL Given the same user reopens the link on the same device within TTL, When validation occurs, Then access is permitted without generating a new link Given a different device attempts to open the same tokenized link within TTL, When validation occurs, Then access is denied with a device mismatch message and an option to request a new link Given staff needs to override device binding, When they regenerate a link from the dashboard, Then a new token is created and the previous token is invalidated
File Type and Batch Size Validation with Client-Side Image Compression
Given the user selects files, When validation runs client-side, Then only JPG/JPEG, PNG, HEIC, and PDF files are accepted and all other types are rejected with an inline error Given the user adds files, When computing batch constraints, Then no more than 50 files are allowed per batch and attempts to exceed 50 are blocked with a clear message Given images exceed the batch size limit, When client-side compression runs, Then images are compressed to reduce size while preserving legibility and the updated sizes are shown before upload Given after compression the batch total still exceeds 100MB, When the user attempts to proceed, Then the upload is blocked with guidance to remove files or split the batch Given PDFs are included, When preparing uploads, Then PDFs are passed without lossy compression and remain within the overall 100MB batch limit Given images contain EXIF orientation, When preparing uploads, Then images are orientation-corrected for preview and orientation metadata is captured for each file
Resumable/Chunked Upload with Mobile-Friendly Progress and Recovery
Given a batch upload starts on a mobile device, When files are uploaded, Then uploads proceed in resumable chunks so completed chunks are not retransmitted after interruptions Given a transient network loss occurs mid-upload, When connectivity returns within TTL, Then the upload resumes from the last acknowledged chunk without user data loss Given an individual file upload fails repeatedly, When retry policy executes, Then the client retries with exponential backoff up to 5 attempts and surfaces a per-file Retry action if exhausted Given multiple files are uploading, When progress is displayed, Then the UI shows per-file percentage, overall progress, remaining count, and estimated time where available Given the user backgrounds the app or locks the screen, When they return within TTL, Then the upload state is preserved and continues from the last known progress Given mobile UI constraints, When rendered on 360–430px width devices, Then the upload flow is responsive, touch-targets are at least 44px, and progress/accessibility labels are screen-reader friendly with sufficient contrast (WCAG AA) Given a file permanently fails, When the batch completes, Then successful files are finalized and the failed file is clearly indicated with an option to remove or retry
Malware Scanning and Quarantine
Given files are received server-side, When malware scanning runs, Then no file is made available to downstream systems until it passes the scan Given a file is flagged as malicious, When quarantine is enforced, Then the file is isolated, excluded from the finalized batch, and cannot be downloaded Given a file is quarantined, When notifying stakeholders, Then the uploader sees a clear message identifying the specific file and the business is notified with scan details Given a batch contains both clean and malicious files, When processing completes, Then clean files are accepted and the batch is marked partially accepted with the quarantined items listed Given scanning completes with all files clean, When finalization occurs, Then the batch status is marked complete and files proceed to subsequent processing
Batch Association, Metadata Capture, and Encrypted Storage
Given an upload session begins from a tokenized link, When the batch is created, Then the batch record is associated with the requesting business and the intended contact Given files are uploaded, When metadata is stored, Then for each file the system records original filename, server-received timestamp (UTC ISO 8601), detected MIME type, and image orientation (if applicable) Given files are persisted, When checking storage configuration, Then files are encrypted at rest and cannot be accessed without proper authorization Given a user attempts direct URL access to stored files, When unauthenticated, Then access is denied and no file bytes are disclosed Given the batch completes, When auditing, Then audit entries exist for batch_created, file_received, file_scanned, file_stored, and batch_finalized with actor and timestamps
Auto-Split and Classification OCR
"As a groomer, I want uploads to be automatically split and labeled by pet and vaccine so that I don’t have to manually transcribe each record."
Description

Ingest images/PDFs and run an OCR/ML pipeline to segment pages, detect and extract pet name, vaccine type (e.g., Rabies, DHPP, Bordetella), administration date, expiration date, lot number, and clinic/provider. Automatically split multi-page documents into logical entries per pet-vaccine, handle mixed batches, and assign confidence scores per field. Include image enhancement (deskew, de-noise, glare/rotation correction), support printed and cursive handwriting, and retain original page snippets linked to extracted fields. Flag low-confidence fields for review.

Acceptance Criteria
Mixed-Batch Auto-Splitting into Pet–Vaccine Entries
Given a batch containing a 5-page PDF with records for pets "Milo" and "Luna" and vaccines Rabies and DHPP, and three JPEG photos for pet "Ollie" When the batch is processed Then the system creates separate entries for each unique pet–vaccine occurrence and associates the correct page(s)/image(s) to each entry And then no page/image is unassigned unless it contains no detectable vaccine information And then each entry includes detected pet name and vaccine type with confidence scores And then entries are grouped by batch for bulk review
Field Extraction and Normalization
Given an entry with printed or handwritten vaccine record When processed Then the system extracts pet name, vaccine type (Rabies, DHPP, Bordetella, or Other), administration date, expiration date, lot number, and clinic/provider And then dates are parsed and normalized to ISO 8601 (YYYY-MM-DD) And then unknown or missing fields are set to null and marked "Missing" without blocking entry creation And then vaccine type synonyms (e.g., "DHP-P", "Parvo") map to the correct canonical type where applicable
Image Enhancement and Preprocessing
Given skewed, rotated (up to ±25°), low-light, or glare-affected images When processed Then the system applies deskew, denoise, contrast/brightness adjustment, rotation correction, and glare mitigation before OCR And then the enhanced image is retained alongside the original for audit And then OCR runs on the enhanced image and returns higher confidence than on the original in at least 80% of the enhancement test set
Cursive Handwriting Recognition
Given a set of cursive handwritten vaccination cards When processed Then the system accurately extracts vaccine type and dates with field-level F1 ≥ 0.85 on the handwriting test set And then misrecognized characters do not create invalid dates (e.g., month > 12); such cases are flagged for review
Confidence Scoring and Threshold-Based Flagging
Given any processed entry When field confidences are computed Then each field includes a confidence score between 0.00 and 1.00 And then per-field review thresholds are configurable; defaults are: pet name 0.80, vaccine type 0.85, administration/expiration dates 0.90, lot number 0.75, clinic/provider 0.80 And then any field below its threshold is marked "Needs Review" with a visible indicator in bulk review And then entries with any "Needs Review" field are included in the "Requires Attention" filter
Snippet Retention and Click-Through Proof
Given an extracted field When the entry is viewed Then the system displays a clickable link that opens the original page snippet bounding box used for that field And then the snippet includes page number, coordinates, and source file reference And then both original page and snippet are stored so reviewers can verify extraction without downloading the entire file And then exporting the entry includes a reference to the snippet location
Smart Card Association and Unmatched Handling
Given extracted pet name (and photo if available) When matching against existing Smart Cards Then the system auto-associates the entry to the correct pet profile if a unique match is found And then if multiple matches or no match exist, the entry is marked "Unmatched" and queued for manual selection without losing any extracted data And then association status is recorded per entry for audit
Smart Card Auto-Routing and De-duplication
"As a walker, I want vaccine entries to land in the right pet’s Smart Card automatically so that my records stay accurate with minimal effort."
Description

Match each extracted entry to the correct Pet Profile Smart Card using pet name, household context (owner phone, prior appointments), and fuzzy matching against existing profiles. Normalize vaccine nomenclature to canonical types, map fields to Smart Card schema, and attach image proof. Prevent duplicates by checking existing records (same vaccine type and overlapping dates/lot), perform upsert logic when newer information is present, and ensure transactional writes with idempotency keys per entry.

Acceptance Criteria
Auto-route entries to correct Smart Cards by pet name and household context
Given a batch containing entries with pet names and an owner phone number associated with existing profiles When the batch is processed Then each entry is linked to the Smart Card whose pet name fuzzy-match score is >= 0.90 within the same household And if multiple candidates meet the threshold, the system selects the candidate with the highest score; ties break by exact owner match and most recent appointment date And if no candidate reaches 0.80, the entry is placed into the review queue with status "Needs Match" and no Smart Card write occurs
Disambiguate identical pet names across different households
Given two or more pets with the same name exist in different households and a batch entry for that name is submitted via a secure link tied to Household A's owner phone When routing is performed Then the entry is matched only to Household A's pet and is not visible to any other household And an audit log records the household identifier and token used for disambiguation
Normalize vaccine labels and map fields to Smart Card schema
Given extracted vaccine labels include synonyms, abbreviations, and case variations When normalization runs Then labels are mapped to canonical vaccine types defined in the schema And the original label text is stored in a source_label field for traceability And required fields (type, administration_date, expiration_date if present, lot_number if present, provider) are mapped and validated And entries with unknown or ambiguous labels are sent to the review queue with reason "Unknown vaccine" and no Smart Card write occurs And on the provided normalization fixture set, at least 95% of labels normalize to the expected canonical type
Attach verifiable image proof to Smart Card entries
Given an entry is successfully written to a Smart Card When the record is saved Then the record includes a link to the source document and page index And the bounding box coordinates of the extracted region are stored And the image proof is retrievable by authorized users within the same account
Duplicate detection and upsert with newer information
Given an existing Smart Card record for a canonical vaccine type and a new batch entry for the same type When duplicate detection runs Then the entry is considered a duplicate if any are true: (a) administration_date within 3 days of an existing record; (b) same lot_number; or (c) overlapping validity window for the same vaccine type And if the new entry has a more recent administration_date or later expiration_date, the system upserts by updating the record and appending the new image proof and metadata while preserving provenance of the prior record And if the new entry is older or identical, it is discarded with a "Duplicate" note and no changes to the Smart Card
Transactional writes with idempotency on batch intake
Given every entry is assigned an idempotency key derived from household_id, candidate_pet_id or pet_name, source_document_id, page_index, and a hash of extracted fields When the same entry is submitted multiple times due to retries or user resubmission Then only one Smart Card write occurs and subsequent requests result in a no-op without creating duplicates And if a write fails mid-transaction, no partial record is visible and a retry with the same key completes the record atomically And concurrent processing of different entries for the same pet does not produce duplicate records or lost updates
Bulk Review Workbench
"As a trainer, I want to quickly review and fix flagged entries in one place so that I can process large batches in minutes."
Description

Provide a single-screen review interface listing all extracted entries from a batch with sortable columns, filters (low confidence, unmatched pet, potential duplicate), and batch actions. Enable inline editing of fields (dates, vaccine type, lot, provider), quick-fix suggestions, keyboard shortcuts, and side-by-side image previews with zoom/rotate/crop. Support multi-select approve/reject, undo, save-as-draft, and display of the target Smart Card prior to commit. Enforce role-based access and track per-entry review status.

Acceptance Criteria
Batch Loads and Review Grid with Sortable Columns
Given a completed batch extraction with N entries When the Bulk Review Workbench is opened from the batch link Then the review grid renders within 2 seconds for N<=200 and displays exactly N rows with columns: Pet Name, Vaccine Type, Date, Lot, Provider, Confidence, Status, Duplicate, Target Smart Card And when a column header is clicked, rows sort ascending by that column and a visual sort indicator is shown And when the same header is clicked again, the sort toggles to descending with the indicator updated And sorting is stable for equal values And the grid displays visible counts for total, filtered, and selected rows
Filters for Low Confidence, Unmatched Pet, and Potential Duplicate
Given a batch containing entries flagged by the extraction engine as low confidence, entries without a matched pet, and entries flagged as potential duplicates When the Low Confidence filter is enabled Then only entries flagged low confidence are shown and the filter badge shows the count of visible rows When the Unmatched Pet filter is enabled Then only entries without a matched pet are shown When the Potential Duplicate filter is enabled Then only entries flagged as potential duplicates are shown When multiple filters are enabled simultaneously Then only entries that satisfy all enabled filters are shown And clearing all filters restores the full unfiltered set
Inline Editing with Validation and Quick-Fix Suggestions
Given a selected row in the review grid When the user edits Date, Vaccine Type, Lot, or Provider inline Then field validation occurs on blur or Enter: Date must be a valid calendar date, Vaccine Type must be one of the system-supported vaccine codes, Lot must be 1–50 characters alphanumeric plus dashes, and Provider must be 1–100 characters And invalid fields are highlighted with an error message and the row cannot be approved until resolved And valid changes save without page reload within 300 ms and the row status updates to Edited Given a field with available quick-fix suggestions When the user opens suggestions and selects one (via click or Alt+Number) Then the field value is replaced with the suggestion, the suggestion indicator clears, and validation passes if the suggestion is valid
Side-by-Side Image and Smart Card Preview with Transform Tools
Given a row with a source image or PDF page When the Preview panel is opened Then the left panel shows the source image for the selected entry and the right panel shows the target Smart Card summary including pet photo, name, and existing vaccine records And zoom controls allow scaling from 25% to 400% with +/- buttons and mouse wheel or pinch And rotate controls rotate the image in 90° increments And crop allows selecting a rectangular region and Reset removes all transforms And visual transforms affect only the review view and do not modify the stored original file And transforms persist while the batch is in draft and reset after approval or explicit reset
Keyboard Shortcuts for Navigation and Actions
Given focus is on the review grid and not in a text input When the user presses Arrow Up/Down Then selection moves to the previous/next row When the user presses Space Then the current row is toggled selected When the user presses Ctrl/Cmd+A Then all visible rows are selected When the user presses A Then selected rows are approved (if valid) When the user presses R Then selected rows are rejected When the user presses E Then inline edit mode opens for the focused cell When the user presses S Then Save as Draft is triggered When the user presses Ctrl/Cmd+Z Then the last action (approve, reject, edit) is undone when possible And when focus is inside an editable input, shortcuts that would alter content are ignored And pressing ? opens an in-app shortcut cheat sheet
Multi-Select Approve/Reject with Undo and Save as Draft
Given multiple rows are selected via Shift+Click, Ctrl/Cmd+Click, or Select All When Approve is triggered Then all selected rows with valid, complete data change status to Approved and are queued for routing to their target Smart Cards And a confirmation toast shows the count approved and offers Undo for 10 seconds When Reject is triggered Then selected rows change status to Rejected and are excluded from routing, with an Undo option for 10 seconds When Save as Draft is triggered Then all edits, selections, filters, and preview transforms persist and are restored upon reopening the same batch until approvals are committed or the draft is discarded And attempting to approve with any invalid row surfaces a blocking error listing the row numbers and fields to fix and prevents approval
Role-Based Access Control and Per-Entry Status Tracking
Given a user without the Review permission/role When they attempt to open the Bulk Review Workbench or call its approve/reject APIs Then access is denied with a 403/forbidden screen and no data is modified Given a user with the Review permission/role When they open the workbench Then all review features (filters, edits, approvals) are available And each entry maintains a status lifecycle: New -> Edited (upon valid edit) -> Approved or Rejected, with Draft indicating uncommitted changes And all status transitions and field changes are audit logged with user, timestamp, before/after values and are retrievable for compliance
Exception Handling and Conflict Resolution
"As a groomer, I want clear options when the system isn’t sure so that I can resolve issues without rework."
Description

Route ambiguous or failing entries into an exceptions queue with clear reasons (unreadable date, multiple pet name matches, unknown vaccine). Provide suggested matches with confidence, a create-new-pet flow, duplicate-merge tools, and one-click SMS requests for clarification (e.g., ask owner to confirm pet or resend a clearer photo). Record all resolutions, propagate corrections to improve future matching, and prevent loops by capping retry attempts with exponential backoff.

Acceptance Criteria
Exceptions Queue Routing and Reason Codes
Given a batch intake is processed and an item has an unreadable date, multiple pet name matches, or an unknown vaccine type When auto-processing completes Then the item appears in the Exceptions queue within 10 seconds And the item shows a machine-readable reason code in {UNREADABLE_DATE, MULTIPLE_PET_MATCH, UNKNOWN_VACCINE} And the UI displays the source snippet or page segment associated with each reason And the entry is not applied to any Smart Card until resolved
Suggested Matches with Confidence
Given an exception caused by multiple pet name matches or partial name recognition When the reviewer opens the item Then the system displays 3–10 candidate pets ranked by confidence (0–100%), highest first And each candidate shows pet photo (if available), owner name, last visit date, and matching attributes And selecting a candidate attaches the intake entry to that pet’s Smart Card and removes the exception And the selection and shown confidence score are recorded in the audit log
Create-New-Pet Flow and Vaccine Mapping
Given an exception with no acceptable pet match When the reviewer selects Create New Pet Then a form prefilled with parsed name opens with required fields Pet Name and Owner And upon save, a new pet record is created, linked to the owner, and the intake entry is attached to its Smart Card And an alias mapping from the parsed name to the new pet is stored for future matching Given an exception with an unknown vaccine label When the reviewer maps it to a standard vaccine type Then the mapping is saved to the vocabulary table and immediately reapplied to reprocess the current batch And future batches auto-classify the same label using the saved mapping
Duplicate Pet Merge with Audit Trail
Given two or more pet records are identified as duplicates in the review screen When the reviewer selects a primary record and confirms Merge Then non-conflicting attributes are merged, conflicting fields retain primary values, and secondary records are archived And vaccine entries are deduplicated by (pet, vaccine type, date) and merged with provenance preserved And all merges are logged with before/after snapshots, actor, timestamp, and are undoable for 24 hours And all linked intake entries and Smart Card references are updated to the primary record
One-Click SMS Clarification and Tracking
Given an exception requires owner clarification (e.g., confirm pet or resend clearer photo) When the reviewer clicks a template (Confirm Pet, Resend Photo) and sends Then an SMS is sent via the secure link service with a unique, expiring link (TTL 7 days) And message send status (queued, sent, delivered, failed) is displayed and stored And owner replies and attachments are threaded in the exception view and attached to the item And STOP/UNSUBSCRIBE is honored and blocks further sends to that number And rate limiting ensures no more than 2 clarification SMS per item per 24 hours
Resolution Recording and Learning Propagation
Given any exception is resolved (match selected, new pet created, vaccine mapped, or item dismissed) When the reviewer completes the action Then a resolution record is stored with resolution type, details, actor, timestamp, and related item IDs And system metrics increment Resolved count and time-to-resolution And learned mappings (name aliases, vaccine label mappings) are versioned and applied to future matching And the system reprocesses remaining affected items in the current batch using the new mappings
Retry Caps and Exponential Backoff
Given an automated retry is configured for transient failures (OCR read error, SMS delivery retry, or reprocessing after pending data) When the system schedules retries Then retries follow an exponential backoff schedule of 15 minutes, 60 minutes, and 240 minutes And after 3 attempts the item is marked Needs Manual Review and no further automated retries are scheduled And retry attempts and outcomes are logged and visible on the exception detail And a manual action resets the retry counter; automated triggers do not exceed the cap
Notifications and Status Updates
"As a rescue coordinator, I want to know when my upload is processed and what succeeded so that I can follow up only when needed."
Description

Deliver real-time processing status for each batch, including ingestion, extraction, review required, and completed. Provide in-app notifications for staff when action is needed and optional SMS updates to the uploader. After completion, send a summary detailing counts of added, updated, and failed entries with links to review. Expose webhook events for integrations and apply rate limiting and digesting to avoid notification fatigue.

Acceptance Criteria
Dashboard Shows Real-Time Batch Status Progress
Given a batch upload is initiated When ingestion starts Then the batch status displays "Ingestion" within 10 seconds and records a timestamp Given extraction begins When parsing is in progress Then the status displays "Extraction" within 10 seconds and records a timestamp Given data requires human review When validation flags items Then the status changes to "Review Required" within 10 seconds and shows the count of items requiring review Given all items are processed When the batch finishes Then the status displays "Completed" within 10 seconds and shows the total item count And the status history is persisted with timestamps for each transition
In-App Staff Notification on Review Required
Given a staff user has permission to process batches and has in-app notifications enabled When a batch enters "Review Required" Then an in-app notification appears within 15 seconds with a badge increment and a link to the bulk review screen And the notification remains unread until opened or dismissed, and is removed when all review items are resolved And duplicate notifications for the same batch and state are suppressed
Optional SMS Updates to Uploader
Given the uploader provided a mobile number and opted in to SMS updates at upload time When the batch enters each distinct status (Ingestion, Extraction, Review Required, Completed) Then an SMS update is queued within 10 seconds containing the batch reference, current status, and a tracking link And a STOP keyword opt-out is honored immediately; no further SMS are sent for that batch or future batches until the user re-opts in And if more than 3 status changes occur within 15 minutes, a single digest SMS is sent summarizing changes And SMS sending for batch updates is limited to a maximum of 3 per hour per phone number
Completion Summary with Counts and Links
Given a batch completes processing When the summary is generated Then the app and, if opted in, SMS include counts of added entries, updated entries, and failed entries And the summary contains deep links to the review screen filtered to failed entries and to affected Smart Cards And the summary appears in the in-app inbox within 15 seconds and the SMS (if enabled) is queued within 15 seconds
Webhook Events for Integrations
Given a webhook subscription is configured for the tenant When a batch transitions status or completes Then a webhook event is sent within 10 seconds with signed HMAC headers, event_id, event_type, tenant_id, batch_id, old_status, new_status, and timestamps in ISO 8601 And webhooks are retried up to 5 times with exponential backoff starting at 30 seconds upon non-2xx responses And deliveries include an Idempotency-Key header and preserve event ordering per batch And rate limiting caps webhook deliveries at 60 per minute per tenant; excess events are queued
Notification Digesting and Rate Limiting
Given multiple batch events occur within short intervals When notifications are generated for a user Then per-user notifications are limited to 10 in-app and 3 SMS per rolling hour And events within a 15-minute window are consolidated into a single digest item per channel summarizing counts and providing links And admin-configurable thresholds exist with defaults as above; configured values are enforced and auditable
Error Handling and Fallback Alerts
Given a notification channel fails (e.g., SMS delivery error or webhook 5xx after retries) When the system detects the failure Then an in-app alert is created for staff within 1 minute with error details and remediation guidance And the system retries SMS via an alternate provider if configured; otherwise logs the failure while respecting rate limits And duplicate alerts for the same failure and batch are suppressed within a 30-minute window
Performance, Security, and SLA
"As an owner-operator, I want uploads to be fast and secure so that my clients trust the process and I can depend on it during busy days."
Description

Scale batch processing to handle up to 200 pages per batch with 95th percentile end-to-end processing under 3 minutes. Implement resilient job orchestration with retries, dead-letter queues, and observability (traces/metrics). Enforce encryption in transit and at rest, strict RBAC for review access, one-time link TTL (e.g., 72 hours), IP/device fingerprint checks, and full audit logging of uploads, edits, and approvals. Provide data retention controls and GDPR/CCPA-aligned deletion on request.

Acceptance Criteria
200-Page Batch Processing Time at P95
Given a batch upload containing 200 pages across one or more pets When the upload completes and server-side processing begins Then the 95th percentile end-to-end time from upload completion to all pages being OCR’d, classified by vaccine type, routed to the correct Smart Cards, and visible in Bulk Review is ≤ 3 minutes measured over ≥ 100 production-like runs And instrumentation confirms no more than 5% of batches exceed 3 minutes and 0% exceed 10 minutes And the timing includes OCR, classification, routing, indexing, and UI availability (no client-side cache assumptions)
Resilient Orchestration with Retries, DLQ, and Idempotency
Given a transient processing failure (e.g., 5xx from OCR or storage timeout) When a page task fails Then it is retried up to 3 times with exponential backoff (initial delay 5s, max delay 60s) and marked successful if a retry completes Given a non-transient or exhausted-retry failure When the final retry fails Then the task is moved to a dead-letter queue with captured error code, stack/trace ID, batch ID, and page ID And the batch continues processing remaining pages without blocking Given a worker crash or deploy restart mid-batch When workers come back online Then the orchestrator resumes the batch within 60 seconds without data loss and without duplicate page processing And idempotency keys ensure each page is processed at most once per batch And a documented DLQ reprocess flow exists that replays messages exactly once and records outcomes
End-to-End Observability and SLO Alerting
Given any batch intake job When processing starts Then a distributed trace is emitted with a correlation ID spanning upload, OCR, classification, routing, and review-index steps (error sampling 100%, success sampling ≥ 10%) And metrics are captured: end-to-end duration histogram, per-step durations, success/failure counts, retry counts, DLQ counts, and queue latencies And dashboards display P50/P90/P95/P99 over selectable time ranges (last 1h/24h/7d) Given an SLO breach When P95 end-to-end duration > 3 minutes for 5 continuous minutes Then a high-severity alert pages on-call within 2 minutes And when DLQ rate > 1% of tasks for 5 minutes, a medium-severity alert is created with runbook link
Secure Upload Link: TLS, Encryption, and One-Time TTL with Device/IP Check
Given an issued secure SMS upload link When the link is accessed Then HTTPS with TLS v1.2+ and HSTS is enforced end-to-end; non-TLS requests are redirected or blocked And uploaded files and derived artifacts are encrypted at rest with AES-256 using KMS-managed keys; keys are rotated at least annually And pre-signed storage URLs (if used) have a maximum validity of 15 minutes Given link lifecycle controls When the first successful upload completes or 72 hours elapse from issuance (whichever occurs first) Then the link becomes invalid and subsequent requests return 410 Gone Given device/IP fingerprinting When the link is first opened Then device and IP fingerprint are recorded And subsequent access attempts with a mismatched fingerprint are blocked with 403 and logged And upload links have ≥ 128 bits of entropy and rate-limiting of ≥ 10 requests/min per IP with exponential backoff to mitigate brute force
RBAC-Gated Bulk Review Access and Tenant Isolation
Given a signed-in user When they attempt to access the Batch Intake Bulk Review screen Then only users with role Reviewer or Admin within the same organization can view and act on batches; others receive 403 Forbidden And users can access only batches scoped to their tenant; cross-tenant IDs return 404 Not Found to avoid information disclosure And permission changes take effect within 2 minutes of update And all access attempts (success and failure) are authorization-checked server-side (no client-only gating)
Comprehensive Audit Logging for Intake Lifecycle
Given any step in the intake lifecycle When an event occurs (link created/accessed, files uploaded, auto-split results, manual edits, approvals/rejections) Then an immutable audit record is written capturing: timestamp (UTC), actor (user ID or link token), org ID, batch ID, affected pet IDs, action, prior/new values, IP/device fingerprint, and correlation/trace ID And audit records are append-only and tamper-evident (hash-chained or WORM storage) with retention per policy And authorized admins can query and export audit logs within 5 minutes of event creation And sensitive payloads (images, full OCR text) are not persisted in logs; only references/IDs are stored
Data Retention Controls and GDPR/CCPA Deletion
Given organization-level settings When an admin configures retention Then defaults are provided (90 days for raw uploads) and configurable between 30–365 days per data class (raw uploads, OCR text, thumbnails) And upon reaching retention, data is purged automatically, with an audit event recorded and search indexes updated within 24 hours Given a verified data deletion request (data subject or admin on behalf) When the request is submitted Then all associated uploads and derived artifacts (OCR text, thumbnails, indexes, cached copies) are deleted from primary systems within 7 days and from backups within 30 days, with a deletion receipt (job ID and scope) provided And deletion is idempotent and propagated across replicas and caches; attempts to access deleted items return 404 and are logged

Auto‑Redact

Before storing or sharing, sensitive owner data (addresses, account numbers, private notes) is automatically redacted from the client‑facing copy while the original is preserved for internal reference. Share clean proof of vaccines without exposing PII.

Requirements

Redaction Rules Engine
"As a business owner using FetchFlow, I want sensitive owner data to be automatically identified and masked according to policy so that anything I send to clients never exposes PII."
Description

Configurable, policy-driven rules to identify and redact sensitive owner data (e.g., street addresses, bank/ACH numbers, credit card PANs, emails, private notes) across all client-facing artifacts. Supports pattern libraries and regex, rule priorities, masking modes (remove, replace token, partial mask such as last4), and per-business exceptions. Integrates into FetchFlow’s booking, invoices/receipts, Pet Profile Smart Cards, vaccine proofs, outbound SMS/email, and shared links. Produces metadata describing what was redacted and why to support downstream auditing and analytics.

Acceptance Criteria
Client-Facing Invoice Redaction
Given a finalized invoice that includes owner street address, owner email, a credit card PAN in a note, and a bank/ACH number in a memo When the system renders or shares the client-facing invoice (web, PDF, or share link) Then the street address is replaced by the configured address token, the email is masked to first-letter + domain (e.g., j***@example.com), the PAN is masked to last4 only if the rule permits (otherwise removed), and the bank/ACH number is removed And the internal staff view of the same invoice remains unredacted And a post-redaction PII scan using the active pattern library finds 0 matches for address, PAN, bank/ACH, and email in the client-facing artifact And redaction completes before send/render and does not add more than 200 ms average latency per invoice (p50) in staging benchmarks And redaction metadata records each redacted field with ruleId, maskingMode, and reason
Vaccine Proof Share Redaction
Given a vaccine proof document containing pet info, vaccine details, owner address, and private notes When generating a client-facing vaccine proof share (Smart Card or link) Then owner address is replaced by the configured token, private notes are omitted, and only pet and vaccine fields remain visible And no PII pattern matches remain in the rendered share content or downloadable proof And internal storage retains the unredacted source And metadata lists all redactions with field paths and ruleIds
Outbound SMS/Email Template Redaction
Given an outbound message composed from templates that may include owner email, address, or private notes variables When the message is prepared for send over SMS/email Then sensitive fields are masked/removed per rules before send while preserving message readability and placeholders for non-sensitive fields And messages exceeding provider limits due to redaction are re-trimmed without re-introducing PII And the sent payload contains 0 matches for configured PII patterns And metadata is attached to the message record indicating redactions performed
Rule Priority and Masking Mode Enforcement
Given multiple matching rules for the same substring (e.g., general PAN regex and business-specific note redaction) with defined priorities and masking modes When redaction is executed Then the highest-priority applicable rule is applied, lower-priority rules are skipped for that span, and the selected masking mode is used And overlapping spans are not double-masked, and output is stable across repeated runs on the same input And a deterministic rule application order is logged in metadata (ruleIds in sequence) And unit tests cover tie-breaker behavior and last4-only masks where allowed
Per-Business Exceptions and Allowlist
Given Business A allows showing owner email on receipts and Business B does not When rendering client-facing receipts for each business Then Business A receipts display email per mask policy defined by its exception, and Business B receipts redact email per global rules And exceptions apply only to the specified artifact types and fields, not globally And changes to exceptions take effect within 5 minutes and are audit-logged with actor, timestamp, and diff And metadata for Business A indicates the exception used for each non-redacted field
Redaction Metadata API and Analytics
Given any artifact passing through redaction When the artifact is stored or sent Then a metadata record is generated capturing artifactId, tenantId, timestamp, fieldPath(s), originalContentHash (not content), ruleId(s), maskingMode(s), reasonCode(s), and counts And the metadata is retrievable via the internal audit API with filtering by date range, ruleId, artifact type, and tenant And analytics can aggregate counts by ruleId per day without accessing original content And PII never appears in metadata fields
Client Artifact Sanitization Pipeline
"As a groomer, I want vaccine proofs and receipts auto-sanitized before I send them so that I can share required documents without exposing addresses or account details."
Description

End-to-end pipeline that sanitizes client-facing copies before storage or sharing, including SMS/email bodies, invoices/receipts, Pet Profile Smart Cards, and vaccine proofs. Performs text extraction (including OCR for images/PDFs), applies redaction boxes and text masking, re-renders sanitized documents preserving layout and legibility, and stores only redacted versions in CDN/cache. Handles multi-page PDFs, large files via streaming, and fallbacks that block sharing and alert staff if OCR or detection confidence is insufficient.

Acceptance Criteria
Redact PII in SMS and Email Bodies Before Send
Given a staff user composes an outbound SMS or email containing an owner street address, a bank account number-like pattern, or text within a "Private Notes" field When they click Send from the dashboard Then the client-facing message is sent with each detected PII token replaced by "[REDACTED]" And a pre-send preview clearly shows the sanitized message exactly as delivered And the original unsanitized body is stored only in the internal system of record with access limited to staff with "View Originals" permission And attempting to reply or copy/paste from the received message reveals no PII tokens in client apps And an audit log entry is recorded with redaction types and counts
Redact PII from Invoices and Receipts
Given an invoice or receipt PDF generated by FetchFlow that includes the owner's postal address or internal "Private Notes" When the sanitization pipeline runs prior to storage or sharing Then black redaction boxes are applied to all detected PII regions and the underlying text is removed from the document text layer And document totals, line items, QR codes, and barcodes remain intact and scannable And the re-rendered PDF preserves page count and margins and passes a copy/paste test with no PII recovered And the sanitized artifact is labeled "REDACTED" in document metadata
OCR and Redaction for Multi‑Page Vaccine PDFs and Images
Given a multi-page vaccine proof uploaded as mixed images and PDFs, including rotated and handwritten pages that contain owner address blocks When the pipeline performs OCR and detection Then all pages are processed, rotated to correct orientation if needed, and any detected PII regions are redacted with confidence >= 0.95 And the output preserves original page order and resolution within 10% And if any page's OCR or detection confidence falls below 0.95, the artifact is not shared and is routed to the fallback path
Streaming Sanitization for Large Files
Given an artifact larger than 50 MB or exceeding 100 pages is submitted for sanitization When the pipeline processes the artifact Then streaming mode is used without loading the entire file into memory And peak memory usage remains under 512 MB per worker And backpressure prevents upload timeouts and a progress indicator emits at least every 2 seconds And a 300 MB, 200-page file completes sanitization and storage within 180 seconds in the staging environment
Fallback Block and Staff Alert on Low Confidence
Given any artifact whose OCR or detection confidence is below 0.95 on any page or whose sanitization step errors When a user attempts to share or fetch a client-facing copy Then sharing is blocked and the UI displays a blocking alert explaining "Sanitization incomplete — review required" And a "Sanitization Review" task is created and assigned to the location queue and a Slack webhook notification is sent within 10 seconds And the share link returns HTTP 403 with error code "SANITIZATION_REQUIRED" until a staff override occurs And the artifact is excluded from CDN/cache until sanitized
Preserve Layout and Apply Redaction Boxes
Given a document with text, tables, and images not overlapping PII regions When the sanitized version is re-rendered Then non-redacted layout elements retain positions within 3% of original bounding boxes And fonts and line breaks are preserved or substituted with metrics that keep line wrap differences to <= 1 line per page And images outside redaction regions are unaltered and remain visually identical on pixel diff with <= 2% variation And redaction boxes are opaque, non-removable, and cover the entire detected region with >= 4 px padding
Serve Only Redacted Versions and Maintain Audit Linkage
Given a request to view or download a client-facing artifact via share link, CDN URL, or embedded Smart Card When the content is served Then only the sanitized version is returned and any request path to the original is denied by policy And CDN/cache keys are namespaced to redacted versions and do not exist for originals And an audit record links the sanitized artifact to its original via content hash, includes timestamp, actor/service id, and redaction rule ids And only users with "View Originals" permission can access originals via short-lived, IP-bound URLs; clients cannot escalate privileges
Original Preservation & Access Controls
"As an owner-operator, I want originals securely preserved with restricted access so that my team can reference full details when needed without risk of accidental disclosure."
Description

Securely preserve unredacted originals in isolated, encrypted storage with separate keys, never exposed to client channels. Enforce role-based and purpose-based access with just-in-time reasons, watermarked “Internal Only” views, and optional download restrictions. Maintain retention policies and lineage linking each redacted artifact to its source to enable internal reference without risking accidental disclosure.

Acceptance Criteria
Isolated Encrypted Storage with Separate Keys
Given an unredacted original is ingested When it is persisted Then it is stored in an isolated storage location not addressable by client-serving services And it is encrypted at rest using a KMS key dedicated to originals, distinct from the key used for redacted artifacts And attempts to retrieve the original via any client-facing API or CDN domain are denied with 403 and no object metadata is leaked And KMS usage logs show access to the originals’ key only by the designated service role under a separate IAM policy
No Client Channel Exposure of Originals
Given any share action (SMS, email, client portal) for a document When the content is rendered or a link is generated Then only the redacted artifact ID/URL is referenced and the original’s location is never embedded, proxied, or cached in client channels And a client request for the content returns the redacted artifact with a response header PII-Redacted: true And observability logs for the message/response contain no original object IDs, keys, or URIs
Role- and Purpose-Based Access with Just-in-Time Reason
Given a staff user attempts to view an unredacted original When the user possesses the "Originals:View" permission and selects an approved purpose and enters a free-text reason (minimum 10 characters) Then access is granted and a time-bound access token (maximum 15 minutes) is issued And if the user lacks permission or omits a purpose/reason, access is denied with 403 and a prompt to provide a valid purpose/reason And an audit log entry is recorded including userId, role, purpose, reason, artifactId, sourceId, timestamp, and IP/device
Internal-Only Watermarked Views
Given an unredacted original is opened in the internal viewer When it renders Then a semi-transparent diagonal watermark "Internal Only — {userName} — {timestamp ISO8601} — {purpose}" appears on all pages/frames at 30–40% opacity And when exporting to on-screen print or generating a view-only PDF, the same watermark is embedded in the output And when sharing to a client, no watermark is present because only the redacted artifact is shareable
Optional Download Restrictions and Auditing
Given the workspace setting "Restrict Original Downloads" is enabled When any user attempts to download an unredacted original Then the download control is hidden/disabled and direct download endpoints return 403 And when the setting is disabled, downloads are permitted only to users with "Originals:Download" permission and require purpose/reason, and the downloaded file is stamped with a footer containing userName and timestamp And every allowed or blocked download attempt is logged with userId, artifactId, success flag, reason, and checksum of the delivered file
Retention Policies and Legal Hold
Given retention policies are configured per document type When an original reaches its retention end date and is not on legal hold Then it and its envelope metadata are purged within 24 hours and the purge is logged with a deletion receipt ID And when a legal hold is active on the original, no purge occurs until the hold is lifted And upon purge completion, the data encryption key for that object is destroyed/unreferenced so recovery is infeasible
Lineage Linking Between Redacted Artifacts and Sources
Given any redacted artifact exists When viewing its metadata Then it includes sourceOriginalId, sourceHash (SHA-256), redactionVersion, redactionTool, and createdAt, and the values match those stored on the source original And given a source original, when querying lineage, all associated redacted artifacts are listed with their IDs and creation timestamps And when a nightly reconciliation job runs, 100% of redacted artifacts map to an existing source original; any orphaned artifacts trigger an alert with count and IDs
Redaction Preview & Safe Share Links
"As a staff member, I want to preview and fine-tune what a client will see and share it via an expiring link so that I can respond quickly while controlling sensitive details."
Description

Dashboard viewer that shows side-by-side original vs. redacted outputs with a “view as client” mode. Allows per-share overrides to temporarily unmask specific approved fields without altering stored redacted copies. Generates expiring, scope-limited share links for SMS/email with device-aware rendering and automatic logging. Ensures overrides are time-bound, revocable, and compliant with business policy.

Acceptance Criteria
Side-by-Side Original vs Redacted Preview
Given a staff user with permission opens a client record containing sensitive fields and attachments When the Redaction Preview loads Then the Original panel displays the full, unredacted data visible only to staff And the Redacted panel displays data with addresses, account numbers, and private notes masked per policy And all attachments shown in the redacted panel have equivalent redactions applied And both panels remain synchronized in pagination and scrolling And initial render completes within 500 ms for records up to 2 MB
View as Client Mode Fidelity
Given a staff user toggles "View as Client" and selects a device type (Mobile or Desktop) When the mode is activated Then only redacted content plus any active per-share overrides are displayed And all internal-only UI elements, IDs, and notes are hidden And the layout matches the published share template for the selected device class And any download or print action from this mode produces identically redacted output
Per-Share Overrides Without Mutating Stored Redactions
Given a staff user creates a share and selects specific approved fields to unmask When the overrides are applied Then the stored redacted copy in the system remains unchanged And the override scope is limited to the generated share link ID only And only fields on the organization’s override whitelist can be selected And the UI presents a confirmation summary of unmasked fields prior to sending And an audit entry records actor, fields unmasked, reason, and timestamp
Time-Bound and Revocable Overrides
Given a share link with overrides and an expiry time T is generated When the current time exceeds T or a staff user manually revokes the share Then the link becomes inaccessible within 60 seconds And any previously unmasked fields render as redacted again And the audit log captures the expiry or revocation event with actor and timestamp And attempting to access the link returns an "expired or revoked" page with no sensitive data content
Expiring, Scope-Limited Share Links for SMS/Email
Given a staff user generates a share via SMS or Email and selects the content scope (e.g., specific pet(s)/document(s)) When the link is created Then a unique, unguessable tokenized URL with at least 128-bit entropy is produced And the link includes only the selected scope and applied overrides And the expiry is set per policy or user input within allowed bounds (1 hour–30 days) And the system logs creation with channel, recipient, scope, and expiry And the sent message includes a human-readable expiry notice
Device-Aware Rendering and Performance
Given a recipient opens the share link on a mobile or desktop browser over typical 4G or Wi‑Fi When the page loads Then the layout adapts to the device and viewport without horizontal scrolling for primary content And core content becomes interactive within 2 seconds at p95 on a simulated 4G connection And media is optimized for the device while preserving redaction overlays And the page meets WCAG 2.1 AA color contrast and keyboard navigation for the shared content
No PII Leakage in Client-Facing Outputs
Given client-facing views, downloads, and shares are generated from a record containing addresses, account numbers, and private notes When inspecting on-screen content, hidden fields, alt text, and downloadable files/metadata Then those sensitive values are absent unless explicitly overridden for that share And copied, printed, or saved outputs from the client view preserve all redactions And URLs and query parameters reveal no internal identifiers or PII in plaintext
Redaction Audit & Compliance Reporting
"As a business owner, I want traceable records of what was redacted and who accessed originals so that I can demonstrate compliance and investigate incidents."
Description

Immutable event logs capturing detection hits, rules applied, versions, user actions (views, overrides, shares), and artifact hashes for integrity. Provides dashboards and exportable reports (CSV/JSON) to demonstrate what data was redacted and when, who accessed originals, and exception trends. Includes alerts for policy violations and periodic summaries to support compliance and incident response.

Acceptance Criteria
Immutable Redaction Event Log Creation
Given any redaction process completes for a client artifact When the system stores the audit event Then the log entry includes event_id (UUIDv4), redaction_id, artifact_type, artifact_hash (SHA-256), detected_pii_types[], detection_hit_count, ruleset_id, ruleset_version, model_version (if used), action=redact, actor (system or user_id), correlation_id, outcome, and timestamp in UTC ISO-8601 with milliseconds And the log store is append-only and rejects update/delete operations And any attempt to mutate an event returns 403 and is logged as a separate security event And the event becomes queryable in the dashboard within 5 seconds of creation
Access to Original Data Audit Trail
Given a user attempts to view an original (unredacted) artifact When access is granted Then an audit event is recorded with user_id, role, reason_code (required), timestamp (UTC), ip_address, device_id, session_id, artifact_hash, and redaction_id And when access is denied, an audit event is recorded with reason=authorization_failed and includes the same context fields And the access event appears on the artifact timeline in the dashboard within 10 seconds And only roles {Owner, Manager, Compliance} can access originals; all others are denied and logged And MFA is required; missing MFA causes denial and is logged
Exportable Compliance Reports (CSV/JSON)
Given a user selects a date range (<= 90 days) and optional filters (client_id, pet_id, user_id, action_type, rule_id, outcome, pii_type) When Export CSV or Export JSON is requested for <= 100,000 events Then the file is generated within 60 seconds and includes headers/keys for all selected fields And the export contains report_hash (SHA-256), row_count, generated_at (UTC), and query_filters metadata And row_count equals the dashboard count for the same query And timestamps are UTC ISO-8601 with milliseconds And the export request is logged with requester user_id and a downloadable URL valid for 24 hours And for > 100,000 events, a background job is enqueued and a link is emailed within 15 minutes
Policy Violation Alerts
Given a policy violation is detected (e.g., unredacted share, prohibited PII present post-redaction, missing reason_code on override) When the violation event is recorded Then an alert is sent via in-app and email to designated recipients within 60 seconds And the alert includes violation_type, severity, redaction_id, artifact_hash, user_id (if any), timestamp, and deep link to details And identical violations are deduplicated within a 10-minute window And alert delivery, acknowledgements, and resolutions are logged with user_id and timestamps
Dashboard Metrics and Exception Trends
Given the compliance dashboard is loaded with a selected date range and filters When the user views Redactions and Exceptions widgets Then metrics display total_redactions, success_rate, overrides_count, violations_count, views_of_originals, top_pii_types[], and daily trend lines And metrics match event-log query results within 1% for the same parameters And widgets refresh within 10 seconds when filters change And only Compliance and Owner roles can see user-level breakdowns; others see aggregate-only
Integrity Verification of Artifacts and Reports
Given a stored artifact with artifact_hash and/or a downloaded report with report_hash When the user runs Verify Integrity Then the system recomputes SHA-256 and returns Pass when hashes match and Fail when they do not And verification results are logged with verifier user_id, timestamp, target_type, and target_id And any Fail generates a policy violation alert with severity=high
Scheduled Periodic Compliance Summaries
Given weekly and monthly compliance summaries are configured with recipients When the schedule triggers (Mon 09:00 and 1st of month 09:00 in business timezone) Then summaries are generated with totals, violations by type, overrides, access-to-originals count, and top clients by redactions for the period And summaries are delivered via email with CSV attachment and available in-app within 15 minutes And delivery failures retry up to 3 times with exponential backoff and are logged And only Owner and Compliance roles can create/edit schedules and recipients
Real-time PII Detection for SMS/MMS and Notes
"As a walker communicating by text, I want sensitive details detected and protected across messages and notes so that I can keep conversations flowing without risking exposure."
Description

On-the-fly scanning of inbound/outbound SMS/MMS and internal notes to detect sensitive details and auto-mask them in client-visible contexts. Provides inline prompts to staff with suggested edits, configurable allowlists/denylists (e.g., allow pet names, restrict owner addresses), and language-aware patterns. Persists sensitivity classifications to ensure consistent redaction across threads and artifacts without disrupting the SMS-first workflow.

Acceptance Criteria
Real-time Redaction for Outbound SMS
Given a staff user composes an outbound SMS up to 500 characters that includes PII (e.g., street address, account number) When the user taps Send Then the client-facing SMS is redacted per active policy before transmission and is delivered without PII And the redaction decision completes within 200 ms at P95 for messages <= 500 characters And the internal message record stores the original unredacted content with access limited to staff roles And an audit log entry is created with message_id, policy_version, detected_entities, and timestamp
Inbound SMS PII Detection and Consistent Redaction
Given an inbound SMS from a client contains PII When the system ingests and processes the message Then the message is classified for sensitive entities and the classification is persisted And client-visible contexts (e.g., client portal, forwarded copies) display the redacted version And internal staff views display the original content And ingestion and classification do not delay staff receipt; P95 classification time <= 200 ms And multipart/segmented messages are reassembled and scanned as a whole
MMS and Note Attachment OCR Redaction
Given an MMS image (JPG/PNG up to 5 MB) or a note attachment (PDF up to 5 pages, 10 MB) that may contain PII (e.g., address on vaccine card) When the asset is received or saved Then text is extracted and scanned for PII and redaction boxes are applied to client-visible copies And P95 processing time <= 3 s for images and <= 5 s for PDFs And generated thumbnails and previews are sanitized, and EXIF GPS/location metadata is removed from client-visible assets And the original asset is preserved internally with restricted access and linked to an audit log entry
Inline Prompts with Suggested Edits
Given the composer detects PII in an outbound draft When the user is presented an inline prompt with a sanitized suggestion Then accepting the suggestion updates the draft with masked content in one action and sends the redacted version to the client And rejecting requires an elevated permission and still results in client-visible masking while preserving the original internally And the prompt appears within 150 ms of detection and supports keyboard-only acceptance And all prompt interactions are logged with user_id and outcome
Configurable Allowlists and Denylists with Precedence
Given an organization configures allowlists (e.g., pet names) and denylists (e.g., owner addresses, account numbers) When policies are updated Then changes are versioned, auditable, and take effect within 60 seconds across all scanners And denylist rules take precedence over allowlist entries in case of conflict And allowlisted terms matching patterns are not redacted; denylisted categories are always redacted in client-visible contexts And policy evaluation is recorded with policy_version on each redaction decision
Language-Aware Detection (English and Spanish)
Given messages may be written in English or Spanish, including diacritics and locale-specific address formats When the system performs language detection and applies language-specific PII patterns Then addresses and account numbers are detected with precision >= 0.90 and recall >= 0.85 on the benchmark set for both languages And pet names and common non-PII terms are not redacted with false-positive rate <= 2% on the benchmark set And if language cannot be confidently determined, the system falls back to a safe default policy without exceeding the false-positive threshold
Persisted Sensitivity Classifications Across Threads and Artifacts
Given an owner’s address is classified as sensitive and linked to owner_id When the same address appears in future SMS/MMS or notes within 180 days Then it is consistently redacted in all client-visible contexts with 99% consistency across occurrences And the classification is idempotent and stored server-side for cross-device consistency And admins can revoke or update a classification, triggering reprocessing of affected artifacts with a new policy_version And all changes are captured in audit logs with before/after states

Label Translator

Understands clinic shorthand and multiple languages (e.g., DHPP/DAPP/DA2PP, KC=Bordetella) and normalizes them to standard vaccine names. Ensures accurate matching to your policy rules no matter how the record is labeled.

Requirements

Canonical Vaccine Lexicon
"As an owner-operator, I want vaccine labels normalized to a single canonical name so that my policies apply consistently regardless of how clinics label records."
Description

A centralized, versioned dictionary of standard vaccine names with multilingual synonyms, abbreviations, and clinic shorthands (e.g., DHPP/DAPP/DA2PP; KC/Bordetella). Includes dose/variant metadata (core vs non-core, species, route), regional aliases, and de-duplication rules. Seeded with veterinary standards and customizable per account. Exposes fast in-memory lookup for on-device and server usage, supports updates without app release, and maintains backward-compatible IDs for policy rule mapping.

Acceptance Criteria
Synonym and Multilingual Normalization to Canonical ID
Given the lexicon contains a canonical entry for Canine DHPP with synonyms DHPP, DAPP, DA2PP When a label equals any of these synonyms Then the same canonicalId is returned And the canonicalName equals the lexicon entry's displayName Given the lexicon contains a canonical entry for Bordetella with synonyms KC, Kennel Cough, Bordetella When a label equals any of these synonyms Then the same canonicalId is returned Given the lexicon contains a canonical entry for Rabies with synonyms Rabies, Rabia, Rage When a label equals any of these synonyms Then the same canonicalId is returned Given a label that has no match in the lexicon When normalization is requested Then mappingStatus equals unmapped And no canonicalId is returned
Versioning and Backward-Compatible IDs
Given lexicon version v1 where label DHPP resolves to canonicalId X When the lexicon is updated to version v2 with added synonyms or metadata Then label DHPP still resolves to canonicalId X Given a policy rule mapped to canonicalId X under v1 When the lexicon updates to v2 Then the policy rule evaluation uses the same canonicalId X without modification And rule outcomes are unchanged for identical inputs Given an on-device client pinned to lexicon version v1 When version v2 is published server-side Then the client continues to use v1 until it receives an explicit lexicon payload update without requiring an app release And the client can switch to v2 at runtime upon receiving the update
Fast In-Memory Lookup (Device and Server)
Given an on-device lexicon of at least 10,000 canonical entries loaded into memory When performing 10,000 random lookups on valid synonyms Then median latency per lookup <= 1 ms And p95 <= 2 ms And p99 <= 3 ms And process memory attributable to the lexicon <= 5 MB And cold initialization time to load the lexicon into memory <= 100 ms on a mid-tier device Given a server instance with the lexicon cached in memory When sustaining 5,000 lookups per second for 5 minutes on cache hits Then p95 latency <= 3 ms And p99 latency <= 5 ms And error rate = 0% And CPU utilization <= 60% on the provisioned instance class
Account-Level Custom Synonyms and Overrides
Given an account creates a custom synonym "KC Booster" mapped to the Bordetella canonicalId When a lookup is performed under that account for "KC Booster" Then it resolves to the Bordetella canonicalId And lookups for "KC Booster" under other accounts remain unmapped unless they define their own mapping Given an account blocks a global synonym "KC" When a lookup is performed under that account for "KC" Then the result is unmapped And other accounts still resolve "KC" to Bordetella Given a global synonym mapping is updated for Bordetella When an account-level override exists for "KC Booster" Then the account-level override continues to resolve to the intended canonicalId unless explicitly changed by the account
Dose/Variant Metadata Completeness and Schema
Given the lexicon is loaded When validating all canonical entries against the schema Then each entry has non-empty fields: canonicalId, canonicalName, species, coreStatus, route And species is one of canine or feline And coreStatus is one of core or non-core And route contains at least one value from {IM, SC, IN, Oral} And 100% of entries pass validation Given canonical entries for DHPP (canine), Bordetella (canine), Rabies (canine), FVRCP (feline) When retrieving their metadata Then DHPP.coreStatus = core And Bordetella.coreStatus = non-core And Rabies.coreStatus = core And FVRCP.species = feline And each entry's route contains at least one allowed value
Regional Alias Resolution
Given region context FR When normalizing label "Rage" Then it resolves to the Rabies canonicalId Given region context ES When normalizing label "Rabia" Then it resolves to the Rabies canonicalId Given region context is unset When normalizing any regional alias Then it resolves using global aliases if present Otherwise the result is unmapped Given any region context When normalizing label "KC" Then it resolves to the Bordetella canonicalId And the canonicalId is identical across regions
De-duplication of Equivalent Vaccine Labels
Given two labels "Nobivac DHPP" and "DHPP" representing the same antigen set When normalized Then they resolve to the same canonicalId And the preferred displayName equals the lexicon's canonicalName (brand stripped) Given Bordetella labels that differ only by route (Intranasal vs Oral) When normalized Then they resolve to the same canonicalId And the route variant is captured in metadata variants without creating duplicate canonical entries Given labels for FVRCP (feline) and DHPP (canine) When normalized Then they do not de-duplicate across species And they resolve to distinct canonicalIds
Fuzzy Text Normalization Engine
"As a groomer texting with clients, I want the system to understand imperfect or shorthand vaccine labels so that I don’t have to interpret them manually during booking."
Description

A language-aware parser that cleans and tokenizes incoming labels from SMS and Smart Card submissions, removes punctuation/diacritics, expands known aliases, and applies fuzzy matching (e.g., n-gram/Levenshtein) to map to a canonical vaccine with a confidence score. Handles misspellings, casing, hyphenation, and combination labels (e.g., "DHPP + KC"). Performs under 300ms per label on-device or server to support conversational flows.

Acceptance Criteria
Canonical mapping of aliases and multilingual labels
- Given an input label "DHPP", When the engine normalizes it, Then it returns canonical "DHPP" with confidence >= 0.95. - Given "DAPP", When normalized, Then canonical is "DHPP" with confidence >= 0.95. - Given "DA2PP", When normalized, Then canonical is "DHPP" with confidence >= 0.95. - Given "KC", When normalized, Then canonical is "Bordetella" with confidence >= 0.95. - Given "Bordetella", When normalized, Then canonical is "Bordetella" with confidence >= 0.99. - Given Spanish "Tos de las perreras", When normalized, Then canonical is "Bordetella" with confidence >= 0.90. - Given French "Maladie de Carré + Parvo", When normalized, Then canonical is "DHPP" with confidence >= 0.85.
Robust handling of misspellings, casing, hyphenation, and diacritics
- Given misspelled "bordetela", When normalized, Then canonical is "Bordetella" with confidence >= 0.85. - Given mixed-case "da2pp", When normalized, Then canonical is "DHPP" with confidence >= 0.95. - Given hyphenated "D-APP" and spaced "D A P P", When normalized, Then canonical is "DHPP" with confidence >= 0.90. - Given diacritic "Bórdétella", When normalized after diacritic stripping, Then canonical is "Bordetella" with confidence >= 0.90. - Given a label whose minimum edit distance to any known vaccine exceeds 3, When normalized, Then no auto-map is applied and the result is flagged needs_review with top candidates and scores included.
Combination label parsing and multi-mapping
- Given "DHPP + KC", When normalized, Then the engine returns two mappings: "DHPP" and "Bordetella", each with confidence >= 0.85. - Given "Rabies/DHPP", When normalized, Then the engine returns ["Rabies", "DHPP"]. - Given "Bordetella & DA2PP, Rabies", When normalized, Then the engine returns ["Bordetella", "DHPP", "Rabies"] in the order encountered with no duplicates. - Given labels containing separators "+", "/", "&", ",", "and", "y", or "et", When normalized, Then the components are split correctly for mapping. - Given an unknown component (e.g., "LymeX") within a combination label, When normalized, Then it is excluded from the auto-mapped list and flagged needs_review with confidence below threshold.
Performance budget under 300ms per label
- Given a test set of 1000 real-world labels, When processed serially on-device and on server, Then median latency <= 100ms and 95th percentile latency <= 300ms per label in both environments. - Given a cold start, When normalizing the first label, Then time-to-first-result <= 600ms and subsequent labels meet the 300ms p95 budget. - Given concurrent processing of 10 labels on server, When executed, Then throughput >= 25 labels/sec with p95 latency <= 300ms per label.
Confidence scoring and thresholded auto-mapping with review fallback
- Given any mapping, When returned, Then the engine emits a confidence score s in [0.0, 1.0] with precision >= 0.01. - Given default threshold t = 0.80, When s >= t, Then the mapping is auto-applied; When s < t, Then the mapping is not auto-applied and the item is flagged needs_review. - Given two top candidates with |s1 - s2| < 0.05 and both >= 0.75, When evaluated, Then no auto-map is applied and the result is flagged ambiguous with the top 3 candidates and scores returned. - Given an org-level configuration for threshold t within [0.70, 0.95], When set, Then the engine uses t for auto-map decisions.
Tokenization and stripping of punctuation, emojis, and diacritics
- Given inputs containing punctuation, emojis, whitespace, and diacritics, When normalized, Then tokens exclude non-alphanumeric characters and diacritics are removed before matching. - Given "D.H.P.P!!", When normalized, Then it maps to "DHPP" with confidence >= 0.95. - Given "DA2PP—Parvo", When normalized, Then it maps to "DHPP" with confidence >= 0.90. - Given "Vacuna de Bordetella 🐶", When normalized, Then it maps to "Bordetella" with confidence >= 0.90. - Given "Rábies", When normalized, Then it maps to "Rabies" with confidence >= 0.90.
Policy alignment via canonicalization
- Given a policy requiring "Bordetella" and "DHPP", When inputs "KC" and "DA2PP" are normalized, Then the canonical results satisfy the policy via exact match on canonical names. - Given a misspelled input "Rabbis", When normalized, Then it maps to "Rabies" with confidence >= 0.85 and satisfies a policy requiring "Rabies". - Given an input "Corona", When normalized, Then it does not map to "DHPP" or any other policy-required vaccine and is flagged needs_review. - Given repeated normalization of the same label, When executed multiple times, Then the canonical result and confidence score are deterministic across runs.
Confidence Thresholds & Review Queue
"As a staff member, I want uncertain matches routed to a simple review queue so that I can quickly resolve edge cases without blocking the booking flow."
Description

Configurable confidence thresholds that auto-accept high-confidence matches, flag low-confidence matches for human review, and fall back to “unknown” when below minimum. Provides a review queue in the dashboard with suggested matches, explanations, and one-click accept/correct actions. Learns from corrections by automatically adding safe synonyms for future matches.

Acceptance Criteria
Threshold Configuration Validation and Persistence
- Given default thresholds exist (Auto-Accept=0.90, Review=0.70), when an Admin opens Threshold Settings, then current values display with allowed range 0.00–1.00. - Given an Admin sets Review=0.75 and Auto-Accept=0.92, when Save is clicked, then values persist, apply to new matches within 1 minute, and an audit log captures user, old values, new values, and timestamp. - Given invalid inputs (non-numeric, out-of-range, Review >= Auto-Accept), when Save is clicked, then validation blocks save and shows inline error messaging for each invalid field.
Auto-Accept High-Confidence Matches
- Given a label resolves to a normalized vaccine with confidence >= Auto-Accept and the top score exceeds the next best by >= 0.05, when ingestion completes, then the match auto-applies to the Pet Profile and policy rules evaluate immediately. - Given an auto-accepted match, when viewing audit details, then algorithm version, confidence score, key features used (e.g., synonym, language), and applied timestamp are present. - Given the top candidate confidence >= Auto-Accept but is tied within 0.05 of another candidate, when ingestion completes, then the item routes to Review instead of auto-accepting.
Route Medium-Confidence to Review Queue
- Given a candidate confidence >= Review and < Auto-Accept or a tie within 0.05, when ingestion completes, then a Review item is created showing original label, detected language, suggested standard vaccine, confidence score, explanation, and policy impact. - Given Review items exist, when opening the queue, then items are ordered oldest-first by default and can be filtered by Vaccine Type and Confidence Range. - Given a Review item is opened, when viewed, then Accept and Correct actions are visible and enabled.
Fallback to Unknown Below Minimum
- Given a candidate confidence < Review, when ingestion completes, then vaccine status is set to Unknown on the Pet Profile and a Review item is created tagged Unknown with no suggested match. - Given an Unknown item, when a reviewer manually assigns a standard vaccine, then the assignment applies, policy re-evaluates, and the item is removed from the queue with an audit entry. - Given an Unknown item remains unassigned, when viewing policy status, then it does not contribute to compliance until assigned.
One-Click Accept and Correct Behavior
- Given a Review item with a suggested match, when Accept is clicked, then the suggestion is applied, the item is removed from the queue within 2 seconds, and audit logs capture reviewer, action, and confidence. - Given a Review item, when Correct is clicked and a standard vaccine is chosen via typeahead, then the chosen vaccine applies, the suggestion is marked overridden, policy rules re-evaluate immediately, and the item is removed. - Given Accept/Correct actions complete, when viewing the queue header, then counts and badges refresh without page reload.
Learn From Corrections via Safe Synonyms
- Given the same shorthand-to-vaccine correction is performed on 3 distinct pets within one business, when the third correction is saved, then a clinic-scoped safe synonym is auto-added and visible in Synonyms settings with source=Corrections. - Given 5 distinct businesses confirm the same correction, when the fifth confirmation occurs, then a global safe synonym is auto-added and marked Auto. - Given a clinic-scoped synonym exists, when a new record with that shorthand is ingested, then the candidate confidence increases by at least 0.10 and routing follows thresholds. - Given an auto-added synonym is revoked by an Admin, when revoked, then future matches stop using it and the system records the revocation to prevent automatic re-addition.
Policy Rule Mapping & Validation
"As a business owner, I want normalized vaccine names to drive automatic policy checks so that eligibility decisions are accurate and consistent."
Description

After normalization, automatically evaluates the pet’s vaccine status against service policies (e.g., boarding requires Bordetella within 6 months). Uses canonical IDs to check recency, dose schedule, and species fit, then returns pass/fail with actionable messages and next steps. Updates Pet Profile Smart Cards and triggers templated SMS follow-ups when requirements are unmet.

Acceptance Criteria
Boarding — Bordetella within policy window (Pass)
Given a DOG pet with a normalized vaccine record for canonicalId "VAC_BORDETELLA" whose lastDoseDate is within the configured recencyDays window (e.g., 180) relative to the serviceDate And a Boarding service policy that requires canonicalId "VAC_BORDETELLA" with recencyDays=180 When the Policy Rule Engine evaluates the pet against the Boarding policy Then the result returns pass=true, ruleId present, canonicalId="VAC_BORDETELLA", reasonCode="VAX_RECENT_OK" And message contains "Bordetella current" and nextSteps=[] And the Pet Profile Smart Card updates Bordetella compliance to status="Compliant" with lastDoseDate and validUntil populated And no SMS follow-up is queued And an audit record is persisted with evaluationId, timestamp, inputs, and result
Boarding — Bordetella expired or missing (Fail + SMS follow-up)
Given a DOG pet with either no normalized record for canonicalId "VAC_BORDETELLA" or a lastDoseDate older than the configured recencyDays window relative to the serviceDate And a Boarding service policy that requires canonicalId "VAC_BORDETELLA" with recencyDays=180 When the Policy Rule Engine evaluates the pet against the Boarding policy Then the result returns pass=false with reasonCode in {"VAX_MISSING","VAX_OUTDATED"}, canonicalId="VAC_BORDETELLA" And message clearly states "Bordetella required within 180 days for Boarding" and includes the most recent known dose date if present And nextSteps includes ["Schedule Bordetella","Upload proof of vaccination"] And the Pet Profile Smart Card updates Bordetella compliance to status="Non-Compliant" and lists an unmetRequirement with canonicalId, requirementSummary, and dueBy And a templated SMS follow-up is queued using templateId "SMS_VAX_REQ_UNMET" with tokens {petName, vaccineName="Bordetella", dueBy, uploadLink} And an audit record is persisted with evaluationId, timestamp, inputs, and result
Daycare — Puppy DHPP dose schedule validation
Given a DOG pet with age and dose history available and normalized records for canonicalId "VAC_DHPP" And a Daycare policy configured for puppies requiring minDoses=3, minIntervalDays=14 between doses, and lastDoseRecencyDays=365 And the pet has only 2 recorded doses spaced >=14 days apart with lastDoseDate 200 days before serviceDate When the Policy Rule Engine evaluates the pet against the Daycare policy Then the result returns pass=false with canonicalId="VAC_DHPP" and reasonCode="DOSE_COUNT_INSUFFICIENT" And message states "DHPP requires 3-dose puppy series; 2 recorded" And nextSteps includes ["Complete remaining DHPP dose(s)"] And the Pet Profile Smart Card shows DHPP status="Non-Compliant" with dosesRecorded=2/minDoses=3 And a templated SMS follow-up is queued with templateId "SMS_VAX_REQ_UNMET" and tokens {petName, vaccineName="DHPP", remainingDoses=1, schedulingLink} And an audit record is persisted with evaluationId, timestamp, inputs, and result
Species Fit — Ignore cat vaccine for dog policies
Given a DOG pet with a normalized record for canonicalId "VAC_FVRCP" (cat vaccine) and no record for required dog vaccine canonicalIds (e.g., "VAC_DHPP" or "VAC_BORDETELLA") And a service policy requiring a dog vaccine canonicalId When the Policy Rule Engine evaluates the pet against the policy Then the cat vaccine record is not considered toward satisfaction And the result returns pass=false with reasonCode in {"VAX_MISSING","SPECIES_MISMATCH"} And the response includes species="DOG" and requiredCanonicalId referencing the dog vaccine And the Pet Profile Smart Card shows the dog vaccine requirement as "Non-Compliant" And an audit record is persisted with evaluationId, timestamp, inputs, and result
Rabies — Validate 1-year vs 3-year windows using latest valid dose
Given a DOG pet with normalized records for canonicalId "VAC_RABIES" where each record includes validityDurationDays (e.g., 365 or 1095) and lastDoseDate And a policy requiring Rabies current per its recorded validity window And the pet has a 1-year dose administered 14 months ago and a 3-year dose administered 2 years ago When the Policy Rule Engine evaluates the pet against the policy as of serviceDate Then the engine selects the latest valid dose by computed validUntil=lastDoseDate + validityDurationDays And the result returns pass=true with reasonCode="VAX_RECENT_OK", validUntil corresponding to the 3-year dose, and message "Rabies valid until <date>" And the Pet Profile Smart Card shows Rabies status="Compliant" with lastDoseDate and validUntil And an audit record is persisted with evaluationId, timestamp, inputs, and result
Result Contract & Audit — Pass/Fail payload and logging
Given an evaluation request containing petId, species, serviceId(s), serviceDate, and timeZone When the Policy Rule Engine completes evaluation Then the API responds 200 with a result per serviceId containing fields: {serviceId, pass, ruleId, canonicalIdsEvaluated, reasonCode, message, nextSteps[], evaluatedAt, validUntil?} And all text fields are human-readable and actionable, and arrays are present even when empty And the Pet Profile Smart Card is updated to reflect new compliance statuses per affected canonicalId And an audit log entry is persisted with the request context, evaluated rules, inputs, and results And if evaluation fails internally, the API returns pass=false with reasonCode starting with "ERROR_" and a non-empty message explaining next steps for resolution
Admin Lexicon Management UI
"As an admin, I want to manage synonyms and mappings myself so that the system stays accurate for my clientele and local clinics."
Description

An admin interface to search, add, edit, and deactivate synonyms; scope aliases by language, region, or account; and preview match outcomes. Supports bulk import/export (CSV), draft/publish with change review, and role-based access control. Provides usage analytics and “unknown label” suggestions for curation.

Acceptance Criteria
Synonym Search and Filter Results
Given a lexicon containing aliases across multiple languages, regions, accounts, and statuses When an admin searches for "da2pp" and applies filters Language=English, Region=US, Status=Active Then results include aliases and canonical names matching the query (case- and punctuation-insensitive) within the applied filters And each result displays alias, canonical vaccine, scopes (language/region/account), status, and last modified And results are sorted by relevance then last modified (desc) and paginated (25/50/100 per page) And p95 time to render first page (25 results) is <= 500 ms with backend p95 <= 300 ms on datasets up to 100k rows And when no matches are found an empty state appears with an "Add Synonym" call-to-action and retained filters
Create/Edit/Deactivate Synonym with Scoped Alias
Given a user with Editor or Admin role When creating a synonym with alias, canonical vaccine, and required scope fields (language required, region/account optional) Then the system validates required fields, trims whitespace, preserves display casing, and normalizes for matching And duplicate detection blocks saving when alias+scope already maps to any canonical (same canonical -> duplicate; different canonical -> conflict) and shows links to conflicting entries When saving a valid record Then the synonym is saved as Draft in the current change set unless "Publish now" is selected by an Admin And an audit entry is recorded with before/after values When editing an Active synonym Then a new Draft revision is created without altering the Active version until publish When deactivating an Active synonym and confirming Then status becomes Inactive upon publish and the alias is excluded from matching after the publish time; reactivation is allowed via edit And p95 save/deactivate actions complete in <= 500 ms
Match Preview and Conflict Resolution Order
Given a test label input and selected scopes (language, region, account) When the user clicks "Preview Match" Then the system displays the matched canonical vaccine, rule path (alias -> canonical -> policy), and a confidence score And shows up to 5 candidate matches with scores when multiple rules could apply And indicates the resolution order used: Account > Region > Language > Global, then Active > Draft And flags if the match would change due to a scheduled activation/deactivation And a toggle "Include Drafts" switches the preview between current production and current change set drafts
Bulk Import/Export with Validation and Idempotency
Given a CSV using the template columns [alias, canonical_vaccine, language, region, account_id, active, notes] When an Editor or Admin uploads a file <= 5 MB and <= 10,000 rows Then the system validates each row, stages valid rows as Draft in a new change set, and produces per-row error messages for invalid rows with a downloadable error CSV And duplicate detection prevents intra-file and existing-record duplicates per scope; conflicts are reported with row numbers And an import job shows status (Queued, Processing, Completed, Completed with Errors, Failed) and counts for processed/created/updated/errored And re-uploading the same file (checksum match) within 24 hours is idempotent and does not create additional records And 95% of imports with 10,000 rows complete within 2 minutes When the user exports with filters applied Then a CSV is generated containing the filtered rows with headers, UTF-8 encoding, and ISO 8601 UTC timestamps; for datasets > 50,000 rows an async download link is provided; RBAC scoping is enforced
Draft/Publish Workflow with Review and Scheduling
Given a change set containing Draft additions/edits/deactivations When the author submits it for review Then the change set status becomes In Review and assigned reviewers are notified When an Approver (Admin) approves the change set Then all changes publish atomically as a new lexicon version with a rollback point recorded and a publish timestamp When a reviewer rejects the change set Then it returns to Draft and a rejection comment is required When a publish is scheduled for a future time T Then the system publishes at T after running pre-publish validation; failures cancel the publish and notify the author with errors If a conflicting change is published after submission but before approval Then approval is blocked and a merge-required diff view is presented
Role-Based Access Control and Audit Logging
Given roles Viewer, Editor, and Admin When a Viewer accesses the Lexicon UI Then they may search, filter, preview, and export, but cannot create, edit, deactivate, import, or publish; restricted actions are disabled and API returns 403 if invoked When an Editor accesses the Lexicon UI Then they may create/edit/deactivate Drafts, import to Draft, submit change sets for review, but cannot approve/publish or set scopes outside their permissions When an Admin accesses the Lexicon UI Then they may perform all actions including approve/publish, set account- and region-wide scopes, and configure analytics thresholds And every mutating action creates a read-only audit log entry with user, timestamp, action, entity id, and before/after values; audit logs are filterable and exportable
Usage Analytics and Unknown Label Suggestions Curation
Given the system aggregates label processing events When an authorized user opens the Analytics tab and selects a date range (7/30/90 days) Then metrics display total labels processed, match rate (%), top 20 aliases and canonical vaccines by match count, and breakdowns by language/region/account with data freshness <= 60 minutes When viewing Unknown Labels Then a table lists unique labels with counts, first/last seen, example sources, and an optional suggested canonical with confidence When a curator accepts a suggestion and chooses a canonical and scopes Then a new synonym Draft is created and linked to the suggestion; the suggestion is marked Resolved When a curator ignores a suggestion Then it is hidden from default views, logged with reason, and only reappears if future frequency exceeds a configurable threshold And exports of analytics and unknown labels respect applied filters and RBAC; Viewers are read-only, Editors/Admins can triage
Auditability & Explainability Logs
"As an operator, I want a clear audit trail of how a label was interpreted and enforced so that I can resolve disputes and meet compliance obligations."
Description

Full trace logging for each normalization: input text, detected language, preprocessing steps, candidate scores, selected canonical ID, lexicon version, reviewer decisions, and policy evaluation results. Exposed in pet profile history and exportable for compliance. Retention and privacy align with FetchFlow’s data policies.

Acceptance Criteria
Normalization Log Completeness and Timeliness
Given any vaccine label normalization is performed When the normalization completes Then one immutable log record is written within 500 ms and contains: correlation_id, ISO 8601 UTC timestamp, raw_input_text, detected_language (code and confidence), preprocessing_steps[] (ordered with operation, input, output, timestamp), candidate_matches[] (canonical_id, display_name, score 0–1), selected_canonical_id, selection_rationale, lexicon_version (semver and hash), model_version (if applicable), policy_evaluation (rule_ids[], result, details), actor (system or user_id), pet_id, org_id And the log record validates against the defined logging JSON schema and all required fields are non-null And the record is append-only; any attempted update does not alter the original and creates a linked correction entry instead
Multi-language and Shorthand Explainability
Given an input vaccine label includes clinic shorthand or a non-English language When the system normalizes the label (e.g., DAPP, DA2PP, KC, or localized names) Then detected_language is recorded with confidence ≥ 0.80 and the preprocessing_steps include explicit shorthand expansion and/or translation operations with references to rule or dictionary IDs And candidate_matches list includes at least the expanded canonical candidates with scores, and selection_rationale explains tie-breaks if scores are equal within 0.01 And policy_evaluation references the canonical_id selected and shows compliant/non-compliant with rule_ids evaluated
Pet Profile History Visibility and Permissions
Given a user with permission "View Audit Logs" opens a pet's Profile > History > Normalization event When the event details are viewed Then the UI presents a human-readable explanation including raw_input_text, detected_language, preprocessing_steps, candidate_matches with scores, selected_canonical_id, policy_evaluation, and any reviewer decisions And the event detail loads in ≤ 2,000 ms for p95 across 10k events dataset And if the user lacks "View Audit Logs" permission, access is denied and no log fields are displayed And PII fields configured as sensitive are masked unless the user also has "View PII in Logs" permission
Compliance Export of Explainability Logs
Given a user with permission "Export Audit Logs" requests an export for a specified org_id and date range When the export is generated Then both CSV and JSON exports are available with a consistent schema, including schema_version and lexicon_version And up to 50,000 records export completes within 30 seconds and is provided via a time-limited, authenticated download link (TTL ≥ 24 hours) And every exported record matches the corresponding stored log fields 1:1 And the export action itself is audit-logged with requester_id, time, filter parameters, and file checksum
Retention, Redaction, and Data Subject Requests
Given the organization retention policy is configured (e.g., N months) for audit logs When a log record exceeds the retention period Then it is purged within 7 days and excluded from queries and exports And purge actions are audit-logged with counts and time windows Given a verified data subject deletion request is received for a pet/owner When the request is processed Then PII fields in associated logs are anonymized or deleted within 30 days while preserving non-PII explainability fields, and logs indicate redaction status and reason And users without "View PII in Logs" see masked values at all times
Reviewer Override Audit Trail
Given a reviewer adjusts a normalization decision (e.g., changes selected_canonical_id) in the UI When the change is submitted Then a new append-only decision entry is recorded with reviewer_id, timestamp, previous value, new value, reason_code, and optional comment And policy_evaluation is recomputed and logged with a new evaluation entry linked to the override And the pet profile history displays the override as a new version with a diff view between versions And exports include both the original and override entries with linkage
Deterministic Replay and Versioning
Given a normalization log contains lexicon_version, model_version (if any), and policy_version When a replay is executed using the same versions and inputs Then the system reproduces the same candidate_matches, scores, and selected_canonical_id exactly And if any required version artifact is unavailable, the replay clearly fails with a "version artifact missing" error and no partial results And a nightly job successfully replays a random 1% sample of the last 24 hours of logs with 100% match rate; any mismatch creates a high-severity alert with example correlation_ids

Rule Studio

A visual builder for payout logic that lets you define percent or fixed splits by service, staff role, partner, location, day/time, or booking source. Choose whether splits calculate before or after fees/taxes, set deposit rules, and stack conditions (e.g., pop‑up nail trims on Saturdays = 60% staff, 10% partner, remainder house). Clear previews reduce errors and eliminate spreadsheet maintenance.

Requirements

Visual Rule Canvas
"As an owner-operator, I want a drag-and-drop interface to create payout rules so that I can configure complex splits without needing spreadsheets or developer help."
Description

Provide a drag-and-drop visual builder within the FetchFlow dashboard to compose payout rules as readable blocks connected into conditions and outcomes. The canvas must support creating, editing, duplicating, and deleting rules, grouping related rules, and inline help/tooltips to reduce configuration errors. It will replace spreadsheet-based setups by offering guided templates for common scenarios (e.g., weekend bonuses, partner splits) and real-time validation to prevent incomplete or conflicting configurations. Integration points include saving rules to the organization’s settings, exposing read-only views in user permission scopes, and triggering downstream recalculation in the payout pipeline when published. Expected outcomes are faster setup, fewer mistakes, and easier maintenance for non-technical users.

Acceptance Criteria
Drag-and-Drop Rule Composition and Structural Validation
Given a blank canvas When the user drags Condition and Outcome blocks onto the canvas and connects them without any dangling connections Then the canvas marks the rule as Valid and the Publish action is enabled Given any required field on any block is empty or a connection is missing When the user attempts to publish Then publishing is blocked, invalid elements are highlighted in place, and specific error text explains how to resolve each issue Given a valid, connected rule When the user clicks Publish Then the rule is saved to the organization’s settings as Published and appears in the Rules list Given the preview panel is visible When any block value changes Then the preview recalculates and displays the resulting payout split summary in real time
Real-Time Conflict Detection and Resolution
Given at least one published rule exists When a new or edited rule creates overlapping scope on service, staff role, partner, location, day/time, or booking source Then the canvas flags a Conflict state, lists the conflicting rule(s) and overlapping dimensions, and disables Publish Given a rule is in Conflict When the user adjusts condition values to remove the overlap or disables the conflicting rule(s) Then the Conflict state clears and Publish becomes enabled Given multiple rules could evaluate for the same booking When conflicts are resolved Then the preview shows exactly one applicable rule path for the provided sample booking inputs
Edit, Duplicate, and Delete Rule Management
Given an existing rule When the user edits block fields and republishes Then the updated rule is saved and the latest configuration is reflected in the Rules list Given an existing rule When the user selects Duplicate Then a new rule with identical blocks and values is created in the same group with a unique name suffix and is Unpublished Given any rule When the user selects Delete and confirms in a modal dialog Then the rule is removed from the canvas and Rules list and is no longer used by the payout pipeline
Rule Grouping and Organization
Given a rules workspace When the user creates a new group and names it Then the group appears in the sidebar or canvas with the given name Given existing rules When the user drags a rule into a different group Then the rule moves to that group and the move persists on reload Given multiple groups When the user collapses or reorders groups Then the collapsed state and order persist on reload
Guided Assistance: Templates and Inline Help
Given the user opens the Templates gallery When the user selects a common scenario template (e.g., Weekend Bonus, Partner Split) Then a pre-populated rule is created on the canvas with placeholder fields clearly marked for completion Given any block has a help icon When the user hovers or taps the icon Then a tooltip appears with a concise definition and example relevant to that block Given placeholders remain in a template-derived rule When the user attempts to publish Then validation identifies the incomplete fields by name and location and blocks publishing until completed Given the preview panel is open on a template-derived rule When the user provides sample booking inputs Then the preview displays the computed payout breakdown before and after fees/taxes based on the current settings
Permission-Based Read-Only View
Given a user with read-only permissions for rules When the user opens a published rule on the canvas Then all editing controls are disabled, values are visible, and Publish/Save actions are hidden Given a read-only user When the user attempts an edit action via UI or API Then the system denies the action with an authorization error and no changes are persisted
Publish Persists Settings and Triggers Recalculation
Given a valid rule on the canvas When the user clicks Publish Then the rule is persisted to the organization’s settings and a success confirmation is shown Given a rule has been published or updated When publishing completes Then a downstream payout recalculation is triggered for affected future payouts and an audit event is recorded with rule ID and timestamp
Condition Targeting Library
"As a business owner, I want to target payout rules by service, staff role, partner, location, day/time, and booking source so that payouts reflect how and where work was booked and performed."
Description

Offer a reusable library of conditions that can be stacked and combined using AND/OR logic to target rules by service, staff role, partner, location, day/time window, and booking source (SMS, link, POS, marketplace). Each condition must support multi-select, inclusions/exclusions, and time-zone aware scheduling. Provide field pickers tied to FetchFlow entities (services, staff, partners, locations) with type-ahead search and IDs to ensure referential integrity. The system must evaluate conditions at booking and at payout time, with snapshots to preserve historical correctness when catalog data changes. Benefits include granular control of payout policies and alignment with real-world operations.

Acceptance Criteria
Multi-Select Inclusion and Exclusion Across Dimensions
Given the rule builder exposes Service, Staff Role, Partner, Location, Day/Time Window, and Booking Source conditions When a user selects multiple inclusions and multiple exclusions within a single condition Then evaluation includes only records whose attribute is in the inclusions and not in the exclusions Given a condition is left empty When the rule is evaluated Then that condition imposes no filtering Given Booking Source options include SMS, Link, POS, and Marketplace When any combination is selected for inclusion and/or exclusion Then only bookings from included sources minus excluded sources match Given a booking lacks a value for a targeted dimension (e.g., no partner) When the condition requires inclusion on that dimension Then the booking does not satisfy that condition
AND/OR Stacked Conditions with Grouping and Precedence
Given a rule with grouped conditions using AND/OR and up to two levels of nesting When the rule is evaluated Then the truth table follows explicit parentheses shown in the UI and evaluates deterministically Given conflicting inclusion and exclusion on the same dimension across groups When the rule is evaluated Then exclusions take precedence over inclusions for that dimension Given a rule structured as (A OR B) AND (C OR D) When a booking matches B and C only Then the rule evaluates to true And when a booking matches only A Then the rule evaluates to false Given a group is empty or contains only invalid conditions When saving the rule Then validation blocks save with a clear error identifying the empty/invalid group
Time-Zone Aware Day/Time Windows Including Cross-Midnight and DST
Given a location set to America/Chicago and a time window defined as Saturday 20:00–02:00 When a booking at that location starts at Sunday 01:30 local time Then it matches the Saturday time window Given a time window 01:00–03:00 on the day of spring-forward DST where 02:00 does not exist locally When evaluating bookings at 01:30 and 03:15 local time Then 01:30 matches and 03:15 does not match (end-exclusive at 03:00) Given a user creates a rule without tying it to a specific location When evaluating time windows Then the business default time zone is used Given a booking is created from a different time zone than the service location When evaluating the rule Then the service location’s time zone is used for time-window checks
Entity Field Pickers with Type-Ahead and Referential Integrity
Given a field picker for Services with 10,000 entities When the user types at least 2 characters Then the top 20 matches (by name prefix and fuzzy match) return within 150 ms p95 and display name and immutable ID Given an entity is selected in any picker When the selection is saved Then the stored value is the immutable ID, not the display name And when the entity is later renamed Then the selection remains valid and displays the updated name on reload Given a selected entity is deleted or deactivated When loading the rule for edit Then the reference is flagged as invalid and activating/saving the rule is blocked until resolved Given a client attempts to save a rule payload containing unknown or mismatched IDs When the API validates the request Then it returns HTTP 400 with a field-level error and does not persist any changes
Evaluation at Booking and at Payout with Snapshot Persistence
Given a booking is created When the rule engine evaluates at booking save Then it stores a snapshot containing matched condition set, selected entity IDs, location time zone, and evaluation timestamp Given the same booking reaches payout When payout calculation runs Then the engine evaluates using the stored snapshot attributes and the booking’s actual service start time for time-window checks Given catalog data (service name, staff role, partner affiliation, or location time zone) changes after booking When payout is calculated Then the rule match outcome equals the outcome that would have occurred at booking time Given the evaluation snapshot is missing or corrupt When payout is calculated Then the system logs the error, falls back to current catalog data for evaluation, and marks the payout for manual review
Snapshot Auditability and Versioning Immutability
Given a booking with a stored evaluation snapshot When viewing the audit log Then it shows rule ID and version, condition values, entity IDs, time zone, booking-evaluation timestamp, and payout-evaluation timestamp Given a non-admin user attempts to modify a stored snapshot When the attempt is made Then access is denied and the attempt is recorded in the audit log Given a rule is versioned or cloned When new bookings are evaluated Then new snapshots reference the new rule version And existing snapshots continue referencing the original version without mutation
Performance and Scale Targets for Evaluation and Search
Given a rule with up to 20 conditions and multi-select lists totaling up to 2,000 selected IDs When evaluated at booking time Then the engine returns a result within 200 ms p95 and 500 ms p99 Given nightly payout processing for 10,000 bookings across 100 active rules When the batch job runs Then throughput sustains at least 50 bookings per second without timeouts or error rates exceeding 0.1% Given type-ahead searches over 50,000 entities across Services, Staff, Partners, and Locations When querying with a 3-character prefix Then API responses return within 300 ms p95 and 700 ms p99
Split Calculator Options
"As an owner, I want to define percent or fixed splits and choose whether they calculate before or after fees and taxes so that payouts are fair, predictable, and match accounting."
Description

Enable configuration of split outcomes as percent or fixed amounts per payee (e.g., staff, partner, house) with support for remainders and rounding rules. Allow selection of the calculation base: before-fees, after-processing fees, after-taxes, or custom base defined by organization settings. Provide ordering of deductions (fees, taxes, discounts, packages) and a clear definition of taxable vs non-taxable items. Validate that total percentage does not exceed 100% and that fixed splits cannot exceed the base. Integrate directly with the payments and invoices subsystem to source actual line items and fees, and write computed allocations to the payout ledger. This ensures predictable, auditable payouts that match financial statements.

Acceptance Criteria
Percent Splits with Remainder and Rounding
Given a rule with Staff 60%, Partner 30%, remainder payee House and rounding rule "Round half up" When applied to a base of $100.01 Then allocations equal Staff $60.01, Partner $30.00, House $10.00 and the sum equals the base Given any rule where total percent > 100% When the user attempts to save or publish the rule Then the action is blocked with the error "Total percentage exceeds 100%" and the rule is not saved Given percent splits totaling < 100% and no remainder payee set When the user attempts to save the rule Then the action is blocked with the error "Remainder payee required" Given a payout preview for a percent-split rule When the calculation runs Then the preview shows per-payee amounts, the configured rounding rule, any penny adjustments, the remainder recipient, and the total equals the selected base
Fixed Splits Validation and Cap Against Base
Given a rule with fixed splits Staff $20.00 and Partner $20.00 with remainder to House When applied to a base of $50.00 Then allocations equal Staff $20.00, Partner $20.00, House $10.00 and the sum equals the base Given a rule with fixed splits that sum to $45.00 When applied to a base of $40.00 in preview or payout Then the calculation is blocked with the error "Fixed splits exceed base" and no ledger entries are written Given a fixed-split rule When applied to invoices with varying bases where fixed sum ≤ base Then the remaining base is assigned to the configured remainder payee and totals match the base amount for each invoice
Calculation Base Selection (Before Fees, After Processing Fees, After Taxes, Custom)
Given an invoice with Service subtotal $100.00 (taxable), Discount $10.00, Tax 8% after discount = $7.20, and Processing fee $3.00, and deduction order Discounts → Taxes → Fees When base selection = Before Fees Then the base amount equals $97.20 (subtotal − discount + tax applied, prior to fees) Given the same invoice and order When base selection = After Processing Fees Then the base amount equals $94.20 ($97.20 − $3.00) Given the same invoice and order When base selection = After Taxes Then the base amount equals $90.00 (subtotal − discount, excluding taxes and fees) Given org custom base is configured to exclude taxes and fees and include discounts When base selection = Custom Then the base amount equals $90.00 and the preview labels the base as Custom with the included/excluded components
Deduction Ordering Configuration Applied to Base
Given deduction order A = Discounts → Packages → Taxes → Fees and an invoice containing a discount, a package credit, a tax, and a processing fee When base selection = Before Fees Then the base excludes fees and reflects prior deductions in the configured order (subtotal minus discount and package plus applicable tax if tax precedes fees) Given the deduction order is changed to Discounts → Fees → Packages → Taxes When the preview is refreshed Then the base calculation reflects the new order, the ordered steps are displayed in the preview, and the computed base changes accordingly Given a rule is published with a specific deduction order When calculations are written to the ledger Then the persisted ledger metadata includes the deduction order used and the rule version identifier
Taxable vs Non‑Taxable Items Affect Base and Taxes
Given an invoice with Taxable items subtotal $80.00 and Non‑taxable items subtotal $20.00 and a 10% tax applied to taxable items with no discounts When taxes are computed Then the tax equals $8.00 and no tax is applied to non‑taxable items Given the same invoice When base selection = After Taxes Then the base amount equals $92.00 (subtotal $100.00 minus $8.00 tax) Given an item’s taxability flag is toggled from taxable to non‑taxable When the preview is refreshed Then the tax line and "After Taxes" base recompute correctly to reflect the new taxability
Payout Ledger Write and Reconciliation
Given a successful split calculation for invoice ID INV-123, rule version RV-1, and base type After Processing Fees When posting the payout Then one ledger entry per payee is written with invoiceId, ruleVersionId, baseType, baseAmount, payeeId, amount, currency, and roundingAdjustment, and the sum of payee amounts equals the base amount with any penny difference captured as a roundingAdjustment Given the same invoice and rule version is reprocessed When an idempotency key of INV-123|RV-1 is used Then no duplicate ledger entries are created and the operation is idempotent Given payouts posted for a settlement period When exporting financial statements Then totals reconcile to invoice totals, fees, and taxes according to the defined base selection and deduction order, with a zero net variance beyond recorded rounding adjustments
Rule Preview and Validation Messaging
Given a valid rule When the user opens the preview against a selected invoice Then the preview shows the selected base type and amount, the ordered deductions with amounts, per‑payee allocations, the remainder recipient, rounding details, and all values in the invoice currency Given an invalid percent rule where total percent > 100 or a fixed‑split rule where fixed sum > base for the previewed invoice When the user attempts to publish Then publish is disabled and inline errors clearly state the violation and the affected payees Given the user edits base selection, deduction order, percentages, or fixed amounts When the preview is refreshed Then all displayed allocations and the base amount update consistently and any prior validation errors are re‑evaluated and cleared if resolved
Deposits and No-Show Fee Rules
"As an owner, I want to control how deposits and no-show fees are applied to payouts so that revenue is handled fairly and automatically without manual adjustments."
Description

Provide configurable rules for deposits, prepayments, and no-show or late-cancel fees specifying whether these amounts are included in split bases, retained by the house, or shared by defined percentages. Support deposit forfeiture logic tied to cancellation windows and attendance status from the appointment record. Allow exceptions by service or booking source and pro-rate logic when partial services are delivered. Integrate with existing package redemptions so that deposit application respects package usage. Ensure all outcomes are reflected in the payout ledger with clear labels for audit and client transparency.

Acceptance Criteria
Deposit Included or Excluded in Split Base
Given an appointment for a $100 service with a $30 deposit, tax $8, and a split rule set to include deposit in the split base calculated before taxes and fees When the appointment is completed and checkout applies the deposit Then the split base equals $100 and payouts allocate $60 to staff, $10 to partner, and $30 to house per a 60/10/house remainder rule, and the payout ledger shows a "Deposit Applied to Service" entry linked to the appointment Given the same service and deposit with the rule set to exclude deposit from the split base and retain deposit by house When the appointment is completed Then the split base equals $70 and payouts allocate $42 to staff, $7 to partner, and $21 to house, and the ledger records a separate $30 "Deposit Retained by House" entry
Deposit Retention vs Sharing Configuration
Given a $120 service with a $20 deposit and a rule that shares deposits 50% staff and 50% house When the appointment is completed Then the payout ledger contains a "Deposit Shared" line allocating $10 to staff and $10 to house in addition to the service split Given the same appointment and the split rule toggled between before and after taxes/fees with taxes = $0 and fees = $0 When checkout completes Then the deposit sharing allocation remains $10 to staff and $10 to house and is unaffected by the toggle Given a rule that retains deposits by house is enabled When a user attempts to also enable deposit sharing Then the system prevents saving the rule and displays a validation error "Deposit cannot be both retained and shared"
Deposit Forfeiture by Cancellation Window and Status
Given an $80 service with a $20 deposit and a rule "Forfeit deposit if cancel within 24h or status = No-Show" When the client cancels 3 hours before the start time Then the deposit is forfeited and a "Deposit Forfeited - Late Cancel" ledger line allocates the $20 per the configured retention/sharing rule Given the same rule When the client cancels 48 hours before the start time Then the deposit is not forfeited and is automatically refunded to the original payment method and recorded as "Deposit Refund - Outside Window" in the ledger Given the appointment is marked No-Show When the scheduled end time passes Then the deposit is forfeited regardless of cancel window and is recorded with status reference and timestamp
No-Show and Late-Cancel Fee Inclusion and Sharing
Given a rule that charges a $25 no-show fee retained 100% by house and excluded from the split base When an appointment is marked No-Show Then the ledger records "No-Show Fee (House Retained) $25" and no portion of this $25 is included in staff or partner payouts Given a rule that charges a $30 late-cancel fee included in the split base and split 60% staff, 10% partner, remainder house When an appointment is canceled 1 hour before the start time Then staff receives $18, partner $3, and house $9 from the fee, and the ledger line is labeled "Late Cancel Fee (Shared)"
Exceptions by Service and Booking Source
Given a default rule requires a 30% deposit for all services and an exception sets $0 deposit for "Pop-Up Nail Trim" When a "Pop-Up Nail Trim" is booked Then no deposit is collected and the exception is referenced in the ledger and booking confirmation Given the default rule requires a 30% deposit and an exception sets a 50% deposit for booking source "Instagram" When a service is booked via "Instagram" Then a 50% deposit is collected and recorded with "Exception Applied - Source: Instagram" Given both a service exception and a booking-source exception could apply and rule order sets service exceptions to higher priority When the qualifying booking is created Then the service exception is applied and the ledger shows the chosen exception and the rule ID used
Pro-Rate Logic for Partial Service Delivered
Given a $100 service with a $30 deposit and a rule to pro-rate deposits when partial services are delivered When 50% of the service is delivered and the client cancels the remainder inside the cancel window Then $15 of the deposit is applied toward the delivered portion and included or excluded from the split base per configuration, and the remaining $15 is forfeited and recorded as "Deposit Forfeited - Partial" Given the same appointment and rule When 50% is delivered and the remainder is canceled outside the window Then $15 of the deposit is applied to the delivered portion and the remaining $15 is refunded and recorded as "Deposit Refund - Partial Outside Window"
Package Redemption Integration with Deposits
Given a $100 service fully covered by a package redemption and a $20 deposit collected at booking When checkout redeems the package Then the deposit is not applied to the covered service, is refunded or applied to add-ons per rule setting "Refund excess deposit", and the ledger shows "Package Redemption" and "Deposit Refund - Package Covered" with zero impact on staff or partner splits from the deposit Given a $100 service with a package covering $60 and a $20 deposit When checkout is completed Then the deposit applies to the $40 cash portion first, reducing client balance accordingly, and splits are calculated on the $40 base per the include or exclude deposit rule, with ledger cross-referencing the package usage Given any package redemption occurs When payouts are generated Then deposits are not double-counted in split bases and the ledger validates that applied deposit + refunded deposit + forfeited deposit equals the original deposit amount
Payout Preview Simulator
"As an owner, I want to preview and simulate example payouts before publishing rules so that I can validate results and avoid costly mistakes."
Description

Include an interactive simulator that lets users input or select example bookings (service, price, staff, partner, location, time, source, fees/taxes) to see calculated splits before publishing changes. The simulator must show step-by-step calculations, bases used, rounding, remainder handling, and final allocations per payee. Provide side-by-side comparison between current published rules and draft changes, plus exportable previews for stakeholder review. Integrate with historical bookings to run batch what-if tests and surface warnings when rules would materially change payouts beyond configurable thresholds.

Acceptance Criteria
Single Booking Simulation with Full Input Coverage
Given a user has a draft rule set open in Rule Studio and the simulator panel is visible When the user inputs or selects an example booking with service, base price, staff, partner, location, booking time (with timezone), booking source, fees, and taxes Then the simulator requires all mandatory fields and shows inline validation for missing or invalid values And the simulator uses the entered data to resolve the applicable rules for that booking And the calculated payout result is displayed within 2 seconds
Transparent Step-by-Step Calculation Display
Given a booking is simulated When results are shown Then a step-by-step breakdown lists each rule evaluated, the condition outcome, and the order of application And the simulator displays the payout base used (before or after fees/taxes) with the exact amounts included/excluded And deposit application is shown with amounts withheld or applied And each split shows whether it is percent or fixed, the exact math performed, and intermediate subtotals And the final allocations per payee are itemized and sum to the payout base
Rounding and Remainder Handling Accuracy
Given the organization’s rounding mode and remainder distribution rules are configured When the simulator computes payouts that produce fractional cents Then rounding is applied using the configured mode and the method name is labeled in the breakdown And any remainder pennies are distributed using the configured strategy with the recipient(s) identified And the sum of allocations exactly equals the payout base with zero discrepancy
Side-by-Side Comparison: Published vs Draft
Given both a published rule set and a draft rule set exist When the user runs a simulation Then results are shown side-by-side for Published and Draft with identical inputs And per-payee amounts, totals, and deltas (absolute and percentage) are displayed and visually highlighted for changes And totals on both sides reconcile to their respective payout bases And the user can switch drafts or rule versions and the comparison updates accordingly within 2 seconds
Exportable Preview Package
Given a simulation (single or batch) has completed When the user exports the preview Then a downloadable PDF and CSV are generated containing inputs, rule versions, calculation steps, per-payee allocations, and deltas And the export includes metadata: organization, user, timestamp, simulator version, and threshold settings And exported files preserve numeric precision to cents and can be re-imported to reproduce totals And the export completes within 10 seconds for single simulations
Batch What-If on Historical Bookings
Given the user selects a historical date range and optional filters (service, staff, partner, location, booking source) When the user runs a batch what-if against draft rules Then the simulator processes the selected bookings and produces a summary: total payout delta, average delta per booking, count and percentage of bookings with increases/decreases, and top impacted payees And a detailed per-booking table is available with inputs, published vs draft amounts, and deltas And the user can export the batch results to CSV And the job can be canceled by the user and reports partial results processed up to cancellation
Threshold-Based Material Change Warnings
Given material change thresholds are configured in percentage and/or absolute currency When a single or batch simulation detects any payee payout change that exceeds a threshold Then a warning banner and per-row indicators are displayed identifying the payees, bookings, and magnitude of change And the simulator provides a filter to show only impacted items and includes these warnings in exports And warnings do not block simulation but persist until thresholds are adjusted or the draft is modified
Rule Priority and Conflict Resolution
"As an owner, I want clear rule priorities and conflict resolution so that the correct payout rule applies consistently when multiple rules overlap."
Description

Implement a deterministic evaluation order with explicit rule priority, tie-breakers, and optional stop-on-match behavior. Provide conflict detection that flags overlapping conditions and suggests consolidations. Allow scopes (global, location, service-level) with inheritance and override semantics, and present the resolved policy view for any booking. Log rule selection at runtime for each payout event to support explainability. Benefits include predictable outcomes, reduced support tickets, and easier troubleshooting when multiple rules could apply.

Acceptance Criteria
Deterministic Priority Ordering for Matching Rules
Given a booking matches three rules with priorities 100, 90, and 80 in the same scope and payout target When the engine evaluates payout rules Then it processes matches in descending priority order (100, 90, 80) And the highest-priority rule determines the final value for any conflicting payout attribute And lower-priority rules only affect non-conflicting attributes And the evaluation completes within 100 ms at p95 for 10 concurrent evaluations
Deterministic Tie-Breakers for Equal-Priority Rules
Given two or more rules with equal priority match the same booking When tie-breakers are applied Then scope specificity decides the winner: service-level overrides location-level, location-level overrides global And if scope specificity is equal, the rule with more match conditions wins And if still tied, the most recently updated rule wins And the evaluation trace records the tie-breaker path and winning rule ID
Stop-on-Match Halts Further Evaluation
Given a booking matches a rule that has stop-on-match enabled and also matches lower-priority rules When the engine applies the matching rule Then evaluation stops immediately after that rule And lower-priority rules are marked as skipped due to stop-on-match in the trace with no side effects applied And the evaluation trace includes stopOnMatch=true for the winning rule
Authoring-Time Conflict Detection and Consolidation Suggestions
Given a user attempts to save a new or edited rule whose conditions overlap an existing active rule in the same scope and payout target When the rule is validated Then the UI displays a conflict warning listing overlapping rule IDs and condition intersections And it offers at least one suggestion: adjust priority, enable stop-on-match, or merge conditions into a single rule And the user must select a resolve action (change priority/stop flag, acknowledge override, or cancel) before saving And the preview shows an estimated count of impacted upcoming bookings for the next 30 days
Scope Inheritance and Override Semantics
Given an active global rule, a location-level override for Location A, and a service-level override for Service X at Location A When a booking for Service X at Location A is evaluated Then the service-level override applies first And the location-level override applies only to attributes not defined by the service-level override And the global rule applies only to attributes not defined by more specific overrides And the resolved policy view labels each attribute with its source scope
Resolved Policy View for Any Booking
Given a booking context including service, staff role, partner, location, day/time, booking source, and fees/taxes settings When a user opens the Resolved Policy view in Rule Studio Then the system displays the ordered list of considered rules with priorities, scopes, and match status And shows the final payout breakdown including percent/fixed values, before/after fees/taxes basis, and deposit behavior And shows reasons non-winning rules were skipped (lower priority, tie-breaker, stop-on-match) And allows copying an evaluation trace with rule IDs and decision steps to the clipboard
Runtime Rule Selection Logging and Retrieval
Given any payout event is executed When the engine evaluates rules Then it writes an immutable audit record containing timestamp, booking ID, payout event ID, evaluated rule IDs in order, tie-breakers used, stop-on-match flag, and final outcome And the record is queryable by booking ID within 5 seconds of event completion And audit records are retained for 365 days and exportable as JSON
Versioning, Draft/Publish and Audit Trail
"As an administrator, I want versioning with draft/publish controls and an audit trail so that payout policy changes are safe, reviewable, and reversible."
Description

Support versioned rule sets with draft, review, and publish states, including scheduled effective dates and rollback to prior versions. Capture a detailed audit trail of who changed what and when, including diffs and approval notes. Restrict publishing via role-based permissions and require confirmation when changes impact active payroll cycles. Notify affected users of upcoming changes and ensure the payout engine uses the correct version at calculation time. This delivers governance, traceability, and operational safety for financial rules.

Acceptance Criteria
Draft and Review Workflow
Given a user with Editor role in Rule Studio, when they create or duplicate a rule set, then a new Draft version with a unique, incrementing version identifier is created and visible to Editor/Approver/Publisher roles only. Given a Draft version, when the user saves changes, then the draft autosaves within 5 seconds and remains non-referencable by the payout engine. Given a Draft version with no critical validation errors, when the Editor submits it for review, then the version state changes to In Review and the submitter cannot publish until an Approver approves.
Scheduled Publish and Effective Dating
Given a version in In Review state, when an Approver approves and a Publisher schedules publish for a future date/time, then the version transitions to Scheduled and will become Active within 60 seconds of the scheduled timestamp. Given an Active version exists for the same scope, when a new version is scheduled, then at activation the previous Active version moves to Archived and no overlapping active windows occur for the same scope. Given a Publisher selects Publish Now, when confirmed, then the version becomes Active within 60 seconds and is used for new calculations immediately thereafter.
Role-Based Permission Enforcement
Given user permissions, when a Viewer or Editor attempts to publish or schedule a version, then the action is blocked with a 403-style error and no state change occurs. Given user permissions, when a Publisher performs publish or schedule, then the action succeeds if all preconditions (approval, validation) are met. Given an unauthorized attempt, when it occurs, then an audit entry is recorded with user, timestamp, and denied action.
Active Payroll Impact Confirmation
Given a version is being published with an effective time within the current payroll cycle window, when the Publisher attempts to publish, then the system displays an impact summary (affected staff count, pending/unprocessed payouts count) and requires explicit typed confirmation before proceeding. Given the Publisher does not provide the required confirmation, when attempting to publish, then the publish is blocked and the version state is unchanged. Given a backdated effective time within the current payroll cycle is selected, when the Publisher attempts to publish, then the action is blocked unless an Admin override is provided with justification notes.
Audit Trail with Diffs and Approval Notes
Given any rule-set event (create, edit, submit for review, approve, publish, schedule change, rollback, permission-denied attempt), when it occurs, then an immutable audit entry records actor identity, role, timestamp, action type, and before/after diffs at rule and parameter level. Given an approval action, when it is performed, then approval notes are required and stored with the audit entry. Given an authorized user views a version, when they open Audit Trail, then they can filter by action type/date/user and export results to CSV.
Rollback to Prior Version
Given a prior version exists, when a Publisher initiates Rollback and confirms, then a new version is created as a copy of the selected prior version, enters Scheduled or Active state based on chosen effective time, and the action is recorded in the audit trail with a reference to the source version. Given a future scheduled version exists, when a rollback is scheduled earlier than that version, then precedence is determined by effective time and both schedules remain intact without overlap. Given rollback is completed, when payout calculations occur after the rollback effective time, then they use the rolled-back version identifier.
Correct Version at Calculation Time
Given a payout calculation request with timestamp T, location L, service S, staff role R, and booking source B, when the engine selects rules, then it uses the highest-priority version whose scope matches (L,S,R,B) and whose effective window includes T. Given a calculation is executed, when the payout record is stored, then it includes the applied version identifier and checksum of rule parameters for audit reproducibility. Given no active version is found for the scope/time, when a calculation is attempted, then the engine returns a hard error and logs an alert to ensure no calculation proceeds with undefined rules.
User Notifications of Upcoming Changes
Given a version is scheduled for future activation, when scheduling is saved, then affected users (publishers, payroll admins, impacted staff/partners) receive in-app and email notifications within 5 minutes including effective date/time, scope, and change summary. Given a scheduled activation is within 24 hours, when the threshold is reached, then a reminder notification is sent to the same recipients. Given an immediate publish occurs, when it completes, then a notification is sent within 5 minutes indicating the change took effect and linking to the audit entry.

Tip Splitter

Handle tips separately with flexible controls—default 100% to staff, enable team pools, or set custom ratios by station or event. Staff see real‑time tip earnings, while owners keep taxes and fees cleanly separated. Encourages tipping with transparent, staff‑friendly distribution and automatic inclusion in payout runs.

Requirements

Tip Policy Engine & Hierarchy
"As an owner, I want to define and apply flexible tip distribution rules so that tips are allocated fairly and consistently across staff, stations, and events."
Description

Provide a configurable engine for defining how tips are distributed, supporting modes such as 100% to the assigned staff member, team pooling, and custom ratio splits. Implement a policy hierarchy with business-wide defaults, station-level and event-level policies, and per-booking overrides with effective dates and versioning. Validate that custom ratios sum to 100% and handle multi-staff bookings, reassignments, and late staff changes prior to closeout. Persist policies in the data model and apply them deterministically at tip capture, recalculating allocations when assignments change. Integrate with booking, staff, and payments services so the computed allocations flow into ledgers, payout runs, and reporting without manual intervention.

Acceptance Criteria
Policy Hierarchy Precedence Resolution
- Given business, station, and event policies exist and a per-booking override is active; When a tip is captured; Then the per-booking override is applied over event, station, and business defaults. - Given both event- and station-level policies are effective; When a tip is captured; Then the event-level policy overrides the station-level policy; And if no event policy exists, the station policy overrides the business default. - Given no lower-level policy is active; When a tip is captured; Then the business default policy is applied. - Given a policy is expired or not yet effective; When a tip is captured; Then that policy is ignored in precedence resolution.
Custom Ratio Validation and Staff Eligibility
- Given a custom ratio policy with staff IDs and ratios; When saving; Then the sum of all ratios equals 100.00% to two decimal places; Else reject with a validation error. - Given ratios sum to 100% but include inactive or nonexistent staff IDs; When saving; Then reject and identify invalid staff IDs. - Given duplicate staff IDs appear in the ratio list; When saving; Then reject the policy and indicate duplicates. - Given a ratio includes 0%; When saving; Then allow 0% only if at least one other ratio > 0% and at least one recipient has > 0%. - Given rounding rules; When allocating a tip amount; Then allocations round to the nearest cent and any remainder cent is assigned deterministically to the highest-ratio recipient.
Deterministic Allocation at Tip Capture Across Modes
- Given mode=100% to assigned staff is active; When a tip is captured via SMS payment; Then 100% is allocated to the currently assigned staff member. - Given team pooling mode is active; When a tip is captured; Then the pool includes eligible staff defined by the policy scope; And the pool allocation rule is applied deterministically. - Given custom ratios mode is active; When a tip is captured; Then allocations match configured ratios after rounding; And a unique allocation ID and version are recorded. - Given identical inputs (policy version, staff set, tip amount); When the allocation is re-run; Then the outputs are identical.
Recalculation on Staff Reassignment Before Closeout
- Given a tip was captured and allocations computed; When the booking’s assigned staff changes before closeout; Then allocations are recalculated using the effective policy at the time of change; And a new allocation version supersedes the prior version; And an audit trail records both. - Given payouts have not yet been generated; When reallocating; Then ledger entries are adjusted rather than duplicated; And no duplicate payouts occur. - Given closeout is completed; When a staff change is attempted; Then the system blocks the change or requires admin override and does not auto-reallocate.
Multi-Staff Booking Allocation
- Given a booking has multiple staff roles (e.g., groomer, bather); When tips are captured; Then allocations honor role-based ratios if configured; Else use the default mode’s multi-staff rule. - Given team pool mode with a multi-staff booking; When applied; Then staff assigned to the booking are included/excluded per policy eligibility rules for the pool. - Given a staff member is added before tip capture; When tip is captured; Then that staff is included per the effective policy. - Given a staff member is removed before tip capture; When tip is captured; Then that staff receives 0% allocation.
Policy Effective Dates and Versioning
- Given a policy with effective_start and optional effective_end; When tip capture occurs; Then the active policy at capture time is used for allocation. - Given a policy is edited; When changes are saved; Then a new immutable policy version is created with incremented version and timestamp; And historical versions remain read-only. - Given overlapping effective windows at different hierarchy levels; When resolving policies; Then the most specific active policy is selected per hierarchy rules. - Given a future-dated policy; When previewing allocations for future bookings; Then the preview uses the future policy only for bookings on/after the start date.
Integration with Ledgers, Payouts, and Reporting
- Given allocations are computed; When persisting; Then allocation records are written to the ledger with links to booking ID, staff ID(s), policy version, allocation version, and tip transaction ID. - Given a payout run is executed; When processing; Then only eligible, unreconciled allocation amounts are included per staff; And tips remain separated from platform taxes/fees. - Given reporting queries are run; When generating tip distribution reports; Then final allocation versions are reflected and superseded allocations are excluded. - Given API/webhook consumers; When allocation events occur; Then events are emitted with idempotency keys and sufficient payload for downstream reconciliation.
Staff Assignment & Station Mapping
"As a manager, I want bookings accurately tied to staff and stations/events so that tip splits reflect who actually worked and where."
Description

Enable precise mapping of bookings to staff, stations, roles, and events so tip policies can be applied accurately. Add data fields and APIs to tag each booking with one or more staff participants and the relevant station/event context. Provide conflict resolution for overlapping assignments and fallback logic when required mapping data is missing. Expose admin tools to manage station catalogs and event schedules, and ensure changes propagate to in-flight bookings with clear rules. This mapping becomes the source of truth for the tip policy engine and downstream calculations.

Acceptance Criteria
Assign Single Booking to Multiple Staff with Roles
Given an existing booking with start and end times and two staff selections (A=Groomer, B=Assistant) with weights 70 and 30 When the booking is saved Then the booking record persists two staffMapping entries including staffId, role, weight, and bookingId and returns 200 Given a booking with duplicate staff-role pairs or weights that do not sum to 100 When saving Then the API returns 422 with field-level errors for duplicates and weightSum Given a booking with staff mappings but no explicit weights When saving Then the system auto-assigns equal weights rounded to whole percentages that sum to 100 and stores the computed weights Given a saved booking with staff mappings When retrieving the booking mapping via GET Then the response matches the stored entries exactly and includes createdBy and createdAt for each mapping
Map Booking to Station and Event Context
Given a stationId and eventId When saving a booking Then both IDs must exist and be active during the booking time window or the API returns 422 with code inactive_or_out_of_window Given a booking initially saved with a station and event When the booking time is modified Then the system revalidates availability and blocks save with 409 if the station capacity or event schedule conflicts Given a booking with at least one of stationId or eventId provided When saving Then the API accepts and sets mappingCompleteness=true only if both are valid, else false Given a saved booking When retrieving mapping Then response includes stationId, stationName, eventId, eventName, and timeWindow fields
Overlap Conflict Detection and Resolution
Given staff S assigned to a booking B1 time window T1 When attempting to assign S to another booking B2 with overlapping time window T2 Then the API rejects with 409 conflict and returns a conflicts array including S, B1, B2, T1, T2 Given a 409 conflict response When a user with Manager role retries with override=true and provides an overrideReason Then the API saves the mapping, marks conflictStatus=overridden, emits an audit event, and notifies affected staff Given a station with capacity=1 assigned to booking B1 at time T1 When attempting to assign the same station to B2 overlapping T1 Then the save is rejected with 409 unless override=true by Manager as above Given conflict checks When executing under nominal load Then p95 conflict-check latency is <=200ms measured over 5-minute windows
Fallback Logic When Mapping Data Is Missing
Given a booking with staff mappings but missing stationId and eventId When saving Then the booking is saved with stationId=unknown, eventId=null, mappingCompleteness=false, and a warning audit event is recorded Given a booking with mappingCompleteness=false When retrieved via GET Then the response includes completeness=false and warnings array with codes missing_station and/or missing_event Given an admin later assigns a valid stationId and/or eventId to the booking When saving Then mappingCompleteness switches to true and a resolution audit event is recorded Given bookings saved with completeness=false When viewed in the Admin Queue Then they appear in a “Needs Mapping” filter within 1 minute of save
Admin Management of Stations and Event Schedules
Given an Owner or Manager role When creating or updating a station Then name is required, code is unique per business, capacity is >=1, and duplicate name+location combinations are rejected with 422 Given a station in use by any future booking When attempting to hard delete Then the API rejects with 409 and suggests deactivate; when deactivated effectiveOn is required and future bookings after that date are flagged for re-mapping Given event schedule creation When saving Then startAt < endAt, timezone is valid, and events on the same station cannot overlap; overlaps return 409 with details of the conflicting event Given non-admin roles When attempting station or event mutations Then the API returns 403
Propagation of Station/Event Changes to In-Flight Bookings
Given a station rename When saved Then all in-flight bookings referencing the station display the new name within 60 seconds without changing stationId, and an audit event is recorded Given a station deactivation with effectiveOn=DATE When saved Then bookings on or after DATE are re-mapped to stationId=unknown, marked needs_remap=true, and owners/managers receive a notification digest within 10 minutes Given an event time change that no longer overlaps a booking’s time window When saved Then the booking’s eventId is cleared, mappingCompleteness is recalculated, and the booking appears in the “Needs Mapping” queue Given propagation jobs When retried idempotently Then no duplicate notifications or audit events are produced
API Tagging and Retrieval for Tip Policy Engine
Given POST/PUT to /bookings with staffMappings[], stationId, and eventId When valid Then the API returns 200 with mappingVersion incremented; invalid references return 404; schema validation errors return 422 with field-specific codes Given GET /bookings/{id}/mapping?asOf=timestamp When called Then the API returns 200 with mapping as of the timestamp, including version, lastUpdated, and staff/station/event details; if no change since ETag Then returns 304 Given nominal traffic When calling GET /bookings/{id}/mapping Then p95 latency <=300ms and error rate <0.1% over 1 hour Given the Tip Policy Engine requests mapping for a booking When mapping exists Then exactly one source of truth is used and any legacy fields are excluded from the response payload
Real-Time Staff Tip Earnings
"As a staff member, I want to see my tip earnings update in real time so that I understand what I’ve earned and can track expected payouts."
Description

Deliver a mobile-first view showing each staff member their real-time tip earnings with per-booking breakdowns, pool shares, pending versus paid status, and time filters (today, pay period, custom). Include live updates as tips arrive and as allocations change due to policy or assignment updates prior to closeout. Support role-based access so staff only see their own data and managers can view team rollups. Provide export/share options and guardrails for privacy, latency handling, and offline states.

Acceptance Criteria
Real-Time Tip Updates on Staff Mobile Dashboard
Given a staff user is viewing My Tips with filter Today and a WebSocket connection is healthy, when a new tip is recorded on a booking assigned to that user, then the total and per-booking list update within 3 seconds without manual refresh and the Last updated timestamp reflects the update within 5 seconds. Given a staff user is viewing My Tips and the WebSocket is unavailable, when a new tip is recorded on a booking assigned to that user, then the view refreshes via polling within 15 seconds and displays a Reconnecting indicator until WebSocket is restored. Given a tip is recorded for a booking not assigned to the viewing staff user, when the tip arrives, then no changes appear in that user's view.
Per-Booking Tip Breakdown and Status Display
Given a staff user opens My Tips for any filter, when the list renders, then each row displays booking reference, service date/time (local), allocation type (direct/pool/ratio), gross tip amount, user share amount, and status (Pending or Paid) and totals shown at top equal the sum of visible rows within ±$0.01 rounding. Given a payout run completes for a booking's tip, when the user refreshes or a live update occurs, then the booking row status changes to Paid and displays the payout date and payout ID. Given rows are sorted by service date/time descending by default, when the user changes the filter, then sorting persists and the list re-renders within 1.5 seconds for up to 500 rows.
Time Filtering: Today, Pay Period, Custom Range
Given the user selects Today, when the device time zone is set, then the list and totals include only tips with timestamps between 00:00 and 23:59:59 of the user's local date and the total matches the sum of those rows. Given the organization pay period is defined (e.g., biweekly starting Monday), when the user selects Pay Period, then the date range matches that definition and all included tips fall within those boundaries. Given the user selects a Custom Range with start and end dates, when start ≤ end and the range ≤ 180 days, then the list and totals reflect only tips in that inclusive range; when the range > 180 days, then the UI displays a validation error and disables Apply.
Role-Based Access: Staff Self-View and Manager Team Rollups
Given a staff (non-manager) is authenticated, when they open My Tips, then only their own earnings are retrievable from the API and attempts to query other staff IDs return 403 and are not rendered in the UI. Given a manager is authenticated, when they open Team Tips, then they can view per-person and team rollup totals for a selected time filter and switching between members updates the view within 1.5 seconds. Given role permissions are changed in the admin console, when a former manager signs in again, then team-level access is removed within 5 minutes and only self-view is available.
Tip Allocation Changes and Pool Shares Before Closeout
Given a tip was initially allocated under one policy, when an admin updates the team pool or station ratio before payout closeout, then affected staff members' totals and booking rows recalculate within 10 seconds and display an Adjusted badge with the new share amount. Given a pool allocation results in fractional cents, when shares are computed, then rounding follows the organization's rule (e.g., round half up) and the sum of all staff shares equals the gross tip amount to the cent. Given a payout closeout has been executed for a period, when policies or assignments change afterward, then Paid amounts for that period remain unchanged in the UI and exports.
Export and Share Tip Earnings with Privacy Guardrails
Given a staff user selects Export for a time filter, when they choose CSV or PDF, then a file is generated within 10 seconds containing: booking reference, service date/time, gross tip, user share, allocation type, status, and payout ID, and excludes client phone/email; client last names are masked to initial. Given a manager exports a team rollup, when the export is generated, then it includes per-person totals and optional detail per booking and does not include earnings of staff outside the selected team. Given an export share link is created, when the link is opened, then authentication is required and the link expires in 24 hours; when offline, then Export and Share actions are disabled with an explanatory message.
Latency and Offline Handling for Tip Earnings View
Given network latency exceeds 2 seconds or connectivity is lost, when the user is on the tips view, then a non-blocking banner indicates Offline or Reconnecting, cached totals and rows from the last successful sync are shown with a Last synced timestamp, and Export actions are disabled. Given connectivity is restored, when the app detects network availability, then the tips data auto-refreshes within 5 seconds and the staleness banner is removed. Given no cached data exists, when the device is offline and the user opens the tips view, then an empty state explains that data requires connectivity and provides a Retry action.
Automatic Tip Inclusion in Payouts
"As an owner, I want tips to flow automatically into payout runs so that staff are paid promptly and accounting stays clean."
Description

Integrate computed tip allocations into scheduled payout runs automatically, ensuring tips are paid out according to the business’s cadence and payment rails. Keep tips separated from service revenue, taxes, and processor fees in ledger entries and payout summaries. Support multiple payout destinations if needed (e.g., different staff accounts), retry failed payouts, and surface payout status to staff and managers. Provide configuration for payout timing, thresholds, and cutoffs, and ensure adjustments made before closeout are reflected in the next run.

Acceptance Criteria
Scheduled Payout Run Automatically Includes Tips
Given a business has configured a payout cadence and there are allocated tip amounts pending payout When the scheduled payout run executes at the configured time Then the system automatically includes all eligible tip allocations in the run without manual intervention And each included tip allocation amount matches the configured distribution rules (e.g., 100% to staff, pool, or custom ratios) And the payout summary lists tip totals per staff and in aggregate for the run
Ledger Separation and Summary Segregation
Given tip allocations are processed for payout When ledger entries and payout summaries are generated Then tips are recorded in entries separate from service revenue, taxes, and processor fees And tips are mapped to the designated tip ledger category/liability account And payout summaries display tips as distinct line items apart from service revenue, taxes, and fees
Multiple Payout Destinations Per Run
Given staff members have configured payout destinations and are assigned tip allocations When a payout run executes Then the system creates separate disbursements per destination (e.g., per staff account) with the correct net tip amounts And each disbursement references the staff member and period covered And the run completes successfully even when multiple payment rails are used, without duplicating disbursements
Retry Failed Tip Disbursements
Given one or more tip disbursements fail during a payout run When the failure is detected Then the system records the failure reason and marks the disbursement status as Failed And the system retries the disbursement according to the configured retry policy and schedule And failed disbursements are excluded from Paid totals and are not double-paid upon retry success And the next eligible run re-attempts failed disbursements if they remain unpaid
Payout Status Visibility to Staff and Managers
Given a staff member or manager opens the payout dashboard When viewing tip payouts for a selected period Then the UI displays per-disbursement status (e.g., Queued, Processing, Paid, Failed, Retrying), amounts, and last update timestamp And staff can only view their own tip payouts, while managers can view all staff payouts And status changes are reflected in the UI within the defined update interval And users can export a report including statuses and amounts for the selected period
Payout Timing, Thresholds, and Cutoffs Configuration
Given a business has configured payout cadence, timezone, minimum payout thresholds, and a cutoff time When a payout run executes Then only tip allocations accrued up to the cutoff time (in the business timezone) are included And individual amounts below the minimum threshold are deferred and carried over to a future run And manual ad-hoc runs still honor thresholds and do not duplicate already paid allocations
Adjustments Reflected in Next Run
Given a manager makes adjustments to tip allocations (e.g., reassignment, void, correction) before closeout for a period When the next payout run executes Then the adjusted amounts are reflected in the run’s disbursements and summaries And an audit trail records the adjustment details (who, when, what changed) And previously paid runs remain immutable, with adjustments applied prospectively to the next eligible run
Tip Pool Closeout & Approvals
"As a manager, I want a closeout and approval process for pooled tips so that distributions are accurate, auditable, and finalized before payouts."
Description

Offer an end-of-day or end-of-event pool closeout workflow where managers can review totals, verify allocations, address variances, and approve final distributions. Allow documented adjustments with reasons and create an immutable audit trail capturing who approved what and when. Lock pools upon approval to prevent retroactive changes, and notify staff of finalized amounts. Integrate with the payout scheduler to ensure approved pools are queued for the next run and surfaced in reports.

Acceptance Criteria
Manager Reviews End-of-Day Pool and Proposed Allocations
Given a tip pool exists for a single business day or event with a defined distribution mode (100% to staff, team pool, or custom ratios by station/event) When a manager opens the Tip Pool Closeout screen for that pool Then the system displays the pool header (pool ID, date/event, location/station), gross tips, deductions (taxes/fees) separated, net poolable amount, participant roster, and proposed allocations based on the configured distribution mode And the sum of proposed allocations equals the net poolable amount to the cent with no negative allocations And Approve is disabled until all validations pass and Unallocated Remainder equals $0.00
Variance Detection and Resolution Before Approval
Given the manager enters counted tips or edits participant eligibility/attendance for the pool When the calculated allocations no longer equal the recorded net poolable amount Then the system flags a variance showing expected vs calculated amounts and requires resolution before approval And the manager can correct source totals or allocations to resolve the variance And Approve remains disabled while variance ≠ $0.00
Documented Adjustment With Reason and Attribution
Given a manager adjusts any participant’s allocation or marks a participant ineligible When the change is saved Then the system requires a reason (predefined code or free text with a minimum of 10 characters) and captures before/after values, who made the change, timestamp, and context (pool ID) And the audit trail entry is created immediately and is visible in the pool history
Approval Locks Pool From Edits
Given a pool has all validations passing When the manager approves the pool and confirms Then the pool status changes to Approved and all allocation and source fields become read-only And any attempt to modify the pool via UI or API is blocked with a "Pool is locked" message and no data is changed
Immutable Audit Trail of Closeout and Approval
Given adjustments and approval events occur for a pool When viewing the audit trail Then each event displays user, role, action, before/after values (where applicable), timestamp with timezone, and reason (if provided) And audit entries cannot be edited or deleted and are exportable (CSV/PDF) with a unique event ID per entry
Staff Notification of Finalized Tip Amounts
Given a pool is approved When approval completes Then each participant receives an SMS with finalized tip amount, pool date/event, and expected payout date, and the amount appears in their Staff Earnings view And failed SMS deliveries are retried up to 3 times and logged
Queue Approved Pool for Next Payout Run and Reporting
Given a pool is approved When the payout scheduler evaluates the next run window Then the pool’s finalized allocations are enqueued for the next eligible payout run with a reference to the run ID And the pool appears in Payouts > Queued with totals matching the approved amounts and is included in financial reports with an Approved/Queued status And the same pool cannot be queued more than once
Tax & Accounting Separation with Reporting
"As an owner/accountant, I want tips and fees tracked separately with clean exports so that compliance and reconciliation are straightforward."
Description

Maintain strict separation of tips, service revenue, taxes, and processor fees in the general ledger and reporting. Classify tips correctly for compliance (e.g., gratuity versus service charge), support W-2/1099 scenarios, and expose exports to accounting systems like QuickBooks and Xero with clear mappings. Provide dashboards and downloadable reports for owners and accountants, including per-staff statements, pool summaries, payout reconciliations, adjustments, refunds, and chargebacks. Ensure jurisdiction-specific settings can be configured without code changes.

Acceptance Criteria
General Ledger Separation of Revenue, Tips, Taxes, and Processor Fees
Given transactions containing service revenue, tips (individual and pooled), sales tax, refunds, chargebacks, and processor fees for a date range When the system posts to the internal ledger Then it creates separate ledger lines/accounts for each category (revenue, tips, taxes, refunds, chargebacks, fees) without commingling and the sum of all lines equals the gross processed amount for the range And rounding differences per payout batch are not greater than $0.01 and are posted to a configured rounding account Given a payout run is generated When reconciling the ledger to the payout batch Then the net of revenue + tips − refunds − chargebacks − processor fees equals the payout amount and the payout batch ID is stored on all related ledger entries Given no-show fees and discounts are present When posting to the ledger Then they are classified to their configured revenue or contra-revenue accounts and excluded from tips, and taxes are computed only on taxable components per settings
Tip Classification Compliance: Gratuity vs Service Charge
Given a transaction includes a customer-added tip marked as gratuity When it is posted Then it is flagged as gratuity, excluded from taxable sales where configured, and appears in tip reports and exports as gratuity with its own account mapping Given a business-defined service charge is added to a ticket When it is posted Then it is flagged as service charge, mapped to the configured revenue or liability account, included or excluded from tax per jurisdiction settings, and appears distinctly in reports and exports Given a user attempts to change a tip’s classification after settlement When they have admin permissions and confirm the change Then the system posts reversing and correcting ledger entries without deleting history and records an immutable audit log entry with user, timestamp, reason, and affected records
W-2 and 1099 Tip Reporting Modes
Given staff profiles include an employment type of W-2 or 1099 When tips and service charges are allocated and a payout statement is generated Then W-2 staff tips appear on employee tip statements and 1099 contractor tips on contractor earnings statements, each separating gratuities, service charges, and adjustments, and totals match the internal ledger Given a staff member’s employment type is changed When new transactions are processed Then new allocations use the updated type while prior posted allocations remain unchanged unless an explicit reprocess is initiated by an admin Given exports are generated for a date range When filtering by employment type Then totals by W-2 and 1099 equal the overall totals and match the ledger for the same range
Accounting Export to QuickBooks/Xero with Clear Mappings
Given GL account mappings are configured for revenue, gratuity tips, service charge tips, taxes, processor fees, refunds, and chargebacks When an export is generated for a date range Then the file/API payload contains journal entries with correct accounts, amounts, locations/classes, and memo references (payout batch ID, staff/pool IDs), and passes schema validation for QuickBooks Online and Xero targets Given a required mapping is missing When the user attempts an export Then the export is blocked with a specific error listing missing mappings and a direct link to the mapping settings Given the exported entries are imported to a test accounting company When totals are compared Then debits equal credits and match both the internal ledger and payment processor settlements for the same period
Owner and Staff Financial Reporting Views
Given an owner views the Finance dashboard for a date range When they open Tip Pools and Payout Reconciliation Then they see per-staff statements, pool summaries, and payout batches with statuses, and totals reconcile to the ledger and to bank deposits by payout batch Given a staff member opens their statement for a pay period When they view details Then they see line items for direct gratuities, pooled distributions, service charges, adjustments, refunds/chargebacks impacts, and net payable, and the totals match the payout issued to them Given a user applies filters (location, staff, pool, payment method) and exports a report When the CSV or PDF is downloaded Then the filtered dataset is reflected exactly and includes footers with totals and report metadata (date range, generation time, version)
Adjustments, Refunds, and Chargebacks Traceability
Given a transaction with allocated tips has a partial refund When the refund is processed Then the system posts proportional reversing entries for revenue, taxes, tips (by staff/pool), and fees, updates affected statements and reports within 1 minute, and links the refund to the original transaction ID Given a chargeback notification is received from the processor When the dispute record is created Then the system posts chargeback entries separating principal, processor fee, and tips reversal, references the original transaction, and marks affected payout reconciliations as needing attention until resolved Given an admin records a manual adjustment When they submit it Then a reason is required, an immutable audit trail entry is created, and the adjustment appears as a distinct column in reports without altering original posted amounts
Jurisdiction-Specific Tax and Tip Settings Configuration
Given a location is configured with jurisdiction-specific settings (tax rates, gratuity taxability, service charge treatment) When new transactions are processed Then tax and classification logic follows the configured settings without code changes and appears consistently in the ledger, reports, and exports Given multiple locations operate under different jurisdictions When generating consolidated reports Then each location’s totals reflect its own settings and the consolidated total equals the sum of location totals Given a setting is changed with an effective date When transactions are processed after that date Then new logic applies only to those transactions; prior transactions remain unchanged unless explicitly reprocessed by an admin with audit logging
SMS Checkout Tip Prompts & Defaults
"As a pet parent, I want clear tip options during SMS checkout so that I can tip fairly and quickly from my phone."
Description

Enhance the SMS-first checkout flow with clear, mobile-friendly tip prompts offering suggested percentages, custom amounts, and contextual messaging that encourages tipping while being transparent about distribution. Remember the customer’s last tip preference where allowed, and support configurable defaults per business. Handle edits within a defined window, reflect changes in real time to staff earnings, and respect accessibility standards. Ensure the prompts integrate with payment capture and tip policy application without adding friction.

Acceptance Criteria
SMS Tip Prompt with Suggested Percentages
Given a customer receives the SMS checkout link for a completed appointment When they open the tip prompt page Then the UI displays at least three suggested tip options as percentage buttons and one "Custom" amount option And the suggested percentages default to the business-configured values; if none are configured, defaults are 15%, 20%, and 25% And the tip prompt includes a clear message that tips are distributed per the business’s tip policy and identifies the intended recipient(s) And currency, formatting, and rounding follow the business’s currency with two-decimal precision And the tip prompt loads within 2 seconds over a 4G mobile connection
Custom Tip Amount Entry via SMS
Given the customer selects the Custom tip option When they enter a numeric amount Then the input accepts only digits and a single decimal separator with up to two decimal places And validation enforces the merchant-configured min/max; if not configured, min is 0.00 and max is 200% of service subtotal And invalid input shows an inline error message and prevents submission until corrected And the order total preview updates immediately to include the entered tip And submitting the tip proceeds without navigating away from the checkout flow
Last Tip Preference Recall and Configurable Defaults
Given the business has enabled "remember last tip" and local regulations permit And the customer has previously completed a checkout with a tip selection for this business When the customer opens a new SMS checkout Then the prior tip selection (percentage or custom amount) is preselected And the customer can change or clear the preselection before payment And if "remember last tip" is disabled or no prior tip exists, the business default suggested percentages are shown with no preselection And no tip preference is stored if the customer opted out or if regulations prohibit storage
Tip Edits Within Allowed Window and Real-Time Staff Earnings Update
Given the business has configured a tip edit window When the customer taps "Edit tip" within the allowed window Then the system allows changing the tip and validates as per normal rules And if payment is authorized but not captured, the authorization amount is adjusted to reflect the new tip And if payment is captured, a separate tip adjustment transaction is created only if enabled; otherwise editing is blocked with an explanatory message And staff earnings and the tip ledger update within 5 seconds of a successful edit, reflecting the new distribution And an immutable audit log records the old value, new value, editor, timestamp, and reason And after the window closes, attempts to edit show a non-blocking message and do not change the charged amount
Accessibility and Mobile Usability of SMS Tip Flow
Given a customer using a mobile device with assistive technologies When the tip prompt page is rendered Then all interactive elements have accessible names and roles and are reachable via keyboard navigation and screen readers And color contrast meets WCAG 2.1 AA and target sizes are at least 44x44 px And the page supports dynamic text scaling up to 200% without loss of content or functionality And the SMS contains a single short link; the page loads in under 2 seconds on a 4G connection and under 4 seconds on a 3G connection And all tip text is localized per customer locale and uses plain language
Payment Capture Integration and Tip Policy Application
Given the customer confirms their tip selection and submits payment When the payment is captured Then the tip amount is included in the captured charge where supported; otherwise a secondary capture is performed transparently without requiring additional customer action And the tip distribution policy is applied: default 100% to assigned staff unless pooling or custom ratios are configured by station or event And the customer-facing receipt and invoice show the tip as a separate line item and exclude it from taxable subtotal where applicable And the staff portal shows real-time tip allocations by recipient consistent with the applied policy and includes the tip in the next scheduled payout run And failures in tip capture or allocation surface a clear error to staff, trigger an alert, and do not block base payment capture

Retro Adjust

When a visit is reassigned, refunded, or add‑ons change after the fact, Retro Adjust recalculates the original split and posts the difference as automatic positive/negative adjustments in the next payout. No manual clawbacks or DM headaches—everyone sees the why and the delta, preserving trust and clean books.

Requirements

Change Event Detection & Visit Versioning
"As an owner-operator, I want the system to automatically detect and capture any changes made to a visit after payout so that I have a clean, auditable record that can be used to recalculate and explain adjustments."
Description

Detect and record after-the-fact changes to a completed visit (e.g., staff reassignment, partial/full refunds, add-on edits, package redemption changes, no-show fee toggles) that occur after the initial payout calculation. When such a change is saved, snapshot the original visit as Version N and create Version N+1 with a precise changelog, timestamps, actor, and reason code. Mark the visit as Retro Adjust–eligible if at least one payout has already been calculated for it. Ensure idempotent event processing and deduplicate overlapping edits so only net-new changes trigger recalculation. Integrate with Booking, Payments, Packages, and Fees modules to listen for state transitions and field deltas without requiring manual reconciliation.

Acceptance Criteria
Snapshot and Visit Version Creation on Post-Payout Change
Given a completed visit with at least one payout calculated, When a staff reassignment, refund (partial or full), add-on edit, package redemption change, or no-show fee toggle is saved, Then the system snapshots the current state as Version N and creates Version N+1. And Version N+1 includes a changelog enumerating each changed field with old_value and new_value. And Version N+1 records actor_id, actor_type, reason_code, and change_detected_at in UTC. And the visit is marked Retro Adjust–eligible. And version numbers for this visit increment by 1 with no gaps.
Idempotent Event Processing and Duplicate Suppression
Given two identical change events with the same idempotency_key or event_id are received, When processed, Then only one new visit version is created and duplicates are ignored. Given a repeat delivery of an already-applied edit producing no delta from the latest version, When processed, Then no new version is created and no recalculation job is enqueued. Given multiple field changes are saved in a single transaction, When processed, Then exactly one new version is created and at most one recalculation job is enqueued if payout-affecting fields changed.
Retro Adjust Eligibility Flagging
Given a visit with no payout calculated, When an after-the-fact change is saved, Then the visit remains not Retro Adjust–eligible and no adjustment job is enqueued. Given a visit with at least one payout calculated, When an after-the-fact change is saved, Then the visit is marked Retro Adjust–eligible. Given a fully refunded visit after payout, When the refund is saved, Then the visit is marked Retro Adjust–eligible and the changelog includes refund_amount and refund_reason. Given a visit already marked Retro Adjust–eligible, When additional edits are saved, Then the flag remains true.
Cross-Module Change Detection (Booking, Payments, Packages, Fees)
Given a committed change in Booking, Payments, Packages, or Fees affecting staff_id, start_time, duration, add_on_items, package_redemptions, no_show_fee_enabled, refund_amount, tip, or base_price, When the change is persisted, Then a visit change event is emitted and versioned for the corresponding visit. Given a change initiated via SMS command, API request, or dashboard UI, When saved, Then it is detected and versioned without manual reconciliation. Given a change to a non–payout-affecting field (e.g., pet notes), When saved, Then it is recorded in the changelog but does not enqueue recalculation. Given multiple module updates occur within the same second, When processed, Then server-assigned sequence ordering is preserved and versions are created deterministically.
Changelog Fidelity and Audit Fields
Given a new version is created, Then the changelog entry for each changed field includes field_name, old_value, new_value, data_type, currency/unit when applicable, and payout_affecting flag. Given a new version is created, Then it persists actor_id, actor_type (user/system/integration), reason_code from an allowed set, change_detected_at (UTC), and source_module. Given a UI-initiated save without a reason_code, When attempted, Then the save is rejected with a validation error and no version is created. Given an API-initiated save with a reason_code not in the allowed set, When attempted, Then the request is rejected and no version is created.
Recalculation Triggering on Net-New Payout-Affecting Changes
Given a new version where payout-affecting fields changed, When the version is committed, Then exactly one recalculation job is enqueued and associated with that version. Given a new version where only non–payout-affecting fields changed, When committed, Then no recalculation job is enqueued. Given multiple edits occur in rapid succession before a recalculation job starts, When the last edit is saved, Then the system recalculates using the latest version only and ensures only one recalculation job exists. Given a recalculation has already been performed for a version, When a duplicate enqueue is attempted for the same version, Then it is ignored.
Historical Split Recalculation Engine
"As a finance-focused owner, I want Retro Adjust to recalculate payouts using the historical rules from the original service date so that adjustments are fair, explainable, and consistent even if my pricing changed later."
Description

Recompute the original payout split deterministically using the pricing, split rules, and fee configurations effective at the time of service, not current settings. Inputs include base service price, add-ons, discounts, tips, taxes, package redemptions, and no-show fees; outputs are per-party deltas (business and staff) between amounts already paid and the recalculated amounts. Handle reassignment by transferring the appropriate portion from Staff A to Staff B based on the original rules. Support precise rounding rules, currency consistency, and repeatable results from the visit’s versioned data. Produce a machine-readable breakdown (line items and formulas) to feed adjustments, reporting, and exports.

Acceptance Criteria
Deterministic recalculation from versioned config
Given a visit with visitId V, capturedAt T, and configVersionId C saved with the visit And current pricing/split/fee configuration differs from C When the engine recalculates the split for V Then it applies only the rules and fees from version C And it ignores any current/default configuration not in C And the resulting per-party amounts and totals are identical across repeated runs (n ≥ 3), producing the same result hash H And the result includes references to configVersionId C and visit data version D
Comprehensive line-item breakdown and delta computation
Given a visit including base service price, add-ons, percentage and fixed discounts, tip, taxes, package redemptions, and no-show fees And previously paid amounts exist for business and staff When the engine recalculates Then the output includes a machine-readable JSON breakdown with one line item per component and applied rule And each line item contains: id, type, amount, currency, ruleId, ruleVersion, formula, allocation {business, staffId}, and references to source inputs And calculation order follows versioned rules (e.g., discounts before taxes, tip base per setting), with intermediate subtotals present And sum(lineItem.amount) equals the recomputed grand total And per-party totals equal the sum of their allocated line items And per-party delta = recomputed per-party total − already paid per-party amount And the sum of all deltas across parties equals 0.00 in the visit currency
Post-payout staff reassignment transfer
Given a visit originally paid to Staff A And the visit is reassigned to Staff B after payout And split rule R effective at service time assigns X% to staff and Y% to business When the engine recalculates using versioned data Then Staff A receives a negative delta equal to the portion previously paid that should now be allocated to Staff B per R And Staff B receives an equal positive delta And the business delta changes only if business-owned components are affected by reassignment per R And both adjustments are queued for the next payout cycle with reasonCode "reassignment" and references {visitId, oldStaffId, newStaffId}
Rounding and currency consistency
Given a visit with currency CUR and rounding mode M captured at service time When computing line items, allocations, and deltas Then amounts are rounded at the configured step(s) in M to the minor unit of CUR And (recomputed grand total − sum(per-party totals)) equals 0.00 CUR And sum(all deltas across parties) equals 0.00 CUR And the output uses CUR consistently with no currency conversion
Repeatability and idempotent adjustment posting
Given the same visit V and identical versioned inputs are recalculated multiple times When adjustments are posted to payouts Then the recalculation output JSON is byte-for-byte identical across runs And a deterministic adjustmentKey prevents duplicate postings for the same visit version And re-running produces zero new adjustments if an identical adjustmentKey already exists
Graceful handling of missing or invalid historical data
Given the visit lacks required versioned data (e.g., missing configVersionId or corrupted input snapshot) or validation fails When the engine attempts recalculation Then no adjustments are produced or posted And an error is returned with code and message describing the missing/invalid elements And an audit log entry is written with {visitId, attemptedBy, timestamp, errorCode} And a remediation hint is included for resolution (e.g., restore snapshot or re-run capture)
Automatic Adjustment Posting to Next Payout
"As a staff member, I want any owed differences from changes to show up automatically in my next payout so that I don’t have to chase manual corrections."
Description

Convert recalculation deltas into positive/negative adjustment entries that are automatically queued and posted in the next eligible payout for each payee. Consolidate multiple deltas per payee into a single payout period while preserving line-item detail for drill-down. Prevent double-posting via unique adjustment IDs and support reversal entries if a change is undone before payout. Respect minimum payout thresholds, cutoffs, and pay cycles already configured in FetchFlow. Write balanced ledger entries that tie back to the original payout ID and visit ID for clean books.

Acceptance Criteria
Auto-Queue Delta as Adjustment in Next Eligible Payout
Given a visit’s payout split is recalculated after initial payout and produces a positive or negative delta for one or more payees And the payees have configured payout schedules and cutoff times in FetchFlow When the recalculation is saved Then the system creates an adjustment record per impacted payee with amount equal to the signed delta, currency matching the original payout, and status "Queued" And each adjustment is assigned to the payee’s next eligible payout respecting the configured cutoff and pay cycle And each adjustment stores references to the visit ID and original payout ID and a system-generated unique adjustment ID And when the next eligible payout is generated, the queued adjustments are included and their status updates to "Posted" with a posted timestamp
Consolidate Multiple Deltas per Payee with Drill‑Down Detail
Given multiple recalculation deltas are generated for the same payee within the same payout period (possibly across different visits) When the payout for that period is generated Then all eligible adjustments for that payee are included in that single payout period And the payout summary shows one roll‑up total labeled "Adjustments" equal to the algebraic sum of included adjustments And the payout detail drill‑down lists each adjustment line item with amount/sign, visit ID, original payout ID, adjustment ID, reason code, and timestamp And removing or adding any eligible delta prior to payout regeneration recomputes the roll‑up and line‑item list accordingly
Idempotent Posting via Unique Adjustment IDs
Given an adjustment record with a unique adjustment ID exists and is eligible for the next payout When the payout job is executed multiple times or retried due to failure Then the adjustment is posted at most once to the ledger and payout And any duplicate processing attempts referencing the same adjustment ID are ignored without changing balances and are logged for audit And API/export surfaces the adjustment ID to enable downstream deduplication
Pre‑Payout Reversal for Undone Changes
Given an adjustment (A1) is queued for payout and the underlying visit change is undone or altered such that the delta is canceled before payout generation When the undo is saved Then the system creates a reversal adjustment (A1R) equal in magnitude and opposite in sign to A1 and queues it in the same payout period And both A1 and A1R retain references to the same visit ID and original payout ID and are linked as reversal pairs And when the payout is generated, the net effect of A1 + A1R is zero in the disbursement while both lines remain visible in drill‑down for audit And if A1 is already marked Posted, no reversal is auto‑created by this flow (out of scope)
Respect Minimum Thresholds, Cutoffs, and Pay Cycles
Given a payee has a minimum payout threshold and a defined payout schedule with cutoff And the net of earnings plus adjustments for the period is below the threshold When payout generation runs Then no disbursement is created for that payee and all queued adjustments remain queued and carry forward to the next eligible payout period And adjustments created after the cutoff for the current period are assigned to the subsequent period by default And when the carried amount meets or exceeds the threshold in a later period, the adjustments post in that payout
Balanced Ledger Entries with Full Traceability
Given adjustments are posted in a payout When ledger entries are written Then total debits equal total credits for the adjustment batch and for each individual adjustment And each ledger entry includes adjustment ID, payout ID, payee ID, visit ID, original payout ID, currency, amount, and timestamp And querying the ledger by any of adjustment ID, visit ID, or original payout ID returns the corresponding adjustment entries And exported reports reflect identical totals and references for reconciliation
Transparent Adjustment Receipts & Audit Trail
"As a groomer, I want a clear explanation of any adjustment and a quick SMS summary so that I understand the change and trust the payout without needing to message the owner."
Description

Generate human-readable adjustment receipts that clearly explain what changed and why, the before/after amounts, and the net delta per party. Each receipt links to the visit, shows timestamps, the actor who made the change, and the reason code. Surface receipts in the web dashboard and allow optional SMS summaries to affected staff, aligned with FetchFlow’s SMS-first experience. Maintain a tamper-evident audit log with version diffs and adjustment IDs so owners, staff, and support can verify accuracy without back-and-forth DMs.

Acceptance Criteria
Dashboard Receipt After Reassignment
Given a completed visit has been reassigned from Staff A to Staff B after the original payout split was calculated When the change is saved Then a human-readable adjustment receipt is generated within 10 seconds And the receipt is visible in the web dashboard under the Visit and under the next Payout for Owner, Staff A, and Staff B (per their permissions) And the receipt includes: Visit ID, Pet name(s), Actor name and role, Business timezone timestamp (ISO 8601), Reason code "Reassignment", and a unique Adjustment ID And the receipt displays before and after payout amounts per party and the net delta per party with correct currency formatting And the next payout displays an automatic positive/negative adjustment line whose amounts exactly match the receipt net delta per party
SMS Summary for Refund Adjustment
Given an after-the-fact refund is issued on a visit that affects staff payout And the affected staff member has SMS summaries enabled When the adjustment receipt is generated Then an SMS is sent to the staff member’s verified number within 2 minutes And the SMS body includes: Visit ID, Pet name(s), change type "Refund", signed net delta with currency, and a short link to the full receipt And staff without SMS enabled receive no SMS And the short link resolves to the exact receipt and requires authentication for logged-in users or a time-limited token flow for others
Add-on Change Receipt Content
Given an add-on line item is added, removed, or quantity changed after checkout with a reason code selected When the change is saved Then the receipt lists each changed line with name, quantity, unit price, tax impact, and before/after line and total amounts And the receipt shows the selected reason code and an optional free-text note (if provided) And the net delta per party equals the difference between before and after totals less fees, reconciled to the cent And all monetary values use the business currency and locale formatting
Tamper-Evident Audit Trail and Version Diffs
Given any adjustment receipt is created or edited Then an audit log entry is appended with: Adjustment ID, Visit ID, Actor ID, Actor role, Reason code, Timestamp, Version number, Previous version hash, Current version hash, and a diff of changed fields And a hash chain validates for all versions of the receipt And any attempt to modify a receipt without creating a new version is rejected and logged with HTTP 409 Conflict And the dashboard provides a read-only diff viewer showing version-to-version changes, timestamps, and actor
Role-Based Visibility and Access
Given a user with role Owner, Manager, Staff, or Support views the dashboard When they access a visit or payout impacted by an adjustment Then Owners and Managers can view all related adjustment receipts and audit diffs And Staff can view only receipts that affect their own payouts And Support can access any receipt via a time-limited support token without write permissions And unauthorized users receive 403 Forbidden and no receipt metadata is exposed
Cross-Linking and ID Consistency
Given an adjustment receipt exists When a user opens the receipt Then the receipt contains links to the originating Visit, impacted Payout line item(s), and the corresponding Audit Log entry And the same Adjustment ID is displayed and consistent across the receipt, Visit page, Payout line item, and Audit Log And the Visit page lists all related receipts in reverse chronological order with timestamps And tokenized share links are valid for 7 days and expire thereafter
Role-Based Approvals & Reason Codes
"As a business owner, I want higher-dollar adjustments to require my approval with standardized reason codes so that I can control risk and ensure consistent practices."
Description

Introduce configurable thresholds that determine when adjustments auto-post versus require approval (e.g., auto-post under $25 delta, approval required above). Enforce role-based permissions for initiating changes, approving adjustments, and viewing details. Require reason codes and optional notes for reassignments and refunds to standardize explanations. Block outbound notifications until approvals are completed, then emit receipts and update the audit trail. Log all approval actions for compliance and training purposes.

Acceptance Criteria
Auto-Post Threshold: Delta At or Below Limit
Given the organization’s auto-post threshold is set to $25 And a user with Initiator permission submits an adjustment with a net delta of $24.99 When the adjustment is created Then the system auto-posts the adjustment without requiring approval And sets the adjustment status to "Posted" And schedules the positive/negative amount in the next payout And displays an "Auto-Posted" indicator on the adjustment detail view And records the threshold value used in the adjustment metadata
Approval Workflow: Delta Above Limit
Given the organization’s auto-post threshold is set to $25 And a user with Initiator permission submits an adjustment with a net delta of $25.01 When the adjustment request is submitted Then the system sets the adjustment status to "Pending Approval" And restricts approval actions to users with Approver permission only And prevents the adjustment from posting to payout until approved When an Approver approves the adjustment Then the system posts the adjustment and schedules it in the next payout And records the approval decision, approver identity, and timestamp When an Approver rejects the adjustment Then the system does not post the adjustment And records the rejection decision, approver identity, and timestamp
Role-Based Permissions: Initiate, Approve, View
Given role-based permissions are configured for Initiator, Approver, and Viewer When a user without Initiator permission attempts to create an adjustment Then the UI disables the action and the API returns HTTP 403 When a user without Approver permission attempts to approve or reject Then the UI hides approval controls and the API returns HTTP 403 When a user with Viewer permission opens an adjustment detail Then they can view amounts, deltas, reason codes, notes, and status And they cannot perform create, edit, approve, or reject actions
Reason Codes and Notes: Required Inputs
Given a reassignment or refund adjustment is initiated When the initiator attempts to submit without selecting a reason code from the configured list Then the system blocks submission with a validation error indicating a reason code is required When the initiator selects a valid reason code and optionally enters notes (0–500 chars) Then the system accepts the submission And stores the reason code and notes with the adjustment And displays them in the adjustment detail, audit trail, and recipient receipts
Notification Hold and Post-Approval Receipts
Given an adjustment is in "Pending Approval" When any outbound client/provider notifications or receipts would be triggered by the adjustment Then the system holds all outbound SMS/email/webhook notifications And no notifications are sent until the approval status is "Approved" When the adjustment is approved Then the system sends the appropriate receipts/notifications to configured recipients And records message IDs and timestamps in the audit trail When the adjustment is rejected Then no receipts/notifications are sent And the audit trail records that notifications were suppressed
Approval Action Logging and Audit Trail
Given any approval action (approve or reject) occurs on an adjustment When the action is completed Then the system appends an immutable audit entry capturing: adjustment ID, action (approve/reject), actor identity, actor role, timestamp, threshold value used, before/after amounts, net delta, selected reason code, and notes And the audit entry is visible in the adjustment’s history view to users with Viewer permission or higher And the audit entries are retrievable via export (CSV/API) with filters for date range, actor, action, and reason code
Negative Payout Handling & Carryover Guardrails
"As a walker, I want adjustments that would make my payout negative to be safely carried forward with clear visibility so that I’m not surprised by zero or negative pay."
Description

Handle scenarios where a payee’s net adjustments would result in a negative payout. Apply configurable caps on per-period clawbacks, carry any remaining negative balance forward, and never withdraw external funds automatically. Display projected recovery across future payouts and clearly show how much will be offset in the current cycle. Respect minimum payout thresholds and prevent payouts from posting if resulting net would violate rules. Provide alerts when carryovers exceed a defined limit.

Acceptance Criteria
Per-Period Clawback Cap With Carryover Remainder
Rule: Applied negative offset in a payout equals min(Clawback Cap, Gross Earnings, Outstanding Negative Balance), where Clawback Cap may be absolute (e.g., $100) or a percentage of Gross Earnings (e.g., 40%). Given Clawback Cap = $100 (absolute), Gross Earnings = $250, and Outstanding Negative Balance = $180 When the payout is calculated Then Applied Negative Offset = $100, Remaining Carryover = $80, and Net Payout Before Fees = $150 Given Clawback Cap = $100 (absolute), Gross Earnings = $60, and Outstanding Negative Balance = $180 When the payout is calculated Then Applied Negative Offset = $60, Remaining Carryover = $120, and Net Payout Before Fees = $0 Given Clawback Cap = 40% (percentage), Gross Earnings = $300, and Outstanding Negative Balance = $200 When the payout is calculated Then Applied Negative Offset = $120, Remaining Carryover = $80, and Net Payout Before Fees = $180
Payout Posting Guardrail
Given a payout where Net Payout Before Fees <= 0 after applying offsets and caps When attempting to post the payout Then posting is blocked, the payout status is set to "Held - Net Negative", and no bank transfer is created Given a payout that violates configured payout rules (e.g., below minimum payout threshold) When attempting to post the payout Then posting is blocked, the violation reason is recorded and displayed in the payout detail, and the payout is retried automatically in the next eligible cycle
Minimum Payout Threshold Enforcement
Given a Minimum Payout Threshold of $25 and a computed Net Payout After Offsets of $18 When finalizing the payout Then the payout is held and rolled into the next cycle, and the UI displays "Below minimum threshold ($25). Held until threshold met." Given the next payout cycle computes Net Payout After Offsets >= $25 When finalizing the payout Then the previously held amount is combined and released in the payout, and the hold reason is cleared from the UI
No External Funds Withdrawal Guarantee
Rule: The system must never initiate any external debit to recover negative balances; recovery occurs only via offsets against future positive earnings. Given Gross Earnings = $0 and Outstanding Negative Balance = $90 When the payout cycle runs Then no bank debit or card charge is attempted, payout amount = $0, and carryover remains = $90 Given Gross Earnings = $50, Clawback Cap = $100, and Outstanding Negative Balance = $200 When the payout is calculated and posted Then payout amount = $0, no external debit is initiated, and carryover after posting = $150
Carryover Ledger, Ordering, and Zero-Earnings Behavior
Rule: Carryover entries are itemized with source adjustment IDs, amounts, and timestamps; application order is FIFO by oldest outstanding entry. Given Carryover entries A:$70 (older) and B:$50 (newer), Gross Earnings = $100, and Clawback Cap = $100 When the payout is calculated Then $70 from entry A and $30 from entry B are applied, entry B retains $20, and Net Payout Before Fees = $0 Given Gross Earnings = $0 and Outstanding Negative Balance > $0 When the payout cycle runs Then no payout record is posted and carryover amounts remain unchanged aside from age metadata
Projection and Current Cycle Offset Display
Given Outstanding Negative Balance = $300, Clawback Cap = $100, and the projection basis = trailing 4-cycle average gross of $120 When viewing the payee payout preview Then the UI displays Current Cycle Offset = $100, Remaining After This Payout = $200, Estimated Cycles to Clear = 2, and shows "Projection basis: trailing 4-cycle average gross $120" and the cap type/value Given Current Cycle Gross Earnings are updated to $70 before posting When viewing the payout preview again Then the Current Cycle Offset updates to min(cap, gross, outstanding) = $70 and projections recalculate accordingly
Carryover Limit Alerting and Notification
Given Carryover Alert Thresholds configured as Amount >= $500 or Age >= 4 cycles and a payee has Carryover Amount = $520 or Oldest Entry Age = 4 cycles When the payout cycle completes Then an in-app alert and an email are sent to the payee and org admin including Payee Name, Current Carryover Amount, Oldest Entry Age, Cap value, projected clearance cycles/date, and a link to the payout statement Given a previously alerted payee's carryover falls below all configured thresholds in a later cycle When that cycle completes Then the alert is auto-resolved and a resolution timestamp is added to the audit log
Accounting Export & Reconciliation of Adjustments
"As a bookkeeper, I want adjustments to export cleanly with links back to original payouts so that my accounting system reflects accurate prior-period corrections without manual journal entries."
Description

Map adjustment entries to accounting exports (QuickBooks, Xero, CSV) using appropriate accounts (e.g., prior period adjustments, payroll liabilities) and include references to original payout IDs and visits. Produce a reconciliation report that shows opening balance, adjustments, payouts, and closing balance per payee and per period. Ensure exported totals match the internal ledger exactly and support re-exports with unique batch IDs to avoid duplication. Provide date filters and accrual/cash-basis toggles to align with bookkeeping practices.

Acceptance Criteria
QuickBooks Export: Adjustment Mapping & References
Given a date range, an organization with valid QuickBooks credentials, and configured GL accounts for each adjustment type When the user exports adjustments and payouts to QuickBooks for that period Then each adjustment line is posted to the correct mapped GL account based on its adjustment type (e.g., Prior Period Adjustments, Payroll Liabilities) And each exported line contains Original Payout ID, Visit ID, Payee ID, Adjustment Type, and a unique Batch ID in designated fields/memo And total debits equal total credits within the export And the export’s total amount by account and grand total exactly match the internal ledger for the same filters to the cent And the export passes schema validation and is acknowledged without errors
Xero and CSV Export: Field Parity & Mapping
Given a date range, destination set to Xero or CSV, and configured account codes for all adjustment types When the user exports adjustments and payouts Then each line maps to the correct Xero account code or CSV account column per adjustment type And the CSV includes required columns: Batch ID, External Transaction ID, Payee ID, Original Payout ID, Visit ID, Adjustment Type, Account, Debit, Credit, Date, Basis And Xero payload includes Tracking Category for Payee (if configured) and Reference fields with Original Payout ID and Visit ID And totals by account and grand totals in the export exactly equal the internal ledger for the selected filters And column order and headers remain consistent across repeated exports
Reconciliation Report Per Payee and Period
Given a selected period and basis (accrual or cash) When the user generates the reconciliation report Then for each payee, Opening Balance + Net Adjustments + Net Payouts = Closing Balance And the sum of all payee Closing Balances equals the system-wide Closing Balance for the period And Opening Balance for a period equals the prior period’s Closing Balance (or configured initial opening for the first period) And each line item includes references to Batch ID(s), Original Payout ID(s), and Visit ID(s) And report totals exactly match the internal ledger for the same period and basis And the report can be exported (PDF/CSV) with identical totals to the on-screen view
Re-Export Idempotency with Unique Batch IDs
Given an export has been completed for a specific date range, basis, and destination When the same export is re-run with identical filters Then a new unique Batch ID is generated for the export And each underlying entry retains a deterministic External Transaction ID that remains unchanged across re-exports And the exported payload includes both Batch ID and External Transaction ID to enable downstream deduplication And the internal ledger does not create duplicate postings on re-export And an audit log records both exports with timestamps, user, filters, Batch IDs, and counts
Date Filters and Accrual/Cash Basis Toggle
Given an organization timezone is configured When the user selects accrual basis and sets a date range Then totals are computed using visit completion date within [start 00:00:00, end 23:59:59] in org timezone And when cash basis is selected for the same range Then totals are computed using payout date within the same bounded window And switching basis updates the on-screen totals and counts accordingly And exported files for each basis match the on-screen preview totals exactly And boundary dates are inclusive and verified by test cases on month/quarter/year ends
Validation on Missing Account Mappings
Given at least one adjustment type lacks a configured destination account/code for the selected export destination When the user attempts to export Then the system blocks the export and displays a list of missing mappings by adjustment type and destination And provides a direct link to the account mapping settings And does not generate a partial export file And once mappings are completed, the same export proceeds successfully and includes all previously blocked entries And an error event is logged with timestamp, user, destination, and missing mappings

Holdback Guard

Protect against chargebacks and no‑shows with configurable reserves. Set rolling holdbacks by service risk, client reliability, or event type; automatically release funds after a dispute window or confirmation artifacts (photos/GPS) are attached. Prevents negative balances for staff while shielding the business from exposure.

Requirements

Configurable Rolling Reserves
"As an owner, I want to define different reserve rules by service risk and client reliability so that higher-risk bookings hold more funds and lower-risk bookings release faster."
Description

Provide a rule builder to configure holdback percentages, minimum/maximum caps, and rolling durations by dimensions such as service type (grooming, walking, training), event type (first visit, recurring, add-on), client reliability tier, payment method risk, package usage, and no-show policy. Include rule priority and conflict resolution, simulation mode to preview impact before enabling, and real-time evaluation at checkout and at settlement. The system must write hold amounts to a dedicated reserve ledger and surface applied rule context in the booking and payout views, enabling granular control that reduces risk while preserving cash flow for low-risk scenarios.

Acceptance Criteria
Rule Builder: Configure Multi-Dimensional Holdback Rule
Given I have Admin permissions When I create a holdback rule with dimensions: service_type=grooming, event_type=first_visit, client_tier=low_reliability, payment_risk=high, package_usage=unused, no_show_policy=strict And I set holdback_percent=25%, min_cap=5.00, max_cap=50.00, rolling_duration=7 days And I enable the rule Then a matching booking at checkout calculates hold = min(max(subtotal * 0.25, 5.00), 50.00) and displays the hold and release date = checkout_time + 7 days And the rule persists with unique rule_id, name, priority, and status=enabled in the rule list
Rule Priority and Conflict Resolution
Given two enabled rules R1(priority=1) and R2(priority=3) both match a booking When evaluating at checkout Then R1 is selected and applied, and R2 is ignored, and the decision reason is recorded as "higher priority" Given two enabled rules with equal priority both match, where R3 specifies more dimensions than R4 When evaluating Then R3 is selected, and the decision reason is recorded as "higher specificity" Given two enabled rules with equal priority and equal specificity both match When evaluating Then the most recently updated rule is selected, and the decision reason is recorded as "latest update" And the decision log stores booking_id, candidate_rule_ids, selected_rule_id, and resolution_reason
Simulation Mode: Preview Financial Impact
Given a draft rule R When I run a simulation over a selected date range using historical completed bookings Then the system returns: total_bookings_evaluated, total_matched, projected_total_hold_amount, per-rule impact, and top affected dimensions, without creating reserve ledger entries or modifying live rules When I compare R against baseline (no new rule) Then differences are shown as absolute and percent deltas in total hold and affected transactions When I export simulation results Then a CSV is generated with per-booking simulated hold_amount, applied_rule_id, and release_at
Real-Time Evaluation at Checkout and Settlement
Given an enabled rule matches a booking When staff proceeds to checkout Then the calculated holdback amount and release date are shown prior to payment capture, and after capture a reserve ledger entry is created with status=held Given a booking with an existing held reserve When settlement/payout processing runs Then the system re-evaluates release eligibility based on rolling_duration and current time, updates the reserve status accordingly, avoids duplicate holds, and maintains idempotency on retried events
Reserve Ledger: Write, Adjust, Release
When a hold is created Then an append-only ledger entry is written with fields: ledger_id, booking_id, client_id, pet_id, rule_id, hold_percent, min_cap, max_cap, hold_amount, currency, created_at, release_at, status=held When a booking total changes (e.g., partial refund) before release Then an adjustment entry is appended with delta and new hold_amount; original entries are not mutated; staff reserve balance remains non-negative When release_at elapses and no active dispute is recorded Then the held amount transitions to available balance, a release entry is written, and the ledger status becomes released with released_at timestamp
UI: Surface Applied Rule Context in Booking and Payout
Given a booking with an applied hold When viewing the booking detail Then the UI shows Hold Amount, Calculation (percent/min/max), Rule Name/ID, Matched Dimensions, Priority, Release Date/Time, and a link to the rule Given a payout including bookings with holds When viewing the payout dashboard Then the UI shows aggregate held amount, count of holds, soonest releases, and supports drill-down to booking-level details And only Finance/Admin roles can view rule context; other roles see masked details
Audit Trail: Rule Evaluations and Changes
When a rule is created, updated, enabled/disabled, or deleted Then an audit event is stored with actor, timestamp, action, and before/after diff When a rule evaluation occurs at checkout or settlement Then a decision log records booking_id, inputs used, candidate rules, selected rule, resolution_reason, and computed outputs; logs are queryable for at least 12 months When an authorized user requests an export of audit and decision logs for a date range Then the system generates a CSV with the requested entries
Risk Scoring & Client Reliability Engine
"As a business owner, I want an automatic reliability score so that reserves scale based on each client’s history without manual work."
Description

Compute a dynamic reliability score per client using signals including completed bookings, late cancellations, no-shows, payment failures, chargebacks, dispute outcomes, tenure, verified contact methods, and presence of up-to-date vaccine records. Support time-decay on negative events, tier thresholds (e.g., Low/Med/High), and admin adjustments. Expose the score synchronously to the reserve rule engine and asynchronously to analytics, with safeguards for cold-start clients. This enables targeted reserves that adapt as behavior changes.

Acceptance Criteria
Deterministic score computation from weighted signals
Given the scoring config: baseline=500; weights {completedBooking:+5, lateCancellation:-10, noShow:-25, paymentFailure:-20, chargeback:-50, disputeWon:+15, disputeLost:-20, tenurePerMonth:+1, verifiedContact:+5 per method, vaccinesUpToDate:+5} When a client has: 12 completed bookings; 1 late cancellation; 0 no-shows; 0 payment failures; 0 chargebacks; tenure 6 months; verified phone only; vaccines up to date Then the computed reliability score equals 566 Given the same config When the client adds a verified email Then the score increases by +5 from the prior value Given the same config When a chargeback is recorded and later the dispute is won Then the score first decreases by -50 and then increases by +15 upon disputeWin, and both changes are timestamped and auditable
Time-decay on negative events
Given decay config: negativeEventHalfLifeDays=30; applyExponentialDecay=true; positiveEventsDoNotDecay=true When a noShow penalty of -25 is applied at T0 Then its effective contribution is -12.5 at T0+30d and -6.25 at T0+60d, within ±0.1 tolerance, and it never flips sign Given the same config When a lateCancellation penalty of -10 reaches age ≥ 120d Then its effective contribution magnitude ≤ 1.0
Tier threshold assignment (Low/Med/High)
Given thresholds: Low < 450, Medium 450–749, High ≥ 750 When scores 449, 450, 749, 750 are evaluated Then tiers are Low, Medium, Medium, High respectively Given thresholds as above When a client's score changes from 748 to 752 Then the tier updates from Medium to High within 1 second and a tierChange event is emitted with previousTier and newTier
Admin adjustments with audit and expiry
Given an admin with role RiskOverride and an active client score S When the admin applies a +30 adjustment with reason "Excellent references" expiring in 14 days Then the displayed score immediately reflects S+30 and an audit record is created containing actorId, reason, delta, ttl, createdAt Given the same adjustment When 14 days elapse Then the adjustment auto-expires and the score reverts to S, and the audit record shows expiredAt Given system guardrails maxAbsoluteAdjustment=±100 When an admin attempts +150 Then the request is rejected with validation error "Adjustment exceeds limit"
Cold-start client safeguards
Given a new client with no historical events and defaultBaseline=500, defaultTier=Medium When the score is requested Then the engine returns score=500, tier=Medium, coldStart=true Given cold-start definition: minObservationEvents=3 or tenure<14 days When the client meets cold-start criteria Then coldStart=true is included with the score and exposed synchronously to the reserve rule engine
Synchronous exposure to reserve rule engine
Given a reserve computation request with clientId X and the latest events committed When the rule engine requests the client's reliability score Then the risk engine returns score, tier, and coldStart within 150 ms p95 and 300 ms p99 Given an event (e.g., paymentFailure) is committed When a subsequent request for the client's score is made after 2 seconds Then the returned score reflects the new event (read-after-write consistency ≤ 2s p95) Given the risk engine encounters a transient error computing score When the rule engine requests the score Then the last-known score not older than 5 minutes is returned with stale=true and the error is logged
Asynchronous analytics feed and schema
Given a score change occurs at time T When analytics export runs Then a record with fields {clientId, score, tier, coldStart, changedAt=T, attributionBreakdown, version} is delivered to the analytics sink within 5 minutes p95 and is idempotent by (clientId, changedAt, version) Given duplicate delivery occurs When the analytics system ingests the record Then it does not create duplicate rows (idempotent upsert) Given schema version increments When exports are emitted Then records include version=N and are backward-compatible with N-1 for at least 90 days
Evidence Capture & Verification
"As a walker, I want to add GPS and photos via text so that funds can release sooner and disputes are easier to win."
Description

Enable staff to attach confirmation artifacts (time-stamped photos, GPS check-in/out, client signature) via SMS links and in-app flows, automatically associating them to the appointment. Validate metadata authenticity, enforce service-specific minimum artifact sets, and provide offline capture with later sync. When required artifacts are present, mark the hold eligible for early release per rule configuration. Include privacy controls, retention policies, and optional client-visible galleries to support dispute defense and accelerate releases.

Acceptance Criteria
SMS and In-App Artifact Capture with Auto-Association
Given a scheduled appointment with an assigned staff member and a verified mobile number And an active session in the mobile app or a secure SMS capture link is available When the staff opens the capture flow within the appointment window Then the flow allows capturing time-stamped photos, GPS check-in, GPS check-out, and client signature And each artifact is stamped with server-received timestamp, uploader user ID, appointment ID, and device ID And the SMS link is single-use, tokenized, and expires at appointment end plus 2 hours And on submit, artifacts are stored and auto-associated to the correct appointment And the appointment timeline displays the newly attached artifacts within 15 seconds of upload And any submission using an expired or invalid link is rejected with a re-authentication prompt
Artifact Metadata Authenticity Validation
Given artifacts are uploaded via SMS link or in-app flow When a photo is processed Then the system extracts EXIF capture time and compares to server-received time; if missing, future-dated, or older than 10 minutes, mark the artifact as Suspect And compute and store a SHA-256 hash of the binary at ingest and record an immutable audit event And images missing EXIF or with mismatched capture times are ineligible for early release until a supervisor approves them When GPS check-in/out events are processed Then accuracy must be <= 50 meters and timestamp within the appointment window ±10 minutes; otherwise mark as Suspect and exclude from minimum set counts And if a service location/geofence is configured, coordinates must lie within 200 meters of the expected location or be marked as Suspect When a client signature is captured Then the system stores signer name, drawn-stroke vector, timestamp, and IP/user agent (for web) and computes a hash of the signature payload And any artifact flagged Suspect does not count toward required minimums until explicitly approved by a supervisor role
Service-Specific Minimum Artifact Set Enforcement
Given service-specific artifact rules are configured And examples include: Dog Walk requires GPS check-in + GPS check-out + at least 1 photo; Training Session requires client signature + at least 1 photo; Drop-in Visit requires at least 1 photo or client signature When staff attempts to mark an appointment complete or when early release evaluation runs Then the system evaluates the configured rule set against available validated artifacts And if requirements are unmet, the UI lists missing items and blocks completion and early release And when requirements are met, completion is allowed and an Artifacts Complete badge is shown on the appointment And the evaluation records rule version, evaluator, timestamp, and outcome in the audit log
Offline Capture with Deferred Sync
Given the staff device is offline or has intermittent connectivity When the staff uses the capture flow Then photos and signatures can be captured offline and stored locally encrypted And GPS check-in/out attempts are queued and retried until a fix is available And upon reconnection, all queued artifacts are uploaded within 5 minutes while preserving original capture timestamps and ordering And duplicate submissions are prevented using client-generated UUIDs for each artifact And artifacts captured offline are labeled Pending Sync until server acknowledgment is received And if artifacts remain unsynced more than 12 hours after appointment end, notify the staff and manager
Early Release Triggering via Holdback Guard
Given an appointment has satisfied its service-specific minimum artifact set with no Suspect artifacts pending approval And early release on artifact completion is enabled by rule configuration When the final required artifact is validated Then the system publishes an artifacts.validated event within 60 seconds containing appointment ID, rule ID, and validation summary And Holdback Guard evaluates the associated hold and, if approved, updates funds status to Eligible for Early Release within 2 minutes And if evaluation is denied or a previously validated artifact is later invalidated, eligibility is revoked and affected parties are notified And all state transitions are audit logged with user, timestamp, rule ID, and reason
Privacy Controls, Retention Policies, and Client Gallery
Given service-level privacy defaults and client consent records exist When an artifact is uploaded or reviewed Then each artifact has a visibility setting of Internal Only, Staff Only, or Client Visible with defaults applied per service configuration And changing visibility requires appropriate role permissions and creates an audit log entry And the client gallery shows only Client Visible artifacts after appointment completion, subject to client opt-out preferences And client-visible galleries omit raw GPS coordinates and exact timestamps by default while retaining full metadata internally And retention policies purge Client Visible artifacts after 180 days and Internal artifacts after 365 days (both configurable); purges run nightly and produce deletion logs And admins can export selected artifacts with metadata for dispute defense; exports are watermarked and logged
Dispute Window & Auto-Release Scheduler
"As an owner, I want funds to release automatically when the dispute window closes so that cash flow is predictable without manual tracking."
Description

Model dispute windows by payment processor and network, set default release delays, and run a calendar-aware scheduler that queues and executes releases at the correct time. Integrate with processor webhooks to shorten or extend holds on dispute events, handle partial refunds, and ensure idempotent release jobs. Display countdowns and eligibility in the dashboard and allow per-booking overrides with audit logging. This automates timely releases while honoring risk windows.

Acceptance Criteria
Model Dispute Window by Processor and Network
Given a global default dispute window of 75 days and a Stripe default of 90 days and a Stripe+Visa override of 120 days When a charge is created via Stripe on Visa on 2025-08-11T10:00:00Z Then the stored dispute_window_end is 2025-12-09T23:59:59Z Given a Stripe default dispute window of 90 days When a charge is created via Stripe on an unknown network on 2025-08-11T10:00:00Z Then the stored dispute_window_end is 2025-11-09T23:59:59Z Given only a global default dispute window of 75 days When a charge is created via an unknown processor on 2025-08-11T10:00:00Z Then the stored dispute_window_end is 2025-10-25T23:59:59Z
Calendar-Aware Auto-Release Scheduling with Time Zones
Given merchant timezone America/New_York and default release delay of 3 business days and US holiday 2025-09-01 configured When a holdback is created on 2025-08-29T16:00:00-04:00 (Friday) Then the scheduled release is 2025-09-04T16:00:00-04:00 (skipping weekend and holiday) And the enqueued job run_at in UTC is 2025-09-04T20:00:00Z And the release job executes at the scheduled time and marks the holdback as Released
Webhook-Driven Hold Adjustments on Disputes
Given a holdback with release scheduled for 2025-08-18T10:00:00Z When a processor webhook dispute.opened is received on 2025-08-16T09:00:00Z for the same charge Then the release is paused and rescheduled to occur no earlier than on dispute.closed And the dashboard status changes to "On hold - dispute" within 1 minute When a processor webhook dispute.closed with outcome "won" is received on 2025-08-17T12:00:00Z Then a release job is enqueued to execute within 15 minutes of receipt When a processor webhook dispute.closed with outcome "lost" is received Then the release is canceled and no funds are released
Partial Refunds Adjust Release Amount and Timing
Given a $200.00 holdback scheduled for release When a $50.00 partial refund is recorded before release execution Then the scheduled release amount updates to $150.00 and the dashboard reflects the new amount within 1 minute When additional partial refunds totaling $100.00 are recorded before execution Then the scheduled release amount updates to $50.00 When a full refund equals or exceeds the original charge amount before execution Then the release is canceled and no funds are released
Idempotent Release Jobs with Safe Retries
Given a release job with idempotency key rel_abc123 for booking BK001 amount $150.00 When the job is retried multiple times due to network timeouts Then exactly one successful transfer is created at the processor And subsequent attempts return a no-op based on the idempotency key And the ledger records a single Released entry and multiple Attempt entries with status Retried
Dashboard Countdown and Eligibility Indicators
Given a holdback scheduled for 2025-09-04T16:00:00-04:00 and user timezone America/New_York When the booking detail page is opened Then the countdown shows the remaining time to release in HH:MM format and the exact scheduled date-time And the status badge shows one of: Scheduled, Eligible now, On hold - dispute And when the scheduled time is reached, the status updates to Eligible now within 1 minute and the countdown stops
Per-Booking Release Override with Audit Trail
Given a user with permission Finance:ModifyHoldbacks and a booking with a scheduled release When the user sets a new release datetime of 2025-09-03T10:00:00-04:00 via the override control and confirms with a reason "Client verified" Then the prior scheduled job is canceled and a new job is scheduled for 2025-09-03T10:00:00-04:00 And an audit log entry is created capturing booking ID, previous and new datetimes, user ID, timestamp, and reason And if a dispute is currently open, the override is rejected with an error "Cannot override while dispute is open" and no schedule changes occur
Staff Payout Negative Balance Shield
"As an admin, I want the system to prevent staff payouts when reserves are insufficient so that we never create negative balances."
Description

Implement a double-entry ledger separating merchant reserve, staff pending, and staff available balances. Block payouts that would create a negative staff balance, and provide configurable clawback rules to recover chargebacks from future earnings with caps and drip rates. Support alerts, hard stops at thresholds, deactivated staff handling, and multi-location routing. This prevents negative balances while fairly allocating risk.

Acceptance Criteria
Double-Entry Ledger Segregation and Integrity
Given a day with 50 bookings, 5 refunds, 3 chargebacks, tips, and platform fees posted When journals are written Then each journal has balanced debits and credits And entries are posted to distinct accounts: merchant_reserve, staff_pending, staff_available, platform_fees And total debits equal total credits across all entries per journal And per-staff sub-ledger balances reconcile to the aggregate ledger within $0.01 And GET /balances/{staffId}?at=T0 returns available and pending that match the ledger snapshot at T0 within $0.01
Payout Request Blocked to Prevent Negative Balance
Given staff available balance is $150.00 and pending balance is $200.00 When the staff requests a payout of $200.00 Then the payout is rejected with HTTP 409 and error_code "NEGATIVE_BALANCE_BLOCK" And no disbursement is scheduled And staff available balance remains $150.00 And an audit event "payout_blocked_negative" is recorded with staffId and requested_amount
Configurable Clawback with Caps and Drip Rates
Given policy cap_percent_per_cycle = 30 and drip_percent_of_outstanding = 20 and outstanding_chargebacks = $120.00 and gross_earnings_this_cycle = $400.00 When the payout calculation runs Then withheld_amount_this_cycle equals min(0.30*400.00, 0.20*120.00) = min(120.00, 24.00) = $24.00 rounded to the nearest cent And outstanding_chargebacks reduces to $96.00 And staff_available is reduced by $24.00 but not below $0.00 Given the same policy and next cycle gross_earnings = $500.00 and outstanding_chargebacks = $96.00 When the payout calculation runs Then withheld_amount_this_cycle equals min(0.30*500.00, 0.20*96.00) = min(150.00, 19.20) = $19.20 And outstanding_chargebacks reduces to $76.80 And all calculations are recorded in a ledger memo with policy parameters and amounts applied
Threshold Alerts and Hard Stops
Given thresholds soft_alert = max(50% of 30d_avg_gross, $300) and hard_stop = max(100% of 30d_avg_gross, $600) and staff 30d_avg_gross = $600 When outstanding_chargebacks become $320 Then a soft alert is issued within 5 minutes via dashboard banner and email to staff and admin And an alert event is recorded with threshold values and outstanding amount Given outstanding_chargebacks become $610 When the staff attempts any payout Then the payout is blocked with HTTP 409 and error_code "HARD_STOP_THRESHOLD" And the block persists until outstanding_chargebacks <= hard_stop or an admin override event is recorded
Deactivated Staff Recovery and Payout Lock
Given staff status = deactivated and outstanding_chargebacks = $150.00 and available_balance = $70.00 When an automated or manual payout is attempted Then the payout is rejected with HTTP 403 and error_code "STAFF_DEACTIVATED" And balances are unchanged and an audit event is recorded Given settlements attributed to the deactivated staff arrive after deactivation totaling $60.00 When the next payout cycle runs Then $60.00 is applied to reduce outstanding_chargebacks to $90.00 and available_balance remains $70.00 Given 30 days have elapsed since deactivation and outstanding_chargebacks > $0.00 When the end-of-window job runs Then the remaining outstanding is journaled from merchant_reserve to settle the debt and the staff sub-ledger is set to $0.00 And a deactivation_settlement report row is generated with amounts and timestamps
Multi-Location Reserve and Routing Respect
Given staff belongs to location A and has outstanding_chargebacks = $100.00 at location A and location B has its own reserves When payouts and clawbacks are processed Then only location A reserves and ledgers are impacted for this staff And no entries are posted to location B ledgers Given the staff transfers from location A to location B and transfer_policy = "migrate_debt" with effective_date = T0 When the transfer job runs at T0 Then outstanding debt is migrated via inter-location journal entries from A to B with zero net effect on platform equity And subsequent clawbacks apply to location B only
Concurrency and Idempotency for Payouts and Clawbacks
Given staff available balance = $150.00 and two concurrent payout requests of $100.00 each with different idempotency keys When both requests are processed under load Then exactly one payout is approved and one is rejected with HTTP 409 and error_code "INSUFFICIENT_AVAILABLE" And the final available balance equals $50.00 Given a payout request is retried with the same idempotency key within 24 hours When the retry is received Then no duplicate payout is created and the original payout id and status are returned Given a chargeback of $80.00 posts while a payout is being processed When ledger writes occur Then if disbursement is not yet committed, the payout is re-evaluated and blocked if it would create a negative available balance Else the chargeback is scheduled for recovery next cycle respecting cap and drip policies and staff_available does not go below $0.00
Holdback Transparency & Notifications
"As a staff member, I want to know why a holdback was applied and how to release it so that I can take the right actions quickly."
Description

Provide clear explanations and proactive notifications to staff and clients when a holdback is applied, including reason, percentage, expected release date, and any required evidence. Support SMS and email templates with localization, rate limits, and opt-in controls. Add in-dashboard indicators on bookings and payouts, plus a self-serve confirmation link for clients that can trigger early release when rules allow. This reduces confusion and accelerates resolution.

Acceptance Criteria
Client SMS: Holdback Applied
Given a booking triggers a holdback event for a client When the holdback is recorded Then an SMS is sent to the client within 60 seconds containing reason, percent, amount, expected release date, evidence required (if any), and a confirmation link And the SMS is sent only if the client has opted into SMS; otherwise no SMS is sent and the event is logged as suppressed_opt_out And the SMS is localized to the client’s preferred locale with fallback to the business default And the message length is <= 320 characters and includes a functional short link that returns HTTP 200 and resolves to the confirmation page And a delivery status (queued/sent/failed) with provider message ID and timestamp is stored in the notification log And duplicate sends for the same holdback event are prevented via an idempotency key (booking_id + event_id + channel) And a rate limit of max 1 holdback SMS per booking per 24 hours is enforced And the SMS contains no sensitive payment data and includes at most the pet first name and booking reference
Staff Email: Holdback Applied to Payout
Given a staff payout includes a holdback When the holdback is calculated Then an email is sent to the staff member within 5 minutes containing payout reference, booking reference(s), percent, amount, expected release date, rule reason, required artifacts, and a deep link to the dashboard details And the email is sent only if the staff member’s email notifications for holdbacks are enabled; otherwise it is not sent and the suppression is logged And the email subject contains the tag "[Holdback]" and the primary booking reference And the email uses the staff member’s locale with fallback to business default and renders with no unresolved placeholders And only one email per holdback event is sent (deduplicated by idempotency key) And the send event, message ID, and rendered locale are audit logged And the deep link opens the correct booking/payout holdback drawer within the dashboard
Dashboard Indicators: Booking and Payout Views
Given a booking has an active holdback When a user opens the booking list or booking detail Then a visible Holdback badge shows percent and held amount on list and detail views And hovering/tapping the badge shows a tooltip with reason, expected release date, and evidence status (e.g., None/Partial/Complete) And clicking the badge opens a side panel with full holdback details and links to upload artifacts or view rules Given a payout includes held amounts When a user opens the payouts list or detail Then a Holdback column/section shows total held amount and status (Active/Released) And indicators update to Released within 60 seconds of a release event And accessibility requirements are met (focusable elements, ARIA labels, and contrast ratio >= 4.5:1) And list and detail indicators render correctly on mobile (<= 414px) and desktop (>= 1024px) breakpoints
Client Self-Serve Confirmation: Early Release
Given a client receives a holdback confirmation link When the client opens the link within 72 hours and enters a valid one-time code sent to their registered channel Then the confirmation page displays booking details and a confirm-completion action And upon confirmation, early release rules are evaluated immediately And if rules permit early release, the holdback is released, the payout updated, and client and staff receive notifications within 60 seconds And the link is single-use; subsequent visits display an Already confirmed state without changing payouts And invalid OTP attempts are limited to 5 before a 15-minute lockout And the confirmation decision, user identifier, timestamps, and rule evaluation outcome are stored in an audit log And if rules do not permit early release, the client sees the reason and the next expected release date
Localization and Template Configuration
Given an admin edits the Holdback Applied templates for SMS and email When saving a template Then required placeholders {reason},{percent},{amount},{expected_release_date} must be present or the save is rejected with a clear error And optional placeholders {evidence_required},{confirmation_link} render when provided and are omitted cleanly when absent And a locale can be selected per template; if a locale is missing, the system falls back to the default locale template And a preview renders with sample data in the selected locale prior to save And the SMS character counter displays the estimated length and warns when > 320 characters And template changes are versioned, attributable to the editor, and can be rolled back to a prior version And template changes propagate to runtime within 5 minutes
Rate Limiting, Idempotency, and Retry Policy
Given a notification send attempt fails with a transient error When the system retries Then it uses exponential backoff with up to 3 retries within 15 minutes and does not exceed channel rate limits And each notification is published with an idempotency key of booking_id + event_id + channel to prevent duplicates across retries and deployments And for holdback events, no more than 1 notification per channel per booking is sent within a 24-hour window, excluding a separate single notification for a release event And suppressed sends due to rate limits or duplicate detection are logged with reason and next eligible time And successful retries update the original log entry rather than creating a new event
Evidence Requirements Display and Reminder
Given a holdback requires evidence (e.g., photos or GPS) When staff views the booking holdback drawer Then a checklist lists each required artifact with status (Missing, Uploaded, Approved) and upload controls And uploads use signed URLs that expire in 24 hours and are virus-scanned before acceptance And if required evidence remains missing 24 hours before the expected release date, send one reminder via the staff’s preferred channel, respecting opt-in and rate limits And upon evidence upload, the system recalculates the expected release date if rules allow and updates UI indicators within 60 seconds And all evidence actions (upload, approve, reject) are audit logged with actor, timestamp, and reason
Reporting, Audit, and Exports
"As a finance manager, I want auditable reports of reserves and releases so that I can reconcile payouts and measure risk."
Description

Deliver reports for held and released amounts, average hold duration, exposure by service, staff, and client tier, and rule performance over time. Maintain a complete audit trail of rule versions, applied rules per transaction, ledger movements, and evidence checks. Provide CSV exports and secured API endpoints for finance and accounting systems. This enables reconciliation, compliance, and continuous optimization of reserve strategies.

Acceptance Criteria
Held and Released Amounts Report Generation
Given a specified date range, timezone, and optional filters (service, staff, client tier, event type) When the user generates the Held and Released Amounts report Then totals for held, released, net reserve change, and transaction counts are displayed and reconcile to the ledger within 0.1% And results can be grouped by day, week, or month with consistent totals across groupings And drill-down reveals the list of underlying transactions with IDs and amounts And the report completes in  5 seconds for up to 100,000 transactions And the report can be exported to CSV with documented column order and headers
Average Hold Duration Calculation
Given a date range and optional filters (service, staff, client tier, event type) When viewing the Average Hold Duration metric Then the mean and median duration (in hours) are calculated using released holds whose release timestamps fall within the range And open holds are excluded from the average and surfaced with a separate count and current average time-open And the metric matches a validated fixture dataset within b1 1 minute for both mean and median And grouping by week or month yields weighted averages that reconcile to the ungrouped result within 0.1% And the metric is exportable via CSV and API with duration in hours to one decimal place
Exposure Breakdown by Service, Staff, and Client Tier
Given current open holds and optional filters (service, staff, client tier, event type) When generating the Exposure report Then exposure totals are shown by service, staff, and client tier with a grand total equal to the sum of categories and the live held balance And categories can be sorted by amount descending and filtered to a configurable Top N And selecting a category drills down to its constituent transactions with IDs and amounts And the report reflects ledger changes within 2 minutes of the change And the API returns identical totals to the UI for the same parameters
Rule Performance Over Time Dashboard
Given holdback rules with version history and effective timestamps When viewing Rule Performance over a selectable period Then per rule-version metrics are displayed: holds created, average hold amount, average duration, release rate within dispute window, chargeback rate, and no-show fee recovery rate And metrics are charted by week or month and can be compared between two selected rule versions And any metric value can be traced to the exact set of audit entries used in its calculation via a drill-through And CSV and API exports include rule_id, rule_version, period_start, period_end, and all metric fields
End-to-End Audit Trail and State Reconstruction
Given any transaction affected by holdbacks When viewing its audit trail Then there exists an immutable, append-only sequence of events covering rule evaluation, applied rules, ledger movements, evidence checks, and releases with UTC timestamps and actors And each event includes transaction_id, rule_id, rule_version, parameters, pre_balance, post_balance, evidence_artifact_ids, and a content hash And a point-in-time reconstruction endpoint returns the ledger and rule state as of a supplied timestamp that matches replay of the audit events And audit exports are available in CSV and JSON for a transaction, a date range, or an account, with consistent schemas And non-admins have read-only access scoped by permissions; any redaction is logged as a redaction event with reason and timestamp
CSV Export for Finance Reconciliation
Given any report view with applied filters and sorting When exporting to CSV Then the file uses UTF-8 encoding, RFC 4180 quoting, and a single header row with canonical column names And all timestamps are ISO 8601 with timezone; currency fields have 2 decimal places with dot decimal separator and no thousand separators And the export returns within 2 minutes for up to 1,000,000 rows via an asynchronous download link with progress and completion notifications And exported rows match the on-screen result set in content and order for the same parameters And a data dictionary endpoint documents column names, units, and semantics for each export
Secured Finance and Accounting API
Given an API client with issued credentials and scopes When requesting reporting, audit, or export endpoints Then access requires OAuth2 client credentials with per-scope authorization; requests without valid scope are denied with HTTP 403 And responses are paginated and filterable by date range, service, staff, and client tier and support ETag-based caching And payload schemas match CSV exports; all currency amounts are provided in minor units and include a currency code And the API enforces a rate limit of 600 requests per minute per client and supports IP allowlists; all endpoints are served over TLS 1.2 or higher And PII fields are minimized and redacted according to scope; all access is logged in a security audit log retrievable by admins

Partner Routing

Route a share to rescues, retail venues, or landlord partners automatically. Support fixed bounties per service or percentage revenue shares, tie rules to referral codes or booking links, and send branded remittance summaries each day. Keeps community partnerships smooth without extra reconciliation work.

Requirements

Partner Profiles & Onboarding
"As a business owner, I want to create and configure partner profiles with payout and branding settings so that shares route correctly and partners receive the right communications without manual setup."
Description

Provide a partner management UI and API to create and maintain profiles for rescues, retail venues, and landlord partners. Each profile captures legal name, partner type, contact channels, payout method (e.g., ACH), tax details (W-9/EIN), time zone, and branding assets (logo, display name, email/SMS sender). Configure default revenue share settings per partner, including supported share types (fixed bounty per service or percentage of revenue), effective dates, and service mappings. Integrate with FetchFlow’s dashboard permissions so only authorized roles can view/edit partner data. Support CSV import, duplicate detection, validation of payout/tax info, and soft-deletion with historical retention to preserve auditability. Expose partner identifiers for use in referral codes, booking links, routing rules, ledger entries, and remittance summaries.

Acceptance Criteria
Create Partner Profile via Dashboard UI
Given an authorized dashboard user with partner:write permission, when they submit the Create Partner form with required fields (legal_name, partner_type, at least one contact channel: email or phone, payout_method, tax details, time_zone, display_name), then the system saves the profile, shows a success confirmation, and navigates to the partner detail within 2 seconds. Given a logo file is uploaded, when the file type is PNG or JPG, size <= 2 MB, and dimensions >= 256x256, then the logo is accepted and rendered in the preview without distortion. Given an email/SMS sender is configured, when the email is RFC-5322 valid and the SMS sender is E.164 valid, then submission is allowed; otherwise field-level errors are shown and submission is blocked. Given time_zone is provided, when it is a valid IANA TZ identifier (e.g., America/Los_Angeles), then the value is accepted; otherwise a validation error is shown. Then an audit log entry records the creator, timestamp, and initial values; the created partner has a unique partner_id and status Active.
Create Partner Profile via API
Given a POST /api/partners request with a valid JSON payload including legal_name, partner_type, contact channels, payout_method, tax details, time_zone, branding assets, and default share settings (optional), when the token has scope partner:write, then the API returns 201 Created with partner_id (UUID), created_at, and persisted fields. Given the payload contains an invalid partner_type, malformed email/SMS, invalid time_zone, or invalid tax ID format, when the request is submitted, then the API returns 400 with field-specific error codes and messages, and no partner is created. Given branding assets are included (multipart or URL), when they meet type/size constraints, then they are stored and returned as URLs; otherwise 415/422 is returned with reasons. Given the caller requests the created resource via GET /api/partners/{partner_id}, then the response includes partner_id, display_name, partner_type, contact channels, time_zone, branding assets, payout/tax validation status (masked sensitive fields), and last_updated timestamps.
Role-Based Access Controls for Partner Data
Given a user without partner:read permission, when they attempt to view the partners list or detail in the UI or API, then a 403 Forbidden is returned and no partner data is displayed. Given a user with partner:read but without partner:write, when they access the partner detail, then edit/delete controls are hidden or disabled in the UI and any update API calls return 403. Given a user with partner:write, when they create or update a partner, then the operation succeeds and an audit trail records before/after values, actor, and timestamp. Given access is attempted via deep link or bookmarked URL, when the user lacks required permission, then the UI redirects to an access denied page and the event is audited.
CSV Import with Validation and Duplicate Detection
Given an authorized user uploads a CSV with headers [legal_name, partner_type, email, phone, payout_method, routing_number, account_number, tax_id_type, tax_id, time_zone, display_name], when the file encoding is UTF-8 and size <= 5 MB, then the system processes the file and reports counts created/updated/skipped within 60 seconds for up to 1,000 rows. Given a row matches an existing partner by exact tax_id or by (legal_name + email or phone), then the row is flagged as duplicate and skipped by default; the summary lists duplicate row numbers and matched partner_ids. Given a row fails validation (e.g., invalid IANA time_zone, unsupported partner_type, invalid email/SMS format, bad EIN pattern NN-NNNNNNN), then that row is rejected with line-specific error codes; valid rows continue to import. Given the import completes, when the user downloads the error report, then it contains the original row data, error codes, and remediation guidance for each failed row. Then an audit log records the import file checksum, submitter, timestamp, and the counts created/updated/skipped/failed.
Payout and Tax Details Validation and Activation Gate
Given payout_method = ACH, when routing_number fails ABA checksum or account_number length is outside allowed range (e.g., 4–17), then save is blocked and field-level errors are displayed. Given tax_id_type = EIN, when tax_id does not match NN-NNNNNNN, then validation fails with a field error; if tax_id_type = SSN is provided, it is masked in storage and responses except last 4. Given W-9 document upload is required for U.S. partners, when a PDF <= 10 MB is uploaded, then it is stored securely and linked to the partner; otherwise 415/422 errors are shown. Given payout and tax details are valid, when the profile is saved, then partner.payout_status = Payout-Ready; until then, partner cannot be selected as payable in routing configuration and a configuration warning is shown in UI/API. Given payout or tax fields are updated, then sensitive values are masked in UI/API, and audit logs redact full numbers while recording change metadata.
Configure Default Revenue Share Settings
Given a partner profile, when an admin adds a default revenue share, then they can select share_type in {fixed_bounty_per_service, percentage_of_revenue}, set effective_start and optional effective_end dates, map one or more services by service_id, and set amount (>= $0.01) or percent (0.01–100.00). Given overlapping effective date ranges exist for the same service and share_type, then the system blocks save and displays a conflict message identifying the overlapping ranges. Given a percentage share is set, when a test calculation is executed for a $100.00 service, then the computed share equals percent * price, rounded to the nearest cent using half-up rounding. Given a fixed bounty is configured, when the partner detail or GET /partners/{id}/shares is viewed, then the configuration shows the bounty amount and mapped services. Given no share is configured for a service, then the partner’s default share for that service is 0 and is reported as such in the shares view/API.
Soft-Deletion with Historical Retention and Identifier Exposure
Given an authorized user performs a soft-delete on a partner, then the partner status changes to Archived, deleted_at is set, records are excluded from default lists and selection dropdowns, and no data is hard-deleted. Given a soft-deleted partner is referenced by referral codes, booking links, ledger entries, or remittance summaries, then those records remain intact and continue to display the partner display_name and partner_id. Given an attempt is made to create a new partner with the same tax_id as an Archived partner, then the system warns of the archived duplicate and offers reactivation; creating a duplicate is blocked without explicit override. Given the API supports listing, when include_archived=true is provided to GET /partners, then Archived partners are returned; otherwise they are omitted by default. Given any partner (active or archived) is retrieved, then the response exposes an immutable partner_id (UUID) and a human-readable partner_code suitable for use in referral codes, booking links, routing rules, ledger entries, and remittance summaries.
Referral Codes & Booking Links
"As a partner marketing lead, I want unique referral codes and booking links that attribute bookings to our organization so that revenue shares are applied automatically without back-and-forth reconciliation."
Description

Enable creation and management of unique referral codes and short booking links per partner to attribute bookings. Generate human-readable codes, QR codes, and short URLs that deep-link into mobile booking flows and SMS threads. Configure expirations, usage limits, and optional campaign metadata (UTM/source). Associate codes/links to a specific partner and optionally to a location, staff member, or service bundle. Ensure attribution persists across SMS-first conversations and rescheduled appointments. Provide analytics on clicks, conversions, and first/last touch attribution. Offer staff tools to insert partner links in outbound texts and printable QR flyers. All events are logged for audit and feed into the routing rules and calculation engine.

Acceptance Criteria
Create & Manage Partner Referral Codes/Links with Associations
Given a partner exists and a user with Partner Admin role is authenticated, When the user creates a referral code/link and selects the partner and optional associations (location, staff, service bundle), Then the system validates required fields and creates a unique, case-insensitive code and short URL, returning identifiers. Given a duplicate code slug is submitted, When saving the referral code, Then the system rejects the save with a clear duplication error and suggested alternatives. Given an existing referral code/link, When name, associations, or status are edited, Then all changes are audit-logged (who, when, before/after) and become effective for new attributions within 60 seconds. Given a partner is inactive or the code status is Disabled/Archived, When the link is used, Then attribution is blocked, the user is redirected to the default booking page with an "inactive or expired" message, and an event is logged.
Generate Human-Readable Codes, Short URLs, and QR Codes
Given no custom code is entered, When creating a referral, Then the system auto-generates a human-readable code 6–12 characters long (dictionary word + digits or hyphen), excluding ambiguous characters (O/0, I/1, l). Given a code is created, When viewing the referral, Then a short URL on the configured domain is provided and a QR code is available for download in PNG and SVG; each file is <= 200 KB and encodes the short URL exactly. Given the short URL or QR is accessed, When resolving the link, Then it 301-redirects to the deep link with tracking parameters intact and achieves p95 redirect latency <= 400 ms. Given an invalid or tampered slug is requested, When resolving, Then a branded 404 fallback page is served and the attempt is logged.
Configure Expiration, Usage Limits, and Campaign Metadata
Given a referral is being configured, When saving, Then fields exist and validate for start/end datetime (with timezone), total click limit, total conversion limit, per-customer conversion limit, and UTM_source/medium/campaign/content/term; end must be after start and limits must be non-negative integers. Given limits or end datetime are set, When a click occurs after expiry or exceeding the click limit, Then no attribution is created, the user is redirected to a fallback booking page, and reason "expired" or "limit_exceeded" is recorded in logs. Given a per-customer conversion limit is set, When the same customer attempts an additional conversion beyond the limit, Then the click is recorded but the conversion is not attributed and reason "per_customer_limit" is stored. Given no limits are set, When traffic is received, Then clicks and conversions are unlimited until the referral is manually disabled.
Deep-Link Into Booking Flow and SMS Thread
Given a user taps the short URL on a mobile device, When the booking experience opens, Then partner attribution is pre-populated and associated location/staff/service bundle are preselected or suggested, with UTM metadata carried through the session. Given the device supports SMS deep linking, When the user chooses Text to book, Then an SMS thread opens to the business number with a prefilled message containing the partner code/link and configurable template placeholders; if unsupported, a copy-to-clipboard fallback is presented. Given a native app is installed and app links are configured, When the link is opened, Then the app opens via app link; otherwise mobile web opens; in both cases the referral token is preserved across the transition.
Persist Attribution Across SMS and Reschedules
Given a customer initiates an SMS thread or starts a booking from a partner link, When the booking is later rescheduled or confirmed via SMS without clicking the link again, Then the original referral token persists and is applied to the booking. Given multiple partner clicks occur before booking, When attribution is computed, Then first-touch is the earliest valid click within 30 days and last-touch is the most recent valid click within 7 days, and both are stored at booking time. Given a booking is cancelled and rebooked within 14 days in the same SMS thread, When the new booking is confirmed, Then first-touch persists and last-touch updates to the most recent valid click prior to the new booking.
Analytics and Audit Logging for Routing Engine
Given referral lifecycle and customer actions occur (created, edited, click, booking_started, booking_completed, rescheduled, cancelled), When events are emitted, Then each event includes timestamp (UTC), actor, partner ID, referral ID, associations, UTM, customer ID (if known), device type, and request IP; events are immutable and retrievable via API and admin filters. Given a booking completes with attribution, When routing is calculated, Then the routing engine receives an event payload containing partner ID, attributed model (first/last), service lines, and revenue amount within 60 seconds; delivery is at-least-once with idempotency keys to prevent double processing. Given analytics are viewed, When loading the dashboard, Then metrics display clicks, unique clickers, conversion rate, attributed revenue, first vs last-touch breakdown, and top sources, reconciling to raw event counts within 1%.
Staff Tools for Inserting Links and Printing QR Flyers
Given a staff user is composing an outbound SMS in the dashboard, When inserting a partner link via the picker, Then the selected partner and associations are applied, a short URL is inserted into the message, and a preview with copy-to-clipboard is shown. Given a staff user generates a printable flyer, When downloading, Then a branded PDF (A4 and Letter) includes partner name/logo, the human-readable code, and a QR code that scans successfully from print at 300 dpi at 12 inches and resolves to the correct deep link. Given role-based access controls are enforced, When creating/inserting links or downloading flyers, Then only authorized users can perform the action and each action is audit-logged with who, when, and context.
Rule-Based Routing Engine
"As an operations manager, I want configurable routing rules by source and service so that the correct partner and rate are applied consistently across bookings."
Description

Implement a condition-action rules engine that determines which partner (if any) receives a share and at what rate. Conditions include referral code/booking link used, service type/ID, location, provider, client segment (new vs. returning), date/time windows, package redemption, and no-show events. Actions include assigning a partner, selecting the revenue share method (fixed bounty per service or percentage of revenue), and setting effective date ranges. Support rule priority, exclusivity vs. stackable co-marketing, and conflict resolution. Provide a simulator to test rules against historical or hypothetical bookings before publishing. Changes are versioned with audit logs. Expose rules via API and attach evaluations to appointments for transparency.

Acceptance Criteria
Condition Matching: Service, Location, Provider, Client Segment, Package Redemption, and No-Show
Given a rule scoped to service_type_ids [SVC-101, SVC-202], When an appointment includes service SVC-101, Then the rule is eligible for evaluation for that appointment Given a rule scoped to location_id LOC-9, When an appointment is scheduled at LOC-9, Then the rule is eligible; When scheduled at any other location, Then the rule is not eligible Given a rule scoped to provider_id PROV-3, When an appointment is performed by PROV-3, Then the rule is eligible; When performed by any other provider, Then the rule is not eligible Given a rule with client_segment = "new", When the client has no prior completed appointments with the business, Then the rule matches; When the client has ≥1 prior completed appointment, Then the rule does not match Given a rule with client_segment = "returning", When the client has ≥1 prior completed appointment with the business, Then the rule matches; When the client has none, Then the rule does not match Given a rule with condition package_redemption = true, When the appointment is fully or partially paid using a package credit, Then the rule matches; When no package credit is redeemed, Then the rule does not match Given a rule with trigger = "no_show", When an appointment status transitions to NO_SHOW, Then the rule evaluates and applies actions; When status is any other value, Then the rule does not apply Then the evaluation record includes matched_conditions listing each satisfied condition and the data values used
Referral Code and Booking Link Routing
Given a referral code RC-ALPHA mapped to Partner PART-1, When a booking is created using RC-ALPHA, Then the engine assigns PART-1 as the partner on evaluation and records rule_id, partner_id, and evaluation_timestamp on the appointment Given a partner booking link BL-OMEGA mapped to Partner PART-2, When a booking is created via BL-OMEGA, Then the engine assigns PART-2 accordingly and records the source = "booking_link" Given no referral code and no partner booking link, When the appointment is evaluated, Then no partner is assigned and evaluation.matched_rule_id = null Given both referral code and booking link are present and mapped to different partners, When evaluated, Then the partner assignment follows rule priority and the evaluation logs both candidates and the reason the winner was selected
Revenue Share Method Selection and Calculation
Given a rule with method = "fixed_bounty_per_service" and amount = 10.00 USD, When an appointment includes 3 eligible service units, Then partner_share_amount = 30.00 USD and currency = USD Given a rule with method = "percentage_of_revenue" and rate = 15%, When the eligible service revenue for the appointment is 100.00, Then partner_share_amount = 15.00 after rounding to 2 decimals (half-up) Given a percentage rule, When the appointment includes taxes and tips, Then the percentage is applied only to service revenue (excluding taxes and tips) Given multiple eligible services under a percentage rule, When the total eligible service revenue is the sum of those services, Then the share is computed on the summed amount Then each evaluation stores: method, rate_or_amount, eligible_revenue_basis, computed_share_amount, currency, and calculation_details for auditability
Rule Priority, Exclusivity, Stackability, and Conflict Resolution
Given two eligible rules R1 (priority 90) and R2 (priority 80), When evaluated, Then only R1 applies if R1 is exclusive; If R1 is stackable, Then R1 and any other stackable rules apply together Given two eligible exclusive rules with equal priority, When evaluated, Then the rule with greater specificity (more matched conditions) applies; If still tied, Then the rule with the lower rule_id applies; The decision is recorded in evaluation.conflict_resolution Given a stackable co-marketing scenario with two rules targeting different partners, When both are eligible and stackable = true, Then both shares are calculated independently and attached to the appointment evaluation Given rules resulting in duplicate partner assignments (same partner from multiple eligible rules), When stackable, Then the shares for the same partner are summed into a single remittance line item and the contributing rule_ids are listed Then evaluation includes: applied_rule_ids (ordered by application), skipped_rule_ids with reasons, exclusivity/stackability flags, and total_partner_share_amount per partner
Effective Date Ranges and Date/Time Windows
Given a rule with effective_date_start = 2025-09-01T00:00 and effective_date_end = 2025-10-01T00:00, When an appointment start_time is 2025-09-01T00:00 in the location's timezone, Then the rule applies; When start_time is 2025-10-01T00:00, Then the rule does not apply (end exclusive) Given a rule with time_window = weekdays 09:00–17:00 in location timezone, When an appointment start_time is Tuesday 16:59 local, Then the rule applies; When 17:00 or later, Then it does not apply Given a multi-location business, When evaluating time windows, Then the location's timezone is used; If location is unknown, Then the business default timezone is used and logged Then evaluation stores the timezone used and whether the appointment met the date range and time window constraints
Rule Simulator for Pre-Publish Testing
Given a draft ruleset, When the user runs the simulator against a selected set of historical appointments, Then the simulator returns, for each appointment, the partner(s) assigned and share amounts identical to what the live engine would produce for the same inputs Given a set of hypothetical appointment payloads provided via JSON, When simulated, Then results include matched/failed conditions, applied_rule_ids, calculation_details, and do not mutate any production data Given simulated results differ from current live results for the same historical appointment, When displayed, Then the simulator highlights the delta (added/removed partners, amount changes) per appointment Given a simulation run, When completed, Then a non-publishable report artifact is generated with timestamp, ruleset version (draft), and summary metrics (e.g., number of affected appointments, total projected partner shares) Then the simulator requires explicit publish action to move draft rules to live and prevents publish if any rule has validation errors
Transparency: Versioning, Audit Logs, API Exposure, and Appointment Evaluations
Given a rules change (create/update/disable) by user U, When saved, Then an audit log entry is recorded with actor, timestamp, action, rule_id, and before/after diffs of conditions and actions Given a publish action, When executed, Then the ruleset version increments (e.g., v17→v18) and subsequent evaluations record rule_version = v18; Prior evaluations remain immutable with their original versions Given an appointment creation or status change, When evaluated, Then the appointment stores an evaluation snapshot including: applied_rule_ids, partner_ids, method, rate_or_amount, eligible_revenue_basis, computed_share_amounts, currency, evaluation_timestamp, and rule_version Given an authenticated API client, When calling the Rules API, Then they can list/read current and draft rules and retrieve evaluation snapshots for specific appointments; Responses include pagination and ETag/last-modified headers Then all APIs enforce authentication and authorization, and return HTTP 4xx errors with validation messages for invalid rule definitions or unauthorized access
Revenue Share Calculation & Checkout Application
"As a groomer at checkout, I want the system to compute the partner share automatically and show the correct net due so that I can finish payment quickly and accurately without manual math."
Description

Calculate partner shares in real time at quote and checkout, and post them to the ledger. Support fixed per-service bounties and percentage-of-revenue shares, with configuration for pre-/post-discount and pre-/post-tax basis. Handle multi-pet, multi-service bookings, packages (purchase and redemption), surcharges, and no-show fees per the rules engine. Implement deterministic rounding, minimum/maximum caps, and proration on partial refunds or cancellations. Display a staff-facing breakdown showing client total, partner share, and provider net; ensure the partner share is not shown on client receipts unless enabled. Recompute on edits, refunds, and reschedules, writing adjustments with traceable links to the originating booking, rule version, and referral source. Support multiple currencies and time zones as per partner settings.

Acceptance Criteria
Real-Time Quote and Checkout Breakdown with Client Receipt Visibility
Given a booking for service "Full Groom" priced 50.00 USD with no discounts and no tax, and a rule "10% of revenue pre-discount, pre-tax" tied to referral code RESCUE10 for partner "Happy Rescue" When a quote is generated Then the partner share is calculated synchronously in the quote response as 5.00 USD and the staff breakdown displays Client Total: 50.00 USD, Partner Share: 5.00 USD, Provider Net: 45.00 USD Given the same booking proceeds to checkout and the payment is captured When the transaction is completed Then a ledger entry is posted for partner "Happy Rescue" for 5.00 USD with links to Booking ID, Referral Source = RESCUE10, and Rule Version ID, and the staff breakdown at checkout matches the quote amounts And the client receipt does not display the partner share by default When the setting "Show partner share on client receipts" is enabled prior to sending the receipt Then the client receipt displays a line "Community Partner Share" for 5.00 USD
Mixed Fixed Bounty and Percentage on Multi-Pet, Multi-Service Booking with Surcharge
Given a completed booking with line items: Walk 30m for Pet A = 20.00 USD, Walk 30m for Pet B = 20.00 USD, Nail Trim for Pet A = 10.00 USD, and a Mobile Trip Fee surcharge = 5.00 USD, with no discounts and no tax And Rule A: Fixed bounty 2.00 USD per "Walk 30m" service line And Rule B: 5% of revenue post-discount, pre-tax including surcharges When the partner share is calculated at checkout Then Rule A contributes 4.00 USD (2 walk lines x 2.00) And Rule B contributes 2.75 USD (5% of 55.00) And the total partner share is 6.75 USD and Provider Net is 48.25 USD and Client Total is 55.00 USD And the ledger records separate components per rule with their Rule IDs and amounts summing to 6.75 USD
Basis Configuration: Pre-/Post-Discount and Pre-/Post-Tax Calculation
Given a booking for service "Training Session" list price 100.00 USD, discount 10% applied, and tax rate 8% applied on post-discount price When a rule "10% pre-discount, pre-tax" is applied Then the partner share is 10.00 USD (10% of 100.00) When a rule "10% post-discount, pre-tax" is applied Then the partner share is 9.00 USD (10% of 90.00) When a rule "10% post-discount, post-tax" is applied Then the partner share is 9.72 USD (10% of 97.20 where 97.20 = 90.00 + 7.20 tax) And the staff breakdown and ledger entries reflect the selected basis with the exact computed amounts and basis type stored alongside the Rule Version ID
Package Purchase and Redemption Handling
Given a package "10 Walks" purchased for 200.00 USD with no tax and no discount And Rule P1: 10% of revenue on package purchases And Rule P2: Fixed bounty 0.50 USD per redeemed "Walk 30m" service When the package purchase is paid Then the partner share posted to the ledger is 20.00 USD linked to the package purchase transaction and Rule P1 Version ID Given a later booking that redeems 1 "Walk 30m" from the package at price 0.00 USD When the booking is completed Then the partner share posted is 0.50 USD linked to the redemption booking and Rule P2 Version ID And the staff breakdown for the redemption shows Client Total: 0.00 USD, Partner Share: 0.50 USD, Provider Net: -0.50 USD
Deterministic Rounding with Min/Max Caps
Given a rule "12.5% post-discount, pre-tax" and currency USD with 2 decimal places and rounding mode = Half Up And a booking with shareable base = 19.99 USD When the partner share is computed Then the raw share 2.49875 rounds to 2.50 USD Given a rule-level minimum cap of 0.50 USD per booking And a computed share of 0.49 USD When the cap is applied Then the final partner share is 0.50 USD Given a rule-level maximum cap of 10.00 USD per booking And a computed share of 25.00 USD When the cap is applied Then the final partner share is 10.00 USD Given currency JPY with 0 decimal places and the same 12.5% rule And a shareable base of 1001 JPY When the partner share is computed Then the raw share 125.125 rounds deterministically to 125 JPY using the configured rounding mode
Adjustments on Refunds, Cancellations, and Reschedules with Traceability
Given a completed booking with price 100.00 USD and a rule "10% pre-discount, pre-tax" When the share is posted at checkout Then the ledger records a partner share of 10.00 USD with links to Booking ID, Referral Source (if any), and Rule Version ID When a partial refund of 30.00 USD is issued against the service price Then an adjustment entry of -3.00 USD (pro-rated 30% of the original share) is posted, linked to the original share entry ID, and the booking balance reflects the net partner share of 7.00 USD When the booking is canceled before service and the payment is fully voided Then any previously posted partner share is reversed so the cumulative partner share for the booking equals 0.00 USD with a reversal entry linked to the original share entry ID When the booking is rescheduled and the service price changes from 100.00 USD to 120.00 USD using the same rule version Then a delta adjustment of +2.00 USD is posted (10% of the 20.00 USD increase) with a link to the reschedule event and the Rule Version ID used for recompute
Multi-Currency and Time Zone Posting
Given a partner configured for currency EUR and time zone Europe/Berlin And a booking priced 80.00 EUR completed at 23:30 on 2025-08-10 in Europe/Berlin When the partner share rule "10% post-discount, pre-tax" is applied and the ledger entry is posted Then the ledger amount is recorded as 8.00 EUR with currency code EUR and symbol € And the ledger timestamp is 2025-08-10 in Europe/Berlin and the entry appears in the daily remittance summary for 2025-08-10 (Berlin local day) And amounts display with 2 decimal places per EUR minor units
Daily Branded Remittance Summaries
"As a rescue partner, I want a daily branded summary of our earnings with detailed line items so that I can reconcile our share without requesting custom reports."
Description

Generate and deliver daily, partner-branded remittance summaries with line-item detail. Each summary includes date range (by partner time zone), gross attributable revenue, share amounts by service/location, adjustments/refunds, opening/closing balances, and payout status. Email delivery uses partner branding; optionally send SMS with a secure portal link. Provide CSV and PDF attachments and a self-serve portal to drill into line items. Allow scheduling (send time and days), resend, and corrections on recalculation events. Ensure secure access with expiring tokens and track delivery/open metrics. Summaries are sourced from the calculation ledger to guarantee alignment with payouts and internal reporting.

Acceptance Criteria
Timezone-Aware Summary Content Generation
Given a partner with time zone set to America/Denver and ledger entries on 2025-08-10 local When the daily summary job runs at 19:00 America/Denver on 2025-08-11 Then the summary covers 2025-08-10 00:00–23:59 America/Denver and excludes entries outside that local window And the summary includes fields: date range, gross attributable revenue, partner share by service and location, adjustments/refunds, opening balance, closing balance, payout status And all currency amounts are rounded to 2 decimal places and displayed in the partner’s currency And line-item detail lists booking_id, service, location, local_timestamp, gross, share_basis, share_rate, share_type, share_amount, adjustment_amount, adjustment_reason
Branded Email Delivery with PDF/CSV Attachments
Given partner branding (logo, brand colors, display name, email sender) is configured When the system sends the daily remittance summary Then the email subject is "[Partner Display Name] Remittance Summary — YYYY-MM-DD" using the partner’s local date And the email body uses the partner’s logo and brand colors and the From address matches the configured sender And a PDF and CSV are attached; both totals equal the email body totals And the CSV includes columns: booking_id, service, location, local_timestamp, gross, share_basis, share_rate, share_type, share_amount, adjustment_amount, adjustment_reason, payout_status And attachment filenames follow "remittance_[partner_slug]_[YYYYMMDD].pdf" and ".csv" And the system records sent, delivery, and open events for this email
SMS Delivery with Secure Portal Link
Given SMS delivery is enabled for the partner and a valid recipient phone number exists When the daily remittance summary is generated Then an SMS is sent containing the partner display name, local date, and a unique HTTPS portal link And the portal link is secured by a signed, expiring token with a configurable TTL (default 48 hours) And accessing the portal after token expiry returns HTTP 401 with guidance to request a new link And if SMS sending fails, the failure is logged and email delivery still proceeds
Portal Drill-Down to Line Items
Given a recipient opens the portal via a valid token When the summary page loads Then the page displays the same totals as the email and attachments And the user can view line-item details and expand any item to see gross, share basis, rate/type, share amount, and adjustments And the user can filter line items by service, location, and booking date and sort by timestamp or amount And the user can export the currently filtered view to CSV And access is restricted to the specific partner and date range bound to the token
Configurable Send Schedule and Timezone
Given a partner schedule is set to send at 19:00 local time on Monday–Friday When the current day is within the schedule Then the system queues and sends the summary at 19:00 in the partner’s time zone And when the current day is excluded, no summary is sent And on DST transition dates, the send occurs at 19:00 local clock time using the correct UTC offset And changes to the schedule take effect for the next unsent cycle
Resend and Auto-Correction on Recalculation
Given a daily summary for 2025-08-10 was sent and a recalculation event modifies ledger totals for that period When recalculation completes Then the system generates a corrected version labeled "v2 — Correction" and sends updated email/SMS with a correction note And the previous version is marked "Superseded" and retained for audit with a link to the latest version And opening/closing balances for subsequent days are recomputed and the next summary reflects the new opening balance And authorized users can manually resend the latest version on demand from the admin UI
Ledger Alignment and Balance Accuracy
Given the calculation ledger snapshot at send time for a partner and period When the summary is generated Then gross revenue, partner share by service/location, adjustments/refunds, opening/closing balances, and payout status equal the ledger values exactly And the sum of line-item share amounts equals the partner share total in the header And closing balance = opening balance + partner share total + adjustments/refunds − payouts for the period
Automated Payouts & Reconciliation
"As a finance admin, I want partner balances to accrue and pay out automatically with proper tax documentation so that month-end reconciliation is hands-off and compliant."
Description

Maintain a partner sub-ledger that accrues balances and supports automated payouts. Configure payout frequency (daily/weekly/monthly), thresholds, and minimum balances; support manual holds and adjustments. Integrate with payment rails (e.g., ACH via Stripe/PayPal) including KYC/verification, W-9 collection, and year-end 1099 reporting. Handle payout failures with retries and notifications, and support negative balance carryforward on refunds. Provide a reconciliation dashboard mapping remittance summaries to ledger entries and payouts, with exports to accounting systems (e.g., QuickBooks/Xero) and complete audit logs. Role-based permissions restrict who can trigger manual payouts or backdate adjustments.

Acceptance Criteria
Scheduled Payouts with Thresholds and Minimum Balances
Given a partner configured with payout frequency Weekly (Monday 09:00 local), payout threshold $100.00, minimum balance $25.00, and an available ledger balance of $340.00 at run time When the payout scheduler runs at 09:00 on Monday Then a payout of $315.00 is created and queued to the payment rail And the partner’s available ledger balance decreases by $315.00 and $25.00 remains as reserved minimum And if the available balance at run time is less than $100.00, no payout is created and the next run is scheduled without changes And the payout record stores partner ID, run timestamp (UTC), local timezone, frequency, currency, amount, unique payout ID, and configuration snapshot
Partner Onboarding: KYC, W‑9, and Eligibility Gate
Given a partner without completed KYC verification or W‑9 tax form When the system evaluates payout eligibility Then payouts are blocked, the ledger continues accruing balances, and the partner sees status Onboarding Required with actionable links When the partner completes KYC via the connected payment provider and submits a valid W‑9 Then the system records provider verification status, KYC reference ID, TIN (masked), and W‑9 version with timestamp And the partner status updates to Eligible and the next scheduled payout includes any balance meeting threshold and minimum rules And at year end, 1099‑NEC forms are generated for eligible U.S. partners meeting the reporting threshold and are available for e‑delivery and export, with audit entries recorded
Automated Payout Execution and Failure Handling
Given an eligible payout of $250.00 via ACH (Stripe) with a configured retry policy of 3 attempts at 1h, 12h, and 24h intervals When the first attempt fails with failure code insufficient_funds Then the payout status updates to Retry Scheduled, the failure reason and provider code are captured, and the next attempt is scheduled at +1h And notifications are sent to the partner and admins with masked bank details, the failure reason, and a link to update payout information When a subsequent retry succeeds Then the payout status updates to Succeeded, the ledger reflects a finalized payout entry with provider transaction ID, and notifications confirm success If all retries fail Then the payout status updates to Failed, funds are returned to the partner’s available balance, and payouts remain blocked until payout details are updated or a manual override is applied by an authorized role
Negative Balance Carryforward on Refunds
Given a $50.00 refund is issued for a transaction previously included in a payout When the refund is posted to the partner sub‑ledger Then a -$50.00 ledger adjustment is created referencing the original transaction and payout IDs And the partner’s available balance decreases by $50.00 and may become negative And no payout is scheduled until subsequent accruals restore the balance above $0.00 and satisfy threshold and minimum balance rules And the next remittance summary shows a line item Carryforward with -$50.00 linked to the refund
Manual Holds, Adjustments, and Role-Based Permissions
Given roles are configured as Owner, Accountant, and Staff When a Staff user attempts to trigger a manual payout or create a backdated adjustment Then access is denied with a 403 error and an audit log entry is recorded with user, action, timestamp, and IP When an Owner places a manual hold on a partner with a required reason and optional expiry date Then the partner status becomes On Hold, scheduled payouts are skipped while the hold is active, and the hold appears on the dashboard with actor, reason, created at, and expiry When an Accountant creates a dated adjustment within the last 30 days with amount, category, reason, and evidence attachment Then an immutable ledger entry is added with effective date, actor, and unique ID; edits are restricted to Owner only and are recorded as reversals, not in-place changes And backdated adjustments more than 30 days old can only be created by an Owner
Reconciliation Dashboard and Remittance Mapping
Given remittance summaries are generated daily per partner When a user opens the Reconciliation dashboard and filters by date range, partner, and referral code Then for each remittance, the UI displays the mapped ledger entries and any associated payouts, and the remittance total minus mapped ledger amounts and payouts equals $0.00 with a tolerance of ±$0.01 for rounding And any discrepancies outside tolerance are flagged with a status Requires Attention and can be drilled into to view transaction-level details And users can export the current view to CSV with columns: partner, date, remittance ID, ledger entry IDs, payout IDs, amounts, currency, and discrepancy status And an Audit Log tab shows immutable entries for creates, edits, exports, holds, adjustments, and payouts with actor, timestamp (UTC), and before/after snapshots
Accounting Export to QuickBooks/Xero with Idempotency
Given QuickBooks Online or Xero is connected and account mappings are configured for Partner Payable, Revenue Share Expense, and Cash/Bank When a user exports partner payouts and adjustments for a selected period Then the system creates journal entries in the target system reflecting gross share, adjustments, refunds carryforward, and payout clearing, using the configured accounts and currencies And each export is idempotent using a stable idempotency key per period and partner; re-exporting the same period does not create duplicates and updates are applied as adjustments And on success, the export record stores external document IDs and links; on failure, the error is displayed with remediation guidance and the export can be retried And the sum of exported amounts matches the ledger movement for the period within ±$0.01, or the export is blocked with a reconciliation error

Credit‑Aware Splits

Make commissions and shares accurate when packages or credits are used. Recognize revenue on redemption, split add‑ons separately, and handle partial redemptions gracefully. Staff are rewarded for the service performed today while the business keeps package accounting clean and compliant.

Requirements

Redemption-Time Revenue Recognition
"As an owner, I want package revenue recognized at redemption so that my financials reflect earned revenue and liabilities accurately."
Description

Defer package sale proceeds into a liability account and recognize revenue only when credits are redeemed against services. On each redemption, convert the appropriate portion of liability to earned revenue, with support for prorating partial redemptions, proper tax treatment, and rounding rules. Maintain real-time package liability balances and aging, generate line-level ledger entries for sale, redemption, and expiration, and surface balances in the checkout and reporting flows to keep accounting clean and compliant.

Acceptance Criteria
Defer Package Sale to Liability
Given a package priced at $300 including 10 service credits and a tax policy of Tax at service When the package sale is completed and payment is captured Then the system posts a journal entry debiting Cash/AR $300 and crediting Package Liability $300 with $0 to Service Revenue And no sales tax is calculated or posted at sale time And the customer's package liability balance increases by $300 within 3 seconds And the ledger entry includes line-level metadata: package_id, customer_id, location_id, sale_id, date_time, currency, amount, account_codes
Recognize Revenue on Redemption (Full and Partial Coverage)
Given a $45 service is checked out and the customer has an available package liability balance with a per-unit value of $30 When 1 package credit ($30) is applied toward the service at checkout Then the system posts DR Package Liability $30 and CR Service Revenue $30 (net of tax per policy) And the remainder ($15) is posted using the selected tender as DR Cash/AR $15 and CR Service Revenue $15 (net of tax per policy) And the package liability balance decreases by $30 and updates within 3 seconds And journal entries are linked to the redemption_id with immutable audit records
Prorated Partial Redemption of a Unit
Given a package unit has a per-unit value of $30 and the service consumes 0.5 of a unit due to duration-based proration When the service is checked out with 0.5 unit applied Then the system reduces Package Liability by $15 and recognizes $15 to Service Revenue (net of tax) And the remaining 0.5 unit retains a $15 liability value on the package balance And all computed amounts round to 2 decimals using half-up rounding at the line level
Tax Treatment at Redemption vs Sale Policy
Given a jurisdiction policy of Tax at service with an 8% tax rate and a $45 service redeemed with a $30 package credit When the checkout is completed Then the system posts DR Package Liability $30; DR Cash/AR $15; CR Service Revenue $41.40; CR Sales Tax Payable $3.60 And no tax is posted at package sale time Given a jurisdiction policy of Tax at sale and the same redemption When the checkout is completed Then no additional tax lines are posted at redemption and only liability-to-revenue and tender-to-revenue entries are created using the net-of-tax per-unit valuation derived at sale
Rounding and Final Penny Adjustment on Last Redemption
Given a package shows a remaining liability of $0.01–$0.02 due to prior rounding When the final redemption on that package is processed Then the system posts a one-time rounding adjustment (<= $0.02) to the Rounding Adjustments account to zero the package liability And the final package liability becomes exactly $0.00 And an audit note stores package_id, redemption_id, user_id, timestamp, and adjustment amount
Ledger Entries for Sale, Redemption, and Expiration
Given configured GL accounts for Package Liability, Service Revenue, Breakage Revenue, Sales Tax Payable, and Rounding Adjustments When a package is sold, a service is redeemed using that package, and later the package expires with an unused balance Then the system creates line-level journal entries for each event with fields: entry_id, date_time, customer_id, package_id, sale_id/redemption_id/expiration_id, location_id, staff_id (if applicable), item/service_code, quantity/units, amounts (gross, net, tax), currency, account_code, memo And the expiration on the policy-defined date moves any remaining liability to Breakage Revenue with DR Package Liability and CR Breakage Revenue And all entries are immutable and corrections require a reversing entry
Real-Time Balances, Aging, and UI Surfacing
Given the checkout screen and revenue reports are open When a package is sold, redeemed, or expires Then the package liability balance and aging buckets (0–30, 31–60, 61–90, 91+ days) update within 3 seconds on checkout and within 60 seconds on reports And checkout displays remaining credits and monetary value per package/pet before payment is finalized And the end-of-day sum of all open package liability balances equals the GL Package Liability account balance with a variance tolerance of $0.00
Configurable Split Engine for Packages vs Add-ons
"As a manager, I want add-ons split separately from package redemptions so that staff are paid fairly and margins are preserved."
Description

Calculate staff commissions and business shares separately for redeemed package value and ad-hoc add-ons within the same ticket. Support rule-based configuration by service category, location, and staff role; handle multi-staff services, discounts, and taxes; and ensure the performer of today’s service is credited for redeemed value while maintaining business margin on add-ons. Execute splits in real time at checkout and store line-level results for payout and reporting.

Acceptance Criteria
Real-time Split of Package vs Add-on at Checkout
Given a ticket with a service covered by a package and one ad-hoc add-on And performer Staff A is assigned to the service and seller Staff A to the add-on And commission rules exist for the service category and the add-on category When the checkout summary is opened Then the split engine returns results in under 300 ms And creates separate split records per line with source_type "package_redemption" and "add_on" And credits Staff A for the redeemed package value on the performed service per configured rate And applies the configured business/staff split for the add-on independently of the package redemption And commission bases exclude taxes and tips And currency rounding is round half up to 2 decimals; any rounding remainder is allocated to the business share And for each line, staff commissions plus business share equals the line pre-tax subtotal minus allocated discounts
Rule-Based Configuration by Category, Location, and Role with Deterministic Precedence
Given rules defined using any combination of service category, location, and staff role When multiple rules match a line Then the engine selects the rule matching the greatest number of dimensions (3 > 2 > 1 > 0) And configuration prevents saving two or more rules with the same matched dimensions that would overlap for the same scope And if legacy overlapping rules exist, checkout for affected lines is blocked with an actionable error code SPLIT_RULE_CONFLICT and message referencing the conflicting rule IDs And when no rule matches, the global default rule is applied and a WARN is logged with rule_resolution="defaulted"
Multi-Staff Service Allocation and Proration
Given a service performed by Staff A and Staff B with allocation 70% and 30% And the service line is covered fully or partially by a package When splits are calculated at checkout Then both the redeemed package value and any attributable add-on amounts are prorated 70/30 between A and B And if no allocation is provided, the engine defaults to an equal split among performers And allocation validation requires the shares sum to 100% ±0.01; otherwise checkout is blocked with error code SPLIT_ALLOCATION_INVALID And the sum of per-staffer commissions and business shares equals the proration base for the line after discounts and before tax
Partial Package Redemption on a Single Service Line
Given a $50 service with $30 remaining package value When the customer checks out Then $30 is recognized as package_redemption credited to today’s performer per the configured package rule And the $20 remainder is treated as a non-package amount and split using the applicable add-on/service cash rule And discount and tax calculations treat the redemption and remainder as distinct components And the persisted split records show two components with correct amounts, sources, staff allocations, and rounding consistent with currency rules
Discounts and Taxes Treatment Across Mixed Lines
Given a ticket with a ticket-level 10% discount and a taxable add-on and a package-covered service When the split engine runs Then the discount is prorated only across non-package amounts based on each eligible line’s pre-discount subtotal share And package redemption amounts are not reduced by discounts And the commission base per line equals line subtotal minus allocated discount, excluding tax and tips And taxes are calculated after discounts and excluded from staff commission calculations And resulting commissions and business shares reconcile to the discounted pre-tax totals of eligible lines
Persistence of Line-Level Split Results for Payout and Reporting
Given checkout is confirmed When the split engine writes results Then a record is stored per staff per line with fields: ticket_id, line_id, staff_id, staff_role, location_id, source_type, commission_base, commission_rate, commission_amount, business_share, currency, rounding_delta, rule_id, allocation_share, created_at And records are immutable post-checkout; adjustments create separate delta records with parent_split_id and audit metadata (actor_id, reason) And payout and reporting services can query by date range, staff_id, location_id and reproduce the totals used at checkout And record persistence is atomic with the payment transaction; on failure the checkout is rolled back
Validation and Fallback Behavior at Checkout
Given a ticket is being checked out When required configuration is missing for a line Then the engine applies the global default rule and logs WARN SPLIT_RULE_MISSING with resolution_details And when performer allocations are invalid or a rule conflict is detected, the engine blocks checkout and returns a specific error code and message (SPLIT_ALLOCATION_INVALID or SPLIT_RULE_CONFLICT) And when a non-recoverable calculation error occurs, no partial split records are persisted and the transaction is not captured And all validation and error outcomes are exposed to the UI/API with machine-readable codes and human-readable messages
Partial Redemption & Mixed Tender Handling
"As a front-desk user, I want to apply partial credits and collect the remainder so that checkout is accurate and frictionless."
Description

Allow a portion of a service price to be covered by package credits with the remainder collected via card, cash, or other tenders. Automatically prorate commissions, taxes, and fees between the redeemed portion and the cash portion, decrement package balances accurately, and reflect results on invoices, receipts, and staff payout summaries. Handle edge cases such as insufficient credits, multi-line services, and tips.

Acceptance Criteria
Partial Redemption on Single Service with Tax and Commission Proration
Given a single service with a defined pre-tax price, applicable tax rate, and staff commission rules And a customer package with a monetary balance sufficient to cover only part of the service When the user applies a specific monetary amount from the package and selects a cash/card tender for the remainder Then the system proportionally allocates the pre-tax service amount between redeemed and cash portions based on the monetary split And proportionally allocates calculated tax between redeemed and cash portions so their sum equals the total tax And proportionally allocates staff commission between redeemed and cash portions using the same ratio And prevents the package balance from going below zero and applies only the available amount And posts totals such that redeemed amount + cash amount = service total (pre-tax + tax), with no discrepancy
Insufficient Credits Auto-Apply and Collect Remainder
Given a package with a remaining balance less than the service total due When the user attempts to apply the package at checkout Then the system automatically applies the maximum available package amount without exceeding the balance And calculates and displays the remaining amount due to be collected via selected tender(s) And blocks completion if any remainder is unpaid And surfaces a clear, inline message indicating how much was applied from the package and how much is still due
Multi-line Invoice with Add-ons and Eligibility Rules
Given an invoice with multiple line items including a base service and one or more add-ons And a package that covers only specific eligible services When the user applies package credits Then credits are applied only to eligible line items and not to ineligible add-ons And each eligible line item shows a line-level split between redeemed and cash portions And if available credits are insufficient to cover all eligible lines, the system allocates by the configured priority (default: highest-priced eligible line first) and allows the user to adjust the allocation before completing checkout And commissions for add-ons are calculated and reported separately from the base service And totals across all lines reconcile to the invoice grand total
Tips Excluded from Package Credits
Given a checkout that includes an optional tip entry When a tip is added on an invoice that also uses partial package redemption Then the tip amount is charged entirely to cash/card tender and never reduces the package balance And the tip is excluded from package redemption proration and from the commission base unless the commission rule explicitly includes tips And the tip is reported in staff payout summaries under the tips bucket
Deterministic Rounding and Penny Reconciliation
Given any allocation that requires splitting amounts between redeemed and cash portions When the system prorates pre-tax amounts, tax, fees, and commission Then each component is rounded to the nearest cent at the component level using a consistent rounding rule And any residual rounding pennies are assigned to the cash portion to avoid overdrawing package balance And the sum of rounded components equals the original unrounded totals for pre-tax, tax, fees, commission, and grand total
Invoice, Receipt, and Client Communication Show Mixed Tender Clearly
Given an invoice that used partial package redemption and cash/card for the remainder When the invoice is finalized and a receipt is generated or sent Then the document shows for each line: unit price, redeemed amount, cash amount, and tax allocation per portion And shows payment breakdown by tender including the package name/identifier and remaining package balance after redemption And totals reconcile so that line-level sums match header totals and payment totals And the staff-facing dashboard mirrors these amounts for auditability
Staff Payout Summary Reflects Split Earnings on Redemption
Given commission rules per role and service type And a service completed with partial package redemption and a cash/card remainder When staff payouts are viewed for the service date or pay period Then the payout summary attributes commission for the redeemed portion and cash portion to the performing staff for the date of service And add-on commissions appear as separate entries if configured And the sum of commission entries equals the prorated commission calculated at checkout And package sale events prior to service do not generate commission in the payout summary
Cross-Staff Attribution & Seller Credit Policy
"As an owner, I want flexible attribution between seller and performer so that incentives align without overpaying."
Description

Provide configurable policies to attribute compensation between the staff member who sold the package and the staff member performing the redemption. Options include seller bonus at sale, at redemption, percentage allocations, or no seller credit. Prevent double-payment, support expiration windows for seller credit, and surface attribution on tickets and payout statements for transparency.

Acceptance Criteria
Configure Seller Credit Policy Options & Validation
Given I am an admin on Compensation Policies When I open Cross-Staff Attribution settings Then I can select exactly one policy: Seller bonus at sale, Seller credit at redemption, Percentage split per redemption, or No seller credit And when Percentage split per redemption is selected and I enter Seller %=30 and Performer %=70 Then the system validates Seller %+Performer %=100 and prevents save if not 100 And when Seller bonus at sale is selected Then I can configure flat $ or % bonus and required funding source, and the configuration persists with an audit entry
Seller Bonus at Sale — Prevent Double Payment on Redemption/Refund
Given a package sold by Staff A under policy Seller bonus at sale and a $20 seller bonus paid at sale When any credits from that package are redeemed by Staff B on future tickets Then no seller credit is attributed to Staff A on redemption lines, and only performer attribution is created And when the original package sale is refunded or voided Then a -$20 seller bonus reversal is queued to Staff A in the next payout cycle with a reference to the sale ID And when a redemption is voided and the ticket is re-closed Then the system does not create duplicate payouts for the same redemption ID (idempotent processing)
Seller Credit at Redemption with Expiration Window
Given policy Seller credit at redemption with an expiration window of 90 days And a package sold by Staff A on 2025-01-01 When a redemption occurs on 2025-02-15 by Staff B Then seller credit is attributed to Staff A per policy for that redemption When a redemption occurs on 2025-04-05 (>90 days after sale) Then no seller credit is attributed; 100% of attribution goes to the performer And the ticket shows Seller credit expired with the computed cutoff date based on business timezone
Percentage Split per Redemption with Partial Credit and Add-ons
Given policy Percentage split per redemption with Seller 30% and Performer 70% And a $100 service where $60 is covered by package credit and $40 is paid by card And a $20 add-on is included When the ticket is closed Then seller credit applies only to the $60 redemption portion (Seller $18.00, Performer $42.00) And the $40 cash portion and $20 add-on attribute 100% to the performer And all attributed amounts are rounded to currency rules (2 decimals) and itemized on the payout statement with ticket and redemption IDs
No Seller Credit Policy Enforcement
Given policy No seller credit is active When any package sold by any staff is redeemed by any performer Then 100% of attribution is assigned to the performer on redemption lines And no seller staff or seller amounts are present on tickets, payout statements, or exports
Transparency on Tickets, Payouts, and Exports
Given any active attribution policy When a ticket includes a package redemption Then the ticket displays: Policy name, Performer, Seller (if applicable), Allocation %, and Amounts for each redemption line And the payout statement itemizes per-line allocations with Ticket ID, Package ID, Redemption ID, Policy, Seller Staff ID (if any), Performer Staff ID, and Amounts And the CSV export includes the same fields and totals match the on-screen statement
Policy Change Effective Date and Non‑Retroactivity
Given the admin updates the attribution policy effective 2025-03-01 When tickets created before 2025-03-01 are viewed or recalculated Then their attribution remains under the prior policy with no retroactive change And tickets created on or after 2025-03-01 use the new policy And each allocation line stores the Policy Version/ID used and this appears on payout statements for auditability
Adjustments, Recalculation & Audit Trail
"As a bookkeeper, I want a complete audit trail and safe recalculation so that corrections don’t break payouts or accounting."
Description

When tickets are edited, services reassigned, prices changed, or redemptions backdated, automatically recalculate splits and revenue while preserving an immutable audit trail. Require appropriate permissions, preview changes before applying, generate reversing and adjusting ledger entries, and flag adjustments that impact already-issued payouts with options to claw back or net forward.

Acceptance Criteria
Edit Ticket Price Recalculates Splits with Preview
Given a ticket for today with 1 service redeemed from a 5-pack (standard rate $50) and an add-on of $20 assigned to Staff A with commission rule 40% on service and 10% on add-ons When a manager with Apply Financial Adjustments permission changes the service price to $60 and clicks Preview Then the preview shows: service revenue recognized today = $60 ($50 from deferred + $10 cash surcharge), deferred revenue decrease = $50, surcharge cash revenue = $10, Staff A commission = $24 (40% of $60) and $2 (10% of $20), and business share updated accordingly And no ledger or payout changes are posted until Apply is confirmed And when Apply is confirmed, reversing and adjusting ledger entries are posted dated today, linked to the ticket and adjustment ID, and payout accruals are updated
Service Reassignment Reallocates Revenue and Commissions
Given a ticket with a package redemption service at $50 and a $20 add-on, both initially assigned to Staff A, and commission rule 40% service, 10% add-on When the service line is reassigned to Staff B and Preview is clicked Then the preview shows Staff A service commission reduced to $0 and Staff B service commission increased to $20 (40% of $50), while the add-on commission of $2 (10% of $20) remains with the staff who owns the add-on line (unchanged if not reassigned) And the sum of all staff splits equals the ticket’s net revenue after fees and discounts And on Apply, the system posts a reversal for Staff A’s prior service commission and a corresponding adjustment for Staff B, with a single immutable adjustment ID
Backdated Redemption Generates Reversing and Adjusting Entries
Given a service redemption recognized on 2025-08-11 with revenue $50 and associated staff commission accruals When an authorized Accounting Admin backdates the redemption to 2025-08-09 and clicks Apply Then the system posts an automatic reversing entry dated 2025-08-11 and an adjusting entry dated 2025-08-09 for both revenue and commissions, preserving net zero across the moved period And if the target date is in a closed period and the user lacks permission, the adjustment is blocked with a clear message and no entries are posted And all preview amounts match the final posted entries to the cent
Permission Enforcement and Safe-Apply Workflow
Given role permissions: Adjust Tickets (preview), Apply Financial Adjustments (apply), Accounting Admin (backdate across closed periods), and View Financials (view amounts) When a user without Apply Financial Adjustments opens an editable ticket and makes a change Then they can see a Preview with masked amounts if lacking View Financials and cannot Apply (Apply button disabled with tooltip) And any attempt to bypass via API returns 403 with reason and is logged in the audit trail And users with all required permissions can Apply only after entering a reason and confirming a summary modal that lists impacted ledger accounts and payout deltas
Payout Impact Flag with Clawback or Net-Forward Options
Given a previously issued payout batch that included $20 commission for Staff A from Ticket #123 When an adjustment reduces Staff A’s commission for Ticket #123 by $8 Then the preview flags “Impacts issued payout” and requires selecting Claw Back in next payroll or Net Forward to next payout before Apply is enabled And choosing Claw Back creates a scheduled -$8 deduction in the next payout for Staff A; choosing Net Forward reduces Staff A’s next payout accrual by $8 And the selected option is stored per staff per adjustment ID and reflected in payout reports, without creating negative balances beyond configured limits
Immutable Audit Trail with Diff and Attribution
Given any applied adjustment to a ticket, service assignment, price, or redemption date When the adjustment is saved Then an immutable audit record is created capturing: before/after values per field, user ID, role, timestamp with timezone, IP/device fingerprint, reason text, and related ledger entry IDs And audit records cannot be edited or deleted by any role and are exportable (CSV/JSON) and filterable by ticket, staff, date range, and adjustment ID And each audit record includes a cryptographic checksum to detect tampering and surfaces a warning if verification fails
Partial Redemption and Add-on Split Integrity
Given a ticket with a package redemption originally using 2.0 units at an effective rate of $25/unit (total $50) and an added $15 nail-trim add-on When the used units are edited to 1.5 and Preview is clicked Then the preview shows deferred revenue recognized = $37.50, returned to deferred = $12.50, remaining package balance increased by 0.5 units, add-on revenue = $15, and staff splits recalculated per rules with amounts rounded to the nearest cent And the sum of staff splits equals total recognized revenue ($52.50) and no negative balances or over-redemptions are produced And on Apply, corresponding reversing and adjusting entries are posted and linked to the package ledger and ticket
Refunds, Chargebacks & Reversals for Packages
"As an owner, I want refunds and chargebacks to automatically adjust credits, revenue, and commissions so that my books and payouts remain accurate."
Description

Support full and partial package refunds and processor chargebacks by restoring credits, updating liability balances, reversing recognized revenue where applicable, and adjusting staff commissions. Apply configurable rules for clawbacks (hold, immediate reversal, or net against next payout), send notifications to affected staff, and reconcile with payment processor webhooks to keep books and payouts accurate.

Acceptance Criteria
Full Refund of Unused Package
Given a purchased package with 0 redeemed credits and a positive liability balance When a full refund is issued by an authorized user Then the package status is set to Refunded and removed from the customer's active packages And all package credits are invalidated (0 available, 0 redeemed) And the liability balance for the package is reduced to $0 And a refund transaction is recorded with amount equal to the original purchase and a reference to the original payment And no revenue reversal entries are created And no staff commission adjustments are created And the processor refund ID is stored and linked to the refund transaction
Partial Refund Restores Credits from Prior Redemption
Given a package with at least 1 redeemed credit applied to a completed service When a partial refund targeting that redemption is issued Then the redeemed credit count decreases by 1 and available credits increase by 1 on the package And recognized revenue for the redeemed service is reversed for the exact redemption amount And package liability increases by the carry value of 1 credit And a commission reversal record is created for the service provider per the active clawback policy And the customer's invoice/ledger shows a redemption reversal line linked to the original service
Processor Chargeback Received via Webhook
Given a captured package payment with processor transaction ID T And a chargeback webhook is received for T with amount A When the webhook is processed Then a single chargeback record is created using the webhook event ID as the idempotency key And the configured clawback policy (Hold, Immediate Reversal, or Net Against Next Payout) is applied And accounting entries reduce cash by A, reduce liability for unredeemed portions, and reverse revenue for redeemed portions proportionally to A And package credits are adjusted: redeemed credits covered by the reversal are restored; unredeemed credits covered by the reversal are removed And commission adjustments are created and linked to the impacted staff payout per policy And affected staff are notified of the chargeback and pending/posted adjustments
Commission Clawback Policy Application
Given an existing reversal (refund or chargeback) tied to staff commissions amount C When the policy is Hold Then a pending negative commission record for C is created and excluded from the current payout When the policy is Immediate Reversal Then a negative commission adjustment for C is applied to the current open payout or the next payout if current is closed When the policy is Net Against Next Payout Then a negative commission item for C is netted against the next payout, capping at the payout amount and carrying forward any remainder And all adjustments reference the original service/redemption and the reversal transaction
Add-ons Split from Package Redemptions
Given an invoice that includes a package redemption for a base service and separate paid add-ons When a refund or chargeback is processed for the package portion only Then only the package redemption revenue and related commission are reversed per policy And add-on revenue and commission remain unchanged And the invoice and ledger display separate lines for the package reversal and add-ons with correct subtotals
Idempotent Reconciliation and Concurrency Control
Given multiple webhooks or user actions attempt to reverse or refund the same package payment When these events are processed concurrently Then only one reversal/refund is applied and subsequent attempts are detected as duplicates using idempotency keys and original transaction references And balances for credits, liability, revenue, cash, and commissions remain consistent with the single-applied outcome And a duplicate-event log entry is recorded without side effects
Staff Notifications and Audit Trail on Reversals
Given a reversal that impacts staff payout or commissions When the reversal is created, updated, or resolved Then the affected staff receive an SMS/app notification within 60 seconds including amount, reason, policy, and a link to details And an immutable audit log entry captures actor (webhook/system/user), timestamps, before/after balances, commission adjustments, and payment references And notifications are retried up to 3 times on failure with final status visible in the audit trail
Payout & Accounting Exports for Credit-Aware Splits
"As a manager, I want exportable payout and accounting reports for credit-aware splits so that I can reconcile and pay staff quickly."
Description

Provide period-based reports and exports that summarize staff payouts, package liability movements, redemption-derived revenue, add-on revenue, fees, and taxes. Include drill-down to ticket and line items, per-staff statements, close/lock periods to prevent changes, and integrations/CSV exports for accounting systems (e.g., QuickBooks, Xero) with configurable chart-of-accounts mapping and scheduling.

Acceptance Criteria
Period Summary Export (Credit-Aware)
Given a date range and location are selected When the user runs the Accounting Summary Then the report includes: opening package liability, package sales (additions), liability reductions from redemptions, ending liability, redemption-derived revenue recognized, add-on revenue, fee revenue, taxes collected, and total staff payouts And opening + additions − reductions = ending liability And redemption-derived revenue + add-on revenue + fee revenue equals total revenue excluding taxes and tips within $0.01 And all amounts display with 2 decimal places and totals reconcile within $0.01 And the user can export the summary to CSV
Drill-Down to Tickets and Line Items
Given the Accounting Summary is displayed for a period When the user clicks any metric or row Then a detailed view lists the matching tickets/line items with columns: date, ticket ID, client, pet, staff, line type (redemption/add-on/fee/tax), quantity, unit price, discount, tax, total, staff earning, package ID (if applicable) And the detailed totals reconcile to the clicked metric within $0.01 And the user can export the detailed view to CSV
Per-Staff Payout Statements
Given a period and staff member are selected When the user generates the staff payout statement Then it lists all services performed in the period with earnings broken out by source (package redemption vs cash/card) and add-ons shown as separate lines And the statement total equals the staff payout amount in the period summary within $0.01 And the statement can be exported as PDF and CSV And the system records generation with timestamp and user
Close and Lock Accounting Periods
Given a period is marked Closed When any user attempts to modify tickets, line items, package redemptions, staff assignments, taxes, discounts, or mappings that affect that period Then the system blocks the change and displays a message to reopen the period or create an auto-adjustment in the next open period (if enabled) And only users with Close Period permission can close or reopen a period And closing a period captures an immutable snapshot ID and audit log entry And reopening a period invalidates scheduled exports for that period until re-closed
QuickBooks/Xero Export with COA Mapping
Given Chart-of-Accounts mappings are configured for Package Liability (deferred revenue), Redemption Revenue, Service Revenue (by category), Add-On Revenue, Fee Revenue, Tax Payable, Tips Payable, and a tracking dimension (class/location) When the user exports the period to QuickBooks or Xero Then the system produces balanced journal entries that debit Package Liability for redemptions and credit the mapped revenue accounts, credit Package Liability for package sales within the period, credit Tax Payable for taxes collected, and separate add-on and fee revenues into their mapped accounts And the export validates all required mappings and fails with actionable errors if any are missing And each export is idempotent: re-exporting the same closed period prevents duplicates or performs an update, recording external IDs and timestamps
Scheduled Accounting Exports and Notifications
Given a schedule (daily/weekly/monthly) and destination (email CSV, QuickBooks, or Xero) are configured When the schedule triggers Then the system generates and delivers exports for the most recently closed period in the account’s time zone And success notifies recipients with links to files and external IDs; failures notify with error details and retry options And runs are logged with status, duration, and payload size; missed runs are retried within 24 hours And if the target period is not closed, the job skips and notifies recipients
Partial Redemptions and Add-On Split Handling
Given a ticket where a service is partially covered by a package credit and the remainder paid via cash/card, and an add-on is attached When the period reports and exports are generated Then only the redeemed value reduces Package Liability And recognized revenue reflects the redemption-derived portion and the cash/card portion in their respective mapped revenue accounts And add-on revenue is reported separately and mapped to its account, with staff earnings split accordingly And staff payout and per-staff statement show earnings based on the service performed today, independent of the original package sale And rounding is applied per line so that the sum of detailed lines equals report totals within $0.01

Split Ledger

An immutable audit trail that explains every payout: who got what, when, and why. Drill into each visit’s split logic, fees, taxes, adjustments, and supporting artifacts. Export to QuickBooks/Xero/CSV and share role‑based views (owner, staff, partner) for friction‑free reconciliation and full transparency.

Requirements

Immutable Payout Ledger
"As an owner, I want a write-once payout ledger so that I have a reliable, traceable record of every financial event without risk of silent edits."
Description

Implement an append-only ledger to record every payout-related event (creation, adjustment, reversal) with precise timestamps, actor identity, source system, and linkage to visits, bookings, invoices, and payments. Each entry captures amounts, currencies, method, and references to applied rules at the time of calculation to ensure a complete historical narrative. The ledger exposes stable identifiers, supports idempotent writes, and prevents in-place edits; corrections are modeled as compensating entries. Designed to integrate with FetchFlow’s booking, reminder, and payment flows so that each visit automatically generates traceable financial records, enabling reliable reconciliation and transparent reporting.

Acceptance Criteria
Append-Only Ledger Entry Creation
Given a valid payout-related event payload (create, adjust, reverse), When the write API is called, Then exactly one new ledger entry is appended and no existing entry is modified or deleted. Given the append succeeds, When the entry is retrieved by its ID, Then it contains event_type ∈ {CREATED, ADJUSTED, REVERSED}, amount, currency (ISO 4217), payout_method, actor_id, actor_type, source_system, and ISO 8601 UTC timestamp with ≥ millisecond precision. Given entity context exists, When the entry is created, Then it includes resolvable references to visit_id, booking_id, invoice_id, and/or payment_id as applicable. Given a malformed or incomplete payload, When the write API is called, Then the request is rejected with 400 and no new entries are appended.
Idempotent Writes with Stable Identifiers
Given a request includes an idempotency_key and identical payload, When the request is retried up to 3 times within 24 hours, Then the same ledger_entry_id and content are returned and only one ledger entry exists. Given a request reuses an idempotency_key with a different payload, When the write API is called, Then the request is rejected with 409 Conflict and no additional entry is created. Given a successful write with an idempotency_key, When the ledger is queried by that key, Then it returns the single corresponding ledger_entry_id.
Corrections via Compensating Entries Only
Given an attempt to update or delete an existing ledger entry by ID, When a PUT, PATCH, or DELETE is issued, Then the API responds 405 Method Not Allowed and the original entry remains unchanged. Given a business correction is required, When a compensating entry is created referencing prior_entry_id and reason_code, Then the new entry reverses or adjusts the financial effect without altering the original entry and appears later in chronological order. Given a compensating entry is posted, When balances are computed for the affected entities, Then the net reflects the combined effect of the original and compensating entries with no gaps in sequence.
Entity Linkage Integrity and Referential Validations
Given an entry is created with visit_id, booking_id, invoice_id, or payment_id, When validated, Then all provided references resolve to existing records or the write is rejected with 422 and no entry is appended. Given entries exist for a visit, When querying the ledger by visit_id, Then all related entries are returned in timestamp order with stable identifiers and correct cross-links. Given optional references are not applicable, When the entry is created, Then the fields are null with a nullable_reason recorded and validation passes.
Rule Snapshot and Split Logic Traceability
Given a payout is calculated, When the ledger entry is recorded, Then it includes an immutable snapshot: rules_version, split breakdowns (percentages/amounts), fees, taxes, package redemptions, and no-show fees used at calculation time. Given the Split Ledger detail view is opened for an entry, When the UI loads, Then it renders the captured snapshot and intermediate totals without recomputing from current rules and provides links to supporting artifacts. Given rules change after the payout, When the historical entry is viewed, Then the original snapshot remains unchanged and auditable.
Automatic Ledgering from Visit Lifecycle Events
Given a visit is created, updated, completed, canceled, or marked no-show with fees, When these events are committed by FetchFlow services, Then corresponding ledger entries are appended within 2 seconds and reference the triggering event and actor. Given a payment capture or refund occurs, When payment status changes to settled or refunded, Then payout and reversal entries are appended preserving chronological order by timestamp. Given event replay after downtime, When duplicate events are processed with the same idempotency keys, Then no duplicate ledger entries are created.
Export Fidelity and Role-Based Access Controls
Given an owner, staff, or partner is authenticated, When accessing the ledger, Then role-based filtering applies: owners see all entries, staff see entries for their scope, partners see entries where they are a beneficiary in the split. Given a user exports to QuickBooks Online, Xero, or CSV for a date range and filters, When the export completes, Then totals reconcile to on-screen results and each row includes stable IDs, timestamps, amounts, currencies, methods, references, and rule snapshot fields. Given an export request up to 100,000 entries, When initiated, Then a downloadable file is produced within 30 seconds; for larger datasets an async job is created, progress is trackable, and the final file preserves ordering and identifiers.
Split Logic Explainer
"As a bookkeeper, I want to see the exact rules and steps used to compute a payout so that I can explain differences and validate amounts during reconciliation."
Description

Provide an explainer view that decomposes each payout into its constituent calculations: base rate, package application, taxes, platform fees, partner/staff splits, discounts, and no-show fees. Display a step-by-step calculation tree with formulas, inputs, and intermediate results, including rounding rules and time-based or role-based modifiers. Persist the rule version used at calculation time and show effective conditions (e.g., location, service type, partner contract) to ensure historical interpretability. Integrates with the ledger to enable drill-through from a payout line to the originating visit, rules, and parameters.

Acceptance Criteria
View Step-by-Step Calculation Tree
Given a payout generated for a completed visit with base rate, package application, discount, taxes, platform fees, partner or staff split, adjustments, and possible no-show fee When a user opens the Split Logic Explainer from the payout line in the ledger Then the explainer renders a calculation tree showing each component as an ordered node with its formula, input values, and intermediate subtotal And the sum of the final node equals the payout amount displayed in the ledger within $0.01 And each monetary value displays currency and rounding to 2 decimal places And an adjustments node appears only when adjustments exist
Display Rounding Rules and Modifiers
Given a payout affected by time-based and role-based modifiers and subject to defined rounding rules for taxes and splits When the explainer renders the calculation nodes Then each node that applies rounding displays the rounding method name and scale And both pre-round and post-round values are shown for that node And each applied modifier shows its name, the matched condition, effective window, and multiplier or delta used And the displayed calculations reproduce the node subtotal within $0.01
Show Rule Version and Effective Conditions
Given a historical payout calculated under a specific ruleset version When the explainer is opened Then the ruleset version identifier and calculation timestamp used are displayed and immutable And the effective conditions list includes location, service type, price book, and partner contract identifiers that determined rule selection And the explainer indicates if current rules differ from the version used at calculation time without altering displayed results
Drill-Through from Payout Line to Originating Artifacts
Given a payout line item in the ledger When the user opens the Split Logic Explainer and selects a drill-through link Then the app navigates to the originating visit record showing date and time, pet, service, duration, and notes And links are available to the applied rule definition snapshot and the parameter set used, including partner contract and price book entries And a back action returns to the explainer preserving scroll position and expanded nodes
Role-Based Visibility in Explainer
Given role-based access controls for owner, staff, and partner roles When a user opens the Split Logic Explainer Then an owner sees all calculation nodes and values And a staff user sees nodes and values relevant to their payout with partner contract and platform fee details redacted per policy And a partner user sees only partner-share nodes and totals with client pricing redacted And redactions are visually indicated without revealing values And unauthorized users receive a 403 access denied message
Error Handling and Data Integrity
Given the explainer encounters missing or corrupted supporting artifacts while loading a historical payout When the view loads Then the explainer renders all available nodes and marks missing artifacts with a clear status and identifier And the final payout total equals the ledger amount within $0.01 And an error event with a correlation ID is recorded And no sensitive data is exposed in the error message shown to the user
Performance and Load Constraints
Given typical payouts have up to 12 calculation nodes and the 95th percentile has up to 40 nodes When loading the explainer on a 4G mobile connection Then time-to-first-render is 2.0 seconds or less at p50 and 4.0 seconds or less at p95 with warm cache And expanding any node completes in 300 ms or less at p50 and 800 ms or less at p95 And the explainer API payload size is 150 KB or less at p50 and 400 KB or less at p95
Supporting Artifacts Attachment
"As a staff member, I want to attach and view evidence for a payout (e.g., check-in photo, SMS approval) so that any dispute can be resolved quickly and fairly."
Description

Allow each ledger entry and visit calculation to reference and display supporting artifacts such as Pet Profile Smart Card snapshots (vaccine records), signed policies, time-stamped SMS threads, GPS check-in/out, photos, and receipts. Store artifacts securely with metadata (type, size, hash, created-by) and enforce access controls consistent with user roles. Provide inline previews where possible and downloadable originals with audit logging of access. Artifacts are immutable once attached; supersessions are recorded as new versions with explicit linkage.

Acceptance Criteria
Attach Artifact to Ledger Entry or Visit Calculation with Metadata Integrity
Given an authenticated owner or staff user with edit permission on a ledger entry or visit calculation When they attach an artifact of a supported type (jpg, png, heic, pdf, txt, json) up to 25 MB Then the system stores the artifact and records metadata: artifact_id, parent_id (ledger entry or visit calculation), type, size (bytes), SHA-256 hash, created_by (user id and role), created_at (UTC), and storage location reference And the stored hash computed from the persisted object matches the recorded SHA-256 hash And the artifact appears in the parent’s Artifacts list within 2 seconds of successful upload And uploads of unsupported types or files > 25 MB are rejected with a clear error code and message
Role-Based Access Control for Artifact Visibility and Actions
Given a ledger entry with attached artifacts and users with roles Owner, Staff, and Partner When an Owner views the entry Then they can preview and download all artifacts for that entry And when a Staff user views the entry Then they can preview and download artifacts for entries/visits they are permitted to access by business rules And when a Partner user views the entry Then they can only see, preview, and download artifacts associated to visits they are assigned to via split and cannot see artifacts from other visits And users without permission receive 403 on artifact preview/download requests and no sensitive metadata is leaked beyond artifact_id And all artifact storage is encrypted at rest (AES-256) and transfers occur over TLS 1.2+
Inline Preview for Supported Types with Fallback to Download
Given a permitted user opens the Artifacts panel on a ledger entry or visit calculation When the artifact is an image (jpg, png, heic) Then a thumbnail renders with click-to-zoom inline preview And when the artifact is a pdf Then the first page thumbnail renders and an in-app PDF viewer opens on click And when the artifact is an SMS thread Then the thread renders inline as a chronological, time-stamped conversation with sender labels And when the artifact is GPS check-in/out Then a map renders inline with start/end pins and timestamps And for unsupported types Then a generic icon and a Download button are shown without inline preview
Download Original with Immutable Access Audit Trail and Hash Verification
Given a permitted user clicks Download on an artifact When the download is initiated Then the system serves the exact original object and verifies the streamed content’s SHA-256 matches the stored hash before marking success And the system writes an immutable audit record with fields: event_id, artifact_id, parent_id, action=download, user_id, role, timestamp_utc, client_ip, user_agent, outcome (success/failure) And each subsequent download creates a distinct audit record And audit records are viewable by Owner and authorized Staff in the Audit tab for the artifact
Immutability and Versioned Supersession with Explicit Linkage
Given an artifact is attached to a ledger entry or visit calculation When any user attempts to edit the file contents or delete the artifact Then the action is blocked and the artifact remains unchanged And when a newer artifact is added to supersede an existing one Then the new artifact is created as a distinct record with supersedes_id set to the prior artifact and the prior artifact updated with superseded_by_id set to the new artifact And the UI marks the latest artifact as Current and retains access to all prior versions And version history is ordered by created_at and shows the linkage clearly
Artifacts in Split Logic Drill-Down and Line-Item Association
Given a ledger entry with fees, taxes, and adjustments and attached artifacts When a user opens the Split Logic drill-down Then each line item can display zero or more linked artifacts via a per-line Artifacts chip with the count And clicking the chip reveals only artifacts linked to that line item with inline previews when supported And unlinked artifacts remain visible in an All Artifacts section for the entry And users can filter artifacts by line item to narrow context
Upload Validation, Malware Scan, and Idempotent Retry
Given a user uploads an artifact When the file fails client- or server-side validation (type/size) or malware scanning Then the system rejects the upload with a specific error code (e.g., VALIDATION_FAILED or MALWARE_DETECTED) and no artifact record is created And when a network interruption occurs during upload Then a resumable, idempotent upload session allows retry within 10 minutes without creating duplicate artifacts And partial or failed uploads leave no orphaned records or objects in durable storage
Role-Based Ledger Views
"As a partner, I want a filtered ledger view that shows my splits and related visit details so that I can verify payments without seeing confidential business data."
Description

Deliver tailored ledger views for owners, staff, and partners with granular role-based access control. Owners see full payout trails, fees, taxes, and counterparty details; staff see only their own payouts with redacted business-sensitive fields; partners view their splits and related visit context. Support shareable, time-limited links for auditors and accountants, field-level redaction, and view audit logs. Integrates with FetchFlow’s existing user/organization model and supports mobile-friendly layouts for SMS-first workflows.

Acceptance Criteria
Owner Full Ledger View & Drilldown
Given an authenticated Owner user in Organization O with at least 20 payouts across two months When the user opens the Ledger view for O Then the list shows all payouts for O with columns: Date, Payout ID, Counterparty, Gross, Fees, Taxes, Net, Status Then sensitive fields are fully visible to the Owner role (no redaction) Then selecting any payout opens a detail view showing visit-level split logic, fees, taxes, adjustments, and links to supporting artifacts Then monetary totals reconcile as Gross - Fees - Taxes ± Adjustments = Net within $0.01 Then results are scoped to Organization O only; records from other organizations are not visible Then initial page (up to 50 rows) renders within 2000 ms on a 4G connection
Staff Restricted Ledger View & Redaction
Given an authenticated Staff user S in Organization O with payouts P_s and other staff payouts P_o When S opens the Ledger Then only records in P_s are visible to S Then business-sensitive fields (counterparty legal name, other staff earnings, owner bank details) are redacted or omitted in list and detail views Then attempting to access a payout not in P_s by direct URL or API returns HTTP 403 with no data leakage Then export actions are limited to S's payouts or disabled per policy Then a view-access event is recorded in the audit log with actor=S, role=Staff, org=O, and resource IDs
Partner Split View & Visit Context
Given an authenticated Partner user P associated with Organization O and designated on visit splits V_p (and unrelated visits V_o) When P opens the Ledger Then only payouts and visit splits where P is a designated counterparty are visible Then the detail view shows P's split amount, calculation method, visit date/time, service type, pet name and photo, and booking reference Then business-sensitive data unrelated to P (other staff earnings, owner bank info, internal notes) are not shown Then attempting to open any payout for visits in V_o returns HTTP 403 Then P's split totals reconcile across list and detail within $0.01
Shareable Time-Limited Access Links
Given an Owner creates a shareable ledger link scoped to Organization O, date range D, role scope "Auditor", with expiry E (UTC) When the link URL is opened by an unauthenticated browser Then a view-only ledger appears limited to O and D with fields permitted by the Auditor scope Then the link grants no write actions and no access outside the defined scope Then the link expires automatically at E and thereafter returns HTTP 410 Gone Then the Owner can revoke the link manually, after which it returns HTTP 410 within 60 seconds Then every access via the link is logged with link ID, timestamp, IP, and user agent
Field-Level Redaction Policy Enforcement
Given a redaction matrix defines visible, masked, and hidden fields for roles Owner, Staff, Partner, and Auditor Link When any subject requests ledger list, detail, or export Then the response includes only fields allowed by the subject's role per the matrix, with masked values rendered as obfuscated placeholders where specified Then disallowed fields are absent from network payloads (not merely hidden in the UI) Then exports generated under restricted roles contain only permitted fields Then attempts to query a hidden field via API return HTTP 403 or a redacted value without revealing the original
View Access Audit Logging & Immutability
Given any ledger list, detail, or export view is accessed When the access completes Then an audit record is appended with actor (user ID or link ID), role, organization ID, resource IDs, action type, timestamp (UTC), IP, and user agent Then audit records are append-only and tamper-evident (hash-chained or write-once storage) and cannot be modified by end users Then an Owner can filter audit records by date range, actor, and resource ID and export results to CSV Then audit log retention is at least 24 months
Mobile-Friendly Ledger Views for SMS-First Workflows
Given a user receives an SMS deep link to a specific payout or the ledger list When the link is opened on a mobile device with viewport 360x640 on a 3G network Then the page displays meaningful content within 3 seconds and becomes interactive within 5 seconds Then layout is responsive with no horizontal scrolling, text size >= 12pt, and tap targets >= 44px Then the deep link routes directly to the intended resource after authentication and returns to the same view after re-authentication Then primary actions (filter, search, drilldown, back) are reachable with one tap and meet WCAG AA contrast
Financial Export & Accounting Mapping
"As an accountant, I want one-click exports to QuickBooks/Xero with correct account mappings so that I can reconcile books without manual re-entry."
Description

Enable export of payouts and fees to QuickBooks Online, Xero, and CSV with configurable mappings to chart of accounts, classes/locations, tax codes, items, and contacts. Support incremental and scheduled exports, detect duplicates via stable external IDs, and reconcile export status per entry. Handle adjustments and reversals as separate transactions, preserve memo details with visit IDs, and provide a dry-run preview. Include OAuth connections, sandbox modes, error handling with retry/backoff, and clear reporting of successes and failures.

Acceptance Criteria
OAuth & Sandbox Connections to QuickBooks/Xero
Given an admin selects QuickBooks Online Sandbox and initiates Connect When the OAuth consent is completed Then the connection is created in FetchFlow with environment=sandbox, tenant/company ID captured, and status=Connected And access and refresh tokens are stored securely and refreshed automatically before expiry And the connection can be disconnected, which revokes tokens and sets status=Disconnected Given an admin selects Xero Production and initiates Connect When scopes for accounting are granted Then FetchFlow can query the chart of accounts from Xero and persists last successful API health check time And all subsequent exports use the selected environment (sandbox or production) without cross-posting
Mapping Configuration for Accounts, Classes/Locations, Tax Codes, Items, Contacts
Given destination is QuickBooks Online, Xero, or CSV When the admin maps payouts, fees, taxes, and adjustments to chart of accounts, classes/locations, tax codes, items, and contacts Then mappings are validated against the destination and saved per account with versioning And missing/invalid mappings block export with clear errors listing the unmapped fields And each exported entry records the mapping version applied And for CSV destination, the exported file includes mapped names/IDs for accounts, classes/locations, tax codes, items, and contacts
Incremental and Scheduled Exports
Given unexported ledger entries exist within a date range When a manual export runs for that range Then only entries not previously exported are sent and a high-water mark cursor is updated on success And re-running for the same range does not create duplicates Given a schedule is configured for daily or weekly runs When the scheduled time occurs Then the export runs automatically using the high-water mark and writes audit logs and notifications And pausing/resuming the schedule preserves the cursor and does not skip or duplicate entries
Duplicate Detection and Export Status Reconciliation
Given a ledger entry has been exported and has a stable external ID generated from destination and FetchFlow ledger entry ID When a subsequent export evaluates the same entry Then it is skipped in create operations and marked Exported (Duplicate Skipped) without creating a second transaction And if the destination record is missing, a re-export recreates it with the same external ID And each ledger entry shows per-destination status (NotExported, Exported, Failed, SkippedDuplicate) with timestamps and attempt counts
Adjustments and Reversals as Separate Transactions
Given a visit payout includes an adjustment or a reversal When exporting to QuickBooks Online, Xero, or CSV Then the adjustment/reversal is exported as a separate transaction with its own date, amount sign, and references And the memo preserves visit ID(s), adjustment reason, and split details And linkage between original and adjustment is recorded in FetchFlow and, where supported, in the destination And destination totals equal the net of original plus adjustments/reversals
Dry-Run Preview and Validation
Given mappings are configured and a date range is selected When the user runs a dry-run to QuickBooks Online, Xero, or CSV Then no transactions are created or modified in the destination And the preview shows the number of transactions, totals, and any validation errors per entry And each preview item displays the destination account, class/location, tax code, item, contact, memo with visit IDs, and the external ID that would be used
Error Handling, Retry/Backoff, and Reporting
Given a transient API error (e.g., 429 or 5xx) occurs during export When the retry policy executes Then retries use exponential backoff up to a configured maximum and respect destination rate limits And permanent 4xx validation errors are not retried and are recorded as Failed with the destination error message And job and per-entry reports show counts and lists of Exported, Failed, SkippedDuplicate, and Retried, with links to error details And users can retry failed entries individually or in bulk without creating duplicates
Reconciliation & Dispute Workflow
"As an owner, I want to match payouts to bank deposits and raise disputes when something looks off so that I can close the books confidently."
Description

Provide a reconciliation dashboard that matches ledger payouts to processor deposits and bank statements, highlighting variances by date, amount, or fees. Allow users to open disputes on specific entries with reason codes, comments, and attached artifacts; route to designated approvers with notifications and SLAs. Support resolution outcomes (approve, adjust via compensating entry, reject) with full audit trails and status tracking. Include bulk match/unmatch actions and CSV import of bank activity for teams without direct bank integrations.

Acceptance Criteria
Auto-Match Payouts to Deposits and Bank Transactions
Given ledger payouts, processor deposits, and bank transactions for the selected date range are available in the reconciliation dashboard When the user runs Auto-Match Then items are matched by exact amount and payout reference, or by amount within $0.01 and date within ±2 days And matches with confidence >= 0.90 are auto-accepted and moved to Matched And matches with confidence < 0.90 are placed in Review with the computed confidence score visible And unmatched items remain in Unmatched with reason tags (No Reference ID, Date Outside Window, Amount Mismatch) And the auto-match process completes within 60 seconds for up to 10,000 records
Variance Detection and Highlighting
Given matched items exist in the dashboard When a variance in amount, fees, taxes, or date exceeds configured tolerances Then the row is flagged with a variance badge indicating the category (Amount, Fees, Taxes, Date) And the delta value and percentage are displayed in-line and in the side panel And clicking the variance opens a drill-down showing split logic, fees, taxes, and supporting artifacts And filters by variance type, amount range, and date range return results within 2 seconds for datasets up to 10,000 rows
CSV Import of Bank Activity
Given a user selects a CSV file conforming to the template When the file is uploaded Then headers, data types, date formats, and required fields are validated before import And duplicate rows (by stable hash of date, amount, reference, memo) are skipped with a count reported And invalid rows cause the import to fail with an error summary listing line numbers and reasons And successful rows are ingested as bank transactions with source=CSV and the original file is stored as an artifact And up to 50,000 rows are imported within 5 minutes
Bulk Match and Unmatch Actions
Given the user has selected multiple payouts/deposits in Unmatched or Review When the user chooses Bulk Match and confirms the selection Then the system creates grouped matches and moves them to Matched, showing the count of affected records And each bulk action creates an audit trail record with actor, timestamp, action, and affected IDs And when the user chooses Bulk Unmatch on selected matched items, the matches are reversed and items return to Unmatched And authorization enforces that only users with the Reconcile permission can perform bulk actions
Open Dispute on a Specific Entry
Given a user is viewing a ledger entry or a match in the reconciliation dashboard When the user clicks Open Dispute and submits a reason code, comment (min 10 characters), and optional attachments (PDF/JPG/PNG up to 10MB each, max 10 files) Then the dispute is created with status Open and a unique ID, linked to the specific entry/match And notifications are sent to the designated approver queue and the creator via in-app and email within 1 minute And the disputed entry is locked from final settlement and bulk actions until resolution or withdrawal And all submitted data and files are stored as immutable artifacts linked to the dispute
Dispute Routing, Notifications, and SLA Timers
Given approver groups and SLA targets are configured per reason code When a dispute is created or reassigned Then it is routed to the designated queue with an SLA due timestamp computed in the org’s timezone and business hours And reminder notifications are sent at 50% and 90% of SLA elapsed to assignee and watchers And on SLA breach, the dispute is escalated to the next tier and marked Overdue And all routing, notifications, and SLA events are recorded in the audit trail
Dispute Resolution Outcomes and Compensating Entries
Given a dispute is Open or In Review When an approver selects Approve, Adjust via Compensating Entry, or Reject and confirms Then Approve sets the dispute status to Resolved—Approved, unlocks the entry, and logs the decision And Adjust creates a compensating ledger entry linked to the original, updates reconciliation balances, sets status Resolved—Adjusted, and preserves the original entry unchanged And Reject sets the dispute status to Resolved—Rejected with recorded reason, leaving financials unchanged And in all cases, stakeholders are notified within 1 minute and the reconciliation view reflects changes within 30 seconds
Tamper Evidence, Retention, and Compliance
"As a compliance reviewer, I want tamper-evident logs with clear retention policies so that I can verify data integrity and meet audit requirements."
Description

Add cryptographic tamper-evidence (per-entry hashes and periodic anchor hashes) and immutable event-store guarantees to prove that the ledger has not been altered. Provide configurable data retention policies, legal hold capabilities, encrypted at-rest and in-transit storage, and time-synchronized server clocks for accurate ordering. Expose exportable audit logs of access and changes (as new events) to support compliance (e.g., SOC 2 readiness) and customer trust. Include backup/restore procedures with verifiable integrity checks.

Acceptance Criteria
Tamper Evidence via Hash Chain and Anchors
- Given a ledger with N events, When a verification job recomputes SHA-256 per-entry hashes with previous-hash linkage, Then 100% of events validate with no gaps and produce the current head hash. - Given any mutation attempt of a historical event payload or metadata, When verification runs, Then it fails within 1 minute with a mismatch at the altered index and emits a TamperDetected alert event. - Given hourly anchor intervals, When the head hash is computed at HH:00 UTC, Then an anchor record is written containing the head hash, UTC timestamp, and anchor ID, signed by the system key; and When anchors are verified, Then signatures validate and anchor times are within ±2 minutes of schedule. - Given external anchoring is enabled, When the anchor is published, Then it appears in the external transparency/anchoring service within 5 minutes and independent verification returns a proof that matches the internal anchor hash. - Given an export request, When a verifier replays the chain from genesis to any anchor, Then it reproduces the same anchor hash deterministically.
Immutable Append-Only Event Store
- Rule: Events are append-only; update/delete operations on past events are not permitted by any API or data path. - Given an attempt to update or delete an existing event, When executed, Then the system returns HTTP 405 and logs a WriteRejected event containing actor, attempted operation, target event ID, and timestamp. - Given a correction is needed, When applied, Then a new compensating event is appended referencing prior_event_id; no historical bytes are changed. - Given concurrent writers, When two events target the same aggregate, Then both are persisted with monotonic sequence numbers and causal links; no race-condition overwrites occur. - Given read queries, When reading an aggregate at time T, Then state is reconstructed by folding events up to T with no gaps, verified by the hash chain up to T.
Data Retention Policies and Legal Holds
- Given retention policies per event type (e.g., access logs 365 days, payouts 7 years), When the policy engine runs daily at 02:00 UTC, Then eligible payloads are purged/redacted and a RetentionApplied event is appended listing counts and IDs. - Given a record under legal hold, When retention evaluation runs, Then no purge/redaction occurs and a RetentionSkippedLegalHold event is appended; holds include holder, reason, start date. - Given an admin edits a retention policy, When saved, Then changes require dual-control approval, have versioning, effective_at >= 24 hours in the future, and all changes are logged as events. - Given purged data, When a read is attempted, Then the system returns 410 Gone with a reference to the RetentionApplied event; the hash chain continues to validate with tombstone metadata. - Given a compliance export request, When exporting retention configuration, Then a JSON file with policies, versions, and holds is produced and validates against the published schema.
Encryption In Transit and At Rest with Key Rotation
- Rule: All in-transit connections use TLS 1.2+ with approved ciphers; HSTS enabled; weaker protocols/ciphers are rejected and logged. - Rule: Ledger and backup data at rest are encrypted with AES-256-GCM using keys from a managed KMS with per-environment separation. - Given a quarterly rotation schedule, When rotation executes, Then new writes use the new key immediately and historical objects are re-encrypted within 7 days without downtime; all actions are logged. - Given a compromised key simulation, When a key is disabled, Then access to data encrypted with that key is denied unless re-encrypted; on-call receives an alert within 5 minutes. - Given service startup, When key access is tested, Then the service refuses to start if KMS access fails and emits a CryptoInitFailed event.
Time-Synchronized Event Ordering
- Rule: All servers synchronize with NTP; maximum clock drift <= 100 ms; drifts beyond threshold trigger alerts. - Given multi-node writes, When events are appended, Then server-assigned UTC timestamps and monotonic sequence numbers ensure total order per aggregate; clock skew does not reorder within an aggregate. - Given a client request timestamp, When compared to server time, Then the difference is recorded; if difference > 2 minutes, a ClientClockSkew event is emitted. - Given audit replay, When rebuilding order across nodes, Then ordering is consistent when sorting by (aggregate_id, sequence) and non-decreasing by timestamp; any out-of-order detection fails the verification job.
Audit Logs as Events and Exporting
- Rule: Every access and change to ledger data emits an immutable Audit event including actor ID, role, action, resource, resource_id, UTC timestamp, IP/user-agent, and outcome (success/failure). - Given an export request by an Auditor, When filters (date range, actor, action) are applied, Then the system produces CSV and JSONL within 60 seconds containing the selected audit events, with header hashes and a manifest including counts and file hashes. - Given a request by a non-authorized user, When attempting audit export, Then access is denied (HTTP 403) and an Audit event is recorded. - Given an integrity check, When verifying exported files, Then recomputed file hashes match the manifest and the event hash chain validates to the selected anchor. - Given PII fields, When exporting, Then sensitive values are masked/minimized per policy while preserving integrity proofs (hashes/references).
Backup, Restore, and Integrity Verification
- Rule: Encrypted backups are performed hourly incremental and daily full; RPO <= 1 hour; RTO <= 4 hours for the ledger service. - Given a backup completion, When integrity checks run, Then checksums and the chain head hash are stored with the backup and verification passes 100% of files. - Given a quarterly disaster recovery drill, When restoring to a clean environment, Then the ledger is restored within the RTO and post-restore verification reproduces the same head and last anchor hashes for the restore point. - Given a point-in-time restore request T within the last 7 days, When executed, Then the system replays events to T ± 1 second and produces a report of skipped/pending events. - Given a tampered backup file, When restoration starts, Then verification fails and restoration halts with a BackupIntegrityFailed event and alert.

Reply Studio

Build branded, auto-reply templates for DMs by trigger (keywords, story replies, mentions, or post-specific campaigns) with variables like pet name and first name. Respect quiet hours, add auto-throttling to avoid spammy behavior, and escalate to a human when messages look sensitive. Keeps responses fast, consistent, and compliant while smoothly handing prospects into SMS.

Requirements

Multi-Channel Trigger Engine
"As a busy groomer, I want DMs to auto-reply based on keywords, story replies, mentions, or specific posts so that prospects get instant answers without me checking my phone."
Description

Build a rules-driven engine that listens to supported social DM channels for configured triggers—keywords (with partial, exact, and phrase matching), story replies, mentions, and post-bound campaigns—and selects the highest-priority matching auto-reply template. Include per-channel connectors, configurable trigger scopes, conflict resolution, per-sender cooldown windows to prevent repeated firing, language-insensitive matching where feasible, and robust logging. Integrate with Quiet Hours, Auto-Throttling, Sensitive Intent Detection, and SMS Handoff to queue, suppress, or modify responses as needed. Expose an admin UI to create, order, test, and simulate triggers before publishing. Expected outcome: fast, reliable, and deterministic auto-replies that reduce manual DM handling while honoring safety and compliance constraints.

Acceptance Criteria
Keyword Matching, Language Normalization, and Priority Selection
Given multiple triggers across supported channels with keywords configured for exact, partial, and phrase matching, and an admin-defined priority order And language normalization enabled (case folding, diacritic removal) When an inbound DM contains text that matches more than one trigger Then the engine normalizes the text and evaluates all triggers according to their match type And selects exactly one auto-reply template from the highest-priority matching trigger And enqueues the auto-reply within 1 second of message receipt And logs trigger IDs, match types, normalization applied, selected priority path, and enqueue timestamp When an inbound DM arrives from an unsupported channel Then it is ignored and an unsupported-channel event is logged When no triggers match Then no auto-reply is sent And a no-match event is logged with channel, sender ID, and message hash
Trigger Scope Enforcement for Story Replies, Mentions, and Post-Bound Campaigns
Given a story-scope trigger is published When a story reply DM is received on a supported channel Then the trigger may fire if other conditions are met And the same trigger does not fire for non-story DMs Given a mention-scope trigger is published When the business account is mentioned and a corresponding DM thread is created Then the trigger may fire And it does not fire for unmentioned DMs Given a post-bound campaign trigger is published for Post X When a DM originates from Post X or includes the campaign deep link/keyword Then the trigger may fire And it never fires for DMs not attributable to Post X And all scope decisions are logged with scope type and source identifiers
Per-Sender Cooldown and Auto-Throttling
Given a per-sender cooldown window of T minutes is configured for Trigger A When the same sender causes Trigger A to match multiple times within T minutes on the same channel Then only the first auto-reply is sent and subsequent matches are suppressed And each suppression is logged with reason=cooldown and next eligible time Given an account-level auto-throttling limit of N replies per minute per channel When inbound matches would exceed N in the current minute Then excess replies are queued or dropped according to policy (queue if allowed, otherwise drop) And queued items include a retry time and are processed FIFO And all throttle decisions are logged with counters and policy outcome
Quiet Hours Compliance
Given Quiet Hours are configured from 21:00 to 07:00 in the business’s local time zone When a trigger matches during Quiet Hours Then no auto-reply is sent immediately And the reply is queued for delivery at or after 07:00 unless the template is marked quiet-hours-exempt And the decision (queued or suppressed) and scheduled time are logged When a queued reply’s Quiet Hours window ends Then the reply is sent within 2 minutes of window end, unless superseded by a newer reply to the same thread
Sensitive Intent Detection and Human Escalation
Given Sensitive Intent Detection flags an inbound DM with a score at or above the configured threshold When any trigger matches for that DM Then the auto-reply is suppressed And a human escalation task is created in the dashboard within 5 seconds with the DM content, sender, channel, and detection score And a notification is sent via the configured channel (email/push) if enabled And the suppression and escalation are logged with correlation IDs
Admin UI for Trigger Creation, Ordering, and Simulation
Given an admin creates or edits a trigger with channel(s), scope, keywords, match type, priority, cooldown, and template selection When saving the trigger Then required fields are validated and duplicate keyword conflicts are surfaced before publish And the trigger’s position in the ordered list can be changed via drag-and-drop and persists When running a simulation with a sample DM and channel Then the UI displays matched triggers, the selected template, any suppressions (quiet hours, cooldown, sensitive), and the final action And no external messages are sent during simulation When publishing Then the trigger becomes active within 60 seconds and appears in the audit log with version and publisher
SMS Handoff and Consent Compliance
Given a selected auto-reply template includes SMS Handoff When it fires on a supported channel Then the DM contains a compliant opt-in CTA or tap-to-text link with branded variables resolved (e.g., pet name, first name) And no SMS is initiated until explicit consent is captured When the recipient consents Then an SMS conversation is created, linked to the sender’s Pet Profile, and the consent timestamp and source are stored And all handoff steps are logged with message IDs and consent artifacts
Template Builder with Personalization Variables
"As a business owner, I want to create branded templates with variables like pet name and first name so that replies feel personal and stay consistent with my voice."
Description

Provide a branded template editor that supports rich text, emojis, and merge variables (e.g., pet_name, owner_first_name, business_name, booking_link, smart_card_link) with validation, fallbacks for missing data, and multi-language variants. Include live preview with sample Pet Profile data and theme styling, template versioning with draft/publish states, reusable snippets, and per-channel adaptations (link formatting, character limits). Integrate with Pet Profile Smart Cards and Booking to auto-insert relevant links and reduce back-and-forth. Expected outcome: on-brand, personalized replies that improve response quality and drive booking and data collection.

Acceptance Criteria
Merge Variables with Validation and Fallbacks
Given I open the template editor and use the variable picker, When I insert variables, Then only supported variables are allowed: pet_name, owner_first_name, business_name, booking_link, smart_card_link, and approved custom fields Given I type an unsupported or malformed variable, When I attempt to save, Then an inline error highlights the token and saving is blocked until corrected Given a variable uses fallback syntax like {{pet_name|"there"}}, When previewing with a Pet Profile missing pet_name, Then the rendered text shows "there" Given booking_link or smart_card_link is in the template, When previewing or sending a test, Then a pet/business-specific URL is generated and resolves to the correct destination Given booking is disabled for the business and booking_link is present, When attempting to publish, Then publish is blocked with guidance to remove or replace the variable
Rich Text and Emoji Support
Given the editor toolbar is visible, When I apply bold, italics, and line breaks, Then the stored content preserves formatting and preview renders it identically Given I insert emojis into the template, When previewing across channels, Then emojis render where supported and a warning displays for channels that do not support specific emojis Given I paste formatted content from an external source, When content is sanitized, Then unsupported styles are stripped and supported styles remain without altering text content
Live Preview with Sample Data and Theme
Given the business theme is configured, When I open live preview, Then theme colors and logo are applied and sample Pet Profile data populates all variables Given I switch the selected sample pet profile, When preview refreshes, Then all variable values and links update to reflect the new sample without manual edits Given I toggle the preview channel, When the channel changes, Then character count, link presentation, and line breaks update to the target channel’s rules
Multi-Language Variants and Fallback
Given a template has en (default) and es variants, When the owner preferred language is es, Then the es variant is selected; When es is unavailable, Then en is used as fallback Given I enable locales for a template, When any enabled locale is missing a required variable, Then publish is blocked with an error listing the missing variables by locale Given I create an RTL language variant, When previewing, Then the text direction renders RTL while preserving variable values and emoji rendering
Template Versioning and Workflow
Given a published template exists, When I save changes, Then a new draft version is created with incremented version number, timestamp, and author recorded Given a draft version is ready, When I publish the draft, Then it becomes the current immutable published version and earlier versions remain viewable with diff and revert options Given automations reference the template, When a new draft is created, Then automations continue using the last published version until the new draft is published
Reusable Snippets: Create, Insert, Update
Given I create a reusable snippet that includes variables, When I insert the snippet into a template, Then preview resolves the snippet’s variables from the template context Given I edit a snippet, When I save changes, Then templates containing that snippet in draft reflect the update; published templates require creating and publishing a new version to receive the change Given snippets can reference other snippets, When saving a snippet, Then circular references are detected and saving is blocked with a clear error
Per-Channel Adaptations: Character Limits and Link Formatting
Given I select a target channel, When previewing the template, Then a live counter reflects the channel’s configured character limit, warns at 90% of the limit, and blocks publish if exceeded Given booking_link or smart_card_link variables are present, When the channel is SMS, Then links are shortened with the configured shortener and include tracking parameters; When the channel is IG DM or Messenger, Then full deep links are used Given a channel that does not support rich text, When previewing, Then formatting is downgraded appropriately and a warning indicates which styles will be removed
Quiet Hours Scheduler with Timezone Support
"As an owner, I want auto-replies to respect quiet hours and holidays so that I don’t annoy clients after hours and stay compliant."
Description

Implement business-configurable quiet hours and holiday schedules with timezone support that suppress or delay auto-replies outside permitted windows. Provide options to send a single compliant holding message, queue messages for the next opening, or bypass for whitelisted clients or emergencies. Ensure the trigger engine consults this service before sending, and record suppression reasons and next-send timestamps for auditability. Expected outcome: compliant, customer-friendly messaging that avoids after-hours noise while maintaining service expectations.

Acceptance Criteria
After-Hours Suppression and Queue for Next Opening
Given business quiet hours are configured as 18:00–08:00 in the business timezone When an inbound message triggers an auto-reply at 19:30 local time and policy = "queue" Then no auto-reply is sent immediately And a queue record is created with suppression reason "quiet_hours" and evaluated timezone And next-send timestamp equals the next opening start time (08:00 local) When the opening window starts Then the queued auto-reply is sent within 2 minutes of 08:00 local And exactly one queued auto-reply is delivered per conversation for that trigger
Single Compliant Holding Message During Quiet Hours
Given policy = "holding_message" and a holding template is configured When the first inbound message in a conversation triggers an auto-reply during quiet hours Then send exactly one holding message immediately using the configured template and variables And do not send additional holding messages for that conversation until the quiet period ends And queue the original auto-reply for the next opening with next-send timestamp and suppression reason "quiet_hours_holding_sent"
Whitelist and Emergency Bypass
Given a client is on the whitelist OR the inbound message matches configured emergency keywords OR an operator has marked the thread as emergency When the message triggers an auto-reply during quiet hours or a holiday closure Then send the auto-reply immediately (bypass) And record bypass reason as "whitelist" or "emergency" with evaluator identity and timestamp And do not create a holding message or queue entry for this event
Holiday Closure Handling
Given a holiday is configured as Closed All Day in the business calendar for 2025-12-25 When an inbound message triggers an auto-reply on 2025-12-25 in the business timezone Then treat the time as outside permitted window for the entire day And apply the selected policy ("queue" or "holding_message") consistently And set next-send timestamp to the next non-holiday opening (e.g., 2025-12-26 08:00 local) And record suppression reason "holiday" with evaluated timezone
Timezone and DST Transition Accuracy
Given business timezone is America/Los_Angeles and quiet hours are 18:00–08:00 When a message arrives at 01:30 local during the spring-forward DST transition Then compute next-send timestamp using the correct post-DST 08:00 boundary on the correct calendar day And persist timestamps in ISO-8601 UTC with timezone offset And ensure no duplicate or missed sends occur across the DST change
Trigger Engine Consultation and Idempotency
Given the trigger engine evaluates an inbound message with id X When deciding whether to send an auto-reply Then it must call the Quiet Hours service synchronously and block sending until a decision is returned And the decision outcome is one of [allow, suppress_queue, suppress_hold, bypass] And repeated evaluations for the same message id X return the same decision and do not produce duplicate sends And the decision, reason, evaluated timezone, and timestamps are logged with a correlation id
Audit Logging and Exportability
Given an admin queries events for a date range and conversation id When auto-replies are suppressed, queued, held, bypassed, or sent Then each event includes: decision, reason, policy in effect, evaluated timezone, quiet hours window, next-send timestamp (if applicable), actor (system/user), and message ids And records are retained for at least 365 days And records can be exported to CSV with all fields intact
Auto-Throttling and Rate Control
"As a small business owner, I want throttling that caps and spaces out auto-replies so that my accounts aren’t flagged for spam."
Description

Add adaptive rate limits at account, channel, campaign, and recipient-thread levels to prevent spam-like behavior and meet platform policies. Support configurable limits (messages/minute, concurrent sends), randomized jitter, exponential backoff on platform errors, and maximum automatic replies per user within a time window. Integrate with the send pipeline to enqueue or drop messages with reason codes, and surface dashboards and alerts when limits are approached. Expected outcome: sustained deliverability and platform health without manual tuning.

Acceptance Criteria
Multi-Scope Rate Limit Enforcement (Account, Channel, Campaign, Thread)
Given per-scope limits are configured for messages-per-minute and max concurrent sends When the system receives a burst that would exceed any applicable scope Then the observed send rate over any rolling 60-second window does not exceed the configured limit for that scope And the number of in-flight sends never exceeds the configured max concurrency for that scope And excess messages are queued with scheduled_at times that respect the most restrictive active scope And deferred sends are tagged with reason code THROTTLED_<SCOPE> And messages that cannot be sent before their TTL expire are dropped with reason code DROPPED_<SCOPE>_LIMIT And per-recipient-thread window caps prevent more than K auto-replies within W, with additional auto-replies dropped with reason code DROPPED_THREAD_WINDOW_LIMIT
Error-Responsive Throttling and Exponential Backoff
Given the platform returns a 429/RateLimit or transient 5xx error for a channel When retrying sends for that channel Then an exponential backoff with jitter is applied (base delay B, multiplier M, capped at MAX_BACKOFF) And no retries occur sooner than the current backoff delay And the effective channel send rate is reduced adaptively by factor F for duration T after ≥N consecutive 429s And successful sends resume normal rate once the error streak clears And permanent 4xx errors (excluding rate limits) cause the message to be dropped without retry with reason code DROPPED_PLATFORM_PERM_ERROR And all retry attempts and outcomes are recorded with attempt number, delay applied, and final disposition
Jittered Pacing of Auto-Replies
Given jitter bounds are configured as [JMIN, JMAX] When throttling schedules messages for paced sending Then each scheduled delay includes a random jitter component within [JMIN, JMAX] And 100% of measured jitters fall within the configured bounds over ≥100 scheduled sends And the distribution is non-degenerate (standard deviation of jitter > 0) And disabling jitter results in deterministic delays with zero jitter applied
Configurable Limits and Defaults Management
Given an admin updates rate limits via dashboard or API for any scope When the update payload passes validation (allowed ranges, units, and scopes) Then the new limits persist, are audit logged with actor, before/after values, and take effect within 60 seconds And per-scope overrides take precedence over global defaults And resetting a scope to default applies the product default values and removes the override And invalid configurations are rejected with actionable error messages and no partial application And time-window settings (e.g., 60s rate window, 24h thread window) are applied consistently in UTC and reflected in the UI
Send Pipeline Integration with Enqueue/Drop and Reason Codes
Given an auto-reply message enters the send pipeline When a limit requires delaying the send Then the message is enqueued in a dedicated rate-control queue with a visible scheduled_at and scope-effective limit metadata And if a message cannot be sent before its TTL or violates a per-thread cap, it is dropped immediately with a specific reason code and no further retries And each pipeline decision (sent, throttled, retried, dropped) emits a structured event with correlation_id, scope, reason_code, and timestamps And idempotency keys prevent duplicate sends across retries or concurrent workers And under parallel load, no message is sent more than once and ordering per recipient-thread is preserved
Alerts and Dashboards for Limit Utilization
Given alert thresholds are configured (e.g., 80% utilization) When any scope’s utilization ≥ threshold for ≥ 60 seconds Then an alert is created and delivered to the configured channels (email/Slack/webhook) within 60 seconds And the alert includes scope, current utilization, configured limit, queue depth, drop rate, and top reason codes And when utilization remains below threshold for 5 continuous minutes, the alert auto-resolves And dashboards display per-scope: current utilization, queued count, drops by reason, retries, 95th percentile send latency, and backoff state And dashboard metrics update at least every 15 seconds and support filtering by account, channel, and campaign
Sensitive Intent Detection & Human Escalation
"As a trainer, I want sensitive DMs to be flagged and routed to me with automation paused so that I can handle them carefully."
Description

Detect potentially sensitive or high-risk messages using keyword rules plus lightweight intent/sentiment signals (e.g., injury, bite, payment dispute, cancellation, complaint) and automatically pause automation on the thread. Send a safe acknowledgment, route the conversation to a human operator in the unified inbox, notify via mobile push/SMS, and attach a reason label and SLA timer. Provide one-click resume/override and maintain a full audit trail. Expected outcome: minimized automation missteps and faster human resolution for sensitive cases.

Acceptance Criteria
Auto-Pause Automation on Sensitive Intent
Given an inbound DM or SMS message matches sensitive intent rules (keyword match or model score >= 0.80), When the message is ingested, Then the conversation’s automation state is set to Paused within 2 seconds and no automated template replies are sent on that thread except the safe acknowledgment. And Then the thread is tagged Sensitive with the highest-priority reason label from the set [injury > bite > payment dispute > cancellation > complaint]. And Then subsequent automation triggers on that thread are suppressed until a human resumes automation. And Then, on the staged validation set (>=200 labeled messages per intent), the detector achieves >=95% recall and <=5% false positive rate per intent.
Send Safe Acknowledgment to Sender
Given automation was paused due to sensitive detection, When quiet hours are inactive, Then send exactly one safe acknowledgment message using the configured template within 5 seconds, with no links or promotional content, and with variables rendered if available (e.g., first name, pet name). And When quiet hours are active, Then do not send an external acknowledgment; instead, queue the acknowledgment to send at quiet hours end and store the scheduled send time in thread metadata. And Then no more than one acknowledgment is sent per thread in any 12-hour window. And Then the acknowledgment’s delivery status (sent, delivered, failed) is recorded in the audit log.
Route to Unified Inbox with Reason Label and SLA Timer
Given automation is paused for sensitivity, When the thread updates, Then it appears in the Unified Inbox Sensitive queue with the detected reason label. And Then an SLA timer is attached based on reason: injury/bite = 15 minutes; payment dispute/complaint = 1 hour; cancellation = 2 hours. And Then the SLA due time is visible on the thread and counts down from the detection timestamp. And When the SLA is breached, Then the thread is escalated (priority raised, red badge) and an escalation notification is sent to the on-call group.
Operator Notifications (Push/SMS) with De-duplication
Given a thread is routed to the Sensitive queue, When detection occurs, Then send a mobile push to the on-duty operator group including business name, channel, customer name/handle, reason label, and last 160 characters of the message. And When no operator acknowledges (opens or assigns) the thread within 5 minutes, Then send an SMS notification to the on-duty fallback number(s). And Then duplicate notifications are suppressed for additional sensitive events on the same thread for 30 minutes. And Then notification send outcomes are logged (success/failure, timestamp).
One-Click Resume/Override Controls
Given a thread is Paused due to sensitivity, When an operator clicks Resume Automation, Then the automation state changes to Active immediately and future inbound triggers process normally; no retroactive templates are sent for messages received during pause. And When an operator clicks Override Reason, Then they can select a new reason or mark Not Sensitive, which updates labels and cancels/adjusts the SLA timer accordingly. And Then only users with role Manager or higher can Resume or Override; unauthorized attempts are blocked and logged. And Then the UI displays the current automation state and last action (who, when).
Full Audit Trail and Export
Given any sensitive detection flow runs, When events occur (detection, matched keywords, model score, acknowledgment queued/sent, routing, notifications, acknowledgments, resumes, overrides, SLA start/breach), Then each event is logged with timestamp, actor/system, values, and before/after state. And Then audit entries are immutable, visible in the thread timeline, and exportable as CSV/JSON for a selected date range within 60 seconds of request. And Then audit entries can be filtered by reason label, SLA status, and operator.
Edge Cases and Reliability
Given a message contains only media or emojis, When detection runs, Then it does not trigger sensitivity unless a caption or a preceding message in the past 5 minutes carries a sensitive intent. And Given multiple sensitive messages arrive in the same thread within 10 minutes, When detection runs, Then only the first event sends an acknowledgment and notifications; subsequent events increment a counter and update the latest reason/score. And When the detection service is unavailable, Then the system fails safe: pauses automation, routes to Sensitive with reason Detection Unavailable, sends no acknowledgment, and notifies operators of degraded mode. And Given English and Spanish messages, When detection runs, Then sensitivity rules apply in both languages meeting the same accuracy thresholds as the validation set.
SMS Opt-in Handoff Flow
"As a groomer, I want DM auto-replies to include an easy SMS opt-in so that I can move the conversation into my scheduling and payments flow."
Description

Embed compliant calls-to-action in auto-replies that guide users to opt into SMS, capturing explicit consent with timestamp, source channel, campaign ID, and optional double opt-in. Support short links to a hosted consent page and “text START” fallback, auto-link the DM user to an existing or new contact, and, upon consent, continue the conversation in FetchFlow SMS with context (thread history, pet profile) while marking the DM thread as handed off. Update CRM and analytics and respect opt-out keywords automatically. Expected outcome: smooth migration of prospects into the core SMS-first workflow, increasing booking conversion and payment speed.

Acceptance Criteria
Hosted Consent via Short Link
Given a DM auto-reply contains a short link with campaign_id and source_channel parameters When the recipient opens the hosted consent page and explicitly agrees Then record a consent entry with fields: phone, consent_timestamp (UTC ISO 8601), source_channel, campaign_id, method='link', ip, user_agent, checkbox=true; persist to CRM and set sms_status='Opted-in' or 'Pending' per double opt-in settings; emit analytics event consent.captured Given double opt-in is enabled for the campaign When the recipient submits the consent form Then send an SMS prompting "Reply YES to confirm" and only upon receiving YES within 24 hours set sms_status='Opted-in' and consent_confirmed=true; otherwise expire without opting in and emit consent.timeout Given double opt-in is disabled for the campaign When the recipient submits the consent form Then set sms_status='Opted-in' immediately and consent_confirmed=true; emit consent.confirmed Given the short link is expired or already redeemed When the recipient attempts to submit the form Then display an expiration message, do not change sms_status, and emit consent.expired with campaign_id and source_channel
Keyword Fallback: Text START
Given a DM auto-reply instructs "Text START to {number}" and attributes campaign_id When the recipient sends START (any case) to the number Then associate the inbound with the campaign_id, record consent with method='keyword', store consent_timestamp (UTC), and set sms_status per double opt-in settings; emit consent.captured Given double opt-in is enabled When START is received Then reply "Reply YES to confirm" and only upon YES within 24 hours set sms_status='Opted-in' and emit consent.confirmed; if no YES, leave not opted-in and emit consent.timeout Given an unsupported keyword is received When the recipient sends any other word Then send a single clarification message with the correct keyword, do not mark consent, and rate-limit further clarifications for 24 hours; emit invalid_keyword Given the inbound is from an unsupported country/carrier When START is received Then do not mark consent, notify the inbox, and emit consent.failed with reason
Contact Linking and Deduplication
Given a consent record contains phone and DM handle When linking to CRM Then if a contact exists with matching phone OR mapped DM handle, link the DM identity to that contact Given separate contacts exist for the same phone and DM handle When linking Then merge into a single contact preferring the phone contact as primary, preserve timeline, pet profiles, campaign attribution, and create a merge audit entry Given no matching contact exists When linking Then create a new contact with phone, DM handle, display name (if available), consent fields, and attach any referenced Pet Profile Smart Card Given the phone matches multiple contacts When linking Then halt auto-merge, flag a duplicate resolution task, and defer initial SMS until resolved
SMS Continuation with Context
Given a contact becomes sms_status='Opted-in' When handoff is initiated Then create or reuse an SMS thread from the business number and send an initial SMS within 5 seconds including {first_name} and {pet_name} variables when available Given handoff occurs When composing the initial SMS Then include a one-line summary of DM intent (last message snippet <=160 chars) and attach/link the Pet Profile Smart Card if present Given the initial SMS is sent When delivery statuses are returned by the provider Then update message status (queued/sent/delivered/failed) and retry transient failures up to 3 attempts with exponential backoff; emit sms.send/sms.delivered/sms.failed events Given DM attachments exist (e.g., vaccine photo) When handoff completes Then store attachments on the contact timeline and include an MMS or secure link depending on file support; do not drop content silently
DM Thread Handoff State
Given the initial SMS has been queued When starting handoff Then set DM thread status='Handed Off', pin a note linking to the SMS thread, and suppress Reply Studio auto-replies on that DM for 7 days or until a human replies Given a new DM arrives within the suppression window When message is received Then route to the human inbox with tag 'After SMS Handoff' and do not send a new opt-in CTA Given SMS delivery fails definitively (e.g., unreachable number) When provider returns final failure Then remove 'Handed Off' from the DM thread, re-enable auto-replies, notify the team, and emit handoff.failed
CRM and Analytics Attribution
Given consent is captured, confirmed, or revoked When the event occurs Then update CRM fields: sms_status, consent_timestamp, consent_method, source_channel, campaign_id, double_opt_in, and append an immutable audit record with actor and timestamp Given lifecycle milestones occur (consent_captured, consent_confirmed, handoff_started, handoff_completed, optout_received) When they happen Then emit analytics events with properties: contact_id, campaign_id, source_channel, method, timestamp; events become queryable within 60 seconds via analytics API Given an external system queries the CRM API When requesting contact consent data Then return the latest consent status and audit entries consistently within 1 second after write
Opt-out Keyword Handling
Given an opted-in contact sends STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, or QUIT to the SMS number When the message is received Then immediately set sms_status='Opted-out', cease all outbound sends, send a compliant confirmation message, and write audit/analytics event optout.received Given an opted-out contact sends START or UNSTOP When the message is received Then re-subscribe per compliance: if double opt-in is required, send "Reply YES to confirm" and only upon YES set sms_status='Opted-in' and emit consent.confirmed Given outbound SMS are scheduled for a contact who opts out When opt-out is processed Then cancel pending messages with suppression_reason='Opt-out' and do not deliver them
Post-Specific Campaign Mapping & Tracking
"As a marketer-owner, I want to attach templates to specific posts and see performance so that I can invest in what works and cut what doesn’t."
Description

Allow owners to bind templates to individual social posts/stories and define per-post trigger conditions. Track end-to-end funnel metrics—DMs received, auto-replies sent, link clicks, SMS opt-ins, bookings, and revenue attributed—tagged by post and campaign. Provide dashboards, export, and automatic campaign expiration or budget caps. Integrate with UTM tagging for downstream analytics. Expected outcome: visibility into which content drives conversations and bookings, enabling data-driven optimization.

Acceptance Criteria
Bind Template to Post with Trigger Conditions
Given an authenticated owner with Reply Studio access And a published social post or story with a retrievable platform post_id And an existing reply template When the owner selects the post and defines per-post trigger conditions (keywords, mentions, story replies, or post-only) And saves the mapping Then the system persists a campaign with a unique campaign_id linked to post_id and template_id And triggers apply only to DMs originating from that specific post/story And a test DM matching conditions produces exactly one auto-reply within 2 seconds And an audit log records creator, timestamp, and trigger definition
Funnel Metrics Capture per Campaign and Post
Given a mapped campaign is active When a DM linked to the mapped post/story is received Then the system records an event tagged with campaign_id, post_id, platform, and timestamp and increments DMs Received When an auto-reply is sent by the campaign Then Auto-Replies Sent increments and the event is tagged accordingly When a tracked link in the auto-reply is clicked Then Link Clicks increments with a unique click_id and timestamp When the user opts into SMS via compliant flow Then SMS Opt-Ins increments and the contact_id is linked When a booking is completed by that contact within 14 days of the first DM Then Bookings and Revenue Attributed increment and are linked to the campaign and post And all metrics are deduplicated to prevent double counting per event And metrics appear in the dashboard within 5 minutes of event creation
UTM Tagging on Auto-Reply Links
Given a campaign auto-reply contains at least one URL When the campaign is saved Then the system appends UTM parameters: utm_source=<platform>, utm_medium=dm, utm_campaign=<campaign_slug>, utm_content=<post_id> And preserves existing query parameters and avoids duplicate UTM keys When the link is clicked Then the destination receives the UTM parameters and a click event is recorded with campaign_id and post_id And owners can override default UTM values at the campaign level
Dashboard Reporting by Post and Campaign
Given one or more campaigns exist across statuses (Active, Paused, Expired) When the owner opens the Campaigns dashboard Then a table and charts display for each campaign: post_id, post thumbnail, campaign name, status, date range, DMs, Auto-Replies, Clicks, SMS Opt-Ins, Bookings, Revenue And the owner can filter by platform, status, and date range, and search by post URL/id or campaign name And drilling into a campaign shows daily time series for each metric and the underlying event list And totals reconcile with the sum of underlying events for the selected date range And all timestamps are shown in the account timezone
CSV Export of Campaign Metrics
Given the owner has applied filters on the Campaigns dashboard When Export CSV is requested Then a CSV is generated within 60 seconds containing one row per campaign per day with: date, campaign_id, campaign_name, post_id, platform, dms_received, auto_replies_sent, link_clicks, sms_opt_ins, bookings, revenue_cents, currency, start_at, end_at, status And the file uses UTF-8 encoding with RFC 4180-compliant escaping and a header row And numeric fields are unformatted numbers (no thousands separators) And a download link is provided immediately, and if the file exceeds 25 MB, an email link is also sent And revenue totals in the CSV match dashboard totals for the same filters
Automatic Campaign Expiration and Budget Caps
Given a campaign has an end date/time and/or budget caps (currency and/or max auto-replies) When the end time passes or a cap is reached Then the campaign status changes to Expired within 2 minutes And new DMs do not receive auto-replies from the expired campaign And a log entry records the termination reason (time_expired or budget_reached), timestamp, and remaining budget/limits And the owner receives an in-app notification and optional email alert And reactivation requires explicit owner action to extend dates or increase caps
Overlap and Priority Handling for Post-Specific Campaigns
Given two or more campaigns target the same post with overlapping trigger conditions When the owner attempts to save the second campaign Then the system enforces one active campaign per post or requires non-overlapping triggers And if a conflict exists without resolution, the save is blocked with a descriptive error naming the conflicting campaign Or if a priority model is enabled, the highest-priority campaign handles matches and others are skipped with a logged reason And a test DM for that post results in at most one auto-reply

IntentSense

Understands the DM’s purpose (book, reschedule, quote, rescue intake, general question) and routes each sender to the best next step—booking link, quick SMS number capture, or live assist. Reduces back-and-forth, shows the right services first, and shortens time-to-book from social chats.

Requirements

Intent Classification Engine with Confidence & Fallback
"As a busy groomer managing messages, I want the system to automatically understand what a client is asking so that I don’t have to manually read and interpret every DM before taking action."
Description

Build an NLP-driven classifier that recognizes inbound DM intents (book, reschedule, cancel, quote, rescue intake, general question) with confidence scores, entity extraction (pet name, service type, date/time, location), and language detection. Provide configurable confidence thresholds, per-intent fallback messaging, and safe default behaviors when confidence is low. Support short-form, emojis, typos, and common grooming/walking/training terminology. Expose the detected intent, confidence, and extracted entities to downstream systems via an internal API, and log all decisions for auditability and continuous model improvement.

Acceptance Criteria
High-Confidence Intent Classification and Routing via Internal API
Given an inbound DM that clearly requests a service action (e.g., booking or rescheduling) in a supported language When the classifier processes the message Then it returns a detected_intent of book or reschedule with confidence >= the configured per-intent threshold And extracts available entities (pet_name, service_type, date_time, location) and normalizes date_time to ISO 8601 and service_type to the service catalog ID And posts a JSON payload to the internal API within p95 <= 750 ms containing request_id, detected_intent, confidence, entities, language, threshold_used, model_version And the router selects the corresponding next action (booking link for book, reschedule flow for reschedule) And no fallback message is sent
Low-Confidence Fallback and Safe Default Behavior
Given an inbound DM with ambiguous or conflicting content When the top predicted intent confidence is < the configured threshold for that intent Then the system sends the configured fallback message for low-confidence classification within p95 <= 2 seconds And no irreversible action is taken (no bookings created, no cancellations executed) And the internal API receives a payload with outcome="low_confidence" and includes the top 3 candidate intents with their confidences And, if enabled, the conversation is queued for live assist with priority "needs clarification"
Per-Intent Confidence Threshold Configuration and Enforcement
Given per-intent thresholds are stored in configuration (book, reschedule, cancel, quote, rescue_intake, general_question) When an authorized admin updates threshold values within [0.50, 0.99] Then invalid values are rejected with a validation error and no changes are applied And valid changes are persisted and versioned with actor, timestamp, and change reason And the classifier enforces the new thresholds within 5 minutes of change propagation And decision logs include the threshold_used value for each classification
Language Detection and Language-Aware Responses
Given an inbound DM written in a non-English supported language (e.g., Spanish) When the message is processed Then language detection returns the correct ISO 639-1 code (e.g., "es") with confidence >= 0.90 And the classifier uses the language-appropriate model or mapping for intent prediction And fallback/clarification messages are sent in the detected language when templates exist; otherwise a safe English default is used And the internal API payload includes language and language_confidence fields
Robustness to Short-Form, Emojis, Typos, and Domain Terminology
Given a curated test set of 100+ noisy messages per intent containing shorthand ("tmrw", "pls"), emojis, and typical grooming/walking/training jargon When the classifier is evaluated against this set Then top-1 intent accuracy is >= 90% overall and >= 85% per intent And p95 classification latency is <= 750 ms per message And examples with only noise (no actionable intent) are classified as general_question with confidence < action thresholds and trigger clarification
Entity Extraction, Normalization, and Clarification Prompts
Given an inbound DM that includes entity hints (pet name, service type, date/time, location) When processed by the entity extractor Then extracted entities include keys: pet_name (string), service_type (catalog ID), date_time (ISO 8601 with timezone), location (normalized address or area) And per-entity validation ensures service_type exists in the catalog and date_time is in the future (for bookings) or matches an existing appointment (for reschedules/cancels) And if a required entity for the detected intent is missing (e.g., date_time for book), a targeted clarification prompt is sent asking only for the missing entity And partial entities already captured are preserved and not re-asked in subsequent prompts
Decision Logging and Auditability for Continuous Improvement
Given any inbound DM processed by the classifier When a decision is made Then a structured log entry is written within 1 second containing: request_id, timestamp, raw_text_id or redacted_text, detected_intent, confidence, top_3_candidates, entities, language, threshold_used, routing_action, model_version, processing_time_ms And logs are queryable by request_id and time range within the logging system within 1 minute of ingestion And logs are retained for at least 90 days and exportable in NDJSON format for offline analysis
Multi-channel DM Ingestion & Identity Linking
"As an independent trainer, I want all DMs from different apps to appear in one place tied to the correct client so that I can respond quickly without juggling multiple inboxes."
Description

Ingest messages from SMS, Instagram DM, Facebook Messenger, WhatsApp, and Google Business Messages via secure webhooks or platform APIs. Normalize messages into a unified format and associate each sender with an existing FetchFlow contact and Pet Profile when possible. When no match exists, create a provisional lead and attempt identity resolution using name, handle, and captured phone number. Maintain channel metadata for compliance and reply routing, and ensure message continuity across channels for the same client.

Acceptance Criteria
Secure Multi-Channel Ingestion and Normalization
Given an authenticated webhook/request from SMS, Instagram DM, Facebook Messenger, WhatsApp, or Google Business Messages, When a message arrives, Then the system validates the platform signature/token, responds 2xx within 500 ms, and persists a normalized record within 2 seconds containing canonical_message_id, platform_message_id, channel, sender_external_id, sender_handle/display_name, message_text, attachments[], received_at_utc, thread_id/conversation_id, and consent_state. Given an incoming request with an invalid or missing signature/token, When processed, Then the system returns 401/403, does not persist any record, and writes a security audit event. Given duplicate deliveries of the same platform_message_id, When processed, Then the system stores a single message using idempotency and marks subsequent deliveries as duplicates without side effects. Given platform API pagination or transient 5xx/429 errors, When fetching/backfilling messages, Then the system retries with exponential backoff up to 5 attempts, preserves message ordering, and achieves eventual consistency within 5 minutes for backlogs under 10,000 messages. Given a normalized message is persisted, When post-processing occurs, Then an event is published to the IntentSense queue/topic within 300 ms.
Deterministic Identity Linking Across Channels
Given a normalized message with a phone number matching a verified phone on an existing FetchFlow contact, When processed, Then the message and all subsequent messages from the same sender are linked to that contact and its default Pet Profile. Given a sender_handle or platform sender ID that is already mapped to a FetchFlow contact, When processed, Then the message is linked to that contact with no provisional lead created. Given multiple contacts would match deterministically (e.g., duplicate verified phone numbers), When processed, Then the message is not auto-linked; a conflict is created for manual resolution and the sender is treated as a provisional lead. Given a deterministic link is made, When viewed in the dashboard, Then the message shows the contact name and Pet Profile badge within 1 second of page load.
Probabilistic Identity Resolution with Confidence Thresholds
Given no deterministic match exists, When similarity is computed using name/handle/display_name against existing contacts, Then if confidence ≥ 0.90 the message is auto-linked to the best match; if 0.60–0.89 a provisional lead is created with candidate matches listed and status "needs_review"; if < 0.60 a provisional lead is created with no candidates. Given a later verified phone number is captured from the same sender, When re-evaluating, Then the system links to a unique matching contact (if found) and updates confidence to 1.0; otherwise retains "needs_review" with candidates. Given an operator rejects an auto-link as incorrect, When unlinked, Then the system records feedback and prevents re-linking to the same contact for that sender unless explicitly overridden.
Provisional Lead Creation and Enrichment
Given no existing contact is matched, When the first message from a sender is ingested, Then a provisional lead is created with fields: lead_id, source_channel, external_handle/sender_id, display_name, first_message_excerpt, created_at, and consent_state="unknown". Given subsequent messages arrive from the same external sender on the same channel, When processed, Then they are attached to the existing provisional lead without creating duplicates. Given a phone capture link is issued and the sender replies with a valid E.164 number and opt-in, When processed, Then the lead is updated with phone, consent_state="opted_in", and identity resolution is re-run to link/merge with an existing contact if confidence ≥ 0.90. Given a provisional lead is merged into an existing contact, When merge completes, Then all associated messages are re-attributed within 5 seconds and a merge audit entry is recorded.
Channel Metadata Persistence and Reply Routing Compliance
Given a message is normalized, When stored, Then platform metadata required for compliance and replies is persisted: thread_id/conversation_id, page_id/phone_number_id, platform_message_id, policy window timestamps, and opt-in tokens/consent state. Given an agent attempts to reply, When within the originating channel's allowed policy window, Then the reply is sent via that channel using the correct thread/context identifiers. Given an agent attempts to reply on WhatsApp outside the 24-hour session window, When sending, Then the system blocks free-form messages and requires an approved template or prompts to switch to SMS capture with explicit consent. Given an agent attempts to send SMS to a sender without recorded opt-in, When sending, Then the system blocks the message and prompts to initiate opt-in capture. Given the originating channel cannot accept replies (e.g., platform outage), When detected, Then the system prevents sending, queues the draft, and suggests alternate compliant channels.
Cross-Channel Conversation Continuity and De-duplication
Given messages from multiple channels are linked to the same contact, When viewed in the dashboard, Then a unified conversation timeline renders in chronological order with channel badges and zero duplicate entries. Given an identity merge occurs, When completed, Then historical messages from all linked channels are re-attributed to the unified contact within 5 seconds and remain deduplicated. Given an unmerge is performed, When completed, Then messages are correctly separated back to their prior identities without data loss and with audit traceability. Given messages include attachments, When displayed, Then attachments are retrievable and rendered with their original channel context and secure URLs.
Auditability, Security, and Error Resilience
Given any identity link, merge, unmerge, or manual override action, When executed, Then an immutable audit log entry is created capturing actor, timestamp, action, rationale, previous/new identifiers, and affected messages. Given PII (contact/lead attributes or messages) is accessed, When viewed via the dashboard or API, Then access is logged with user, timestamp, and purpose and is retrievable for compliance reports. Given messages and identity data are stored, When at rest or in transit, Then they are encrypted (AES-256 at rest, TLS 1.2+ in transit) and encryption status is verifiable via system health checks. Given a platform outage or internal ingestion failure, When detected, Then inbound payloads are queued durably for at least 24 hours, retries use exponential backoff with jitter, and an alert is raised if backlog age exceeds 5 minutes or failure rate exceeds 1%. Given a GDPR/CCPA deletion request for a contact, When processed, Then linked identities and messages across channels are deleted or anonymized within 30 days, reply routing tokens invalidated, and a deletion audit record created.
Adaptive Routing Orchestrator
"As a walker, I want each DM to be routed to the fastest resolution path so that clients can book or get answers without prolonged back-and-forth."
Description

Implement a rules- and context-driven router that, based on detected intent, confidence, business hours, staff availability, and client status (new vs. returning), determines the best next step: personalized booking link, quick number capture, automated answer, or live assist. Allow admins to configure per-intent routing rules, SLAs, and escalation paths. Ensure idempotent routing decisions and record outcomes (sent link, captured info, escalated) for analytics and re-engagement.

Acceptance Criteria
Returning Client: High-Confidence Booking During Business Hours
Given an inbound message from a known client with an existing Pet Profile and a detected intent of "book" at or above the configured confidence threshold And the current time is within configured business hours When the router evaluates routing rules Then it sends a personalized booking link within 2 seconds that prioritizes the client's last-used or eligible services and pre-fills pet details And records a routing decision with outcome=sent_link, intent=book, confidence_score, client_status=returning, rule_version, and timestamp And ensures only one message is sent per decision within the deduplication window
New Client Number Capture on Low-Confidence DM
Given an inbound social DM from an unknown sender with no phone on file and detected intent confidence below the booking threshold or multiple competing intents When the router applies per-intent rules Then it initiates a quick number capture prompt in-channel, validates E.164 format or local rules, and stores explicit SMS opt-in consent And associates the captured number with a new client profile and Pet Profile Smart Card placeholder And re-evaluates the latest message with updated context within 5 seconds And records outcome=captured_info with metadata (intent_candidates, chosen_path=number_capture, timestamp, rule_version)
After-Hours or No-Staff Escalation to Live Assist with SLA
Given an inbound message with intent "reschedule" or "rescue intake" or configured human_required intents And current time is outside business hours or no agent with required skill is available When the router evaluates SLA rules for this intent Then it assigns the conversation to the appropriate queue with SLA target per configuration And sends an automated acknowledgment with expected response time and escalation path And records outcome=escalated with queue, sla_target, eta, and rule_version And triggers secondary escalation notification if SLA breaches
Admin Can Configure Per-Intent Rules, Thresholds, and Escalation Paths
Given an admin with proper permissions edits routing rules (confidence thresholds, business hours, per-intent next steps, queues, SLA targets, fallback priorities) in the dashboard When they save changes Then inputs are validated (required fields, ranges, and timezones) And a new versioned rule set is persisted with audit info (who, when, diff) And the new rules take effect for messages received after save time without affecting in-flight conversations And only users with Admin role can modify or publish rules And the system provides a read-only simulation to preview routing outcomes for sample messages before publishing
Idempotent Routing Decisions and Deduplication
Given the same inbound message is delivered multiple times within a 2-minute deduplication window due to provider retries When the router processes the duplicates Then the same decision_id is reused and only one outward action (message/link/assignment) is executed And only one analytics event is emitted for the decision And subsequent duplicates are acknowledged without side effects
Outcome Recording for Analytics and Re-Engagement
Given any routing action is executed When the action completes Then an immutable outcome record is stored and an event is emitted containing decision_id, outcome_type (sent_link|captured_info|auto_answered|escalated), intent, confidence_bucket, client_status, channel, rule_version, timestamp And the outcome is queryable in the analytics dashboard and API within 1 minute And if outcome_type=sent_link and the link is not clicked within the configured re-engagement window, a follow-up task or message is created per rules
Automated Answer for High-Confidence General Question
Given an inbound message with intent "general question" at or above the configured confidence threshold and a matching knowledge base answer exists When the router evaluates the request Then it sends an automated answer within 2 seconds including the answer and a link to more details And offers an explicit option to escalate to a human agent And records outcome=auto_answered with kb_article_id and rule_version
Contextual Service Pre-filtering & Deep Links
"As a groomer, I want the booking link to already show the right services and times for that pet so that clients can confirm in a couple of taps."
Description

Generate booking deep links that pre-select services, staff, location, and available windows based on extracted intent, Pet Profile Smart Card data (breed/size, preferences, vaccine status), client history, package balances, and no-show policies. For quotes, compute and surface price estimates inline using configured rules. Respect staff schedules and blackout dates, and fall back gracefully when data is incomplete. Track link clicks and conversions to close the loop with routing performance.

Acceptance Criteria
Returning client books repeat service with package balance via DM deep link
Given a recognized client DM mentions booking the same service for a named pet with an applicable package balance When IntentSense generates a booking deep link Then the link is generated within 1.5 seconds at p95 and includes a unique tracking token tied to the DM thread and intent And the service is pre-selected to the pet’s last completed service within the same category And the staff is pre-selected to the last provider if available; otherwise staff is set to Any and reason "provider_unavailable" is logged And the location is pre-selected to the client’s preferred location; if none, default to last used And the first 10 available windows within the next 14 days are returned for the selected staff/location, respecting staff schedules and blackout dates And the pricing summary reflects package credit usage (e.g., "uses 1 of N") and displays the no-show policy notice When the client opens the link Then the booking page loads with all pre-selections applied in under 2 seconds on 4G and analytics records "link_opened" When the client confirms the booking Then analytics records "booking_confirmed" attributed to the tracking token and the conversion is visible in the dashboard within 60 seconds
New client missing Smart Card data receives graceful pre-filter fallback
Given an unrecognized sender requests a dog walk and has no Pet Profile Smart Card on file When a deep link is generated Then the link routes first to Smart Card capture requesting pet name, breed/size, and vaccine status And service category "Dog Walking" is pre-selected with default duration and price range shown as an estimate And available windows are hidden until required Smart Card fields are completed; upon completion, windows generate respecting staff schedules and blackout dates And if staff schedules cannot be retrieved, a message "No slots available; we'll follow up" is shown and the flow routes to live assist; fallback reason is logged as "schedule_unavailable" And analytics logs link_generated, profile_started, profile_completed (if completed), and routed_to_assist (if routed)
Reschedule flow preserves constraints and policies
Given a client references an existing appointment to reschedule via DM When a reschedule deep link is generated Then service, location, add-ons, and pet are pre-selected from the original appointment And staff is pre-selected to the original provider unless policy disallows; if disallowed, staff is set to Any with reason "policy_conflict" And available windows exclude blackout dates and any times outside the policy-defined reschedule window (e.g., 24h cutoff) And any late reschedule fee is displayed inline prior to confirmation When the client confirms a new slot Then the original appointment is canceled and the new one created atomically without double-booking, and analytics records "reschedule_confirmed" linked to the original appointment ID
Quote intent returns inline price estimate with deep link CTA
Given a DM asks for a price for a specific service and the pet’s breed/size is known from the Smart Card When IntentSense computes a quote Then the estimate applies configured rules: size upcharge, add-ons, location modifiers, package discounts (if available), and tax And the estimate returns within 800 ms at p95 and is displayed inline with a breakdown and a "Book now" deep link carrying the priced configuration And if configuration ambiguity exists (e.g., unknown coat condition), a min–max range is displayed with stated assumptions And bookings from the quote link are attributed to the quote ID in analytics
Scheduling respects staff calendars, blackout dates, and time zones
Given a deep link needs to present availability for service S at location L When computing availability Then only slots where the selected staff (or Any) is scheduled at L and not on a blackout date/time are offered And org/location holiday blackout dates remove all slots on those dates And all times are rendered in L’s time zone while bookings are stored in UTC with the correct offset And attempting to use a stale link to a now-blackout slot shows a clear error and offers the next 5 alternatives And availability computation completes within 1.2 seconds at p95 for a 14-day search window
Tracking of link clicks, views, and conversions with deduplication
Given any generated deep link is sent to a client When the link is delivered Then a unique, signed, non-PII tracking token including intent, channel, and timestamp is attached When the client clicks the link across devices multiple times Then events link_sent, link_clicked, page_viewed, booking_started, and booking_confirmed are recorded with device metadata and deduplicated by token+session within a 30-minute window And tokens expire after 30 days; expired tokens redirect to a fresh flow while preserving attribution And routing performance reports show CTR and conversion rate per intent/channel updated at least hourly
Multi-pet disambiguation and preference-aware filtering
Given a client with multiple pets requests a specific service for a named pet via DM When generating a booking deep link Then the pet is resolved by name; if ambiguous, a pet selector defaults to matching names, otherwise lists all pets And the service list is pre-filtered to those compatible with the selected pet’s Smart Card preferences and vaccine status And if vaccine status is expired or missing for required services, the flow requires a vaccine update step before slot selection And staff and time windows exclude providers and times marked incompatible with the pet’s preferences (e.g., no crates, quiet hours only)
Quick Number Capture & Consent Compliance
"As a trainer, I want the system to collect a client’s phone number and permission automatically so that I can text reminders and confirmations without worrying about compliance."
Description

For social DMs without a verified phone number, trigger a compliant, minimal-friction SMS opt-in flow that captures the client’s mobile number and consent (TCPA/CTIA), stores timestamps and channel source, and links the number to the client record. Provide dynamic prompts tailored to intent, retry/backoff logic, and clear opt-out instructions. Block outbound SMS until consent is secured and display consent status in the dashboard.

Acceptance Criteria
Instagram DM Booking Intent – Quick Number Capture Prompt
Given a social DM thread is classified as booking intent and the sender has no verified mobile on file When IntentSense responds in the DM Then the system sends exactly one concise prompt requesting the user's mobile number to continue booking And the prompt dynamically references the booking intent (e.g., grooming appointment) and next best step And the prompt states that SMS will be used for scheduling and includes a link to Terms and Privacy And no additional prompts are sent until the user replies or 15 minutes elapse And the prompt send event is logged with channel source and thread identifier
SMS Double Opt-In and Compliance Record Creation
Given the user supplies a phone number via DM When the system receives the number Then the number is normalized to E.164 format and stored with provisional status and channel source And an initial SMS is sent containing program name (FetchFlow), expected frequency, HELP/STOP instructions, Terms/Privacy links, and 'Msg & data rates may apply' And the SMS explicitly asks the user to reply YES to consent And no non-consent content is sent via SMS before a YES is received When the user replies YES Then consent status becomes Active and the timestamp, phone, channel source, and consent-text version ID are recorded on the client record and audit log When the user replies with anything other than YES (excluding HELP/STOP keywords) Then consent remains Pending and no non-consent messages are sent
Outbound SMS Blocking Until Consent Active
Given a client has no consent or Pending consent When any automated or manual outbound SMS is attempted (reminders, booking links, broadcasts, 1:1) Then the system blocks the send and shows a 'Consent required' notice with an option to re-send the opt-in SMS And suggests alternate channels (e.g., reply in DM) if available And prevents queued SMS from sending until consent becomes Active And all blocked attempts are logged with user, time, and reason
Retry/Backoff and Live Assist Fallback for Unresponsive Users
Given the initial DM prompt was sent and no number is provided When 15 minutes elapse without a response Then send exactly one gentle DM nudge to request the mobile number When 24 hours elapse without a number Then stop nudges, mark capture as Unsuccessful, and place the thread in the Live Assist queue Given a number was received but the user has not replied YES to the opt-in SMS When 12 hours elapse after the initial opt-in SMS Then send exactly one reminder SMS including HELP/STOP and Terms/Privacy links When 7 days elapse from the initial opt-in SMS without YES Then cease reminders, keep consent as Pending, and surface the client for Live Assist follow-up
Dashboard Consent Status, Source, and Audit Linkage
Given a client record exists When the consent flow progresses through states Then the dashboard displays a consent badge with states: None, Pending, Active, Revoked And shows the last updated timestamp, channel source (e.g., Instagram DM), and a link to the consent audit log And disables SMS send actions for None, Pending, and Revoked with an explanatory tooltip And upon Active consent, links the mobile number to the client record and enables SMS actions And allows staff to filter and search clients by consent status and channel source
Opt-Out and Help Keywords Enforcement
Given a client with Active consent When the client sends STOP, END, CANCEL, UNSUBSCRIBE, or QUIT Then the system immediately marks consent as Revoked, sends a single confirmation SMS, and blocks all future outbound SMS When the client sends HELP Then the system replies with program name, support contact, and HELP/STOP instructions, and logs the event When the client sends START, YES, or UNSTOP after Revoked Then the system re-activates consent, records the timestamp and source, and updates the dashboard badge and audit log
Phone Number Validation, E.164 Formatting, and Duplicate Handling
Given the user provides a phone number in DM When the number is parsed Then the system validates it as a possible mobile and normalizes it to E.164; if invalid, sends a single DM prompt asking for a valid mobile with an example format And if the normalized number matches an existing client record, the system links it without creating a duplicate and logs the linkage And if the number is already linked to a different client, the system blocks auto-linking, flags for review, and routes to Live Assist And all number entries, validations, corrections, and link decisions are recorded with timestamps and channel source
Live Assist Handoff with Auto-Summary
"As a sole proprietor, I want tricky DMs to come to me with a concise summary and suggested actions so that I can resolve them quickly without rereading the entire conversation."
Description

When automation confidence is low or rules require human intervention, escalate the thread to a human with an AI-generated summary that includes detected intent, key entities (pet, service, date), client history highlights, suggested replies, and recommended next actions. Show real-time typing indicators, queue position, and SLA timers. Allow agents to override intent, trigger booking links, or schedule follow-ups directly from the conversation view.

Acceptance Criteria
Handoff on Low Confidence with Complete Auto-Summary
Given a detected intent confidence score below 0.60 or an escalate_required rule evaluates true When the inbound message is processed Then the conversation is escalated to a human agent within 2 seconds of classification And an auto-summary is generated and attached before the agent’s first reply is possible And the summary includes fields: detected_intent, pet_name(s), service_type, date_time, client_history_highlights (last 3 interactions and any packages/no-show notes), suggested_replies (>=2), recommended_next_actions (>=2) And any missing entity is displayed as "Unknown" rather than omitted And the escalation event is logged with confidence score and trigger reason
Rule-Based Handoff to Specialist Queues (Rescue Intake / Payment Dispute)
Given a message matches a configured handoff rule (e.g., rescue_intake, payment_dispute, policy_exception) by rule ID or exact pattern When classification occurs Then automation is halted and the thread is routed to the mapped Specialist queue within 2 seconds And the client receives an immediate system SMS: "Connecting you to a specialist now" And the auto-summary includes policy_flags with the triggering rule ID, and risk_notes if present And queue routing, rule ID, and timestamp are recorded in the audit log
Real-Time Typing Indicators During Live Assist
Given a conversation is in Live Assist state When the agent types for >300ms Then the client sees "Agent is typing…" within 500ms and it clears within 1s after the agent stops typing When the client types for >300ms Then the agent sees "Client is typing…" within 500ms and it clears within 1s after typing stops And on network disconnects, any active typing indicator auto-clears within 2 seconds
Queue Position and SLA Timers Visible to Client and Agent
Given a handoff has been initiated When the client view loads Then the client sees queue position (e.g., "You’re #N in line") and estimated wait time based on live queue metrics And position updates within 2 seconds when it changes and is accurate within ±1 position And the agent console shows a first-response SLA countdown using the configured target (e.g., 2 minutes) And the timer turns amber at 30 seconds remaining and red upon breach, with a breach alert logged and notified to the assigned agent
Agent Overrides and Quick Actions from Conversation View
Given the agent is viewing an escalated thread with an auto-summary When the agent overrides the detected intent via the Intent dropdown Then the new intent is saved, reflected in analytics, and the previous intent is preserved in the audit trail When the agent clicks "Send Booking Link" Then a link is sent via SMS prefilled with service_type, pet_name, and suggested date_time from the summary, and the send event is tracked When the agent clicks "Schedule Follow-Up" Then a follow-up task is created with client contact prefilled and date/time selected; a confirmation SMS is sent to the client And quick actions are idempotent (repeated clicks within 5 seconds do not create duplicates) and each action is audit-logged
Auditability and Data Redaction for Handoff Events
Given any Live Assist handoff occurs When audit records are written Then each record includes: timestamp, agent_id (if assigned), previous_intent, new_intent (if overridden), summary_version, actions_taken, and SLA status at first response And stored summaries redact PII (mask phone and email) while retaining pet names and service names And users without the "See AI Suggestions" permission cannot view suggested reply content And retrieving the last 100 audit events completes in <500ms under normal load
Conversion Analytics & Prompt A/B Testing
"As a business owner, I want to see how well automated replies and links convert across channels so that I can tune prompts and improve booking speed."
Description

Provide dashboards tracking time-to-book, first-contact resolution, conversion rate by channel and intent, abandonment points, and human escalation rates. Enable A/B testing of prompts, reply templates, and routing thresholds, and attribute performance to specific configurations. Export metrics to the main FetchFlow analytics and allow CSV export. Use results to recommend optimizations to admins.

Acceptance Criteria
Dashboard shows conversion KPIs by channel and intent
Given a selected date range, When the dashboard loads, Then it displays time-to-book (median and p90), first-contact resolution rate, conversion rate, abandonment rate, and human escalation rate broken down by channel and intent. Given filters for date range, channel, intent, and prompt variant, When a filter is applied, Then all widgets update within 2 seconds for datasets ≤100k conversations and display a "Last updated" timestamp. Given metric definitions, When a user hovers a metric, Then a tooltip shows the formula and event definitions used. Given an empty dataset for the selected filters, When the dashboard loads, Then it shows a "No data" state with zero metrics and no errors. Given backend event logs, When metrics are recomputed offline, Then displayed values match within 1% for counts ≥1,000 and within 0.5 minutes for time-to-book.
Funnel visualization highlights abandonment points
Given a selected cohort, When viewing the funnel, Then the stages are displayed: message_received → intent_classified → next_step_offered → booking_link_clicked → booking_started → booking_completed. Given the funnel, When computed, Then drop-off percentage is shown at each transition and the top 3 abandonment points are highlighted. Given a highlighted abandonment point, When selected, Then the user can drill down to up to 50 sample redacted transcripts filtered to that step. Given event logs, When comparing counts, Then funnel stage counts match raw events within 1% for stages with ≥1,000 events. Given a date range change, When applied, Then the funnel recomputes within 3 seconds for ≤100k conversations.
Create and run A/B tests for prompts, reply templates, and routing thresholds
Given admin permissions, When creating a test, Then they can choose target type (prompt, reply template, routing threshold), define 2–5 variants, and assign traffic splits totaling 100%. Given a sender engages, When assigned to a test, Then randomization occurs at the sender level and the same variant persists across all sessions for 30 days or until test end. Given start and end times, When the end time is reached, Then the test status changes to Completed and traffic routes to the preselected default or winning variant if selected. Given conflicting tests for the same intent, When attempting to launch, Then the system blocks launch and explains the conflict. Given test activity, When events are logged, Then each message and conversion includes testId and variantId in analytics.
Attribute performance and compute significance for variants
Given a running test, When viewing results, Then the user can select a primary metric (conversion rate, time-to-book, FCR) and see per-variant sample size, metric value, 95% confidence interval, and p-value vs control. Given guardrails, When a variant increases human escalation rate by >5% absolute vs control, Then it is flagged and excluded from auto-recommendations. Given attribution rules, When computing conversions, Then only conversions within 7 days of first message and matching intent are attributed to the variant. Given minimum evidence, When per-variant sample size is <200 conversations or <20 conversions, Then the UI labels the result "Insufficient sample" and withholds winner calls. Given CSV export of results, When recomputed externally, Then statistics match within 0.5% for rates and 0.5 minutes for time-to-book.
Export analytics to main FetchFlow dashboards and CSV
Given export configuration, When on-demand export is triggered, Then records for the current filters are sent to main FetchFlow analytics with deterministic unique event IDs and no duplicates. Given permissions, When a user without export rights attempts export, Then access is denied with an explanation. Given CSV export, When downloaded, Then the file contains headers, is UTF-8 encoded, uses ISO 8601 timestamps with timezone, and reflects current filters including testId and variantId. Given dataset size ≤100k rows, When exporting CSV, Then the export completes within 60 seconds; for >100k rows, pagination is offered. Given PII policy, When exporting, Then customer phone numbers are hashed and message content is excluded.
System recommends prompt and routing optimizations to admins
Given a completed test that meets significance and guardrails, When results are available, Then an in-app recommendation card shows the winning variant, expected lift with 95% CI, and rationale. Given the recommendation card, When the admin clicks "Apply", Then the system sets the winning variant as default, records an audit log entry with user, timestamp, and before/after config, and confirms success. Given multiple recommendations, When listing, Then items are ranked by estimated impact and dismissed items are hidden for 14 days. Given inconclusive results, When no variant meets significance thresholds, Then the system recommends extending the test with suggested sample size and duration. Given owner notifications, When a recommendation is generated, Then an email and in-app notification is sent to owners within 5 minutes.

Deep Prefill

Pre-fills the SMS booking link beyond handle and pet name—auto-detects service interest from the DM, suggests nearby time windows, applies known packages, and attaches consent flows. Clients tap once to confirm, cutting typing and errors while boosting conversion from social to scheduled.

Requirements

Intent Detection & Entity Extraction
"As an independent groomer using SMS, I want the system to understand what service and when from a client's text so that I don't have to manually interpret and type details into the booking."
Description

Parse inbound SMS/DM text to infer requested service type (groom, walk, train), referenced pet(s), preferred dates/times, location, and special instructions using lightweight NLP with rule-based fallbacks. Cross-reference the sender to client records and Pet Profile Smart Cards to resolve pet names and known preferences, normalize entities (duration, breed constraints), and map to catalog SKUs. Output a structured intent object with confidence scores and missing-field flags for downstream prefill. Support abbreviations, emojis, and common typos; handle multi-pet messages and colloquialisms across common social and SMS channels.

Acceptance Criteria
Infer Service Intent from Noisy SMS/DM
Given an inbound message "can u do a ✂️ for Coco tmrw?" from SMS and the language model is available When the message is processed Then intent.service_type = "groom" and intent.confidence.service_type >= 0.85 Given an inbound message "need a walk for Max" from Instagram DM When processed Then intent.service_type = "walk" and intent.confidence.service_type >= 0.85 Given an inbound message "can you train my pup?" When processed Then intent.service_type = "train" and intent.confidence.service_type >= 0.85 Given an inbound message "grom for coco" with a common typo When processed Then intent.service_type = "groom" and intent.confidence.service_type >= 0.8 Given an inbound message "thanks!" that contains no service request When processed Then intent.service_type = null and intent.confidence.service_type < 0.5 and intent.missing_fields includes "service_type"
Client and Pet Resolution via Smart Cards
Given a sender phone number that matches a client record with pet "Coconut" (nickname "Coco") and message "groom for Coco" When processed Then intent.pets contains [{pet_id: <Coconut_ID>, name: "Coconut"}] and intent.confidence.pets >= 0.9 Given a client with pets ["Max", "Luna"] and message "walk Max & Luna" When processed Then intent.pets contains both pet_ids for Max and Luna Given a client with a single pet "Buddy" and message "he needs a walk" When processed Then pronoun resolution sets intent.pets = [{pet_id: <Buddy_ID>, name: "Buddy"}] with confidence >= 0.8 Given a message "groom for Rocky" and no matching pet in client profile When processed Then intent.pets = [{pet_id: null, name: "Rocky"}] and intent.missing_fields includes "pet_profile"
Normalize Date/Time Preferences from Natural Language
Given business timezone America/Los_Angeles and settings morning=08:00-12:00, afternoon=12:00-17:00, evening=17:00-20:00 and message "tmrw 10-12" received on 2025-08-11 When processed Then intent.date_time.preferences = [{start:"2025-08-12T10:00:00-07:00", end:"2025-08-12T12:00:00-07:00"}] and confidence >= 0.85 Given the same settings and message "next Fri afternoon" When processed Then intent.date_time.preferences = one window on the next Friday from 12:00 to 17:00 in business timezone and confidence >= 0.8 Given message "11ish tomorrow" When processed Then intent.date_time.preferences = [{start: tomorrow at 10:45, end: tomorrow at 11:15 in business timezone}] using ±15m rule for "ish" Given message "any time after 4" When processed Then intent.date_time.preferences first window starts at 16:00 and ends at the configured day end and confidence >= 0.75 Given message "soon" with no concrete time reference When processed Then intent.date_time.preferences = [] and intent.missing_fields includes "date_time"
Map to Catalog SKUs, Packages, and Consent
Given catalog synonyms mapping "nail trim" -> SKU "groom_nail_trim" and message "nail trim for Max" When processed Then intent.sku_id = "groom_nail_trim" and confidence.sku_id >= 0.85 Given client has an active package credit applicable to SKU "groom_nail_trim" When processed Then intent.package_applied = true and intent.package_id is populated Given selected SKU requires consent_flow_id "GROOM_STANDARD" and client has no valid consent on file When processed Then intent.consent_flows includes "GROOM_STANDARD" Given message "groom or bath for Coco" and mappings to multiple SKUs with equal scores When processed Then intent.sku_id = null and intent.missing_fields includes "service_subtype" and intent.disambiguation_options includes the tied SKU ids Given message contains a recognized service subtype alias (e.g., "deshed") When processed Then intent.sku_id maps to the configured deshed SKU with confidence >= 0.8
Handle Multi-Pet, Mixed Services, and Special Instructions
Given message "30m walk for Luna at the park; full groom for Max, he hates dryers" and client with pets Luna and Max with saved location "park" for walks When processed Then intent.line_items = [ {pet_id:<Luna_ID>, service_type:"walk", duration_minutes:30, location:"park"}, {pet_id:<Max_ID>, service_type:"groom", duration_minutes:null, location:null, special_instructions contains "hates dryers"} ] Given Smart Card for Max notes breed constraint "no high-heat drying" When processed Then that constraint is merged into the groom line item's special_instructions without duplication Given durations specified as "30m" or "1 hr" When processed Then durations are normalized to minutes as integers (e.g., 30, 60)
Low-Confidence Fallback, Partial Prefill, and Flags
Given message "need a wak for lna tmrw" and client pets include "Luna" When processed Then rule-based fallback corrects walk/wak and "Luna" via edit distance ≤ 2, producing intent.service_type="walk" and pets includes <Luna_ID> with confidence >= 0.75 Given any message where intent.confidence.service_type < 0.7 after ML pass When processed Then rule-based patterns are applied; if still < 0.7 Then intent.service_type = null and intent.missing_fields includes "service_type" Given a message containing only an image with no text When processed Then intent has no extracted entities, all confidences for core fields are 0, and missing_fields includes ["service_type","pets","date_time"] Given processing load within normal limits and message length ≤ 280 chars When processed Then 95th percentile end-to-end parsing latency ≤ 500 ms Given channels SMS and Instagram DM are enabled When processed Then channel-specific metadata is handled without altering extracted entities or confidences
Smart Time Window Suggestions
"As a dog walker, I want the SMS link to suggest realistic time windows that fit my schedule so that I can confirm bookings quickly without back-and-forth."
Description

Generate up to three nearby time window options based on the intent object, provider real-time availability, service duration, travel buffers, blackout times, and client timezone. Avoid double-booking, respect recurring blocks, daylight/curfew constraints, and prep/cleanup times. Learn client preferences and seasonality to rank options. Expose configuration for window size and count, return windows with reservation tokens, and hold them with a short TTL to reduce conflicts until the client confirms.

Acceptance Criteria
Ranked nearby windows by intent and preferences
Given an intent object with serviceId SVC-101, anchorDateTime 2025-08-15T09:00:00-04:00, and serviceDuration 60 minutes And the client profile indicates a learned preference for morning appointments And seasonality data for August favors morning slots for this service And provider PROV-22 has open availability on the anchor date And configuration sets maxWindowCount=3 and windowLength=120 minutes When the system generates time window suggestions Then it returns no more than 3 window options within ±120 minutes of the anchorDateTime in the client timezone And each window range is 120 minutes long and contains at least one feasible start time that accommodates serviceDuration plus required buffers And the windows are ranked by predicted client acceptance, with morning-aligned windows ranked higher when available And each window is conflict-free at generation time against the provider’s schedule and holds
Conflict-free windows with real-time availability
Given provider PROV-22 has a confirmed appointment 2025-08-15T10:00:00-04:00 to 11:00:00-04:00 And a soft-hold exists from 12:00:00-04:00 to 12:30:00-04:00 And travel buffers of 15 minutes before and after each engagement are required And the requested serviceDuration is 45 minutes When the system generates time window suggestions for that date Then no suggested window overlaps any confirmed appointment or existing hold when buffers are applied And no feasible start time within any suggested window would create an overlap when buffers and serviceDuration are applied And availability is checked against the latest schedule state at generation time
Travel, prep, and cleanup buffers honored
Given a service with serviceDuration 60 minutes And required prep time of 10 minutes, cleanup time of 10 minutes, and travel buffer of 15 minutes before and after When the system generates time window suggestions Then each suggested window contains at least one start time where the total blocked time (prep + travel-before + serviceDuration + cleanup + travel-after) fits without overlapping other commitments And the final booking created from any suggested window reserves the total blocked time including all buffers
Blackouts, recurring blocks, curfew, and DST boundaries respected
Given provider PROV-22 has a blackout 13:00–14:00 local time on 2025-11-03 And a recurring block every Monday 09:00–10:00 local time And business curfew is 20:00–07:00 local time And the anchor date falls on a weekend with a DST change (fall-back) in the region When the system generates time window suggestions for that period Then no window starts before 07:00 or ends after 20:00 in the client timezone And no window overlaps the 13:00–14:00 blackout or the Monday 09:00–10:00 recurring block when mapped to local time And windows that span the DST change are computed in UTC and mapped correctly so no duplicate or missing hour occurs in the client display
Client timezone display and UTC storage
Given provider timezone is America/Los_Angeles and client timezone is America/New_York And the anchorDateTime is 2025-09-10T09:00:00-04:00 (client time) When the system generates time window suggestions Then all window timestamps are displayed to the client in America/New_York And internal scheduling and reservation tokens store the corresponding UTC instants and include timezone metadata And confirming a window schedules the provider’s calendar at the correct America/Los_Angeles local time corresponding to the displayed client time
Configurable window count and length with cap
Given admin configuration sets windowCount=5 and windowLength=90 minutes When the system generates time window suggestions Then it returns no more than 3 windows regardless of configuration exceeding the cap And each returned window range is exactly 90 minutes long And if configuration is later updated to windowCount=2, subsequent calls return no more than 2 windows And attempts to set windowLength below 30 minutes or above 240 minutes are rejected with validation errors and no change is applied
Reservation tokens with TTL holds and concurrency safety
Given holdTTL=5 minutes is configured And the system generates 3 window suggestions each with an attached reservation token When a client opens the link at T0 and receives the suggestions Then a soft hold is created for each underlying feasible slot until T0+5 minutes or until one is confirmed And each token is opaque, non-guessable, single-use, and expires at T0+5 minutes And confirming with a valid, unexpired token converts the hold to a booking and invalidates all other tokens for overlapping slots And confirming with an expired or already-consumed token fails with a 409/Conflict without creating a booking And competing confirmations for the same slot across clients result in exactly one success and consistent release of the losing holds
Package and Pricing Auto-Application
"As a trainer, I want the system to auto-apply my clients’ existing packages and correct pricing so that checkout is accurate and I don’t need to fix invoices later."
Description

Automatically attach applicable packages, passes, and pricing rules to the prefilled booking based on the client’s entitlements, pet profile, and service context. Deduct sessions from active packages, include commonly purchased add-ons, and pre-calculate taxes, surcharges, and no-show/late-cancel policies. Validate package eligibility and expiry, surface remaining balances, and provide provider override hooks prior to sending the link. Ensure price transparency in the confirmation screen and reflect changes in invoices and reports.

Acceptance Criteria
Eligibility & Expiry Validation for Package Application
Given a client opens an SMS booking link prefilled by Deep Prefill for a package-covered service When the system evaluates entitlements Then it attaches the package only if the package is active (today <= expiry), the service type matches, the pet profile is eligible (species/size), and the location matches package rules Given a package is expired or ineligible When evaluation runs Then no package is attached and the confirmation screen shows a non-blocking notice with reason code: EXPIRED, SERVICE_MISMATCH, PET_INELIGIBLE, or LOCATION_MISMATCH Given multiple eligible packages exist When evaluation runs Then the system selects the package that minimizes client out-of-pocket and exposes a toggle to switch packages before sending the link
Session Deduction & Remaining Balance Display
Given a package with N remaining sessions When a single-service booking is prefilled Then the pre-calculation deducts 1 session and displays "N-1 sessions remaining" in the confirmation Given Q identical services are in the booking and N < Q When pre-calculation runs Then min(Q, N) sessions are deducted and the remainder is priced per configured non-package rate or discount rules Given the provider disables package application via override before sending When the link is sent Then no sessions are deducted and the confirmation shows "Package not applied (overridden)" Given the booking is abandoned or canceled prior to confirmation When no appointment is created Then no package balance changes are persisted
Auto-Apply Common Add-ons with Provider Override
Given the client historically purchases add-on A with service S and the pet profile is eligible When prefill runs Then add-on A is preselected and included in the price calculation Given the provider deselects a preselected add-on before sending When the link is sent Then the confirmation and price exclude that add-on and an audit note "addon_removed_by_provider" is recorded Given an add-on requires consent When prefill includes it Then the corresponding consent flow is attached and must be acknowledged by the client to confirm
Transparent Price Breakdown in Client Confirmation
Given a prefilled booking When the client opens the confirmation screen Then an itemized breakdown is shown including: base price, package deduction value, add-ons, taxes, surcharges (holiday/after-hours/zone), and projected total due, using the business currency Given jurisdictional tax rules are configured When pre-calculation runs Then taxes are computed per applicable rates and displayed as separate labeled line items Given any price component was overridden by the provider When the client views the link Then the overridden value is shown with a "provider adjusted" indicator
No-Show and Late-Cancel Policy Pre-Calculation
Given no-show and late-cancel policies are configured (flat or percentage) When the booking is prefilled Then the confirmation displays the policy summary and the maximum fee estimate based on the current appointment value Given the appointment time qualifies for an after-hours or holiday surcharge When pre-calculation runs Then the surcharge is included and labeled, and if the policy is percentage-based the estimate reflects the surcharged amount Given the client acknowledges the policy When they confirm the booking Then the consent is stored with timestamp and policy version
Multi-Pet and Multi-Service Allocation
Given a booking contains multiple pets and services When pre-calculation runs Then eligible package sessions are allocated per pet-service mapping and balances reduced accordingly Given packages exist for only one pet in a multi-pet booking When pre-calculation runs Then only that pet’s eligible services use package sessions; other pets are priced per standard or configured discount rules Given insufficient sessions remain to cover all services When allocation runs Then remaining sessions are applied to minimize client out-of-pocket and the UI labels which items are covered vs not covered
Invoice, Receipt, and Reporting Consistency
Given the client confirms the booking When the invoice and receipt are generated Then applied packages, deductions, add-ons, taxes, surcharges, and policy consents appear with totals matching the confirmation screen Given a provider override was applied before sending When sales, package liability, and redemption reports are generated Then they reflect the override, adjust package balances, and recognize revenue per configured rules Given a booking is edited or canceled under policy When post-event processing runs Then invoices and reports update accordingly and any session restorations or fees are applied with an audit log entry
Consent and Policy Auto-Attach
"As a client, I want any required waivers to be included and prefilled in the link so that I can confirm in one tap without repeating forms."
Description

Attach required consents and policies (e.g., grooming consent, off-leash waiver, vaccine acknowledgement) to the prefilled booking based on the requested service and Pet Profile document status. Prefill known answers from the pet’s Smart Card, request only missing fields, capture e-signature and timestamp, and store signed artifacts linked to the appointment. Block confirmation when mandatory consents are missing while allowing optional policies to be deferred, and localize language for client preference with mobile-accessible, ADA-compliant UX.

Acceptance Criteria
Auto-Attach Required Consents Based on Service and Pet Profile Status
Given a Deep Prefill booking link for a detected service type When the booking page loads Then all consents/policies marked mandatory for that service and region are attached to the flow Given the pet’s profile has a valid, non-expired version of a required consent When the booking page loads Then that consent is not requested again unless a newer, re-consent-required version is active Given optional policies exist for the detected service When the booking page loads Then optional policies are attached and clearly labeled as optional Given no service is detected on load When the client selects a service Then the system attaches the corresponding mandatory and optional policies immediately
Prefill Known Answers and Request Only Missing Fields
Given the pet’s Smart Card contains data referenced by a consent (e.g., pet name, breed, vaccines, owner name) When the consent form renders Then those fields are prefilled and read-only where appropriate Given some required fields are missing or expired in the Smart Card When the consent form renders Then only the missing/expired fields are requested with inline validation and clear error messaging Given a booking includes multiple pets When a consent requires per-pet responses Then each pet’s section is prefilled from its Smart Card and the UI allows confirm/edit per pet before submission
Capture E‑Signature and Timestamp; Store Signed Artifact
Given a client completes the consent inputs When they provide an e-signature (typed, drawn, or tap-to-sign) and submit Then the system records the signature, server timestamp (ISO 8601), consent/policy version ID, and signer identity (client profile reference) Given the consent is successfully signed When the submission completes Then a non-editable signed artifact is generated and stored and a unique consent ID is created Given the artifact is stored When the appointment view is opened by staff or client Then the signed consent is retrievable and viewable/downloadable
Block Confirmation for Missing Mandatory Consents; Allow Optional Deferral
Given one or more mandatory consents are incomplete When the client taps Confirm Booking Then the booking does not finalize and the UI highlights missing mandatory consents with direct links to complete Given all mandatory consents are complete and optional policies remain When the client taps Confirm Booking Then the booking finalizes successfully and optional policies remain accessible to complete later without blocking Given the client completes the last mandatory consent When they return to the confirmation step Then the Confirm Booking action becomes enabled without page reload
Localized Consent Flow to Client Language Preference
Given the client has a language preference on profile or via device/SMS locale When the consent flow renders Then all policy texts, labels, and validation messages display in that language Given a required translation is unavailable When the consent flow renders Then the UI falls back to the business default language and presents a language switcher option Given a consent is signed When the artifact is generated Then the artifact metadata records the language code and policy version used
Mobile & ADA‑Compliant Consent UX
Given the consent flow is accessed on a mobile device (viewport 320–1024px) When evaluated against WCAG 2.1 AA Then interactive elements have accessible names, 4.5:1 contrast, logical focus order, keyboard/assistive tech operability, and text scales to 200% without loss of content Given a user interacts with touch When tapping form controls Then touch targets are at least 44×44 px and there is no horizontal scrolling Given intermittent connectivity When the user partially completes a consent Then progress is preserved during the session and restored on reconnect within 15 minutes
Store and Link Signed Artifacts to Appointment and Profiles
Given a consent is signed for a booking When the booking is confirmed Then the signed artifact is linked to the appointment, pet profile(s), and client profile and appears in the appointment timeline with timestamp Given an appointment is rescheduled with the same service within the consent validity window When the new time is confirmed Then the prior signed consent remains attached and valid Given the service changes or the consent is expired When attempting to confirm the updated booking Then the system requires re-consent before finalization
One-Tap Confirmation Link Generation
"As a busy pet owner on my phone, I want to confirm my appointment from a single link with everything filled in so that I don’t have to type details."
Description

Create a short, secure, expiring booking link embedded in the SMS/DM that opens to a mobile web confirmation screen with all fields prefilled (service, pet(s), time window, price, consents). Bind per-link tokens to the message thread and optionally device fingerprint to mitigate misuse. Support single-tap confirm, optional tip selection, payment-on-file confirmation, and post-confirmation flows that update the calendar, send receipts, and trigger reminders. Handle link expiry, re-issuance, and conflict resolution with graceful fallback to manual booking.

Acceptance Criteria
Secure, Expiring One-Tap Link Creation
Given a booking intent exists in an identified SMS/DM thread and a client record is present When a confirmation link is generated Then the URL is HTTPS, unique, single-use, and its total length is <= 60 characters with a path length <= 22 characters And the token has >= 128 bits of entropy, is cryptographically signed, and expires 24 hours after issuance And the token is bound to client_id and thread_id; redemption validates both and returns 403 on mismatch And if device_fingerprint_binding is enabled, redemption from a non-matching device requires a one-time SMS OTP before proceeding And on successful generation, an audit event booking_link.created is recorded with token_id, hashed client_id, thread_id, and expiry
Prefilled Confirmation Screen Accuracy
Given a valid link is opened When the confirmation screen loads Then it displays prefilled service, pet(s), time window, applied package credits, fees/taxes, and total matching the backend quote for the token And required consents are displayed at the latest version and must be accepted before enabling Confirm And prefilled fields are non-editable except tip selection; currency formatting matches the business locale And Largest Contentful Paint <= 2.5s on simulated 3G Fast; page load error rate < 1% at p95
One-Tap Confirmation and Payment-on-File
Given the client has a payment method on file and required consents are accepted When the user taps Confirm Then the booking is created atomically, the calendar is updated, and the link token is invalidated And the payment intent is confirmed for the total (or $0 if pay-after-service), with 3DS step-up handled inline when required; failures show an actionable error and do not create a booking And on success, a receipt is sent via SMS/DM, an email receipt is sent if available, and reminders are scheduled per business rules And end-to-end confirm latency (tap to success) <= 5s at p95
Link Expiry and Re-Issuance Fallback
Given a link is expired or invalid When it is opened Then an expired link message is shown with a single-tap Request New Link action When Request New Link is tapped Then a new link with a new token is issued to the same thread and all prior tokens are invalidated; max 3 re-issues per 24h per client per thread And if re-issue is blocked (rate limit or identity mismatch), route to manual booking with available prefill data And all attempts are audited with reason codes; re-issue success rate in staging QA > 95%
Time Window Conflict Resolution
Given the suggested time window is unavailable at open or confirm When the conflict is detected Then the user is offered the next three available windows within ±3 days matching service duration and provider constraints And selecting an alternative updates the quote and preserves applied packages; declining routes to manual booking And the system prevents double-booking via optimistic concurrency/locks; at most one booking can be created per token And conflict outcomes are logged with resolution status
Tip Selection Optionality and UX Performance
Given the confirmation screen is displayed When the user does not change tip Then confirmation remains a single tap When the user selects a tip (0%, 10%, 15%, or custom amount) Then the total updates within 200ms and confirmation remains a single tap And custom tip validation rejects negative values and tips > 100% of service subtotal with a clear inline message And from link open to success screen completes in <= 10s at p95 and uses <= 3 client-visible screens
Analytics, Abuse Mitigation, and Privacy
Given link lifecycle events occur When generate, open, expire, confirm, or fail events happen Then analytics events are emitted with a standardized schema at 100% in staging and >= 95% in production And rate limits cap at 10 open attempts/min per token and 5 active tokens per client per thread; excess returns 429 with retry-after And CSRF protection is enforced for confirm; tokens rotate on redemption and are stored only as hashes; PII in logs is minimized/redacted; audit logs retained 180 days And security tests demonstrate no successful token reuse/forgery/brute-force above 1e-9 probability per attempt
Confidence Thresholds and Clarification Fallbacks
"As a walker, I want the system to ask smart clarifying questions when a client’s text is unclear so that I avoid mistakes and still book fast."
Description

Apply confidence scoring to each extracted field and gate prefill actions with configurable thresholds. When low confidence or conflicting data is detected, trigger concise clarification prompts via SMS with quick-reply options to resolve missing fields before link generation. Log decisions and interactions for auditability and model improvement, and provide a manual override panel in the provider dashboard to edit and approve the prefill prior to sending.

Acceptance Criteria
Threshold Gating for Prefill Fields
- Given an inbound SMS has extracted fields with confidence scores in [0,1], when a field's score >= its configured threshold, then that field is auto-prefilled in the booking link and marked as Auto. - Given a field's score < its configured threshold or is within delta 0.05 of a competing candidate, then the field is not auto-prefilled and is flagged as Needs Clarification. - Default thresholds are set per account on provision: service_type=0.85, pet_id=0.90, time_window=0.75, package_id=0.80, location=0.80; these are applied unless overridden. - Prefill decision latency per message does not exceed 2,000 ms at p95. - Each prefill decision stores field, chosen value, score, threshold, and decision outcome in the audit log.
SMS Clarification Prompt on Low Confidence
- When any required field is flagged Needs Clarification, the system sends one SMS prompt identifying the field with 2–4 numeric quick-reply options and an "Other" option. - Each clarification SMS is <= 160 characters; if options exceed this limit, options are truncated to the top 4 by confidence. - If no reply is received within 15 minutes, the conversation is marked Awaiting Manual Review and the provider dashboard is notified. - Upon receiving a valid quick reply, the corresponding value is set, confidence is updated to 1.0 (user confirmed), and link generation proceeds if all required fields are resolved. - A maximum of two sequential clarification prompts are sent per conversation; remaining unresolved fields are routed to manual review.
Conflict Resolution for Multiple Candidates
- When multiple candidates for the same field have scores within 0.05 of each other, the system treats it as a conflict and triggers a clarification listing up to the top 3 candidates ranked by score. - Options include unique disambiguators (e.g., pet names) and numeric selectors (1–3); repeated replies within a 2-minute window allow multi-select where applicable (e.g., multiple pets). - Selecting an option resolves the conflict; non-selected candidates are discarded for this booking attempt. - If user input does not match any offered option, the system replies once with instructions and re-lists options; subsequent mismatch routes to manual review.
Package Auto-Application with Confidence and Overrides
- If an active package exists that matches the inferred service and pet, and package_match_score >= configured package threshold, then the package is auto-applied to the prefill. - If more than one package qualifies or the score is below threshold, the clarification prompt includes "Use package X (credits left: N)" as an option. - The confirmation link displays applied package, remaining credits, and any price due; double-charging is prevented by reserving the package credit upon confirmation. - Provider can deselect or change the package in the manual override panel before sending; all changes are audit-logged with user and timestamp.
Provider-Configurable Confidence Thresholds
- Providers can configure per-field thresholds (service_type, pet_id, time_window, package_id, location) within 0.50–0.99 via Settings > Deep Prefill. - Threshold changes validate and save within 300 ms p95 and take effect for new conversations immediately; the previous values are versioned for rollback. - A test preview shows historical examples with pass/fail outcomes under the proposed thresholds before saving. - If a configured value is missing or invalid, the system falls back to the default and surfaces a non-blocking warning.
Audit Log and Decision Traceability
- For each conversation, the system records a structured decision log capturing: request_id, timestamps, extracted fields and candidates, scores, thresholds at decision time, conflicts, prompts sent, user replies, manual overrides, and final prefill payload. - Decision logs are queryable in the dashboard within 5 seconds of the event and exportable to CSV and JSON for a selected date range. - Sensitive data is never stored; PII is redacted per policy while retaining traceability of decisions. - Logs are retained for at least 18 months and are 99% available.
Manual Override Review and Approval
- Prefills blocked by low confidence or unanswered clarifications appear in a Pending Review queue in the provider dashboard within 10 seconds. - The override panel displays each field, extracted value, score, threshold, decision rationale, and allows edit, approve, or reject. - Sending the booking link is gated until a provider clicks Approve; approve regenerates the link with edits applied. - All edits and approvals capture actor, timestamp, before/after values, and are written to the audit log. - The panel is keyboard-navigable and responsive down to 360px width.

Comment Catcher

Turn public interest into bookings. When someone comments a keyword (e.g., ‘book’, ‘nails’), automatically send a compliant DM that collects SMS consent and shares a personalized link. Tracks which post/story drove the conversion so you can double down on what works.

Requirements

Keyword Listener & Per-Post Mapping
"As a business owner using FetchFlow, I want comments with specific keywords on my posts to be detected automatically so that interested followers receive timely outreach without me manually monitoring every thread."
Description

Implements real-time detection of public comments containing configured keywords on supported social platforms and maps them to specific posts/stories. Supports per-post keyword lists, synonyms, case-insensitive and emoji-aware matching, and negative keywords to avoid false triggers. Deduplicates by commenter and post, handles language variations, and filters bot/spam patterns. Integrates with platform webhooks/streams to capture comment metadata (handle, post ID, timestamp) and hands off matches to the outreach workflow. Provides resilience via retries and idempotency keys to prevent duplicate actions.

Acceptance Criteria
Real-time keyword match on configured post triggers outreach
Given Post A is configured with keyword list ["book","nails","💅"] and synonyms ["reserve","appointment"], case-insensitive and emoji-aware And Post B is configured with no keywords When a public comment "BOOK pls 💅" is added to Post A Then the system detects a keyword match within 5 seconds of webhook receipt And creates exactly one match record with fields: post_id=A, comment_id, platform, commenter_handle, matched_terms=["book","💅"], event_timestamp_utc And invokes the outreach workflow exactly once with an idempotency key scoped to (platform, post_id, comment_id) And no match is created if the identical comment is added to Post B
Negative keywords suppress triggers even when trigger words are present
Given Post A has trigger keywords ["nails"] and negative keywords ["no nails","not now","just asking"] When a public comment "nails not now" is added to Post A Then no outreach is invoked and no match record is created And the event is logged with outcome="suppressed_by_negative" and reason="matched: not now" And when a comment "nails please" is added to Post A, the system triggers outreach as normal
Deduplicate matches by commenter and post within a 24-hour window
Given a 24-hour deduplication window for (commenter, post) And Post A is configured with keyword ["book"] When commenter @petparent posts three matching comments on Post A within 24 hours Then only the first matching comment triggers outreach and creates a match record And subsequent matching comments by @petparent on Post A within the window do not trigger outreach and are logged as outcome="suppressed_duplicate" And if @petparent comments "book" on Post B (also configured), outreach is triggered for Post B And if 24 hours have elapsed since the first trigger on Post A, a new matching comment on Post A triggers outreach again
Bot/spam pattern filtering prevents false triggers
Given bot/spam rules are enabled (e.g., comment contains >2 URLs, >5 @mentions, repeated characters >30, or commenter flagged as bot) And Post A is configured with keyword ["book"] When a public comment "BOOK now http://x.tld http://y.tld http://z.tld" is added to Post A Then no outreach is invoked and no match record is created And the event is logged with outcome="suppressed_by_spam" and includes the matched spam rule And when a legitimate comment "book please" is added to Post A by a non-bot account, the system triggers outreach within 5 seconds
Language and emoji-aware matching across configured variations
Given Post A is configured with base keyword "book" and language variations ["reservar"] and emoji synonym "💅" mapped to "nails" When a public comment "Reservar mañana" is added to Post A Then the system detects a match for "book" via "reservar" (case-insensitive) and triggers outreach And when a public comment "💅 pls" is added to Post A, the system matches "nails" via emoji and triggers outreach And when a public comment "prenotare" is added and no Italian variation is configured, no match is created
Webhook processing latency, retries, and idempotent outreach handoff
Given the platform may deliver duplicate webhooks for the same comment and the outreach service may intermittently fail When the system receives duplicate webhook deliveries for the same (platform, post_id, comment_id) Then exactly one match record is persisted and the outreach workflow is invoked at most once due to idempotency And if the outreach handoff returns a retryable error (HTTP 5xx or timeout), the system retries up to 3 times with exponential backoff (e.g., 1s, 2s, 4s) And total processing time from initial webhook receipt to successful outreach handoff is ≤ 5 seconds under normal load; ≤ 15 seconds with one retry And after exhausting retries, the match is marked status="handoff_failed" without triggering duplicate actions
Comment metadata capture and per-post mapping auditability
Given a match is detected on a comment Then the system stores non-null metadata fields: platform, post_id or story_id, comment_id, commenter_handle, matched_term(s), language_detected (if available), event_timestamp_utc And the dashboard audit log shows the source post/story and which term matched for each event And the outreach workflow receives the same metadata payload, including the originating post/story ID, for attribution reporting And attribution reports can filter conversions by post_id/story_id and show counts of matches per post
Compliant Auto-DM Opt-in Flow
"As a compliance-conscious owner, I want DMs to collect explicit SMS consent with the right disclosures so that I can message clients legally and protect my business."
Description

Automatically initiates a platform-compliant DM to commenters who match keywords, guiding them through a clear SMS opt-in process that collects phone number and consent. Includes templated, customizable DM scripts with required disclosures (brand, message frequency, HELP/STOP, carrier rates) and double opt-in when needed. Captures and stores consent artifacts (timestamp, platform handle, consent text version, source post/story) in a tamper-evident ledger. Syncs opt-outs (e.g., STOP) back to the SMS provider and suppresses future outreach. Falls back gracefully if the platform restricts DMs (e.g., send a reply prompting them to DM first) and respects user privacy and platform policies.

Acceptance Criteria
Auto-DM Trigger on Keyword Comment with Compliance Content
Given a configured keyword list and a new public comment matching a keyword on a tracked post/story by a user not suppressed and not auto-DMed for that post in the last 24 hours When the system receives the comment event and passes platform rate/DM eligibility checks Then send a single DM to the commenter within 60 seconds using the selected template that includes brand name, message frequency, HELP/STOP, and carrier rates disclosures And include a personalized link prefilled with the commenter’s handle And record the source platform, post/story ID, comment ID, and DM message ID And do not send an auto-DM if the user is suppressed or has blocked DMs
Double Opt-in SMS Consent via DM Flow
Given the commenter receives the initial DM and provides a valid mobile number When the system validates the number format and sends an SMS confirmation request containing the required disclosures and clear instructions (e.g., Reply YES to consent) Then activate the opt-in only after the user replies YES via SMS within 30 minutes when double opt-in is required by configuration or region And if single opt-in is configured, activate upon DM confirmation after presenting required disclosures And associate the phone number with the platform handle and the originating post/story And if YES is not received within 30 minutes (when required), keep opt-in inactive and send a single DM reminder; no further reminders are sent
Consent Artifacts Captured in Tamper-Evident Ledger
Given an opt-in is activated When the system writes a consent record Then the record includes UTC timestamp, platform handle, masked phone number, consent text version ID, initial DM template ID, source post/story and comment IDs, and SMS provider message IDs And the record is stored in a tamper-evident ledger with an immutable hash and previous-hash linkage And the consent record can be retrieved via admin UI or API by handle or phone within 2 seconds And any amendment writes a new version while preserving prior versions
Opt-out Synchronization and Global Suppression
Given a subscribed user sends STOP/UNSUBSCRIBE via SMS or requests opt-out via DM When the system receives the event from the SMS provider webhook or DM channel Then mark the user as globally suppressed within 15 seconds And propagate the suppression to the SMS provider via API within 30 seconds And prevent all future SMS and auto-DM outreach from Comment Catcher to that user And log UTC timestamp, channel, reason, and operator (if manual) into the suppression ledger
Fallback When Platform Blocks DMs
Given platform delivery checks fail (e.g., user disallows business DMs, rate limit reached, or API error) When a keyword match occurs Then do not retry DM more than 2 times within 10 minutes And post a compliant public reply mentioning the user to invite them to DM first without collecting PII in the reply And record the fallback event with error code, retry count, and resolution state And ensure only one public reply is posted per comment
DM Template Customization with Required Disclosures Enforcement
Given a user with appropriate permissions edits an auto-DM template When they attempt to save the template Then block saving unless the content includes placeholders or text for brand identification, message frequency, HELP, STOP, and carrier rates disclosures And validate the rendered message length against the platform DM limit using sample merge data and show a preview And require a successful test send to a sandbox recipient before activation And save the template with a new version ID and change author
Source Attribution and Conversion Tracking
Given a DM is sent due to a keyword on a post/story When the recipient clicks the personalized link and completes opt-in Then attribute the opt-in to the exact post/story and comment ID And surface conversion metrics by post/story in the dashboard within 5 minutes And use unique, expiring links per recipient (expire upon completion or after 7 days) And emit analytics events with UTM parameters accessible via export API
Personalized Smart-Link Generation
"As a prospect who commented on a post, I want a personalized link that takes me directly to the right booking or pet profile flow so that I can complete my action quickly on my phone."
Description

Generates a unique, expiring, user-specific link included in the DM that deep-links to FetchFlow with prefilled context: source platform, post/story ID, keyword, and follower handle. Prefills or creates a Pet Profile Smart Card when possible, collecting pet name, photo, vaccines, and preferences, and routes the user to the appropriate next step (e.g., book nails, buy a package, request callback). Supports UTM tagging for marketing analytics, secure tokens to prevent abuse, and mobile-first rendering for frictionless conversion.

Acceptance Criteria
Unique Expiring Smart-Link in DM
Given a qualifying keyword comment is detected on a connected social post with a resolvable follower handle When the system prepares and sends the DM reply Then it generates a unique, user-specific smart-link and inserts it into the DM body And the link is signed and time-bound with a configurable TTL (default 72 hours) And the token is single-use and is invalidated after successful completion of a goal action or upon expiry, whichever occurs first And subsequent clicks after invalidation show an "expired or used" page with a one-tap request-new-link option And the DM is not sent if link generation fails; the event is logged with a correlation ID
Context Prefill and Source Attribution on Landing
Given the recipient clicks the smart-link When the landing session is initialized Then source_platform, content_id (post/story ID), keyword, and follower_handle are resolved from the token server-side And these fields are attached to the user session and stored for analytics And query parameters expose only the opaque token and UTM values; no PII is present in the URL And if any field cannot be resolved, a default value of "unknown" is applied and an error is logged without blocking the user
Token Verification and Abuse Prevention
Given any request is made using a smart-link When server-side token verification runs Then tampered or malformed tokens are rejected with 401 and a safe error page And no context or PII is returned for invalid tokens And after 5 invalid requests per token or IP within 1 hour, further requests are rate-limited for 60 minutes And for valid tokens, the max number of goal submissions is enforced at 1 per token
Pet Profile Smart Card Prefill/Create
Given the smart-link session initializes When an existing client match by social handle-to-client mapping or phone exists Then existing Pet Profile Smart Cards are displayed with prefilled data for confirmation and update And missing required fields (pet name, vaccines) are clearly indicated and must be completed before proceeding Given no existing match is found When the user continues Then a new Pet Profile Smart Card form is presented, allowing upload of photo (jpg/png/pdf up to 10 MB) and entry of vaccines and preferences And on submission, the card is created/updated and linked to the session without page reload
Keyword-Based Routing to Next Step
Given keyword-to-route mappings are configured (e.g., book, nails, package, callback) When the user opens the smart-link Then the app routes to the mapped screen with relevant options preselected (e.g., service = nails) And if multiple pets exist, a pet selector is shown before the action screen And if the keyword is unmapped, the default route is "Request Callback" And the selected route name is recorded with the attribution payload
UTM Tagging and Analytics Consistency
Given a smart-link is generated When the link URL is constructed Then UTM parameters are appended as follows: utm_source = platform, utm_medium = comment-dm, utm_content = content_id + ":" + keyword, utm_campaign = configured campaign name or account handle And all redirects preserve UTM parameters through to the goal completion page And a conversion event is recorded upon goal completion including UTM and internal attribution fields within 500 ms
Mobile-First Rendering and Compatibility
Given the smart-link is opened on a mobile device (iOS Safari, Android Chrome, IG/FB in-app) When the landing page loads Then the page renders without layout shifts at 320–428 px widths and does not require horizontal scrolling And Core Web Vitals on a simulated 4G profile meet thresholds: LCP <= 2.5 s, CLS <= 0.1, INP <= 200 ms And if the in-app browser blocks deep linking, a visible "Open in your browser" fallback is provided And interactive controls meet WCAG 2.1 AA contrast and are reachable via screen reader focus order
Conversion Attribution Pipeline
"As a marketer for my grooming business, I want to see which posts and keywords drive bookings and revenue so that I can double down on what works and stop what doesn’t."
Description

Tracks the full funnel from comment keyword detection through DM sent, link click, SMS consent obtained, profile completion, booking, package purchase, and payment. Associates each conversion with the originating post/story and keyword to measure performance. Exposes metrics in the FetchFlow dashboard (reach, DM rate, click-through, opt-in rate, booking rate, revenue, no-show delta) and supports export/filters by date, platform, and campaign. Provides event streaming to internal analytics and ensures data quality via idempotent event IDs and late-arrival handling.

Acceptance Criteria
Keyword Comment Detection → Compliant DM Dispatch
Given a public comment contains a registered keyword on a supported platform and the commenter is not in an active DM flow for that keyword When the comment is posted Then a comment_detected event is logged with unique event_id, post_id/story_id, platform, keyword, commenter_id within 15 seconds And a compliant DM containing opt-in language and a personalized, tracked link is sent within 30 seconds And a dm_sent event is logged and correlated to the comment_detected event via correlation_id And duplicate webhooks or retries with the same event_id do not produce additional dm_sent events
Link Click Capture → SMS Consent Attribution
Given a recipient receives a DM with a tracked link When the link is clicked Then a link_clicked event is logged with contact_id, post_id/story_id, platform, keyword, campaign_id, and timestamp And a consent prompt is presented that requires explicit, double opt-in per policy When the recipient opts in via the prompt Then an sms_opt_in_obtained event is logged within 60 seconds and linked to the originating post/story, keyword, and campaign And dashboard click-through rate and opt-in rate update within 5 minutes of the event And if consent is declined, the contact is marked do_not_text and is excluded from opt-in numerator and subsequent SMS steps
Profile Completion → Booking/Package/Payment Conversion Tracking
Given a contact has opted in via SMS When they complete a Pet Profile Smart Card via the personalized link Then a profile_completed event is logged with owner_id and pet_id and attributed to the originating post/story and keyword When they create a booking, purchase a package, and/or complete a payment Then booking_created, package_purchased, and payment_captured events are logged with amounts, currency, and booking_id as applicable And dashboard booking rate and revenue update within 5 minutes of the events And if no booking occurs within 30 days of opt-in, the funnel path is marked no_booking_in_window
Dashboard Metrics, Filters, and Export
Given an authorized user opens the Conversion Attribution dashboard When they apply filters for date range, platform, and campaign Then the dashboard displays metrics with the following definitions: - Reach = unique commenters matched by keyword in scope - DM rate = dm_sent / reach - Click-through rate = unique link_clickers / dm_sent - Opt-in rate = unique contacts with sms_opt_in_obtained / dm_sent - Booking rate = unique contacts with booking_created / unique contacts with sms_opt_in_obtained - Revenue = sum(payment_captured.amount) attributed in scope - No-show delta = (no-show rate of attributed bookings) − (business baseline no-show rate over prior 90 days) And metrics reflect events with a data freshness SLA of <= 5 minutes And Export CSV respects filters, includes one row per day per campaign with the above metrics, and completes within 10 seconds for up to 90 days
Event Streaming to Internal Analytics
Given any funnel event is logged When the event is published to the analytics stream Then it is emitted within 60 seconds with schema {event_id, type, occurred_at, contact_id, post_id/story_id, platform, keyword, campaign_id, amounts?, currency?, correlation_id, schema_version} And delivery is at-least-once with dedup_key = event_id, with 99.9% delivered within 2 minutes And retries use exponential backoff for up to 24 hours, after which events are sent to a dead-letter queue with alerting And PII fields are flagged and handled per data policy
Idempotency, Deduplication, and Late-Arrival Corrections
Given duplicate events with the same event_id arrive within a 7-day window Then only one is stored, streamed, and counted in metrics Given events arrive up to 72 hours late or out of order Then aggregates, dashboard, and exports are corrected via upsert within 10 minutes of arrival without double counting And recomputing metrics from raw events produces identical results to the online aggregates for any 7-day slice
Attribution Rules for Multiple Posts/Keywords and Reattribution
Given a user makes multiple eligible comments before DM Then the conversion is attributed to the most recent eligible comment on the same platform within a 24-hour lookback Given a user receives multiple DMs and clicks a newer link before booking/payment Then attribution switches to the last clicked link within a 30-day window prior to booking/payment And once a payment_captured event is logged, attribution fields (post_id/story_id, keyword, campaign_id) are locked for that conversion And deterministic reprocessing yields the same attribution as the online pipeline for a sample set of 1,000 conversions
Rate Limiting, Spam Guard & Suppression
"As an account owner, I want the system to throttle and suppress outreach intelligently so that we avoid platform penalties and provide a respectful experience for followers."
Description

Implements platform-aware throttling, DM queues, and frequency caps to stay within API limits and avoid spam flags. Enforces per-user cooldown windows, cross-campaign suppression (don’t DM the same person repeatedly), and keyword safety checks. Supports do-not-contact lists, quiet hours by business timezone, and automatic backoff on rate-limit responses. Surfaces skipped/suppressed events with reasons in the dashboard and provides controls to retry or override where compliant.

Acceptance Criteria
Platform-Aware Throttling and DM Queue Backoff
Given per-platform send-rate limits are configured When inbound comment-trigger volume exceeds a platform’s configured rate Then DMs are enqueued and dispatched at or below the configured per-platform rate without exceeding the cap And dispatch order is FIFO per recipient and per platform Given a platform returns a rate-limit response (e.g., 429/613) When retry logic executes Then exponential backoff per configured schedule is applied and retries occur up to the configured max attempts And final failure (if any) is logged with reason "rate_limited_exhausted" without sending duplicates
Per-User Cooldown and Frequency Caps
Given a per-user cooldown window is configured When a user triggers multiple keywords within the active cooldown window Then only the first trigger sends a DM and subsequent triggers are suppressed with reason "cooldown_active" Given a rolling frequency cap per user is configured (e.g., N DMs per 7 days) When the user exceeds the cap within the window Then the additional DMs are suppressed with reason "frequency_cap" And the suppression is recorded with timestamp and campaign IDs Given the cooldown or frequency window elapses When a new trigger occurs Then the DM is eligible to send subject to other guards
Cross-Campaign Suppression for Repeat Contacts
Given multiple active campaigns can target the same user When the user triggers across different posts/stories within the cooldown window Then only one DM is sent and other campaign events are suppressed with reason "cross_campaign_suppression" And suppressed events retain the originating post/story and campaign attribution for reporting
Do-Not-Contact, Opt-Out, and Quiet Hours Enforcement
Given a user is on the do-not-contact list or has opted out via supported methods When any trigger occurs Then no DM is sent and the event is suppressed with reason "dnc" or "opt_out" And override is disabled for these reasons Given business quiet hours are configured by business timezone When a trigger occurs during quiet hours Then the DM is queued with status "quiet_hours" And it is automatically sent at the next allowed time respecting platform rate limits Given a queued DM exits quiet hours When dispatch occurs Then the event status updates to "sent" or "failed" with a machine-readable reason
Keyword Safety Validation Prior to DM
Given allowed keywords and a banned/unsafe terms list are configured When a comment is received Then the system matches allowed keywords case-insensitively And suppresses any event containing a banned term with reason "unsafe_keyword" Given a comment passes safety checks and matches an allowed keyword When preparing the outbound DM Then the platform-compliant message template is used and includes the SMS consent capture link before any further promotional messaging
Dashboard Visibility and Retry/Override Controls
Given an event is skipped or suppressed by any guard When viewing the dashboard Then the event is listed with user handle, source post/story, campaign, timestamp, and a standardized reason code (e.g., cooldown_active, frequency_cap, cross_campaign_suppression, dnc, opt_out, quiet_hours, unsafe_keyword, rate_limited_exhausted) Given a suppressed event is selected When the user clicks Retry Then the system re-evaluates all guards and queues the DM only if compliant at that moment And audits the action with actor, timestamp, and outcome Given a suppressed event is selected When the user clicks Override Then sending is allowed only for overridable reasons (e.g., cross_campaign_suppression, cooldown expired) and blocked for non-compliant reasons (e.g., dnc, opt_out, quiet_hours, unsafe_keyword) And the decision is audited
Business Configuration Console
"As a busy owner, I want an easy console to set keywords, templates, and rules per post so that I can launch campaigns quickly without developer help."
Description

Provides a self-serve UI to configure Comment Catcher: connect social accounts, select posts/stories, define keyword lists and negative keywords per post, edit DM templates and consent language, choose the target flow for the personalized link (booking type, package, or profile capture), and set throttling/cooldown rules. Includes preview and test modes, sandbox verification posts, and real-time validation of platform permissions. Offers audit history of changes and role-based access controls for staff.

Acceptance Criteria
Connect Social Accounts with Real-Time Permission Validation
- Given I am an Admin with Integrations:Manage permission and no connected accounts, when I select Connect for a supported social platform and complete OAuth, then the chosen account appears in Connected Accounts with account name, handle, and avatar. - Given OAuth completes, when the console checks required scopes, then within 2 seconds it shows a green Ready badge if all scopes are present or a red Missing Scopes badge listing each missing scope. - Given a connected account loses required permissions, when a revocation webhook arrives or the 60-second health check runs, then the account shows Disconnected within 60 seconds and a re-authenticate CTA appears. - Given multiple accounts are connected, when I disconnect one account, then only that account is removed and others remain connected. - Given an OAuth error occurs, when the flow returns to the console, then an actionable error with retry is displayed and no partial connection is saved.
Select Posts/Stories and Configure Keywords and Negative Keywords
- Given at least one connected account, when I open the Post/Story Picker, then the latest 50 posts and 50 stories load with thumbnails, captions, and timestamps within 3 seconds. - Given I select a post, when I add up to 50 keywords and up to 50 negative keywords, then duplicates are rejected case-insensitively and saved per post on Save. - Given keywords and negative keywords are configured, when I run Test Comment "book nails", then the matching result is shown and the winning post/story ID is displayed. - Given a comment contains both a keyword and a negative keyword, when matching runs, then the negative keyword suppresses the trigger and no DM would be sent. - Given I remove a keyword and save, when I re-run the same Test Comment, then it no longer matches.
Edit DM Template and Consent Language with Live Preview
- Given I open the DM Template editor, when I insert variables {first_name}, {pet_name}, {business_name}, {link}, then a live preview resolves them using sample data. - Given consent language is required, when I attempt to publish a template missing the consent directive configured in Compliance Settings, then Publish is disabled with a validation message. - Given template content exceeds 1000 characters or contains an unknown variable, when I click Publish, then the console blocks publish and highlights the offending section. - Given Test Mode is on, when I click Send Test DM to the sandbox recipient, then the message is delivered to the sandbox only and a success toast shows within 5 seconds.
Choose Target Flow and Generate Personalized, Attributed Links
- Given I select Target Flow = Booking Type and choose a booking option, when I click Generate Link, then a personalized link is produced containing source=social, medium=comment, post_id, and a unique ccid. - Given I change Target Flow to Package or Profile Capture, when I Generate Link, then the link deep-links to the chosen package or profile capture flow respectively. - Given I copy the Test Link and open it, when I complete the flow, then an attribution record appears in the Activity log within 5 minutes showing the originating post/story and ccid. - Given I send a Test DM containing the personalized link in Test Mode, when I click it, then the preview flow opens in sandbox without affecting production metrics.
Configure Throttling and Cooldown Rules with Simulation
- Given I open Throttling & Cooldown, when I set per-user cooldown to 24 hours, per-post hourly cap to 100, and global daily cap to 1000, then these values persist on Save and survive reload. - Given simulated comments are fired in Test Mode, when a commenter triggers more than one match within the cooldown window, then only the first would send and subsequent attempts show "Suppressed: Cooldown" in the event log. - Given the per-post hourly cap is reached in simulation, when additional matches occur, then they are marked "Suppressed: Post Cap" and increment the throttled counter. - Given the global daily cap is reached in simulation, when additional matches occur, then they are marked "Suppressed: Global Cap" and no further DMs would be sent that day.
Preview and Test Mode Using Sandbox Verification Posts
- Given Test Mode is toggled on, when I view the header, then a Test Mode banner is visible and all outbound DMs are rerouted to the sandbox recipient. - Given I create a Sandbox Verification Post, when I comment a configured keyword on that post, then within 10 seconds the console displays a captured event with the matched keyword and the DM preview. - Given Test Mode is toggled off, when I confirm the modal, then the banner disappears and subsequent triggers are sent live according to throttling rules.
Audit History and Role-Based Access Control Enforcement
- Given I modify any configuration (keywords, template, throttling, target flow), when I click Publish, then an audit entry is created with actor, role, timestamp, entity, and before/after values. - Given I open the Audit tab, when I filter by user and date range, then matching entries are returned and can be exported to CSV. - Given I am a Staff role, when I attempt to edit or publish, then the action is blocked with "Insufficient permissions" and an audit entry records the attempt. - Given two users edit the same template concurrently, when the second user tries to publish with a stale version, then a version conflict error is shown and no changes are lost without explicit overwrite.
Multi-Platform Connectors & OAuth Management
"As a user who posts across multiple platforms, I want Comment Catcher to work reliably wherever I engage my audience so that I can run unified campaigns from one place."
Description

Delivers robust connectors for Instagram and Facebook (Graph API) and TikTok, with OAuth flows for account linking, permissions scopes for comments and messaging, token refresh, and revocation handling. Subscribes to comment events via webhooks or polling fallbacks where necessary. Normalizes platform-specific payloads into a common event model and monitors connector health with alerts, retries, and dead-letter queues. Ensures compliance with each platform’s policies and provides a status page in the dashboard.

Acceptance Criteria
Multi-Platform OAuth Link & Scope Management
Given a user opens Settings > Connectors and clicks Connect for Instagram, Facebook, or TikTok, When the OAuth consent screen is presented, Then only the minimum required scopes are requested (comments read, messaging read/send where permitted, basic profile) and the app name and privacy links are shown. Given the user completes OAuth successfully, When the platform redirects to FetchFlow, Then the connector is marked Connected, access and refresh tokens are stored encrypted at rest, scopes granted are recorded and shown in the UI, and a success audit log entry is created with no token values in logs. Given the user cancels or denies any requested scope, When the OAuth flow returns, Then the connector remains Disconnected, an actionable error message is shown explaining the missing scope, and a re-try option is provided. Given the connected account lacks required account type (e.g., non-Business IG account), When OAuth completes, Then the connector is set to Degraded with a guidance message and a link to migration steps. Given an existing connection, When the user clicks Reauthorize, Then tokens and scopes are refreshed without duplicating webhook subscriptions and the previous tokens are revoked.
Token Refresh, Expiry, and Revocation Handling
Given a connector is Connected with a refreshable token, When the token is within 24 hours of expiry, Then the system refreshes it automatically without user action and updates the stored expiry timestamp. Given a transient refresh failure, When a refresh attempt fails, Then the system retries up to 3 times with exponential backoff over 15 minutes before marking the connector Degraded. Given a platform sends a revocation/scope change event or refresh repeatedly fails, When revocation is detected, Then the connector transitions to Revoked, event processing halts within 2 minutes, and the user receives an in-app alert and email prompting Reauthorize. Given logging is enabled, When token operations occur, Then tokens are redacted in logs and data at rest is encrypted; any plaintext token in logs causes the automated test to fail. Given the user clicks Unlink, When confirmation is accepted, Then the platform tokens are revoked via API, webhook subscriptions are removed, pending jobs are canceled, and the connector state becomes Disconnected.
Comment Event Intake via Webhooks with Polling Fallback
Given a connector is Connected, When subscription is created, Then the platform webhook handshake challenge is responded to within 2 seconds and the subscription status shows Active. Given comment events are delivered via webhook, When duplicate deliveries or out-of-order messages occur, Then events are deduplicated using platform event IDs and processed exactly once. Given transient webhook delivery failures (HTTP 5xx/timeouts), When a failure occurs, Then the system retries up to 5 times with exponential backoff (max 10 minutes) before sending the event to the dead-letter queue. Given the platform webhook service is unavailable or unsupported, When polling fallback is enabled, Then the system polls every 2 minutes, respects platform rate limits, and achieves p95 end-to-end delay from comment to normalized event under 5 minutes. Given a high-volume post, When 1,000+ comments arrive in 10 minutes, Then no more than 0.5% of events are DLQ’d and no events are dropped.
Normalized Common Event Model for Comments and Messaging
Given events arrive from Instagram, Facebook, or TikTok, When normalized, Then each event includes the required fields: event_version, platform, account_id, post_id, comment_id, conversation_id (if applicable), parent_comment_id (nullable), author_handle, author_platform_id, text, media_type, source_post_url, created_at (UTC), received_at (UTC), trigger_source (webhook|polling), and correlation_id. Given normalization succeeds, When the event is validated, Then it conforms to the JSON Schema v1.0 and passes structural validation; non-conforming events are not emitted downstream. Given a platform-specific field is unknown, When encountered, Then it is preserved in a raw_payload blob attached to the event without blocking downstream processing. Given schema evolution is needed, When a breaking change is introduced, Then the event_version is incremented, backward-compat parsing is supported for 30 days, and documentation is updated. Given malformed or incomplete upstream payloads, When required fields are missing, Then the event is rejected to the DLQ with a deterministic error code and sample payload for debugging.
Connector Health Monitoring, Alerts, Retries, and Dead-Letter Queue
Given connectors are processing events, When monitoring runs, Then the following SLOs are met over a rolling 24 hours: webhook delivery p95 to normalized event under 3 seconds; polling backlog under 500 events; token refresh success rate at or above 99%; DLQ rate under 0.5%. Given an SLO breach persists for 5 minutes, When detected, Then alerts are sent to the account owner via in-app and email within 1 minute and the connector state is set to Degraded with a descriptive reason. Given transient API errors (HTTP 429/5xx), When retries occur, Then exponential backoff with jitter is applied, a per-connector circuit breaker opens after 10 consecutive failures for 5 minutes, and idempotency keys prevent duplicate side effects. Given events enter the DLQ, When observed, Then events are retained for 14 days, visible in the dashboard with error codes, and can be replayed individually or in batch; replay maintains per-post ordering. Given alert noise, When the same error repeats, Then alerts are deduplicated and rate-limited to no more than 1 notification per error type per 30 minutes.
Dashboard Status Page and OAuth Management Controls
Given a user navigates to the Status page, When connectors exist, Then each platform shows state (Connected, Degraded, Revoked, Disconnected), last successful event time, last error summary, subscription status, and current rate-limit usage. Given the user clicks Test Webhook, When invoked, Then a synthetic event is sent and the round-trip result is displayed within 10 seconds or a diagnostic error is shown. Given the user needs to manage access, When viewing a connector, Then granted scopes are listed, with actions to Reauthorize and Unlink; each action requires explicit confirmation and writes an audit trail with user, timestamp, and action. Given data freshness is critical, When the page is open, Then metrics auto-refresh every 30 seconds, with a manual Refresh option, and access is restricted to users with Admin role. Given accessibility requirements, When tested, Then the page meets WCAG 2.1 AA for color contrast, keyboard navigation, and screen reader labels.

ThreadSync

Carry context from Instagram into SMS automatically—IG handle, last DM summary, and any tagged pet details appear at the top of the SMS thread and on the client profile. Pros never ask clients to repeat themselves, and handoffs feel seamless and professional.

Requirements

IG Account Linking & Client Mapping
"As a busy pet pro, I want client IG handles linked to their profiles so that Instagram context automatically appears in SMS and I don’t have to re-enter details."
Description

Enable businesses to securely connect their Instagram account to FetchFlow, store the IG handle on the corresponding client profile, and map each incoming DM to an existing client via phone number, name matching, or prior conversation history. Provide a guided flow to link an IG handle to an existing client or create a new client when none exists, preventing duplicate records with fuzzy matching and explicit confirmation. Persist the IG handle as a primary identifier on the client profile and surface it across the dashboard, SMS composer, and contact details. Support multi-location and multi-staff access with role-based permissions to view and manage linked handles. Ensure that unlinking an IG handle cleanly removes the association without deleting historical context. This creates the foundation for carrying context from Instagram into SMS without manual data entry.

Acceptance Criteria
Instagram Account Linking via OAuth
Given a business admin with Integrations:Manage permission is logged in, When they click “Connect Instagram” and complete OAuth with required scopes, Then FetchFlow stores a valid access token and IG business account ID and marks the status as Connected. Given the Instagram account is successfully linked, When the admin visits the Integrations page, Then the connected IG handle is displayed with the handle, connected timestamp, and a Disconnect control. Given OAuth completes with missing or revoked scopes, When the redirect returns to FetchFlow, Then the user sees an actionable error describing required scopes and no link record is saved. Given the Instagram account is linked, When any user requests integration details via UI or API, Then no access token values are returned and secrets are redacted, and token storage is encrypted at rest. Given an IG account is already linked to this business, When another admin attempts to link the same IG account, Then the system prevents duplicate linkage and shows that the IG account is already connected.
Auto-Mapping Incoming DMs to Existing Clients
Given an IG handle is explicitly linked to Client A, When a new Instagram DM arrives from that handle, Then the message is attached to Client A and the thread header shows the IG handle. Given no explicit link exists but prior DMs from the same handle were mapped to Client B, When a new DM arrives, Then the message auto-attaches to Client B. Given no prior mapping, When the incoming DM contains a phone number that exactly matches one existing client, Then the message maps to that client and the IG handle is proposed for linking. Given no phone number match, When the IG display name yields a single fuzzy match with similarity ≥ 0.85, Then the message maps to that client, the confidence score is logged, and the IG handle is proposed for linking. Given multiple candidates meet the threshold or no candidates are found, When a new DM arrives, Then the message is routed to the Unmapped queue and the guided linking flow is triggered.
Guided Linking or New Client Creation with Duplicate Prevention
Given a DM is in the Unmapped queue, When a staff member opens the guided linking flow, Then up to 5 candidate clients are shown with similarity scores and last interaction context, sorted by score. Given a staff member selects an existing client and confirms, When they click Save, Then the IG handle is linked to that client, the current DM is mapped, and future DMs auto-map to that client. Given a staff member opts to create a new client, When the entered phone or email exactly matches an existing client or the name/phone/email similarity is ≥ 0.80, Then a duplicate warning is shown and explicit confirmation is required to proceed. Given an IG handle is already linked to another client, When a staff member attempts to link it to a different client, Then the system blocks the action unless they confirm a transfer and have permission; on confirmation, future DMs map to the new client while historical messages remain with their original client. Given linking completes successfully, Then the IG handle is persisted as the client’s primary social identifier and no two clients within the business can share the same IG handle.
IG Handle Persistence and Visibility Across UI
Given a client has a linked IG handle, When viewing the client profile, Then the IG handle appears in the header with an Instagram icon and links to the IG profile. Given a client has a linked IG handle, When composing or viewing an SMS thread for that client, Then the IG handle is displayed in the thread header and SMS composer context. Given a client has a linked IG handle, When viewing dashboard client overviews or contact details, Then the IG handle or badge is visible in those views. Given a client’s IG handle link state changes (link or unlink), When any authorized user has the screen open, Then the UI reflects the change within 10 seconds or on next navigation refresh. Given a user lacks SocialLinks:Manage permission, When viewing the IG handle, Then the field is read-only and link/unlink controls are hidden.
Role-Based Access for Viewing and Managing IG Links
Given a user has Owner or Admin role, When they access client profiles or Integrations, Then they can view and manage IG handle links (link, transfer, unlink). Given a user has SocialLinks:Manage permission, When they access client profiles, Then they can link or unlink IG handles; without this permission they can only view if they also have Client:View and location access. Given a user lacks required permissions, When they attempt to call link/unlink APIs, Then the request is rejected with HTTP 403 and no changes are made. Given permissions change for a user, When they refresh the app, Then their access to view or manage IG handles updates immediately according to the new role.
Multi-Location Handling of Linked IG Handles
Given a business has multiple locations and a client is assigned to Location A only, When a staff member restricted to Location B views clients, Then they cannot see the client’s IG handle or manage its link. Given a client is shared across Locations A and B, When authorized staff at either location view the client, Then the linked IG handle is visible and consistent across both locations. Given an attempt is made to link the same IG handle to a different client in another location, Then the system prevents duplicate IG handle usage across the business and suggests the existing client linked to that handle. Given a staff member has cross-location access, When they link an IG handle to a client at Location A, Then staff with access to that client at other locations can view the same linked handle.
Unlinking IG Handle Without Loss of Historical Context
Given a client has a linked IG handle and the user has permission, When the user clicks Disconnect and confirms, Then the association is removed and the IG handle no longer appears as linked on the client profile. Given the IG handle is unlinked, When viewing the client timeline, Then all historical Instagram DMs remain readable and timestamped, and are not deleted. Given the IG handle is unlinked, When a new DM arrives from that handle, Then it is not auto-mapped and appears in the Unmapped queue for guidance. Given an unlink operation completes, Then no invoices, packages, notes, or SMS messages are deleted or altered as a result. Given a previously unlinked IG handle is re-linked to any client, Then historical DMs remain associated with the original client while new DMs map to the newly linked client.
DM Summary Generation & Attachment
"As a groomer, I want a concise summary of the latest IG DM in my SMS view so that I immediately know what the client asked for without rereading the entire conversation."
Description

Automatically generate a concise, human-readable summary of the last Instagram DM exchange (e.g., last 5–10 messages) and attach it to both the client profile and the header of the associated SMS thread. Summaries should capture key intent (e.g., service requested, preferred dates/times, special instructions) while omitting sensitive or redundant content and clearly showing the DM timestamp. Handle multilingual messages and emojis, preserve links and media references, and update the summary when new DMs arrive. Provide a one-click refresh in the SMS thread and profile to re-pull the latest context. Store a time-stamped snapshot for auditability and allow configuration of how long summaries are retained. This ensures handoffs feel seamless and pros never ask clients to repeat themselves.

Acceptance Criteria
Auto-Generate Summary on New DM
Given a client profile is linked to an Instagram handle and a new DM arrives, When the IG webhook is received and the last 5–10 messages are available, Then the system generates a concise, human-readable summary within 5 seconds and persists it. Then the summary is ≤600 characters and captures, if present: service intent, preferred dates/times, and any special instructions. Then the summary includes the last DM timestamp formatted in the workspace timezone as YYYY-MM-DD HH:mm TZ. Then redundant content (greetings, signatures, repeated confirmations) is omitted. Then sensitive data (full emails, full phone numbers, payment numbers) is masked or redacted per policy. Then URLs are preserved as-is and media are represented as tokens (e.g., [photo], [video], [voice]) with a count if multiple.
Attach Summary to Profile and SMS Thread
Given a summary has been generated or refreshed for a linked IG handle, When the summary is saved, Then it appears at the top of the associated SMS thread header and in the client profile Context section within 1 second of generation. Then the header displays the IG handle, pet tags (if any), the last DM timestamp, and the summary text (truncated to two lines with expand/collapse). Then the profile and thread show the same summary version and updated-at timestamp. Then the display persists across app reload and navigation.
One-Click Refresh and Manual Re-Pull
Given a user is viewing the SMS thread or client profile, When the user taps “Refresh IG Context,” Then the system fetches the latest 5–10 DMs and regenerates the summary within 5 seconds. Then both the SMS thread header and client profile update atomically to the new version. Then a non-blocking confirmation toast “IG context refreshed” displays for 3 seconds. If the IG API returns a rate limit error, Then an error message shows a retry-after time and the refresh button is disabled until that time elapses; the last successful summary remains visible. If a network error occurs, Then the last successful summary remains visible and a retry action is offered.
Continuous Update on New DM
Given IG webhooks are configured for the workspace and a client is linked to an IG handle, When a new DM is received on the linked thread, Then the summary auto-updates and both UI locations reflect the update within 10 seconds without user action. Then an “Updated just now” badge appears for 5 seconds in the SMS thread header. Then the operation is idempotent: duplicate webhooks do not create duplicate snapshots or notifications.
Snapshot Storage and Retention Configuration
Given a summary is generated, When the system saves it, Then an immutable snapshot is stored with: snapshot_id, client_id, ig_handle, covered_message_ids, last_dm_timestamp, summary_text, version, created_at. Given a workspace retention setting in days (30–365, default 180), When the daily purge runs at 02:00 UTC, Then snapshots older than the retention period are deleted and a purge event is logged to the audit log. When an admin searches the audit log by client or date range up to 10,000 entries, Then results return within 3 seconds. When snapshots are exported for a client, Then a JSON export completes within 60 seconds and is available only to Admin or Compliance roles. Given retention is set to 0, When a summary would be generated, Then no snapshot is stored and the UI indicates “Snapshots disabled by policy.”
Multilingual, Emoji, and Link/Media Handling
Given the last 5–10 DMs contain mixed languages and emojis, When generating the summary, Then the summary language matches the primary detected language (>70% of tokens); otherwise English is used. Then non-Latin scripts render correctly without mojibake. Then consecutive emoji runs are limited to a maximum of 3 repeated emojis while preserving single emojis within key phrases. Then URLs remain clickable and display truncated to ≤30 characters while retaining the full target on tap. Then media are represented as [photo], [video], [voice], or [file] with counts if multiple (e.g., [photo x3]). Across validated English and Spanish test sets, Then the summarizer extracts service intent, preferred dates/times, and special instructions with ≥95% F1 score.
Pet Tag & Metadata Extraction to Smart Cards
"As a trainer, I want tagged pet info from IG DMs to auto-fill Pet Profiles so that I spend less time collecting details and can schedule faster."
Description

Parse Instagram DMs for tagged pet details (names, photos, breed/size notes, preferences, vaccine images) and map them into FetchFlow Pet Profile Smart Cards. Detect explicit mentions, hashtags, and structured phrases, then propose new pet profiles or updates to existing pets with confidence scoring. Route extracted vaccine photos to the existing auto-collect pipeline, prompting clients via SMS to confirm or provide missing fields. Avoid duplicate pets by using name-plus-owner matching and allow staff to review and approve suggested changes before they go live. Maintain an attribution trail linking each piece of pet data back to the originating IG message. This accelerates setup and reduces back-and-forth while keeping pet records current.

Acceptance Criteria
IG DM Extraction and Mapping to Smart Cards
Given an Instagram DM is ingested via ThreadSync containing pet-related text and/or media When the message includes any of: pet name, photo, breed/size notes, preferences, or vaccine image Then the system extracts each detected field into a structured payload and maps it to the corresponding Smart Card attributes And extraction completes within 5 seconds for messages under 1,000 characters or one image under 5MB And multi-pet mentions (e.g., “Milo & Luna”) result in separate candidate records per pet And the proposed changes appear in the Pet Suggestions queue with a summary of detected fields
Detection of Mentions, Hashtags, and Structured Phrases
Given a DM contains pet details expressed as explicit mentions (e.g., “Pet: Milo, Breed: Goldendoodle, 45 lb”), hashtags (e.g., “#Milo #vaccines #allergicToChicken”), or structured phrases (e.g., “Milo prefers early mornings; timid with large dogs”) When the parser runs Then the system recognizes and normalizes values into canonical fields: name, breed, size/weight, preferences, behavior notes, vaccine docs And hashtags map to fields when unambiguous (#vaccines -> vaccine docs present; #allergicToChicken -> dietary notes) And ambiguous tags or phrases are flagged with confidence < 0.6 for manual review And each normalized field records the source token(s) and offsets for traceability
Confidence Scoring and Suggestion Generation
Given extracted fields from an IG DM When the system generates update suggestions Then each field-level suggestion includes a confidence score between 0.0 and 1.0 And suggestions with confidence ≥ 0.6 are pre-selected in the review UI; < 0.6 are unselected with a warning icon And a single message can generate both new-pet proposals and updates to existing pets And the UI displays a human-readable rationale snippet (e.g., matched pattern or hashtag) for each suggestion
Vaccine Image Routing to Auto-Collect Pipeline
Given an IG DM includes an image likely to be a vaccine record When the ingestion completes Then the image is routed to the existing vaccine auto-collect pipeline within 10 seconds And the client receives an SMS confirmation link requesting missing vaccine fields if OCR cannot extract expiration or vaccine type And the pet profile shows a pending vaccine verification badge until client or staff confirms And failed OCR attempts trigger a retry (up to 2) before prompting for manual upload
Duplicate Pet Detection and Merge Safeguards
Given a suggestion proposes a new pet named X for owner Y (IG handle mapped to existing client or a new contact) When an existing pet with name ≈ X (Levenshtein distance ≤ 1) under the same owner is found Then the system suppresses auto-creation and converts the proposal into an update suggestion And the review UI shows a merge banner with side-by-side diffs And no duplicate pet record is created until staff explicitly approves a split And all merges preserve historical appointments and payments for the surviving pet
Staff Review and Approval Workflow
Given pet data suggestions exist for a client When a staff member opens the Pet Suggestions queue Then suggestions are grouped by pet candidate and sorted by newest first And staff can Approve, Edit, or Reject each field-level suggestion And only Approved fields update live Smart Cards; Edited fields capture the editor as the actor; Rejected fields are archived And all actions are audit-logged with timestamp, staff ID, and suggestion IDs
Attribution Trail Linking Back to IG Message
Given any field on a Pet Profile was created or updated from an IG DM When viewing the field’s details Then the UI displays attribution: IG handle, message timestamp, message ID, and a 120-character snippet And a “View Source” action opens the original message context in ThreadSync And exporting the pet profile includes the attribution metadata And deleting the source message does not remove the attribution record, which remains immutable in the audit log
SMS Thread Context Header
"As a walker, I want a context banner in the SMS thread that shows the IG handle and latest DM summary so that I can respond quickly with the right information."
Description

Display a collapsible context header at the top of each SMS conversation showing the client’s IG handle, a fresh DM summary, and any newly detected pet details or action prompts (e.g., confirm vaccine record). Include the DM timestamp, a deep link back to the IG DM for quick reference, and visual indicators when the summary is older than a configurable threshold. Allow staff to pin, dismiss, or refresh the header and have their choice persist per thread. Ensure the header is consistent across mobile and desktop, loads instantly, and degrades gracefully if IG data is temporarily unavailable. This creates an at-a-glance view that speeds replies and improves professionalism.

Acceptance Criteria
Header content and instant load on thread open
Given an SMS thread linked to an IG handle with recent DM activity and at least one tagged pet When a staff member opens the thread Then a context header renders at the top within 300ms at p95 without blocking the SMS composer And the header is expanded by default and includes: IG handle (tap-to-copy), last DM summary truncated to 280 characters with ellipsis, DM timestamp in the staff member’s local timezone, a deep link control labeled "Open IG DM", tagged pet details (name, species/breed if known, photo thumbnail), and any applicable action prompts And if no tagged pet details or prompts exist, the corresponding sections are omitted without empty placeholders
Collapse, pin, dismiss, refresh with per-thread persistence
Given the context header is visible on a specific client SMS thread for a specific staff member When the staff member collapses the header Then the header remains collapsed for that staff member on this thread across sessions and devices for 30 days or until manually expanded When the staff member pins the header Then it remains expanded and is not auto-collapsed by any default behaviors When the staff member dismisses the header Then it hides for that staff member on this thread until new IG activity is detected or 7 days elapse, whichever occurs first When the staff member taps Refresh Then the header re-fetches IG summary and tagged pet data and updates visible content within 2 seconds at p95, showing a non-blocking progress state during fetch And persistence does not affect other threads or staff members
Freshness indicator and configurable threshold
Given a default staleness threshold of 24 hours (org-configurable between 1 and 72 hours) When the last IG DM summary timestamp exceeds the threshold Then the header displays a "Stale" badge and warning icon, and the DM timestamp is shown as relative time with absolute time on hover/long-press When the threshold is updated in org settings Then subsequent header renders honor the new threshold without requiring app updates or user sign-out/in When a new IG DM arrives for the client while the thread is open Then the header updates within 5 seconds to show the new summary, updates the timestamp, and clears any "Stale" badge When a manual Refresh succeeds Then the summary timestamp reflects the fetch time (e.g., "Just now"); if it fails, a non-blocking error toast is shown and prior content remains unchanged
Deep link opens correct IG DM
Given a valid IG handle is present in the header When the staff member activates the "Open IG DM" link on mobile Then the native Instagram app opens directly to that handle’s DM thread within 2 seconds or falls back to the web DM if the app is unavailable When the link is activated on desktop Then a new browser tab opens to the correct DM conversation URL, and the SMS thread in FetchFlow remains open and focused If the IG handle is missing or invalid Then the deep link control is disabled with a tooltip "IG handle unavailable" and no navigation occurs
Action prompts lifecycle
Given the client is missing a required vaccine record When the thread header renders Then an action prompt "Confirm vaccine record" appears with a primary action that opens the Pet Profile Smart Card and a secondary Dismiss When the primary action is completed successfully Then the prompt disappears and does not reappear for that staff member on this thread, and the client profile reflects updated vaccine status When the prompt is dismissed Then it is hidden for that staff member on this thread for 14 days unless the underlying requirement changes (e.g., vaccine expires), in which case it reappears When multiple prompts are eligible Then up to 3 are shown in priority order with a "View more" control to reveal additional prompts inline
Graceful degradation when IG unavailable
Given the IG API is unreachable or returns an error/timeout When a staff member opens the SMS thread Then the header displays a non-blocking placeholder state for up to 2 seconds and then shows fallback content: client name, any locally stored pet details, and a message "IG data unavailable — retry" with a Refresh control And the deep link control is hidden or disabled during the outage; no blocking error modals are shown; the SMS composer remains usable When connectivity to IG recovers Then a background retry populates the header within 10 seconds without requiring a reload; after 3 failed retries, the header remains in fallback until manual Refresh
Cross-platform consistency and performance
Given the header renders on both mobile and desktop When viewed at 360x640, 768x1024, and 1440x900 Then the same data elements and controls are present, layout adapts without horizontal scrolling, and cumulative layout shift (CLS) on first render is <= 0.05 And visual styling matches design system components with spacing and typography variance <= 1px And input latency in the SMS composer does not increase by more than 10ms at p95 due to the header And automated unit, integration, and visual regression tests cover header behaviors with >= 90% coverage for header-related modules
Client Profile Auto-Update & Merge Rules
"As an office manager, I want Instagram-sourced updates to apply safely to client profiles so that our data stays accurate without constant manual cleanup."
Description

Define deterministic rules to automatically update client profiles from Instagram-derived data while protecting verified information. Apply field-by-field precedence (e.g., confirmed phone/email in FetchFlow outranks IG-inferred data) and queue risky changes for manual review. Provide a merge workflow when IG data implies two client records belong to the same person, with side-by-side comparison, selected-field merge, and full rollback. Record all changes in an audit log with actor, source (Instagram), and timestamp, and surface a change history on the client profile. This keeps profiles clean, prevents data loss, and avoids duplicate contacts as context flows in from IG.

Acceptance Criteria
Auto-Update Field Precedence Protects Verified Contact Info
Given a client profile has a Verified phone or email in FetchFlow When ThreadSync imports a different IG-derived value for that same field Then the existing Verified value remains unchanged and the IG value is stored as a secondary, marked Unverified, with source=Instagram and a timestamp, and an audit entry is recorded Given a client profile lacks a phone or email When ThreadSync receives a valid IG-derived value (E.164 phone or RFC-compliant email) Then the field is populated, marked Unverified, source=Instagram, timestamped, and an audit entry is recorded Given a client profile has an Unverified value for a field When ThreadSync receives a different, valid IG-derived value Then the IG value replaces the Unverified value, remains Unverified, and the change is recorded in the audit log with source=Instagram
Risky Changes Queued for Manual Review with Safe Defaults
Given an IG-derived update would modify any field currently marked Verified (e.g., phone, email, legal name) When ThreadSync processes the update Then no change is applied automatically, a Review Queue item is created containing field, old value, proposed value, source=Instagram, timestamp, and the client profile shows a Pending Review indicator Given a reviewer opens a Review Queue item When they Approve the change Then the field is updated accordingly, verification status persists only if the Verified value was retained, the action is logged with actor=Reviewer and source=Instagram, and the Pending Review indicator is cleared Given a reviewer opens a Review Queue item When they Reject the change Then the client data remains unchanged, the suggestion is dismissed for 30 days, the action is logged with actor=Reviewer and source=Instagram, and the Pending Review indicator is cleared
Deterministic Merge Candidate Identification from Instagram Context
Given two distinct client profiles are both linked to the same Instagram handle via ThreadSync When the nightly dedup job runs or a new IG sync occurs Then a Merge Candidate is created referencing both profiles with reason="Shared Instagram Handle", and no auto-merge occurs Given pets with the same Smart Card ID are associated to two different client profiles and that Smart Card was referenced via Instagram context When ThreadSync evaluates merge rules Then a Merge Candidate is created with reason="Shared Pet Smart Card via Instagram", and no auto-merge occurs Given a Merge Candidate exists When viewed in the Merge Center Then the system displays both profiles side-by-side with highlighted conflicts, reasons, and last activity, and provides actions to Merge or Dismiss
Selected-Field Merge Workflow Preserves Verification and Relationships
Given a Merge Candidate is opened When a user selects a base profile and chooses field-by-field which values to keep (including primary phone/email) Then saving the merge produces a single merged client with the selected values, retains Verified flags for any retained verified fields, and does not automatically mark newly adopted values as Verified Given two profiles are merged When the merge completes Then all related entities (pets by Smart Card ID, appointments, invoices, packages, messages, notes) are consolidated under the merged client, duplicate pets by identical Smart Card ID are de-duplicated, and the source profile is archived to enable rollback Given a merge completes When viewing the audit log Then an entry exists with actor=User, action=Merge, source=Instagram (if IG-derived inference was used), affected fields, source/target IDs, and timestamp
Full Merge Rollback Restores Pre-Merge State Without Data Loss
Given a client was created via merge of two profiles When a user triggers Rollback from the merge audit entry Then the system restores the two original profiles with their original IDs, field values, and verification flags, re-associates all related entities to their pre-merge owners, removes the merged composite profile, and records a rollback audit entry with actor=User and timestamp Given a rollback completes When comparing counts of related entities (pets, appointments, invoices, packages, messages) before merge and after rollback Then the counts match and no data loss or duplication is present Given a rollback completes When viewing Change History on each restored profile Then entries show the original merge and the rollback events in chronological order with source=Instagram where applicable
Audit Log and Client Profile Change History Visibility
Given any auto-update, manual approval/rejection, merge, or rollback occurs When the action is completed Then an immutable audit log entry is written with actor (ThreadSync Auto-Update or User), source (Instagram where applicable), field(s) or action, old/new values, and ISO 8601 timestamp, and it appears in the client profile Change History within 3 seconds Given a user opens a client profile Change History When filtering by action type (Auto-Update, Review Approved, Review Rejected, Merge, Rollback) or by field name Then the list updates to show matching entries in reverse chronological order with pagination for over 100 entries Given a user opens a specific audit entry When inspecting details Then the entry displays the exact fields changed, old and new values, actor identity, source=Instagram where applicable, and a deep link to related merge or review records
Privacy, Consent, and Data Retention Controls
"As a business owner, I want consent and retention settings for IG-derived data so that we stay compliant and respect client privacy."
Description

Provide clear controls to obtain and record client consent to link their Instagram context to SMS communications, with the ability to opt out at any time. Allow businesses to configure retention windows for DM summaries and derived metadata, automatically purging expired snapshots while retaining necessary audit entries. Limit visibility of IG-derived data based on staff roles, and mask or redact sensitive content by default. Support export/delete requests to align with data protection policies and display source attribution wherever IG data appears. These controls build trust and ensure compliant, respectful handling of client information.

Acceptance Criteria
IG-to-SMS Linking Consent Capture
Given a client without recorded IG consent, when any staff member opens the client's SMS thread or profile, then no IG-derived context is shown and a consent capture option is presented. Given a client completes consent via SMS keyword or web checkbox, when submission is received, then the system records a consent entry with clientId, channel (SMS/Web/Admin), timestamp (UTC), actorId (if admin), policyVersion, and IP (if web). Given valid consent exists, when ThreadSync enriches the SMS thread header or client profile, then IG handle, last DM summary, and tagged pet details appear with a "Consented" indicator and source attribution. Given consent exists, when an audit export is generated for the client, then the consent record is included with all captured fields.
Client Opt-Out from IG Context Enrichment
Given a client with prior consent, when they send the SMS keyword "STOP IG" or toggle the privacy switch in the client portal, then the system revokes consent, logs the event, and stops IG context enrichment within 60 seconds. Given consent is revoked, when any staff views the client's SMS thread or profile, then IG-derived fields are hidden, labeled "Opted out", and no new IG data is fetched. Given consent is revoked, when staff attempts to re-enable enrichment, then the system requires new explicit client consent before showing IG-derived data. Given consent is revoked, when the system responds to the client, then an SMS confirmation is sent with a link to re-consent.
Source Attribution on IG-Derived Data
Given IG-derived data is displayed in the SMS thread header or client profile, when it renders, then a source attribution is shown including "Source: Instagram", the @handle, and the capture timestamp. Given source attribution is displayed, when the "Learn more" link is tapped, then the privacy notice opens explaining usage, consent, retention, and how to opt out. Given no consent exists, when viewing the thread header or profile, then neither IG-derived data nor source attribution is displayed.
Configurable Retention for IG-Derived Snapshots
Given an account owner opens Privacy settings, when they set a retention window between 30 and 365 days for DM summaries and derived metadata, then the setting saves with a versioned policyId and applies account-wide. Given a retention window is saved, when the setting is changed, then the new policy version, actor, and effectiveAt timestamp are logged. Given a retention policy is active, when new IG snapshots are created, then each snapshot stores a purgeAt timestamp = captureAt + retentionDays.
Automatic Purge with Audit Preservation
Given IG-derived snapshots have purgeAt in the past, when the purge job runs hourly, then those snapshots are permanently deleted and removed from thread headers, profiles, and search within 24 hours. Given a snapshot is purged, when audit logs are queried, then a deletion event exists containing clientId, snapshotId, purgeAt, deletedAt, policyId, and actor=system. Given a snapshot is purged, when an admin views the client's activity log, then a non-sensitive entry "IG snapshot purged per retention policy" is visible.
Role-Based Visibility and Default Redaction of IG Content
Given role permissions are configured, when a user without the "View IG Context" permission views a thread or profile, then IG-derived data is hidden and replaced with "Hidden — insufficient permission". Given role permissions are configured, when a user with the "View IG Context" permission views a thread or profile, then IG-derived data is visible only if client consent exists and the data is within retention. Given IG-derived text contains emails, phone numbers, street addresses, or payment-like numbers, when it is displayed, then those elements are masked by default (e.g., j***@***.com, ***-***-1234) with a "Redacted" indicator. Given a user has the "Unmask Sensitive" permission, when they click Reveal, then sensitive elements are shown for the current session and an unmask event is logged with userId, clientId, timestamp, and surface. Given a user lacks the "Unmask Sensitive" permission, when they attempt to reveal sensitive elements, then the action is blocked and an access-denied audit entry is recorded.
Client Data Export and Deletion Requests for IG-Derived Data
Given an admin requests a client data export, when the export is generated, then it includes IG-derived fields (handle, last DM summary snapshots, tagged pet details), consent history, source attribution, retention metadata, and related audit entries, delivered within 24 hours. Given a client or admin requests deletion of IG-derived data, when the request is processed, then all IG-derived snapshots and metadata are deleted within 24 hours while retaining minimal audit entries (consent and deletion events) as allowed by policy. Given IG-derived data is deleted, when any staff views the thread header or client profile, then no IG-derived content is displayed and enrichment remains disabled until new consent is obtained. Given an export or deletion request is completed, when notifications are sent, then the requester receives a confirmation with requestId, scope, and completion timestamp.
Sync Reliability, Monitoring, and Fallbacks
"As support staff, I want reliable sync and clear status indicators so that I can trust the IG context shown in SMS and quickly resolve issues if they arise."
Description

Implement robust sync pipelines for Instagram context, including webhook ingestion, rate-limit handling, retries with exponential backoff, and idempotent processing to prevent duplicates. Expose a health dashboard showing last successful sync time, queued jobs, and any errors, with alerts when permissions expire or syncs fail. Provide a manual fallback to pull the latest DM summary on demand when automated sync is delayed, and clearly communicate the sync status in the SMS thread header. Include sandbox/testing tools to validate configuration without affecting production data. This ensures ThreadSync remains dependable during peak times and platform changes.

Acceptance Criteria
Idempotent Webhook Ingestion and Duplicate Prevention
Given a valid Instagram webhook event with a verified signature When the same event (identical event_id/message_id) is delivered 3 times at-least-once Then the system responds 200 OK within 500 ms for each delivery And only one job is enqueued and processed And the SMS thread header and client profile are updated exactly once And subsequent duplicate deliveries are logged as "already processed" with no side effects Given out-of-order deliveries for the same Instagram thread When processing the events Then updates are applied in chronological order using Instagram timestamps Given a malformed payload or invalid signature When received Then the system responds 400 without enqueuing a job and records a security warning log entry
Rate-Limit Handling and Exponential Backoff
Given the Instagram API returns 429 or 5xx during a fetch When retrying the job Then retries use exponential backoff of 2s, 4s, 8s, 16s, 32s with ±20% jitter, up to 5 retries And concurrent Instagram API calls per account are limited to 2 And after the final failed attempt, the job is marked failed, no partial updates are applied, and an alert is emitted Given the API recovers before the max retries When a retry succeeds Then the SMS thread header and client profile reflect the updated context within 10 seconds of success And prior failed attempts are logged with error codes and attempt counts
Sync Health Dashboard Observability
Given a user with Admin or Support role opens the Sync Health dashboard When viewing Then they see per-account: last successful sync timestamp, queued job count, p95 latency (last 24h), retry/failure counts by error code, and next retry ETA Given the dashboard is open When metrics update Then the data auto-refreshes at least every 15 seconds without full page reload Given an account is selected When exporting metrics Then a CSV is downloaded containing timestamps, account identifier, and metric values Given a non-authorized user attempts access When loading the dashboard Then access is denied with HTTP 403 and no metrics are returned
Permission Expiry and Failure Alerts
Given the system detects 401/403 from Instagram or a failed subscription validation When the condition is first observed Then the account owner receives an in-app banner and email within 5 minutes And the SMS thread header shows status "Action required" with a re-auth link And an internal Ops alert is created in the monitoring channel And duplicate alerts are suppressed for 6 hours per account until the state changes Given the user re-authenticates successfully When a subsequent sync succeeds Then the alert auto-resolves and the status chip returns to "Up to date" within 10 seconds
Manual Fallback: Pull Latest DM Summary
Given a pro is in an SMS thread with an Instagram-connected client When they tap "Refresh IG context" Then a fetch is initiated immediately And the status chip shows "Refreshing" with a spinner And on success the last DM summary, Instagram handle, and tagged pet details update within 10 seconds And an audit log entry is recorded with actor, timestamp, and result Given the fetch returns 401/403 When the action completes Then the UI shows "Re-auth required" with a link to reconnect And no stale data overwrites existing context Given the account is rate-limited When "Refresh IG context" is tapped during cooldown Then the button is disabled for the backoff duration and a tooltip explains the remaining cooldown time
Sync Status Communication in SMS Thread Header
Rule: The status chip displays one of: Up to date (last sync < 15 minutes), Refreshing (active job), Delayed (last sync ≥ 15 minutes), Action required (permission issue) Rule: The chip includes a relative timestamp (e.g., "Updated 3m ago"), is keyboard focusable, and is screen-reader accessible (aria-live=polite) Rule: Visuals meet WCAG AA contrast for text and icons Given a state transition (job start, job completion, or error) When it occurs Then the chip updates within 3 seconds and any tooltip reflects the latest status or last error
Sandbox and Configuration Validation Tools
Given an account is set to Sandbox mode When simulated Instagram webhooks and API responses are sent Then events are routed to a sandbox namespace and do not persist or alter production client profiles, threads, or metrics Given a tester uses the webhook replay tool When selecting a stored sample and replaying Then the system processes it end-to-end and shows pass/fail with execution logs without contacting live Instagram APIs Given a tester runs the configuration validator When validating Then the system verifies app credentials, webhook subscription handshake, permission scopes, and callback URL reachability, returning a checklist within 30 seconds Given sandbox artifacts exceed 24 hours in age When cleanup runs Then artifacts are automatically purged and the purge is logged

Bridge Insights

See your full funnel from DM to SMS to paid visit: conversion rates by post/story, time-to-first-reply, best-performing templates, and drop-off points. Get recommendations on reply timing and copy to lift bookings and make Instagram an accountable revenue channel.

Requirements

DM Source Attribution
"As a business owner, I want to know which IG content initiates customer conversations so that I can invest in posts and stories that drive bookings."
Description

Capture and persist the origin of each Instagram DM (post, story, ad, link sticker, profile link) and associated metadata (media ID, timestamp, campaign tags) to attribute downstream SMS conversations, bookings, and paid visits. Support UTM-style parameters on links, map DM threads to contacts and Pet Profile Smart Cards once a phone number is collected, deduplicate multi-touch interactions, and handle missing or partial metadata gracefully. Store attribution as first-touch and last-touch dimensions for reporting, normalize time zones, and provide multi-account support for studios with multiple brand handles. Ingest events via Meta Graph API webhooks where available, with queuing and retries for reliability, and expose these dimensions throughout analytics and exports.

Acceptance Criteria
Capture DM Source & Metadata
- Given a valid IG DM webhook event with source_type in {post, story, ad, link_sticker, profile_link} and media_id/timestamp/campaign_tags present, When the event is received, Then the system persists an attribution record linked to thread_id and ig_user_id with source_type, media_id, campaign_tags, and timestamps stored in both UTC and studio_time_zone, and the record is queryable via API within 60s (P95). - Given an IG DM webhook event missing media_id or campaign_tags, When ingested, Then the record is persisted with nullable fields and source_detail set to "unknown" where absent, without throwing errors, and a completeness_flag is set to "partial". - Given any DM attribution record, When viewed in the dashboard raw event log, Then the stored source_type and timestamps display exactly as saved, and values pass schema validation (type/length) 100% of the time.
UTM Parameter Support on IG Links
- Given a DM initiated from an IG profile link or link sticker that includes UTM-style query params (utm_source, utm_medium, utm_campaign, utm_content, utm_term), When the associated referral metadata is received, Then the system parses case-insensitive utm_* keys and persists each value (max 255 chars) to the attribution record. - Given unknown non-utm_* query params present, When ingested, Then they are stored in a custom_params map for analytics and export. - Given malformed query strings, When parsed, Then ingestion succeeds and invalid pairs are ignored without blocking the event; parsing error rate <= 0.1%.
Thread-to-Contact & Pet Profile Mapping
- Given a DM thread without a phone number at first touch, When the customer's phone number is later collected via SMS or Pet Profile Smart Card, Then the system deterministically links the thread_id to the contact and associated Pet Profile(s) within 5 minutes (P95). - Given the link is established, When downstream SMS conversations, bookings, and paid visits are created, Then first_touch_* and last_touch_* attribution dimensions from the linked thread are stamped onto those events and become filterable in analytics within 15 minutes (P95). - Given multiple pets under the same contact, When attribution is applied, Then attribution attaches at the contact level and is reflected for all associated Pet Profiles consistently.
Multi-Touch Deduplication & Attribution Windows
- Given an IG user engages multiple sources before conversion, When a qualifying conversion event occurs, Then first_touch is the earliest IG interaction within a 90-day lookback and last_touch is the most recent within a 7-day lookback. - Given two interactions share the exact timestamp, When tie-breaking, Then precedence is applied in order: ad > post > story > link_sticker > profile_link. - Given duplicate webhook deliveries for the same event, When processed, Then idempotency prevents duplicate attribution records (0 duplicates in tests of 10k replays).
Multi-Account Attribution Support
- Given a studio connects multiple IG accounts, When DMs arrive from any connected account, Then each attribution record includes account_id and account_handle, and analytics can filter/group by these fields. - Given the same IG user DMs multiple connected accounts, When computing first/last touch, Then deduplication occurs at the studio level across accounts while preserving the originating account dimension on each touch. - Given exports are generated, When downloaded, Then each row includes account_id and account_handle columns with non-null values for attributed events.
Webhook Ingestion Reliability & Idempotency
- Given Meta Graph API webhooks are temporarily unavailable or return 5xx, When events are enqueued, Then the system retries with exponential backoff and jitter for up to 7 attempts, persisting the queue to durable storage; successful delivery ratio >= 99.9% daily. - Given webhook redelivery of the same event, When processed, Then an idempotency key composed of {app_id, page_id, thread_id, event_ts} ensures only one persisted attribution record. - Given a sustained outage of 30 minutes, When service recovers, Then backlog is drained and all events within the retention window are persisted within 10 minutes (P95).
Analytics & Export Dimension Exposure
- Given a user opens Bridge Insights, When filtering or grouping by attribution, Then first_touch_source, first_touch_media_id, first_touch_campaign, last_touch_source, last_touch_media_id, last_touch_campaign, utm_source/medium/campaign/content/term, and account_handle are available as dimensions. - Given an attribution-linked conversion funnel (DM -> SMS -> booking -> paid visit), When viewing conversion by source, Then conversion rates and counts match underlying event totals within 0.5% tolerance. - Given a CSV export is generated, When downloaded, Then it includes the above attribution columns with ISO 8601 UTC timestamps and studio-local timestamps, and values match the dashboard for the selected date range.
DM-to-SMS Bridge Opt-in
"As a groomer, I want prospects to move from IG DMs into my SMS queue so that I can manage all leads and bookings in one place."
Description

Provide a frictionless path from Instagram DMs into FetchFlow SMS by sending an automated DM with an opt-in link, securely capturing a phone number and consent, and creating or merging a contact tied to the originating IG handle. Initialize a lead with the pet’s name if provided, attach attribution metadata, and start a two-way SMS thread inside the existing FetchFlow inbox. Measure and store time-to-first-reply (from customer DM to staff’s first response across channels), surface errors (e.g., rate limits, invalid numbers), and offer fallback instructions when automation is restricted. Ensure duplicate detection and merge logic to avoid fragmented conversations.

Acceptance Criteria
Auto DM With Opt-In Link on New Instagram DM
Given an inbound Instagram DM to a connected business account from a user who has not opted into SMS When the DM is received Then an automated DM containing a unique opt-in link is sent within 5 seconds And the message uses the configured template with populated business name and IG handle And at most one automated DM is sent per user per 24-hour window unless manually re-triggered by staff And an event auto_dm_sent is recorded with IG user ID, DM thread ID, timestamp, and link ID
Secure Phone Capture and Explicit SMS Consent
Given the recipient taps the opt-in link When the web form loads Then it is served over HTTPS (TLS 1.2+) and loads in under 2 seconds on 4G And the form displays compliant consent text (business name, message frequency, STOP/HELP, carrier rates) And the phone field validates E.164 formatting with country default and server-side verification And submission requires an explicit consent checkbox unchecked by default And on submit, phone, consent text, timestamp (UTC), IP, user agent, source=Instagram, and IG user ID are stored And logs mask the phone except last 2 digits; no PII appears in client-side analytics payloads
Contact Create-or-Merge From IG Handle and Phone
Given a successful consent submission with a valid phone When no existing contact matches the phone Then a new contact is created with the IG handle, IG user ID, and consent status SMS=opted_in When an existing contact matches the phone Then the IG handle and IG user ID are attached to the existing contact and no duplicate contact is created And existing pet profiles, packages, and notes are preserved And if the IG handle was previously linked to a different phone, that phone is retained as an alternate and marked superseded And the operation is idempotent: repeated submissions with the same link do not create additional contacts And an audit log entry records before/after contact identifiers and merge action
Two-Way SMS Thread Initialization in FetchFlow Inbox
Given a contact has opted into SMS via the opt-in link When consent is stored Then a welcome SMS is sent from the business number within 5 seconds using the configured template And a single conversation thread appears in the FetchFlow inbox linked to the contact and IG handle And staff replies from the inbox are delivered via SMS to the contact’s phone and customer replies appear in the same thread And no additional threads are created for messages within a rolling 24-hour window for the same contact and channel And the thread displays the attribution source as Instagram with the originating DM link
Pet Name Initialization From IG DM or Opt-In Flow
Given the originating DM contains a pet name or the opt-in form includes an optional Pet Name field When the opt-in is completed Then the lead is initialized with a Pet Profile containing the provided pet name And if both sources provide a name, the form input takes precedence and the DM-extracted value is kept as a note And if no name is provided or confidently extracted, no placeholder pet is created And the Pet Profile is visible on the contact and in the inbox sidebar
Attribution and Funnel Metrics Including Time-to-First-Reply
Given a user sends the first DM in a thread When automation and staff interactions occur across IG and SMS Then the system records attribution metadata: IG media type (post/story), media ID, campaign ID (if available), template ID used, and link ID And time_to_first_reply is calculated as the elapsed time from the customer’s first DM timestamp to the first non-automated staff reply on either IG or SMS, to the nearest second And automated messages (e.g., auto DM, welcome SMS) are excluded from the first reply calculation And metrics are persisted per contact and per bridge session and are queryable by Bridge Insights APIs and reports
Error Handling, Rate Limits, and Fallback Instructions
Given an attempt to send an automated DM or process an opt-in When Instagram API rate limits, permission errors, or delivery failures occur Then the system logs an error event with code, reason, IG thread ID, and timestamp and does not retry more than 3 times with exponential backoff And the FetchFlow inbox displays a banner on the affected conversation with human-readable reason and a “Copy Fallback Message” action And the fallback message includes the business SMS number and instructions to text a keyword to opt in manually And if the web form receives an invalid phone number, client- and server-side validation prevents submission and shows a specific error And all failures are visible in an errors dashboard filterable by reason within the last 30 days
Full-Funnel Analytics Dashboard
"As a business owner, I want a clear view of conversions from IG to paid visits so that I can measure ROI and identify bottlenecks."
Description

Deliver a dashboard that visualizes the funnel from IG DM → SMS conversation → booking → paid visit, including conversion rates, counts, revenue attribution, time-to-first-reply distributions, and stage-specific drop-off points. Enable filters by date range, post/story, campaign tags, service type, staff member, and location; provide drill-down from aggregate metrics to the underlying conversation and visit; and allow CSV/PDF export. Include cohorting by content and reply latency buckets, annotations for campaign changes, configurable goals, and near–real-time data refresh with clear freshness indicators. Optimize for mobile with performant queries and caching.

Acceptance Criteria
Funnel Visualization & Stage Conversions
Given a user with permissions and connected IG, SMS, booking, and payment integrations And a selected date range is applied When the user opens the Full-Funnel Analytics Dashboard Then the dashboard displays a four-stage funnel labeled IG DM → SMS Conversation → Booking → Paid Visit And each stage shows counts and stage-to-stage conversion rates (%) and an overall DM-to-Paid conversion rate (%) And each transition shows drop-off counts and percentages And all metrics reflect only records within the selected date range and active filters And selecting a content source (post, story, campaign tag) updates the funnel within 500 ms at p95 And tapping/hovering a stage shows a tooltip with metric definition and time range And totals and rates reconcile with the drill-down results within ±0.1%
Reply Latency Distribution & Content Cohorts
Given inbound IG DMs that initiated SMS conversations exist within the selected date range When the user views the Reply Latency panel Then a histogram displays time-to-first-reply distribution with buckets: 0–5m, 5–15m, 15–60m, 1–4h, >4h And the panel shows p50, p90, and p95 latency values And a cohort table groups by content (post, story, campaign tag) × latency bucket showing counts, booking conversion rate (%), and paid conversion rate (%) And cohorts exclude rows with n<30 by default with an option to include them And all calculations respect active filters, business timezone, and deduplicate conversations by unique thread
Interactive Filters & Segmentation
Given the user applies filters for date range (relative and absolute), content (post, story), campaign tags, service type, staff member, and location When filters are modified Then all widgets and totals update using the intersection (AND) of selected filter values And multi-select is supported for every filter And the default date range is Last 30 Days in the business timezone And filter chips reflect selections and can be cleared individually or all at once And applying or clearing filters completes within 500 ms at p95 for cached queries and 1500 ms at p95 for uncached queries And an empty-state message appears if no data matches the filters
Drill-down & Export
Given an aggregate metric or visualization is displayed When the user taps a stage, bar, cohort row, or metric chip Then a drill-down list opens showing the exact result set used to compute the aggregate with columns: timestamp, content source (post/story), campaign tags, client, pet, staff, stage outcome, revenue, reply latency And the number of rows equals the aggregate count used in the visualization And the user can open a conversation or visit detail from any row and navigate back to the list and dashboard And the user can export the current view to CSV and the full dashboard to PDF And CSV exports include a UTF-8 header, respect all active filters, use the business timezone for timestamps, and support up to 100,000 rows via pagination or an async job completed within 60 seconds at p95 And PDF exports include the funnel, charts, annotations, goals, and a filter summary and render within 30 seconds at p95 with selectable Letter/A4 page size
Annotations & Configurable Goals
Given the user needs to mark campaign changes and set performance targets When the user adds an annotation with date/time, label, and optional campaign tag Then the annotation appears on relevant charts and can be edited or deleted by its creator or admins And annotations are included in PDF exports When the user configures goals for metrics (e.g., booking rate %, paid conversion %, reply latency p50) with target values and effective dates Then the dashboard displays target lines/badges and computes status (On Track/At Risk/Off Track) with rules: On Track when metric ≥ target (for rates) or ≤ target (for latency), Off Track when outside by ≥10%, else At Risk And goal status and comparisons respect active filters and selected date range
Data Freshness & Near–Real-Time Updates
Given new IG DMs, SMS messages, bookings, or payments are received When up to 2 minutes have elapsed since event receipt Then the dashboard ingests and reflects the new events in metrics and visualizations And a freshness indicator displays “Data as of [timestamp]” in the business timezone And the indicator turns amber when freshness age >10 minutes and red when >30 minutes And users can manually refresh (pull-to-refresh or tap) to fetch the latest snapshot And late-arriving events backfill without double-counting and with idempotent processing And if upstream sources are delayed, the dashboard shows a non-blocking banner naming affected sources and current lag
Mobile Optimization & Caching
Given the user opens the dashboard on a modern iOS or Android device over 4G or better When the dashboard loads and the user navigates, filters, and drills down Then initial time-to-interactive is ≤2.5s at p95 (≤1.5s at p50) for the default date range And subsequent interactions (filter apply, tab switch, drill-down open) complete in ≤300ms at p95 And initial API payload size is ≤800KB at p95 with gzip/brotli compression and lazy-loaded images And client-side caching stores the last successful dashboard view for 24 hours for instant reopen (≤300ms) and provides a read-only stale view when offline with a “Stale” badge And touch targets are ≥44px, content meets WCAG 2.1 AA contrast, and hover interactions have tap/long-press equivalents
Reply Timing Recommendations
"As a walker, I want guidance on when to reply to DMs and texts so that I can maximize conversions without being online all day."
Description

Analyze historical reply latency versus conversion outcomes to recommend optimal reply-time targets by day of week and hour, and suggest SLA tiers (e.g., reply within 10 minutes) that maximize bookings. Present actionable guidance in the dashboard and inbox (e.g., "most successful replies occur within 7–12 minutes after first DM"), simulate potential lift from meeting targets, and track adherence over time. Allow users to set goals, receive nudges when at risk of breaching SLAs, and adjust recommendations based on seasonality and service type.

Acceptance Criteria
Compute and display optimal reply-time targets by day/hour
- Given at least 90 days of historical DM-to-booking data, when the nightly job runs at 02:00 local time, then the system computes conversion-maximizing reply latency windows (median and 80th-percentile) per day-of-week and hour-of-day for each service type with ≥50 qualifying DMs and versions them with a timestamp. - Given a user opens the Bridge Insights dashboard or the inbox, when targets exist for the current context (day/hour/service type), then show the top recommended window (e.g., “7–12 minutes”) with sample size and confidence badge, and expose a tooltip linking to methodology. - Given a segment has <50 qualifying DMs in the lookback, when loading recommendations, then fall back to the account-level baseline window and display a “Low data” badge. - Given data ingestion is delayed, when the nightly job fails, then the system retains the last successful recommendation set and surfaces a non-blocking warning banner.
SLA tier suggestions and goal setting
- Given recommended latency windows are available, when the user clicks “Set SLA”, then the system suggests 2–3 SLA tiers (e.g., 5, 10, 20 minutes) with predicted booking lift for each tier rounded to the nearest 1% and allows save per service type and globally. - Given the user saves an SLA goal, then it persists across sessions, is editable, and is audit-logged with user, timestamp, previous and new values. - Given a global SLA exists, when a service type override is created, then the override is applied to that service type and the global SLA continues to apply to all others. - Given an SLA tier is deleted, when confirmed, then it is removed and historical adherence reports remain unchanged (snapshot at time of event).
Real-time SLA breach risk nudges
- Given an inbound DM without a reply, when elapsed time reaches 80% of the active SLA threshold, then send an in-app nudge within 5 seconds including contact name, elapsed timer, SLA target, and one-tap quick replies; if push notifications are enabled, also send a mobile push. - Given >3 open conversations between 80% and 100% of SLA, when generating nudges, then batch into a single notification summarizing count and list the highest predicted booking value first. - Given Do Not Disturb hours are configured, when within DND, then suppress real-time nudges, queue them, and send a single digest within 2 minutes after DND ends. - Given a reply is sent before reaching 100% of SLA, then any pending breach nudge for that conversation is canceled.
Lift simulation and what-if modeling
- Given the user selects a proposed SLA tier and clicks “Simulate lift”, when sufficient data exists (≥200 DMs in the last 90 days for the selected filters), then display predicted change in booking conversion and revenue for the next 30 days with 68% and 95% confidence intervals, sample size, lookback window, and key assumptions. - Given data is sparse (50–199 DMs), when simulating, then display “Low confidence” badge and wider intervals; if <50 DMs, then disable simulation and explain the data requirement. - Given the user adjusts filters (service type, day-of-week, campaign source), when changes are applied, then recompute results within 2 seconds or show a loading state if longer. - Given the user downloads results, when clicking “Export”, then a CSV is generated including inputs, predicted lift ranges, and timestamp.
Adherence tracking and reporting
- Given SLAs are active, when viewing the SLA Adherence tab, then show weekly adherence percentage, median reply time, and breach count broken down by service type and day-of-week, with trend vs prior period and the ability to export to CSV. - Given a reply is sent, when calculating adherence, then attribute it to the first inbound DM timestamp in the thread and count once per thread for SLA evaluation. - Given the user selects a past period, when rendering charts and tables, then values match the exported CSV within ±0.5% relative difference and time zones are consistently applied. - Given a breach occurs, when listing incidents, then each item includes conversation link, SLA target, actual response time, delta, and responsible user.
Seasonality and service-type adjustments
- Given ≥12 weeks of historical data, when the detected optimal window for a segment shifts by ≥10% or passes a drift threshold, then automatically update recommendations, record a changelog entry with old/new windows and impacted segments, and display a “Updated due to seasonality” badge for 7 days. - Given a new service type is added or renamed, when generating recommendations, then initialize with account-level baseline and mark the segment as “Learning” until 50 DMs are collected for that service type. - Given the user defines seasonal periods (e.g., holidays) or exclusions, when computing recommendations, then respect these periods and tag outputs derived from them as “Seasonal” in the UI and exports. - Given seasonality toggles are turned off, when recomputing, then revert to non-seasonal models at the next nightly job and record the change in the changelog.
Template Performance Insights
"As a trainer, I want to know which reply templates work best so that I can use wording that gets more bookings."
Description

Track performance of reply templates and common snippets across IG DM and SMS contexts, ranking them by downstream booking conversion, revenue per lead, reply rate, and time-to-first-reply impact. Surface best-performing templates for similar inquiry types, highlight underperformers, and enable lightweight A/B comparison with statistical confidence indicators. Provide suggestions to refine or archive low performers, support tagging and version history, and ensure results are segmentable by service, audience, and campaign.

Acceptance Criteria
Rank Templates by Downstream Performance
Given templates/snippets with usage data across IG DM and SMS and a selected date range, When the user selects a metric to sort by (Booking Conversion, Revenue per Lead, Reply Rate, Time-to-First-Reply impact), Then the list displays all items ranked by the selected metric with the metric value and sample size. Given an attribution window of 14 days from last template/snippet use to paid visit (configurable), When calculating booking conversion and revenue per lead, Then the system applies the window and excludes leads outside it, and a “How it’s calculated” link explains formulas. Given any item with sample size < 30 leads in the selected range, When ranking, Then it is labeled “Low sample” and excluded from Top 3 badges but remains visible. Given the user toggles Channel = All, IG DM, or SMS, When viewing the list, Then metrics are recalculated per selection and displayed consistently. Given two items have identical metric values, Then ties are broken by higher sample size, then alphabetically by item name, producing a deterministic order. Given up to 200 items in scope, When the ranked list is requested, Then it loads within 3 seconds on median network conditions.
Quantify Time-to-First-Reply Impact and Recommendations
Given a date range and channel selection, When the system computes time-to-first-reply (TTFR) for each template/snippet, Then it shows median TTFR and delta vs account median with a 95% confidence interval. Given an item’s TTFR is significantly faster and correlates with higher booking conversion (p < 0.05) with sample size ≥ 30, When recommendations are generated, Then the user sees “Send within X–Y minutes” guidance and top two copy cues correlated with lift. Given correlation is not significant (p ≥ 0.05) or sample size < 30, Then no timing recommendation is shown and the UI displays “Insufficient evidence for timing guidance.”
Suggest Best Templates for Similar Inquiries
Given an incoming inquiry classified by service and intent, When the user opens Suggestions, Then the system displays the top 3 templates/snippets for the same service and intent, ordered by booking conversion, with expected lift vs baseline and sample size. Given the user clicks Apply on a suggestion, Then the selected template/snippet text is inserted into the reply composer with channel-appropriate placeholders resolved. Given no item meets minimum sample size (≥ 30) in the segment, Then the system falls back to account-wide results and marks each suggestion with an “All-segment” badge.
Detect Underperformers and Recommend Actions
Given the selected date range and segment, When a template/snippet’s booking conversion or revenue per lead is ≥ 20% below the segment median with sample size ≥ 30, Then it is flagged as Underperforming. Given an item is flagged, When viewing its details, Then recommended actions are shown: copy refinement suggestions, optimal send-time window, and an Archive action. Given the user archives an item, Then it is removed from suggestion pools and disabled for new replies while its historical metrics remain visible in reports; an Unarchive option is available.
Lightweight A/B Comparison With Confidence Indicators
Given two templates/snippets selected within the same channel, service, audience, campaign, and date range, When Compare is clicked, Then the system shows conversion rate, revenue per lead, reply rate, and TTFR for each, plus absolute and relative lift. Given both items have sample size ≥ 30, Then a two-proportion z-test is run for conversion and reply rate; results show p-value, 95% confidence intervals, and a Significance badge (Significant if p < 0.05). Given either item has sample size < 30, Then comparison metrics are shown but significance is marked “Insufficient sample,” and the UI displays the additional leads required to reach power 0.8 at α = 0.05 (estimate).
Manage Tags and Version History
Given a template/snippet, When the user creates, edits, or deletes tags, Then tags are persisted and become available as filters within 1 second. Given a template/snippet edit, When changes are saved, Then a new version is created capturing author, timestamp, and diff of text and tags. Given version history is displayed, When the user selects a prior version and clicks Restore, Then that version becomes current for future use, and metrics attribution follows the version active at send time.
Segment, Filter, and Export Results
Given service, audience, and campaign filters, When any filter is applied, Then all metrics, rankings, flags, and recommendations recompute to reflect only data in the selected segment and display updated counts. Given multiple filters are active, Then the UI shows filter chips and totals reconcile to the unfiltered view when filters are cleared. Given the user clicks Export, When the export is requested, Then a CSV for the current view is generated within 10 seconds including item ID, name, channel, metrics, sample sizes, flags, and segment values.
Compliance and Data Governance
"As an owner, I want confidence that tracking and messaging comply with platform rules and privacy laws so that I can use insights without risking my accounts."
Description

Capture and store explicit consent for SMS opt-in with timestamps and source, respect Instagram messaging policies and rate limits, and provide easy opt-out flows synced across channels. Mask PII in analytics views, enforce role-based access to revenue and contact details, and maintain audit logs for attribution edits and merges. Define data retention windows for messaging and attribution data, document metric definitions, and ensure alignment with Meta platform terms and applicable privacy regulations.

Acceptance Criteria
SMS Opt-in Consent Capture with Timestamp and Source
Given a contact attempts to receive SMS without prior consent When any user initiates an outbound SMS Then the send action is blocked with an error indicating consent is required Given a contact grants SMS consent via Instagram DM keyword flow or Pet Profile Smart Card or web form When consent is confirmed Then a consent record is stored with fields: contact_id, phone_e164, status=granted, source (IG DM|Smart Card|Web), campaign_or_post_id (if available), granted_at_utc, granted_at_tz, actor_user_id (if applicable), and phone_hash Given a phone number has no granted consent When a bulk or automated SMS is scheduled Then those messages are excluded from the job and logged with reason=consent_missing Given multiple contact records share the same phone When a merge occurs Then the latest consent status by timestamp is preserved and all historical consent records are retained for audit Given an admin with permission exports the consent log When an export is generated Then it includes the stored fields and is filtered by date range and source, and export access is logged
Cross-Channel Opt-Out Sync
Given a recipient sends STOP, CANCEL, UNSUBSCRIBE, QUIT, or END via SMS When the message is received Then the system sets consent status=revoked for the phone within 1 second, halts further SMS, and sends a single confirmation reply Given a recipient sends START, UNSTOP, or SUBSCRIBE via SMS When the message is received Then the system sets consent status=granted and allows SMS sending Given an operator manually toggles unsubscribe in the UI When saved Then the suppression state is applied to the phone and contact across SMS and Instagram outreach within 60 seconds and an audit entry is created Given any outbound attempt to a suppressed phone When the send is triggered Then the system blocks the send and surfaces reason=suppressed with a link to manage consent Given suppression is changed on any channel When analytics refresh runs Then conversion and messaging metrics exclude suppressed messages from sendable counts
Instagram Policy and Rate Limit Compliance
Given a user drafts a reply to an Instagram DM When the last customer interaction is older than 24 hours Then the send action is disabled and a policy notice is displayed Given the system is sending messages via Instagram Graph API When rate limit usage exceeds 80% of the window Then subsequent sends are queued with exponential backoff so that API limits are not exceeded Given the Instagram API returns a policy or rate limit error When handling the response Then the system logs code/subcode, suppresses retry until reset time, and notifies operators via in-app alert Given any Instagram message is sent or received When persisted Then the platform stores the Meta message/thread IDs and timestamps for auditability Given a release is prepared When compliance checks run Then deployment is blocked unless the Instagram messaging policy checklist is signed off for the release and app review scopes match actual usage
PII Masking in Analytics and Exports
Given a user opens Bridge Insights dashboards When metrics are rendered Then PII is masked: phone numbers hashed or last4, contact names truncated, and no raw message content is displayed Given a user without Export PII permission downloads analytics CSV When the file is generated Then PII columns (full phone, full name, message content, addresses, emails) are excluded Given a user with Export PII permission downloads analytics CSV When the file is generated Then PII columns are included and the export event is recorded with actor, timestamp, and dataset parameters Given a user drills into metric details When rows are shown Then only metadata (direction, status, timestamps, template IDs, post IDs) is visible and message bodies are redacted Given a public/shared link to a dashboard is created When the link is accessed Then PII remains masked regardless of viewer role
Role-Based Access to Revenue and Contact Details
Given a Staff role user views bookings and insights When revenue totals or ARPU are displayed Then values are obfuscated or hidden per role policy Given a Staff role user views a contact profile When phone, email, and payment details are requested Then fields are redacted and the access attempt is logged as denied Given a Manager role user exports revenue reports When the export is requested Then the export succeeds and is logged with actor, timestamp, and scope Given an admin changes a users role When the change is saved Then permissions take effect within 60 seconds across API and UI Given an unauthorized API token calls endpoints returning contact PII or revenue When the request is processed Then the API responds 403 with an error code=insufficient_scope and the event is audit-logged
Audit Logs for Attribution Edits and Contact Merges
Given a user edits attribution on a lead or booking (e.g., assigns IG post/story, changes campaign) When the change is saved Then an immutable audit entry is written capturing actor_user_id, entity_id, field_name, before_value, after_value, timestamp_utc, and reason (if provided) Given two contacts are merged When the merge completes Then an audit entry records source_contact_id, target_contact_id, merge_strategy, fields_resolved, surviving_consent_record_id, and all affected message and attribution IDs Given an auditor queries the audit log When filters (actor, date range, entity type, entity id) are applied Then the API returns matching entries within 2 seconds for up to 10k results Given an audit entry exists When any user attempts to modify or delete it Then the action is blocked and logged with reason=immutable
Data Retention and Metric Definitions Alignment
Given retention windows are configured (messages_content_days=X, attribution_days=Y) When the nightly purge job runs Then message bodies older than X days and attribution records older than Y days are hard-deleted while aggregate metrics remain intact Given records are purged by retention or DSAR deletion When the purge completes Then a non-PII tombstone is recorded with entity_id, purge_reason, purge_at_utc, and actor (system|user) Given a Data Subject Access Request is fulfilled When deletion is executed Then the contacts message content is removed within SLA and suppression/consent records are retained per legal basis Given Bridge Insights displays a metric When the user opens Definitions Then the metrics name, calculation, inclusions/exclusions, attribution window, and version are shown and match the query used Given metric definitions change When changes are published Then a new definition version is created, analytics queries are tagged with that version, and exports include the definition version Given a compliance validation runs pre-release When checks evaluate Then the build fails unless retention configs are set, metric definitions are present, and platform use aligns with Meta terms and applicable privacy regulations
Funnel Health Alerts
"As a solo groomer, I want alerts when my reply times or conversions slip so that I can act quickly to prevent lost bookings."
Description

Offer configurable alerts when funnel health degrades, including spikes in time-to-first-reply, rising drop-off between DM and SMS, or conversion declines for specific posts/stories. Deliver alerts via in-app notifications, email, or SMS with quiet hours, thresholds, and schedules. Include context (affected content, recent changes) and recommended actions drawn from historical patterns, and provide daily/weekly digests with trends to help users prioritize fixes.

Acceptance Criteria
Time-to-First-Reply Spike Alert
Given a rolling 14-day baseline median time-to-first-reply (TFR) with a minimum of 30 conversations and a configured alert threshold of >=25% increase and a minimum of 10 conversations in the last 24h When the last 24h median TFR exceeds the baseline by >=25% and at least 10 conversations meet the criteria Then an alert is created within 5 minutes containing: metric=TFR, baseline value, current value, delta %, sample size, and evaluated time window And the alert payload lists top contributing hours-of-day and up to 3 templates with the longest TFR if identifiable And the alert includes a deep link to filtered conversations for the affected window And no more than 1 alert per metric is sent within any 6-hour period unless the delta increases by an additional >=10%
DM-to-SMS Drop-off Increase Alert
Given a 14-day baseline DM-to-SMS conversion rate and a configured absolute drop threshold of >=8 percentage points with a minimum of 50 DMs in the last 24h When the current 24h DM-to-SMS conversion rate drops by >=8 percentage points vs baseline and sample-size conditions are met Then an alert is generated within 5 minutes including: baseline %, current %, delta pp, sample sizes, and affected entry sources (post, story, CTA) And the alert enumerates up to 5 posts/stories contributing >=60% of the drop with links to each And the alert lists recent changes in the last 7 days (auto-reply template edits, schedule/availability changes, promotion spend shifts) if detected And the alert includes recommended actions sourced from prior successful remedies with a confidence score (0–1) and example copy
Post/Story Conversion Decline Alert by Content
Given per-content monitoring of DM-to-Paid conversion with a 28-day baseline and a configured relative drop threshold of >=20% with a minimum of 30 DMs per content in the last 7 days When a specific post/story’s DM-to-Paid conversion declines by >=20% vs its baseline and sample-size conditions are met Then the alert includes the content’s thumbnail, title, link, publish date, promotion status, baseline vs current rates for each funnel stage, and the primary drop-off stage And the alert lists recent changes within 7 days (price/package updates, template edits, time-off blocks, targeting/budget shifts) And the alert provides recommended actions with expected lift ranges and references to similar content that recovered after applying them And quick-action links are included for Pause Promotion and Update Template
Quiet Hours and Alert Scheduling Enforcement
Given quiet hours are configured (e.g., 21:00–07:00 local) and per-channel delivery rules (in-app, email, SMS) are set When an alert triggers during quiet hours Then in-app and email are sent immediately only if allowed by configuration, and SMS is deferred until the next allowed window And the deferred SMS is queued with an ETA and is delivered within 5 minutes after quiet hours end And multiple alerts of the same type queued during quiet hours are summarized into a single batched SMS containing count and highest-severity delta
Alert Threshold Configuration and Preview
Given the user edits thresholds per metric (TFR spike, DM-to-SMS drop, content conversion decline), selects lookback windows, and sets minimum sample sizes When the user saves settings Then input validation enforces allowable ranges (relative thresholds 5–200%, absolute thresholds 1–50 percentage points, minimum sample size >=10) And the system persists settings and applies them within 2 minutes And a preview report displays how many alerts would have fired in the last 7 days under the new settings, broken down by alert type And per-channel delivery toggles can be enabled/disabled per alert type and take effect within 2 minutes
Daily and Weekly Funnel Health Digest
Given the user subscribes to a daily 08:00 digest and a weekly Monday 08:00 digest When the digest is generated for its period Then it includes top funnel metrics with trend arrows and % change vs prior period, top 3 at-risk content items, top 3 recommended actions, and resolved alerts count And items are ranked by estimated revenue impact derived from historical conversion rates and average order value And links deep-link to Bridge Insights filtered to the digest period And the digest is suppressed if no metric movement exceeds a minimal threshold (e.g., <3% across all tracked metrics)
Recommendations Derived from Historical Patterns
Given at least 50 historical instances mapping recommendations to improved funnel outcomes exist for the user segment When generating an alert that includes recommendations Then each recommendation cites its pattern source (e.g., similar accounts, past self-history), includes a confidence score (0–1), and an expected lift range And recommendations are filtered to avoid repeating actions the user tried within the last 14 days And when the user marks a recommendation as applied, the system tracks post-action outcomes for 7–14 days and uses results to update recommendation scoring

GeoLock Release

Deliver gate or lockbox codes only after the walker taps the on‑site SMS check‑in and GPS confirms they’re within a configurable geofence and appointment window. Prevents premature sharing, cuts risk, and keeps owners confident that access is granted only when it should be.

Requirements

Configurable Geofence & Time Window
"As a business owner, I want to configure geofence size and release windows per service and client so that access codes only release when walkers are truly on-site and on time."
Description

Provide per-appointment and per-service-type settings to define the geofence (radius or polygon) and the valid release window relative to the scheduled start time. Include sensible defaults (e.g., 75–150m radius; -10/+30 minute window), client-specific overrides, and map preview to validate boundaries. Integrate with FetchFlow’s appointment templates so settings inherit automatically for recurring services. Enforce server-side validation of geofence parameters and persist configurations in the existing scheduling data model for consistent behavior across SMS reminders and dashboard views. Expected outcome: precise control over when and where codes can be released, reducing premature access and support escalations.

Acceptance Criteria
Service-Type Defaults Prepopulate Geofence and Release Window
Given the platform defaults are radius 100m and release window -10/+30 minutes And a service type has no custom geofence or time window configured When a new appointment is created for that service type Then the appointment geofence prepopulates as a radius geofence of 100m And the release window prepopulates as -10/+30 minutes relative to the scheduled start time And the map preview reflects these defaults before save And the user can save the appointment without additional configuration
Override Precedence: Appointment > Client > Service Type > Global
Given global defaults are radius 100m and window -10/+30 And the "Dog Walk" service type is set to radius 150m and window -5/+20 And Client A has a client-specific override set to radius 120m and window -10/+15 When an appointment for Client A and service type "Dog Walk" is created with no appointment-level override Then the effective configuration is radius 120m and window -10/+15 When the appointment-level override is set to radius 90m and window -15/+10 Then the effective configuration is radius 90m and window -15/+10 When the appointment-level override is cleared Then the effective configuration reverts to the client-specific override When the client-specific override is removed Then the effective configuration reverts to the service-type settings
Polygon Geofence Creation and Map Preview Validation
Given a user selects Polygon as the geofence type for an appointment When the user draws a polygon with at least 3 vertices on the map and saves Then the map preview displays the exact polygon boundary with all vertices And the saved appointment stores the polygon coordinates And reopening the appointment shows the same polygon in the preview without distortion When the user switches the geofence type to Radius and sets 100m Then the preview updates to a circular boundary of 100m and the polygon is no longer enforced
Server-Side Validation of Geofence and Time Window Parameters
Given a user attempts to save a radius geofence with a non-positive value When the save request is sent Then the server rejects the request with a validation error specifying the radius must be greater than 0 Given a user attempts to save a polygon with fewer than 3 vertices or with self-intersections When the save request is sent Then the server rejects the request with a validation error describing the polygon constraint violation Given a user attempts to save a time window where the start offset is greater than the end offset When the save request is sent Then the server rejects the request with a validation error indicating an invalid window range Given a user tampers with the client payload to set a radius outside the permitted platform range When the save request is sent Then the server enforces constraints and rejects the request with an appropriate error code and message
Template Inheritance for Recurring Services (Future-Only Propagation)
Given an appointment template for a weekly recurring service has geofence radius 100m and window -10/+30 And ten future occurrences are generated When the template is updated to radius 150m and window -5/+20 Then all future occurrences not yet started inherit the updated settings And past or in-progress occurrences remain unchanged And any occurrence with an appointment-level override retains its override and does not inherit the template change When a new occurrence is generated from the template after the update Then it uses radius 150m and window -5/+20
Persistence and Cross-Channel Enforcement (Dashboard and SMS)
Given an appointment is saved with a polygon geofence and window -10/+30 When the appointment is retrieved via the scheduling API and in the dashboard Then the same geofence shape and time window are returned and displayed consistently When an SMS reminder triggers the access-code gating logic Then the gating logic uses the stored geofence and window identically to the dashboard check-in flow And updating the appointment’s geofence or window reflects in both SMS and dashboard behaviors without delay
Time Zone and DST-Accurate Release Window Computation
Given an appointment scheduled at 9:00 AM local time with window -10/+30 minutes When the appointment location is in a time zone with a DST transition on that date Then the release window opens at 8:50 AM local time and closes at 9:30 AM local time accounting for the DST offset Given the business user and the appointment location are in different time zones When the appointment is viewed or edited Then the window is computed and displayed relative to the appointment location’s time zone consistently across API, dashboard, and SMS
One-Tap SMS Check-In Gate
"As a walker, I want to check in with one tap from an SMS so that codes release automatically when I arrive without calling the owner or dispatcher."
Description

Embed a secure, expiring deep link in reminder/arrival SMS that opens a mobile web check-in capturing high-accuracy GPS and appointment context. If within the configured geofence and time window, trigger immediate code release; otherwise return clear guidance (e.g., too early/too far) and auto-retry prompts. Support graceful fallbacks for low-accuracy readings, background permissions nudges, and localized copy. Log check-in attempts and outcomes to the appointment timeline in FetchFlow’s dashboard for staff visibility. Expected outcome: a frictionless, SMS-first gate that initiates GeoLock verification without requiring an app install.

Acceptance Criteria
Secure Expiring Deep Link via SMS
- Given an appointment A and a reminder/arrival SMS containing a signed deep link with TTL T, When the recipient taps the link, Then a mobile web check‑in loads in‑browser without requiring an app install. - Given the deep link token, When validated, Then it must match appointment A and recipient P, be unexpired (now < issued_at + T), and be not previously redeemed. - Given an invalid, expired, or already‑redeemed token, When the link is opened, Then no access code is rendered, an appropriate message is shown (e.g., "Link expired"), and a "Send new link" action is available that issues a fresh link to P. - Given a successful code release for A, When the same link is opened again, Then access is blocked with "Link already used" and the attempt is logged without exposing any code.
Code Release Within Geofence and Time Window
- Given a validated token for appointment A with geofence radius R and time window [start − G_early, end + G_late], When the check‑in captures a GPS fix with accuracy ≤ A_accuracy and the position is within radius R of the appointment location and the current time is within the window, Then the gate/lockbox code for A is revealed immediately on‑screen with tap‑to‑copy and confirm controls. - Then the code is not present in the DOM or network responses until the success condition is met. - And the release event is recorded with timestamp, lat/long, accuracy, and user agent.
Clear Guidance and Auto-Retry When Too Early or Too Far
- Given a validated token, When current time is before (start − G_early), Then show "Too early" with the local time when check‑in becomes available, keep the code hidden, and provide "Remind me at start" and "Try again" actions; auto‑retry time/location checks every 10 seconds while the page is open. - Given a validated token, When location is outside radius R, Then show distance‑based guidance (e.g., "You're approximately D meters away"), keep the code hidden, offer "Open in Maps" and "Try again", and auto‑retry every 10 seconds or on manual retry. - Given the time and location conditions become satisfied during auto‑retry, Then proceed to code release without requiring an additional tap.
Low-Accuracy Fallbacks and Permission Nudges
- Given a validated token, When the best available location fix has accuracy > A_accuracy after N consecutive samples within M seconds, Then do not release the code; display guidance to enable Precise Location and Wi‑Fi/Bluetooth scanning and present OS permission prompts where supported. - When the user updates permissions or settings and returns, Then the page automatically re‑attempts location capture and proceeds if accuracy ≤ A_accuracy. - Given the user declines precise location or accuracy remains > A_accuracy after K attempts, Then show "Cannot verify location" with Retry, Directions, and Contact Owner options, log reason = low_accuracy, and keep the code hidden.
Localized Copy Selection
- Given a recipient language preference L (profile) or device/browser Accept‑Language, When rendering the check‑in page and messages, Then all strings (labels, guidance, errors, CTAs) appear in L with fallback to English if untranslated. - Given time values in messages, When displayed, Then they use the appointment’s local timezone and locale‑appropriate formats. - Given Spanish (es) and English (en) are supported, When Accept‑Language is es-* or profile preference is es, Then Spanish strings are used; QA toggles verify language switching.
Appointment Timeline Logging
- Given any check‑in attempt for appointment A, When the check‑in page loads, Then an attempt_started entry is written to A’s timeline with timestamp (UTC and local), hashed token ID, and user agent. - Given each outcome (success, too_early, out_of_geofence, low_accuracy, expired_link, token_reuse), When it occurs, Then an attempt_outcome entry is recorded with outcome code, reason, lat/long (rounded), accuracy, and code_shown = true/false; code values are masked except last 2 characters if shown. - Given the staff dashboard timeline, When viewed, Then all entries appear in chronological order with filters by outcome and export to CSV available.
GPS Verification & Spoofing Defense
"As a pet owner, I want the system to verify the walker’s location accurately and prevent spoofing so that my home access is only granted when someone is truly on-site."
Description

Validate the device location with high-accuracy requests, minimum horizontal accuracy thresholds, speed and movement sanity checks, and mock-location detection on supported platforms. Cross-check against last-known device location and appointment address, and require TLS to the verifier endpoint. Apply rate limiting and nonce-based request signing to prevent replay. Store a privacy-preserving location signature (hashed coordinates + accuracy + timestamp) tied to the appointment. Surface verification confidence in the dashboard. Expected outcome: reliable, tamper-resistant location validation that owners can trust.

Acceptance Criteria
High-Accuracy Fix Meets Horizontal Accuracy Threshold
Given the organization accuracy threshold is 25 meters and the device supports high-accuracy mode When the walker taps on-site SMS check-in and the app requests a fresh location Then the OS request is made in high-accuracy mode and the returned horizontalAccuracy is 25 meters or better and the fix age is 10 seconds or newer And the verification result is "Verified: High-Accuracy Fix" Given the returned horizontalAccuracy is worse than 25 meters or the fix age exceeds 10 seconds When the verification is evaluated Then the verification result is "Failed: Low Accuracy or Stale Fix" and GeoLock code release is blocked
Geofence and Appointment Window Cross-Check
Given the appointment geofence radius is configured to 50 meters and the window is from 10 minutes before to 30 minutes after the appointment start When the verified fix is within 50 meters of the appointment address and occurs within the configured window Then the verification result is "Verified: On-Site Within Window" and GeoLock code release is allowed Given the verified fix is outside 50 meters of the appointment address or occurs outside the configured window When the verification is evaluated Then the verification result is "Failed: Out of Geofence/Window" and GeoLock code release is blocked
Mock Location Detection Blocks Verification
Given the device indicates mock location usage or a known mock location provider is detected on a supported platform When location verification is requested Then the verification result is "Failed: Mock Location Detected" and GeoLock code release is blocked and the event is logged with reason "Mock Location" Given the platform does not support mock-location detection and all other checks pass When location verification is requested Then the verification result includes confidence "Medium" with reason "Mock Detection Unsupported"
Speed and Movement Sanity Checks
Given the last-known device location L1 at time T1 and the current fix L2 at time T2 When the implied speed between L1 and L2 exceeds 200 km/h or the displacement exceeds 1000 meters within 10 seconds Then the verification result is "Failed: Implausible Movement" and GeoLock code release is blocked Given the implied speed is 200 km/h or less and displacement is plausible When the verification is evaluated Then movement checks pass and processing continues
TLS Enforcement to Verifier Endpoint
Given the client must use TLS to call the verifier endpoint When a request is sent over HTTPS with TLS 1.2 or higher and the certificate validates Then the request is accepted for processing Given a request attempts plaintext HTTP, TLS below 1.2, or an untrusted/invalid certificate When the request is made Then the request is rejected locally without sending sensitive data and the event is logged with reason "TLS Enforcement"
Nonce-Based Signing, Replay Prevention, and Rate Limiting
Given a server-issued nonce with a 60-second TTL and a per-device signing key When the client signs the request payload including the nonce and timestamp and submits it once within the TTL Then the verifier validates signature and nonce freshness/uniqueness and accepts the request Given the same nonce is reused or the nonce is expired When the request is submitted Then the verifier rejects the request with "Failed: Replay/Expired Nonce" Given more than 5 verification requests are received from the same device or appointment within 60 seconds When additional requests arrive within that window Then the verifier responds 429 "Rate Limit Exceeded" and does not evaluate location
Location Signature Storage and Dashboard Confidence Display
Given a verification attempt completes When persisting verification artifacts Then only a salted hash signature of latitude, longitude, horizontalAccuracy, and timestamp tied to the appointment ID is stored and raw coordinates are not stored Given a user views the appointment in the dashboard When the verification record exists Then the dashboard shows verification status and confidence level (High, Medium, Low) with factors: accuracy (m), distance to geofence (m), fix age (s), movement check result, mock-location check result And the verification record becomes visible within 2 seconds of completion
Encrypted Code Vault & Just-in-Time Release
"As a client, I want my codes stored securely and only shared after verification so that my property remains protected."
Description

Store gate and lockbox codes encrypted at rest with role-based access, masking codes in the UI until conditions are met. On successful verification, deliver codes to the walker via secure SMS and reveal them in the dashboard with a clear "Released" state. Support per-appointment multi-use or one-time-use flags, auto-expiration outside the window, and optional partial masking in messages. Integrate with Pet Profile Smart Cards to associate codes with each property and pet, and sync updates across recurring appointments. Expected outcome: codes stay protected and are shared only at the right moment, reducing risk and owner friction.

Acceptance Criteria
Vault Encryption & Role-Based Access Control
Given a gate or lockbox code is saved or updated When it is persisted to storage Then it is encrypted at rest using KMS-managed keys and no plaintext is written to logs And access to plaintext is limited to Business Owner, designated Admin roles, and the GeoLock service And any request by other roles returns masked values and a 403 for plaintext API fields And each authorized plaintext access is audit logged with timestamp, actor, and appointment ID
UI Masking and Released State in Dashboard
Given an upcoming appointment with a stored code When an authorized user views the appointment before release conditions are met Then the code field is masked, copy/reveal actions are disabled, and a lock indicator explains the prerequisites Given release conditions are met When the appointment detail is refreshed Then the full code is visible, copy is enabled, and a Released badge shows timestamp and releaser identity
GeoLock On-Site SMS Check-in Gate
Given a walker receives the appointment’s SMS check-in link When they tap the link within the configured appointment window W and their GPS is within the configured geofence radius R meters Then the system marks the appointment as on-site verified and sets code release eligibility to true And if either the time window or geofence check fails Then the code is not released and the walker receives an SMS explaining the unmet condition and how to retry
Secure SMS Delivery with Optional Partial Masking
Given code release eligibility becomes true When the system sends the code via SMS Then the SMS is dispatched within the defined SLA and delivery status is recorded (Queued, Sent, Delivered, Failed) And if Partial Masking in Messages is enabled for the business or appointment Then the SMS contains a masked code per policy (only the allowed portion visible) And if Partial Masking is disabled Then the SMS contains the full code And no plaintext code is included in any failure or retry logs
One-Time vs Multi-Use Behavior and Auto-Expiration
Given an appointment code is flagged One-Time Use When the code is first revealed or delivered Then subsequent reveal or send attempts during the window return Expired and the UI re-masks the code Given an appointment code is flagged Multi-Use When the code is released Then it remains visible and copyable until the appointment window closes Given the current time is outside the configured appointment window When any actor requests the code Then the system auto-expires the code, hides it, and prevents SMS delivery
Smart Card Association and Recurring Sync
Given a property or pet has a code stored on its Pet Profile Smart Card When a new appointment or recurring series instance is created Then the appointment is associated with the Smart Card code in the vault without exposing plaintext and marked as Inherited from Smart Card Given the Smart Card code is updated When future appointments exist in a recurring series Then those future appointments are updated to reference the new code while past and completed appointments remain unchanged Given the Smart Card code is removed When syncing future appointments Then unsent appointment codes are cleared and flagged as Missing Code
Owner Alerts & Access Audit Trail
"As a business owner, I want an auditable trail and optional owner notifications so that I can prove compliance and increase client confidence."
Description

Generate a detailed audit record for every check-in and code release, including timestamps, verification confidence, approximate location distance, user identity, and code type released. Provide real-time optional owner notifications via SMS with concise status (e.g., "Walker on-site; code released at 10:02 AM"). Expose a filterable, exportable log in the FetchFlow dashboard and include entries in the appointment timeline for dispute resolution. Expected outcome: transparent traceability and proactive communication that builds trust and reduces support tickets.

Acceptance Criteria
Audit Entries for Check‑In and Code Release
Given a scheduled appointment with GeoLock enabled and the walker taps on-site check-in within the appointment window, When GPS confirms the device is inside the configured geofence, Then the system creates a check_in audit entry within 2 seconds containing: appointment_id, event_type=check_in, event_timestamp (ISO 8601 UTC plus local timezone), user_id and display_name, owner_id, pet_ids, verification_confidence ∈ {high, medium, low}, approx_distance_m (integer), gps_accuracy_m (integer), geofence_id, and code_type=null, and the entry appears in the Access Log and appointment timeline within 5 seconds. Given the same appointment and code release is triggered by GeoLock, When the system releases the access code, Then the system creates a code_release audit entry within 2 seconds containing: appointment_id, event_type=code_release, event_timestamp (ISO 8601 UTC plus local timezone), user_id and display_name, owner_id, pet_ids, verification_confidence ∈ {high, medium, low}, approx_distance_m, gps_accuracy_m, geofence_id, code_type ∈ {gate, lockbox}, release_method ∈ {auto, override}, and outcome=success, and the entry appears in the Access Log and appointment timeline within 5 seconds.
Owner SMS Notification on Code Release
Given owner access notifications are enabled for the appointment, When GeoLock releases a code, Then an SMS is sent to the owner’s primary number within 5 seconds containing a concise status line (e.g., "Walker on-site; code released at 10:02 AM") using the owner’s local timezone and containing no access code digits. And only one SMS is sent per code release (duplicates suppressed within a 2-minute window). And the SMS delivery status (queued|sent|failed) and provider message_id are captured and associated with the corresponding code_release audit entry. Given notifications are disabled, When a code is released, Then no SMS is sent and no SMS delivery sub-entry is created, while the code_release audit entry is still recorded.
Filterable, Exportable Access Log in Dashboard
Given a staff user with permission to view access logs, When they open the Access Log, Then they can filter by: date range (up to 90 days), staff member, owner, pet, appointment_id, event_type ∈ {check_in, code_release, code_release_blocked, code_release_override, sms_delivery}, code_type ∈ {gate, lockbox}, verification_confidence band, and distance range buckets (e.g., 0–10 m, 11–50 m, >50 m). And applying or clearing filters updates the result set and total count within 1 second for datasets ≤1,000 entries. And results can be sorted by timestamp, staff member, verification_confidence, and approx_distance_m. And clicking Export with active filters generates a CSV within 30 seconds for up to 10,000 rows containing: event_id, appointment_id, event_type, timestamp_utc, timestamp_local, staff_name, owner_name, pet_names, verification_confidence, approx_distance_m, gps_accuracy_m, code_type, outcome, reason, sms_status (if any), with no access code values included. And the exported CSV reflects exactly the currently applied filters and sort order.
Appointment Timeline Shows Access Events
Given an appointment has check-in and subsequent code release events, When a user opens the appointment details view, Then the timeline displays both events in chronological order with exact timestamps and relative time (e.g., "2m ago"). And selecting an event reveals full audit details identical to the Access Log entry fields. And if a code release was blocked, Then the timeline shows a code_release_blocked event with the reason displayed (e.g., outside_geofence, outside_window, gps_unavailable, policy). And the timeline is readable and functional on mobile screens ≥320px width with no horizontal scrolling.
Audit Trail Integrity and Confidence Metrics
Given any audit entry is created, Then it is append-only and cannot be edited or deleted via UI or API (attempts return HTTP 403 and no data is modified). And corrections or administrative notes result in a new audit entry that references the original via related_event_id, preserving the original record unchanged. And verification_confidence is computed and stored using rules: high when approx_distance_m ≤ geofence_radius_m and gps_accuracy_m ≤ 25; medium when gps_accuracy_m ≤ 75 and approx_distance_m ≤ geofence_radius_m + 25; low otherwise or if spoofing is suspected; the band is displayed in UI and exported data. And audit entries never store the actual access code value; only code_type and non-sensitive metadata are persisted and shown.
Failure, Retry, and Edge Case Logging
Given a walker attempts to release a code while outside the geofence or appointment window, When GeoLock blocks the action, Then a code_release_blocked audit entry is created within 2 seconds with fields: appointment_id, event_type=code_release_blocked, timestamp, user_id, approx_distance_m, gps_accuracy_m, verification_confidence, outcome=blocked, and reason ∈ {outside_geofence, outside_window, gps_unavailable, policy}; no owner SMS is sent. Given GPS is unavailable and an admin performs a manual override to grant access, When the override is confirmed, Then a code_release_override audit entry is created with actor_user_id, reason text, and timestamp, and an owner SMS is sent only if notifications are enabled with the message "Access granted by admin override at HH:MM". And all SMS attempts use up to 3 retries over 10 minutes; the final status is captured as sent or failed and linked to the relevant audit entry. And if the audit store write fails, Then the system retries with exponential backoff for up to 60 seconds; if still failing, a dashboard alert is shown and code release is prevented until logging succeeds, ensuring no unlogged access occurs.
Secure Manual Override with 2FA
"As support staff, I want a secure override process with safeguards so that I can help walkers proceed when GPS fails without weakening security."
Description

Enable dispatch or owners to release codes when verification fails due to edge cases (e.g., GPS outage) via a controlled override flow. Require two-factor authentication, justification reason codes, and optional evidence (photo check-in) before release. Enforce policy rules such as cooldowns, supervisor approval outside the window, and automatic flagging in the audit trail. Expected outcome: service continuity without compromising security or accountability.

Acceptance Criteria
GPS Outage Override Within Window
Given a scheduled appointment is within its configured window and the walker’s on-site check-in fails GPS verification twice within 90 seconds When dispatch or the owner initiates a manual override from the appointment detail screen Then the system requires selection of a reason code of "GPS outage" (or equivalent) and blocks continuation without a reason code And the system optionally accepts a photo check-in (JPEG/PNG up to 10 MB) And upon successful 2FA of the initiator, the system releases the access code only to the assigned walker via SMS and in-app, masking the code until explicit reveal And the override can be used only once per appointment unless a supervisor approval is obtained for additional releases And the confirmation screen shows a timestamp and auto-redacts the code after 30 seconds
Override Outside Window Requires Supervisor
Given the current time is outside the configured appointment window or the device location is more than the configured geofence radius from the service address When a manual override is requested Then the system blocks immediate release and routes the request to a supervisor approval step And the supervisor must approve within 10 minutes and complete 2FA for the approval to be valid And if approval is denied or times out, no code is released and the request is marked as failed And if approved, the system releases the code to the assigned walker following the standard secure delivery rules
Two-Factor Authentication Enforcement
Given a user with role Dispatch or Owner initiates a manual override When the system requests second factor via the configured method (SMS OTP or TOTP) Then only valid, unexpired OTPs (<= 60 seconds) or TOTP codes are accepted And after 3 consecutive failed 2FA attempts, the override flow is locked for 10 minutes and an alert is sent to the account owner And if more than 5 attempts occur in 24 hours for the same user, further attempts require supervisor intervention And if 5 minutes elapse after successful 2FA without completion of the override, the session expires and 2FA must be revalidated
Justification Reason and Evidence Capture
Given a manual override flow has been initiated When the user proceeds to submit the override Then the system requires selection of a justification reason code from a configurable list and prevents submission without it And the system accepts optional photo evidence (JPEG/PNG, max 10 MB) and stores capture timestamp And if a photo is provided, it is displayed in the appointment timeline and linked in the audit log And an optional free-text note (0–500 chars) may be saved; input exceeding limits is rejected with inline validation
Cooldown and Rate Limit Controls
Given an override has been approved for an appointment When another override is attempted for the same appointment, location, or by the same initiator Then a cooldown of 15 minutes per appointment and 30 minutes per service location is enforced unless supervisor approval is granted And no more than 2 approved overrides per initiator are allowed in any rolling 60-minute window; additional attempts are blocked with a descriptive error And all blocked attempts are recorded with reason codes "cooldown" or "rate-limit"
Audit Trail, Flags, and Notifications
Given any manual override attempt (approved, denied, or expired) When the attempt concludes Then an immutable audit record is created containing appointment ID, initiator role and ID, reason code, timestamps (requested/approved/released), 2FA outcome, supervisor approval outcome, geo context, and evidence references And overrides outside the appointment window, outside geofence, or following rate-limit blocks are auto-flagged with tags and visible in the audit dashboard filters And notifications are sent to the account owner for: approved overrides outside window, and three consecutive failed 2FA attempts And audit records are exportable to CSV and retained for at least 365 days
Multi-Code Sequencing & Stepwise Release
"As a walker, I want codes released in the correct sequence with clear instructions so that I can navigate multi-step property access without confusion."
Description

Support multiple access steps per appointment (e.g., gate code, building door, unit lockbox) with ordered, named stages. Release the next code only after the prior step is verified or acknowledged by the walker, with optional mini-geofences per step if coordinates are provided. Present clear step labels in SMS and dashboard, and allow admins to configure per-step instructions and expirations. Expected outcome: fewer entry errors and smoother access for complex properties.

Acceptance Criteria
Sequential Release After Prior Step Completion
Given an appointment with three access steps configured in order "Gate", "Building", and "Lockbox" And the walker has checked in on-site and is within the primary geofence and appointment window When the walker completes the "Gate" step by sending NEXT via SMS or tapping Complete Step in the mobile link Then the system releases the "Building" code via the same channel within 5 seconds And the "Lockbox" code remains hidden until "Building" is completed And each release event logs step name, timestamp, channel, and walker ID
Per-Step Mini-Geofence Enforcement
Given the step "Gate" has a mini-geofence configured at latitude X, longitude Y with a radius of 20 meters And the walker's most recent GPS fix is older than 120 seconds or has accuracy worse than 30 meters When the walker requests release of the "Gate" code Then the system withholds the code and replies via SMS: "Get closer to the gate to receive this code" And when the GPS fix is within 20 meters, age <= 120 seconds, and accuracy <= 30 meters Then the system releases the "Gate" code and marks the step as eligible for completion
Appointment Window Gating for Step Release
Given an appointment window from 2:00 PM to 3:00 PM with a configured early buffer of 10 minutes and late buffer of 15 minutes When the walker attempts to release any step before 1:50 PM or after 3:15 PM Then the system blocks the release and returns a message indicating the earliest and latest allowed times And the dashboard displays a disabled Release control with tooltip "Outside appointment window"
Step Labels and Instructions in SMS and Dashboard
Given an appointment configured with steps labeled "Gate", "Building Door", and "Unit Lockbox" with per-step instructions When the first step is released Then the SMS shows "Step 1 of 3 — Gate: <code>" followed by the configured instructions And the dashboard shows the same label, ordinal (1/3), and instructions alongside the code And for unreleased steps, both SMS and dashboard show the label and ordinal with the code masked as "Locked — Complete prior step to unlock"
Per-Step Expiration and Masking
Given the step "Building Door" has an expiration of 15 minutes after release When 15 minutes elapse after the code is released without the step being marked complete Then the code is masked in SMS and dashboard and marked "Expired" And any subsequent request to view or resend the code returns: "Step expired — request admin override" And the audit log records the expiration with timestamp and step name
Admin Configuration and Validation of Multi-Step Templates
Given an admin creates or edits a property access template with ordered steps, labels, per-step instructions, optional mini-geofence (lat, lon, radius meters), and per-step expiration (minutes) When the admin saves the template Then validation fails with inline errors if any step is missing a label, duplicates an order number, has radius outside 5–200 meters, or expiration outside 1–240 minutes And on success, the template version is incremented, time-stamped, and associated to future appointments And existing scheduled appointments retain their prior step definitions unless the admin explicitly opts to apply the new version

Stepwise Drip

Release access in stages—send the gate code first, then the unit/door code after a second proximity check or quick ‘At Door’ confirm. Ideal for complexes with multiple barriers, minimizing exposure by only sharing what’s needed, when it’s needed.

Requirements

Geofenced Verification & At-Door Confirmation
"As a provider arriving at a gated complex, I want to verify my location or confirm I’m at the door via a quick SMS link so that I can unlock the first access step without calling the client."
Description

Provide an SMS-delivered verification flow that confirms provider proximity before releasing access details. The SMS links to a browser-based, no-app experience that requests consented location verification within a configurable geofence or offers a one-tap “At Door” confirm via Smart Card or SMS keyword fallback for low-signal areas. Implement basic anti-spoofing (tap time window, IP/location mismatch heuristics), clear privacy copy, and audit logging of verification outcome. Integrate with FetchFlow’s message threads and Pet Profile Smart Cards so the verification link includes pet name/photo and visit context. Expose a webhook/callback so downstream steps (code release) trigger only on a successful verification event.

Acceptance Criteria
SMS Link Geofence Verification
Given a scheduled visit with geofence verification enabled and a geofence_radius_m configured (default 100m, allowed range 25–250m) And an SMS containing a unique, signed, single-use verification link is sent to the assigned provider When the provider opens the link in a mobile browser and explicitly consents to share location Then the system attempts to capture GPS within 10 seconds with reported accuracy <= 100 meters And if the captured coordinates are within geofence_radius_m of the visit location, the verification result is marked Success And if coordinates are outside the radius or no valid location is captured within 10 seconds, the result is marked Failure with a reason_code And the verification token becomes unusable after completion or 10 minutes from issuance, whichever occurs first
At-Door One-Tap and SMS Fallback
Given the provider cannot or will not share precise location (e.g., low signal or permission denied) When the provider taps the At Door button on the Smart Card verification page OR replies in the original SMS thread with the configured keyword (e.g., AT DOOR) Then the system binds the action to the visit and provider via the verification token and thread metadata And applies anti-spoofing heuristics (token not expired, action within 5 minutes of link delivery, same phone number thread, no active proxy/VPN detected) And on heuristic pass, marks verification Success with method=at_door_fallback; on heuristic fail, marks Failure with reason_code And posts a confirmation status message to the FetchFlow thread reflecting Success or Failure
Anti-Spoofing and Expiry Enforcement
Given each verification link contains a signed, time-limited token bound to visit_id and provider_id When the link is opened after 10 minutes from issuance OR from an IP with country mismatch to the resolved device geolocation OR after 3 failed attempts within 5 minutes Then the system denies verification, displays an error message, and records result=Failure with reason_code And no webhook/callback is fired for denied attempts And the system rate-limits further attempts from the same token/number for 5 minutes
Privacy Copy and Explicit Consent
Given the verification page loads in the browser Then the page displays clear privacy copy describing data collected (approximate location, IP, user agent), purpose (visit access verification), and retention/usage with a link to the full policy And the page requires explicit consent (Allow Location) before attempting location capture and offers a Decline option When the provider declines, no location is requested, the attempt is marked Failure with reason_code=consent_declined, and only minimal metadata (token, decline flag, timestamp) is stored When the provider consents, location is captured per platform guidelines and processing proceeds per geofence rules
Audit Logging of Verification Outcome
Given any verification attempt completes (Success or Failure) When the result is recorded Then the system appends an immutable audit record containing: event_id, timestamp (UTC), visit_id, provider_id, message_thread_id, method (gps|at_door_card|at_door_sms), result (success|failure), reason_code (on failure), lat/lng (truncated), accuracy_m (if available), ip, user_agent, token_id, geofence_radius_m, and stage (if provided) And the audit record is queryable via admin tools and API and cannot be altered post-write And the audit record can be correlated to downstream webhook deliveries via idempotency_key
Webhook Callback on Successful Verification
Given a webhook URL and signing secret are configured for the account When a verification result=Success is recorded Then the system POSTs a JSON payload to the webhook within 2 seconds including: event_type=verification.success, idempotency_key, visit_id, provider_id, method, coordinates (if available), accuracy_m, geofence_radius_m, stage (if provided), and occurred_at (UTC) And the request is signed with an HMAC-SHA256 signature header and includes a retry-count header And the delivery is retried with exponential backoff up to 6 times on 5xx/timeout, is not retried on 2xx/410, and preserves idempotency on receiver replays And no webhook is emitted for Failure results
Thread and Smart Card Context Integration
Given a verification SMS is sent for a scheduled visit Then the message contains pet name and photo thumbnail, visit date/time, and a unique verification link When the link is opened, the Smart Card page displays the same pet details and visit context and offers both Share Location and At Door actions And upon completion, the FetchFlow message thread posts an inline status (e.g., Proximity verified via GPS) with timestamp And access code release messages are withheld until a verification.success webhook is acknowledged by downstream logic
Stage-One Code Delivery (Gate Access)
"As a dog walker, I want the gate code to be sent to me immediately after I verify proximity so that I can enter the complex without delay."
Description

After successful initial verification, automatically release the gate code and entry instructions via the same SMS thread and Smart Card. Support property-specific guidance (e.g., which keypad, lane, elevator) and safety notes. Mask the code by default in the thread and reveal on tap with a short-lived view token; log reveal time, device, and user. Allow owners to configure which code is considered “Stage One,” whether it’s one-time or static, and attach parking/wayfinding notes. Ensure delivery is atomic with verification (idempotent) and recorded in the visit timeline for audit and support.

Acceptance Criteria
Auto Gate Code Delivery after Initial Verification
Given a scheduled visit with Stepwise Drip enabled and Stage One configured for gate access And the visitor is verified by the initial verification method (e.g., proximity or host confirm) When the verification event is processed Then the system sends exactly one SMS in the existing thread containing a masked gate code and entry instructions And the associated Smart Card displays the same masked gate code and entry instructions And the delivery of SMS and Smart Card update occurs atomically with the verification event (all succeed or none) And repeated processing of the same verification event (retries) does not create duplicate messages or duplicate timeline entries And the unit/door code (Stage Two) is not included in any Stage One delivery And a visit timeline entry is recorded with the delivery status and message identifiers
Masked Code with Tap-to-Reveal and Expiring Token
Given the Stage One gate code is presented masked in both SMS and Smart Card When the user taps the Reveal action on the Smart Card or link from the SMS Then a short-lived view token is generated with a configurable TTL (default ≤ 90 seconds) And the full gate code is displayed only while the token is valid and automatically re-masked on expiry or navigation away And reveal attempts are rate-limited per user/device to prevent brute-force or replay And if a token is reused after expiry, the code is not shown and an error state is presented And the reveal event logs timestamp, user identity, and device fingerprint And screenshots or copy actions are not blocked but are not logged as code reveals
Property Guidance and Safety Notes Included in Stage One
Given a property has configured guidance (keypad location, lane, elevator) and safety/parking notes for Stage One When Stage One is delivered Then the SMS includes concise guidance and safety notes within 1–2 segments with a link to full details on the Smart Card And the Smart Card renders full guidance, parking/wayfinding notes, and safety warnings And content is sanitized, line-broken, and free of PII beyond what is configured And if guidance exceeds SMS length limits, it is truncated with an ellipsis and preserved in full on the Smart Card And guidance is specific to the property of the visit and not inherited from other properties
Owner Configuration of Stage One Code and Type
Given an owner configures which code is considered Stage One for a property And selects code type as one-time or static When the configuration is saved Then validation ensures Stage One cannot reference Stage Two fields And one-time codes are reserved/generated per visit at scheduling or verification time without collision And static codes are applied consistently across visits until changed And owners can attach or edit parking/wayfinding notes and preview the Stage One message/Smart Card before publishing And audit history records who changed the configuration and when
Audit Trail for Delivery and Reveal
Given Stage One delivery or reveal occurs When the SMS and Smart Card updates are executed Then the visit timeline records delivery time, channel, message IDs, and delivery outcome And each reveal event logs timestamp, user identity (or phone number), and device fingerprint/hash And support users can view an immutable audit listing with filters by visit, user, and device And PII in logs respects data retention and redaction policies And exporting the audit for a single visit produces a time-ordered JSON/CSV artifact
Resilience, Idempotency, and Failure Recovery
Given initial verification succeeds but downstream delivery fails partially (e.g., SMS sent but Smart Card update fails) When the system detects the partial failure Then the entire Stage One transaction is rolled back or completed via automatic retry so that SMS and Smart Card are eventually consistent And retries are idempotent, producing no duplicate SMS, tokens, or timeline entries And if delivery cannot be completed after max retries, the system records a failure status in the timeline and alerts the owner per configuration And manual re-trigger from support tools produces at most one successful delivery And Stage Two content is never exposed during Stage One failure handling
Stage-Two Code Delivery (Unit/Door Access)
"As a trainer entering a building with multiple barriers, I want the unit code released only after I confirm I’m at the correct door so that access is secure and limited to what I need, when I need it."
Description

Gate the unit/door code behind a second verification step: a tighter geofence, an “At Door” quick confirm, or a client live-approval. Only after this second check, deliver the unit/door code with any unit-specific notes (alarm, latch, stairs). Keep Stage Two independent of Stage One so owners can enable either step separately. Support retries with configurable cooldowns and auto-expire the code after use or time window. Log every release event with visit ID, pet, and location snapshot. Ensure message copy is consistent and branded via Templates.

Acceptance Criteria
Second Verification Methods Trigger Unit Code Delivery
Given Stage Two verification method is Geofence with radius 10 m and the visit is active, When the provider's device reports location within 10 m of the unit address for at least 10 seconds with GPS accuracy ≤ 20 m, Then the unit/door code is delivered via the configured channel within 5 seconds. Given Stage Two verification method is At Door confirm, When the provider taps At Door and confirms, Then the unit/door code is delivered within 5 seconds. Given Stage Two verification method is Client Live-Approval, When the client approves within 5 minutes of the request, Then the unit/door code is delivered within 5 seconds. Given Stage Two verification method is Client Live-Approval, When the client rejects or there is no response for 5 minutes, Then the unit/door code is not delivered and the attempt is marked as rejected or timed out. Given Stage Two verification is not completed, When the provider requests the unit/door code, Then the code is not delivered.
Unit Code Message Includes Unit-Specific Notes
Given unit-specific notes (e.g., alarm, latch, stairs) are stored for the visit, When Stage Two delivers the unit/door code, Then the message contains the unit/door code and the unit-specific notes in the same message/thread. Given no unit-specific notes exist for the visit, When the unit/door code is delivered, Then no empty or placeholder notes label is shown. Given the message is rendered from data, When sending, Then all placeholders for code and notes are resolved with concrete values and no raw tokens remain.
Auto-Expire Unit Code After Use or Time Window
Given a unit/door code TTL of 15 minutes is configured, When the code is delivered, Then the code becomes invalid 15 minutes after delivery if not used. Given the access/check-in system reports first use of the code, When the use event is received, Then the code becomes invalid within 30 seconds of the event. Given a per-visit TTL override of 45 minutes is configured, When the code is delivered, Then the code expires after 45 minutes if not used. Given a code is expired or already used, When the provider attempts Stage Two again, Then the system indicates the code is expired and does not reissue unless explicitly reset by an authorized user.
Retries with Configurable Cooldown and Max Attempts
Given Stage Two verification fails and a cooldown of 3 minutes is configured, When the provider retries before 3 minutes elapse, Then the attempt is blocked, no code is delivered, and the UI/SMS indicates remaining cooldown time. Given a cooldown of 3 minutes is configured, When the provider retries at or after 3 minutes and passes verification, Then the unit/door code is delivered. Given a maximum retry attempts value of 3 is configured, When the provider reaches 3 failed attempts, Then further attempts are blocked until an admin resets or a new visit window begins. Given any blocked or failed retry occurs, When the event is logged, Then the reason and cooldown/attempt count are recorded.
Stage Two Configuration Independent of Stage One
Given Stage Two is enabled and Stage One is disabled for a location, When the provider completes Stage Two verification, Then the unit/door code is delivered even though no Stage One gate code was sent. Given Stage One is enabled and Stage Two is disabled, When Stage One succeeds, Then the unit/door code is delivered without requiring Stage Two verification. Given Stage One and Stage Two are both enabled, When Stage One succeeds but Stage Two is pending, Then the unit/door code is not delivered until Stage Two succeeds. Given Stage Two settings are updated, When a new visit is created after the change, Then the new visit follows the updated Stage Two configuration.
Event Logging with Visit, Pet, and Location Snapshot
Given a unit/door code is delivered, Then an event log entry is created containing visit ID, pet IDs and names, provider ID, timestamp (UTC), and delivery channel. Given any Stage Two attempt results (success, reject, timeout, cooldown block), Then an event log entry includes the outcome and reason. Given a geofence-based success, Then the location snapshot includes latitude, longitude, horizontal accuracy in meters, and timestamp; Given an At Door success, include confirm timestamp; Given a Live-Approval success, include approver ID and timestamp. Given an audit review, When querying by visit ID, Then all related Stage Two events are retrievable in chronological order.
Template-Driven, Branded Message Consistency
Given a messaging template named "Stage Two: Unit Code" is configured and active, When the unit/door code is delivered via SMS or in-app message, Then the message body uses that template ID/version. Given the template includes placeholders {UnitCode}, {UnitNotes}, {VisitId}, {PetNames}, {BusinessName}, When the message is sent, Then all placeholders are populated and no placeholder text remains. Given a recipient locale has a localized template variant, When sending the message, Then the localized variant is used. Given business branding is configured, When the message is sent, Then the message includes the business name or signature as defined in the template.
Time-Bound Code Expiration & Redaction
"As a client, I want access codes to expire and be masked so that my building and unit remain secure even if messages are forwarded."
Description

Apply time-to-live policies per stage that automatically expire codes after a window relative to appointment start or first reveal. Redact expired codes from SMS threads and dashboard views, leaving an event placeholder for audit. Limit reveal attempts and throttle requests to mitigate sharing and brute force. Encrypt stored codes at rest, restrict by role, and enforce masking in notifications. Provide a reissue flow that regenerates a short-lived reveal link without exposing the code inline. Ensure compliance with data retention policies and export controls.

Acceptance Criteria
Stage TTL Expiration Enforcement
Given an appointment scheduled at T0 with gate code TTL = 60 minutes relative to start and door code TTL = 15 minutes relative to first reveal When current time is between T0−15m and T0+60m Then the gate code can be revealed and is considered valid When current time is after T0+60m Then the gate code cannot be revealed and is marked expired with an audit entry Given the door code is first revealed at Tr When current time is within 15 minutes of Tr Then the door code is valid and can be used When current time is after Tr+15m Then the door code is marked expired and blocked from further reveal And all expirations are logged with timestamp, stage, appointment ID, and actor
Expired Code Redaction in Threads and Dashboard
Given a stage code has expired When viewing the FetchFlow conversation transcript or the dashboard appointment view Then any previously displayed code value is replaced with a redacted placeholder “Code expired — tap to request reissue” and a lock indicator And the placeholder displays stage name, expiration time, and original revealer And the raw code value is not retrievable via UI or API And an audit event records placeholder insertion with before/after visibility, actor, timestamp, and channel
Reveal Attempt Limits and Throttling
Given per-stage limits of max 3 reveal attempts per recipient per 10 minutes and max 10 per recipient per day When a user exceeds 3 attempts within 10 minutes Then further reveals for that stage are blocked for 15 minutes and a 429-style error message is shown and logged When cumulative attempts exceed 10 within the same calendar day Then reveals for that recipient are blocked until 00:00 local time and an admin alert is generated And all blocks include appointment ID, stage, recipient, IP/device fingerprint, and reason in audit logs
Role-Based Reveal and Masked Notifications
Given roles include a permission “Reveal Access Codes” When a user without the permission views messages, notifications, or dashboard fields Then the code is masked to show only the last 2 characters (e.g., ****5G) and no reveal link is present When a permitted user initiates a reveal Then 2FA verification is required unless the same user successfully revealed within the last 8 hours And notification bodies (SMS, push, email) never include the full code inline; only masked values or a reveal link And API requests from non-permitted tokens receive HTTP 403 with no code leakage
Short-Lived Reissue Link Flow
Given a code is expired or reveals are currently throttled When an authorized user requests a reissue Then the system generates a single-use reveal URL valid for 10 minutes and bound to the intended recipient and device fingerprint And any prior reveal URLs for the same stage are immediately revoked And the SMS sent includes only the reveal URL and a masked suffix of the code; the full code value is never inlined When the URL is opened after expiry or from a different device Then a message “Link expired — request new” is shown and no code value or partial is leaked And all reissues are logged with correlation IDs linking to the original appointment and stage
Encryption at Rest and Key Management
Given access codes are persisted by the service Then they are encrypted at rest using KMS-managed keys with rotation at least quarterly, and never written in plaintext to logs When inspecting a database snapshot or raw export at the storage layer Then code fields appear as ciphertext and cannot be decrypted without service-held keys And automated checks verify encryption status and key rotation; failures block deployment and raise alerts
Data Retention and Export Controls
Given the retention policy is configured to 90 days for access code data When the retention period elapses Then stored code values and reveal URLs are purged or irreversibly redacted, while non-sensitive audit placeholders remain accessible When generating conversation or appointment exports (CSV, PDF, API) Then full code values are excluded and only masked values or redaction placeholders are present And attempts to export decrypted codes are blocked, logged with user/time/reason, and surfaced in the security audit report
Owner Rules & Messaging Templates
"As an owner, I want to set rules and templates for how and when each access code is shared so that providers get just-in-time instructions without me manually texting them."
Description

Offer a rules configuration UI to define Stepwise Drip behavior per property or visit type: geofence radius, allowed verification methods, timeouts, retry limits, after-hours restrictions, and required second checks. Include message templates with variables (pet name, appointment time, building name) to standardize SMS copy across stages. Allow presets (e.g., “Gated Complex Default”) and per-visit overrides via the dashboard or API. Validate rules for conflicts and simulate flows in a test mode. Localize templates and support media (map pins, keypad photos) where allowed.

Acceptance Criteria
Preset Creation and Assignment at Property Level
Given an owner with Manage Rules permission opens the Rules UI When they create a preset named "Gated Complex Default" with defined geofence, verification methods, timeouts, retry limits, after-hours window, and second-check settings Then the preset is validated, versioned, saved, and appears in the preset list with creator and timestamp And the preset is assignable to a property and visit types, and assignment is audit-logged When the preset is assigned to Property P for Visit Type "Walk" Then new visits for Property P of type "Walk" inherit the preset by default And GET /rules/effective?property_id=P&visit_type=Walk returns the effective rules with source "preset:Gated Complex Default"
Stage Access Controls: Geofence, Verification Methods, and Required Second Check
Given effective rules define geofence_radius=100m, allowed_verification_methods=[GPS, AT_DOOR_CONFIRM], and required_second_check=true for unit code When the provider requests access and is within 100m with GPS accuracy <=50m Then the system releases the gate code via stage-1 template When the provider completes the required second check (AT_DOOR_CONFIRM keyword) Then the system releases the unit/door code via stage-2 template When location permission is denied and GPS is unavailable but AT_DOOR_CONFIRM is allowed Then sending the AT_DOOR_CONFIRM keyword satisfies proximity for the applicable stage When no allowed verification method can be fulfilled Then no code is sent and the user receives a guidance SMS; event is logged with reason INSUFFICIENT_VERIFICATION
Stage Timing Controls: Timeouts, Retry Limits, and After-Hours Restrictions
Given stage_timeouts={stage1:5m, stage2:3m}, retry_limit=2, retry_interval=2m, and after_hours_window=20:00–07:00 in the property timezone When a stage request occurs at 21:30 local time Then the system blocks code delivery, sends the after-hours message template, and queues release until 07:00 unless owner_override_after_hours=true When a stage request occurs at 14:00 and no confirmation is received within the stage timeout Then the system sends reminder attempts up to retry_limit at retry_interval and cancels the stage after exceeding the limit, logging TIMEOUT_EXCEEDED And all timestamps in UI, logs, and API reflect the property timezone
Messaging Templates: Variables, Localization, and Media Support
Given stage templates include variables {pet_name, appointment_time, building_name} with locales [en-US, es-MX] and media placeholders {map_pin_url, keypad_photo_url} When sending a stage-1 message to a contact with locale es-MX and carrier supports MMS and media_allowed=true and media assets exist Then the message renders in es-MX with variables resolved and includes MMS attachments; if MMS unsupported or media_allowed=false, it falls back to SMS text with shortened links When a required variable is missing at render time Then sending is blocked with error identifying the missing variable and the fallback template is used if configured; error is logged And template render latency is p95 <=200ms and signed media URLs remain valid for >=2 hours
Per-Visit Overrides via Dashboard and API with Precedence Rules
Given a visit has overrides {geofence_radius:75, retry_limit:1} and the assigned preset sets {geofence_radius:50, retry_limit:2} When effective rules are computed for the visit Then overrides take precedence and remaining fields inherit from the preset; UI labels overridden vs inherited fields And GET /visits/{id}/rules returns the merged rules plus lineage per field {source: override|preset|default} When updating overrides via dashboard or PATCH /visits/{id}/rules Then validation is atomic; on success, changes are audit-logged with actor (user_id/service_account_id) and new version
Rule Conflict Detection and Operator Guidance
Given a configuration sets required_second_check=true and allowed_verification_methods=[] When the user clicks Save Then the save is blocked and conflicting fields are highlighted with guidance to enable a verification method or disable the second check; error code RULE_CONFLICT_1001 is returned in API When after_hours_window lacks timezone or has an invalid range (start == end) Then validation fails with specific error codes and links to help docs And geofence_radius=0 is rejected with a minimum threshold error; all detected conflicts are listed by a Run Validation action
Test Mode Flow Simulation and Exportable Results
Given Test Mode is enabled for a property and a preset is selected When the user simulates a visit with chosen inputs (locale, time-of-day, carrier MMS support, mock location events, AT_DOOR_CONFIRM) Then a step-by-step timeline is produced showing decision points, rendered messages, media attachments, and state transitions without sending real SMS/MMS And the user can vary inputs to explore edge cases and export a JSON report containing inputs, effective rules, rendered outputs, and expected API/webhook events And simulator logs are retained for at least 24 hours and labeled TEST_MODE in analytics
Exception Handling & Escalation
"As a dispatcher, I want clear fallback options and a one-tap owner approval path when a provider can’t verify proximity so that visits can proceed without compromising security."
Description

Define a structured fallback when verification fails or access is blocked: prompt the client for live approval (Yes/No), offer a concierge or callbox option, allow secure photo/video door confirmation, and provide a one-time emergency release with owner acknowledgment. Trigger alerts to the owner or dispatcher with a deep link to approve/deny release and attach notes to the visit outcome. Rate-limit exception pathways, capture reasons, and optionally apply configured no-show or trip-fee policies if access cannot be obtained. All exceptions are recorded in the visit audit trail.

Acceptance Criteria
Live Approval Prompt on Failed Proximity Check
Given a visit is in Stepwise Drip stage 1 and the second verification fails (no proximity or no At Door confirm within 90 seconds) When the provider taps Request Live Approval Then the client receives an SMS with Yes/No options and visit details within 5 seconds And the client’s Yes reply unlocks stage 2 access within 5 seconds of receipt And the client’s No reply denies access and records the denial reason And if no reply within 120 seconds, the system auto-escalates to the next configured exception path And all messages and outcomes are timestamped and linked to the visit
Concierge or Callbox Escalation Path
Given the live approval attempt is denied or times out When the provider selects Concierge/Callbox from the exception menu Then the app displays the stored concierge/callbox number(s) for the property And the provider can initiate a call from in-app dialer And the system records call start/end times and outcome (connected, no answer, voicemail) entered by the provider And if a temporary access code is provided, the provider can enter it and proceed; otherwise the flow returns to exception options And all actions are appended to the visit audit trail
Secure Photo/Video Door Confirmation
Given the provider is at the door without access after stage 1 When the provider selects Secure Photo/Video Confirmation Then the app opens a secure capture mode requiring a front-door photo or 10–20s video And media is timestamped, geo-stamped within 50 meters of the service address, and uploaded over TLS And successful upload triggers a notification to the owner/dispatcher with a deep link to approve/deny stage 2 release And the approval grants stage 2 access and stores the approver identity; denial records reason And media is stored per retention policy and referenced in the visit audit trail
One-Time Emergency Release with Owner Acknowledgment
Given org defaults: emergencyReleasePerVisit = 1 and emergencyReleaseOrgPerDay = 5 And the provider has attempted at least one other exception path When the provider selects Emergency Release Then the owner/dispatcher receives a deep link requiring acknowledgment of emergency release with liability notice And upon acknowledgment, a one-time code valid for 10 minutes is issued and delivered to the provider And exceeding configured limits blocks the action with an explanatory message And issuance, validity window, and use of the code are recorded in the audit trail
Owner/Dispatcher Alert with Deep-Link Approve/Deny
Given any exception path requires owner/dispatcher decision When the system sends an alert Then the recipient gets an actionable deep link via SMS and push containing visit ID, provider name, timestamp, and exception type And tapping Approve grants the minimal next-step access only (e.g., stage 2 code) and captures optional notes And tapping Deny blocks access, requires a denial reason, and offers to reschedule And the decision state syncs to the provider’s app within 5 seconds And all fields are persisted for reporting
Rate Limiting and Reason Capture for Exceptions
Given org defaults: maxExceptionAttemptsPerVisit = 3 and exceptionCooldownMinutes = 5 When the provider initiates exception paths repeatedly in a single visit Then after the third completed attempt, additional attempts are blocked until the visit is rescheduled or access obtained And attempts within the cooldown window surface a message indicating remaining wait time And each attempt requires selection of a structured reason (e.g., Gate Busy, Code Invalid, Client Unreachable) and optional free-text notes (max 500 chars) And all attempts, reasons, and cooldown blocks are recorded in the audit trail
Policy Application on Unresolved Access and Audit Completeness
Given access is not obtained after allowed exception attempts When the provider marks the visit as Unable to Access Then the system evaluates configured policies and applies the appropriate no-show or trip fee to the client’s invoice And the fee application is visible in the visit outcome with breakdown and policy reference And the provider must attach a final reason and optional media And the visit audit trail shows a complete, ordered timeline of all exceptions, alerts, decisions, media, fees, and timestamps

One‑Time Reveal

Share codes through a short‑lived secure link that reveals the code for a few seconds, then auto‑hides and redacts the SMS preview. Supports re‑request with re‑auth, reducing lingering secrets in message histories and preventing copy‑paste leaks.

Requirements

Ephemeral Single-Use Link Tokens
"As a provider sharing an entry code, I want to send a short‑lived, single‑use link so that the code can be viewed once without persisting in message threads or being reused."
Description

Generate cryptographically strong, single-use tokens for One‑Time Reveal links with configurable time-to-live (e.g., 5–30 minutes) and max views (default: 1). Bind tokens to the appointment, pet profile, and intended recipient phone number to mitigate forwarding. Store only hashed tokens server-side; invalidate on first successful reveal or on expiry. Implement anti-replay and race-condition handling to ensure concurrent opens do not leak the secret. Design tokens to be scanner-safe: the initial GET from link preview or carrier security bots must not redeem the token; redemption occurs only after an explicit user gesture (e.g., POST with proof-of-interaction). Ensure all transport over TLS, enforce rate limiting, and provide immediate server-side revocation.

Acceptance Criteria
TTL and Max Views Enforcement
Given a token is issued with ttl_minutes=10 and max_views=1, When 10 minutes elapse without redemption, Then any redemption attempt returns 410 Gone and no secret is revealed. Given a token is issued without an explicit max_views, When it is created, Then max_views defaults to 1. Given a token with max_views=1, When the first successful redemption occurs within TTL, Then the token is immediately invalidated and any subsequent redemption attempts return 409 Conflict with no secret disclosed. Given an issuance request specifies ttl_minutes outside the range 5–30, When creation is attempted, Then the API responds 400 Bad Request with error code TTL_OUT_OF_RANGE and no token is created. Given a token with max_views=3, When it is redeemed three times within TTL by the intended recipient, Then each of the three reveals succeeds and the fourth attempt returns 409 Conflict with no secret disclosed.
Token Binding to Appointment, Pet, and Recipient
Given a token bound to appointment_id=A1, pet_id=P1, and recipient_phone=+15551230000, When a redemption attempt lacks proof that the requester controls +15551230000, Then the server responds 403 Forbidden and no secret is revealed. Given a token bound to appointment_id and pet_id, When a redemption request references a different appointment_id or pet_id than the binding, Then the server responds 403 Forbidden and no secret is revealed. Given the intended recipient at +15551230000 completes required re-auth (e.g., SMS OTP) and submits the redeem POST, When the token is within TTL and views remaining, Then the reveal succeeds with 200 OK.
Hashed Token Storage and No Plaintext Secrets
Given a token is generated, When inspecting database and cache records, Then only a salted hash of the token is stored and the plaintext token value is not persisted. Given server logging is enabled, When requests are processed, Then neither the plaintext token nor the revealed secret appear in application logs, access logs, or analytics events. Given token verification occurs, When the provided token is checked against the stored hash, Then verification is performed without exposing the plaintext token and no secret is written to persistent storage.
Scanner-Safe Reveal and Explicit User Gesture
Given a tokenized link is fetched via HTTP GET by a link preview or bot (e.g., identifiable User-Agent or HEAD/GET), When the request is received, Then the server returns a non-redeeming placeholder and the token remains unredeemed with no secret exposure. Given the intended recipient opens the link, When they perform an explicit Reveal action that sends a POST including proof-of-interaction tied to the recipient_phone, Then the server redeems the token and returns the secret with 200 OK. Given any GET or HEAD request to the token URL, When received, Then the token is never redeemed and the response never contains the secret. Given a POST without required proof-of-interaction, When received, Then the server responds 401 Unauthorized and the token remains unredeemed.
Single-Use Redemption with Anti-Replay and Concurrency Control
Given two or more concurrent valid POST redemption requests for the same token, When processed, Then exactly one request succeeds and returns the secret and all others receive 409 Conflict with no secret disclosed. Given a token has been successfully redeemed once, When the same token value is replayed, Then the server responds 409 Conflict and the secret is not returned. Given network retries of a previously successful client redemption occur, When retried with the same idempotency key, Then the server returns a non-leaking idempotent success without re-revealing the secret or increasing view count.
Immediate Server-Side Revocation
Given an active token exists, When an authorized user or process calls the revoke endpoint for that token, Then revocation takes effect across all servers within 2 seconds. Given a token has been revoked, When a redemption attempt is made thereafter, Then the server responds 410 Gone (revoked) and the secret is not revealed. Given a token has been revoked, When the link is opened in a client, Then the UI displays an expired/invalid message and offers a re-request flow without leaking the secret.
Transport Security and Abuse Controls
Given any request to token endpoints over HTTP, When attempted, Then the server does not reveal any secret and enforces HTTPS (e.g., redirect or 400) with HSTS enabled on the HTTPS domain. Given HTTPS connections, When clients negotiate TLS, Then TLS 1.2 or higher is required and weak cipher suites are rejected. Given more than 5 failed redemption attempts for the same token or IP occur within 1 minute, When the next attempt is made, Then the server responds 429 Too Many Requests and the token state (views, redeemed) remains unchanged. Given rate limiting has been triggered for an origin performing failures, When the intended recipient with proper proof-of-interaction attempts redemption after the cooldown window, Then the request succeeds if the token is otherwise valid.
Auto‑Hide Reveal Window
"As a recipient, I want the code to auto‑hide after a brief reveal so that it doesn’t linger on my screen or get accidentally captured."
Description

Present the secret in a minimal web view that reveals the code for a few seconds (configurable, default 5s) with a visible countdown, then automatically blurs and redacts the value. Disable text selection and clipboard copy where possible; do not render the code in the DOM after hide. Clear any in-memory value immediately after the timer or on tab change, backgrounding, or loss of focus. Best-effort screenshot and screen‑record detection triggers immediate hide with a notice. Provide a tap‑to‑reveal interaction to prevent accidental exposure and support dark mode and small-screen accessibility.

Acceptance Criteria
Tap‑to‑Reveal with Countdown and Auto‑Hide (Default 5s)
Given a valid secure reveal link opens the minimal web view with default settings And the code is concealed by default behind a Reveal control When the user taps the Reveal control Then the code value renders within 100ms And a visible countdown starts at 5 and decrements every 1s And the countdown is visually adjacent to the code When 5s elapse or the countdown reaches 0 Then the code is blurred and redacted within 150ms And the interface returns to the hidden state with a tap‑to‑reveal affordance And the code characters are not readable at up to 200% zoom
Configurable Reveal Duration
Given the reveal duration configuration is set to 3 seconds When the user taps Reveal Then the countdown displays 3 and the code auto‑hides at 3.0s ±100ms Given the reveal duration configuration is set to 10 seconds When the user taps Reveal Then the countdown displays 10 and the code auto‑hides at 10.0s ±100ms Given no custom configuration is provided When the user loads the view Then the default reveal duration is 5 seconds
Hide on Blur, Background, or Tab Change
Given the code is currently visible during an active countdown When the app is backgrounded, the tab visibility becomes hidden, or the window loses focus Then the code is hidden and redacted within 200ms of the event And the countdown stops immediately And the in‑memory code value is cleared within 50ms of the event And returning to the view shows the hidden state only (no auto‑reveal)
No DOM/Text Access and Clipboard/Selection Disabled
Given the code value equals "ABC123" When the user attempts to select text via long‑press, drag, or keyboard shortcuts while the code is visible Then no text selection handles appear and the clipboard remains unchanged And context menus do not offer copy/share for the code element Given the code has been hidden (by timer expiry or blur) Then document.body.innerText does not contain "ABC123" And no element's textContent, value, aria attributes, or data-* attributes contain "ABC123" And accessing window, localStorage, sessionStorage, or indexedDB returns no value equal to "ABC123"
Screenshot/Screen Recording Detection Triggers Immediate Hide
Given the code is visible When a supported screenshot or screen‑recording detection event fires Then the code is hidden and redacted within 150ms And a non‑blocking notice appears within 300ms indicating the hide reason And the code remains hidden until the user explicitly taps Reveal again Given the platform does not expose capture detection When no detection event is available Then no errors are thrown and normal auto‑hide behavior continues
Dark Mode and Small‑Screen Accessibility Compliance
Given the device is in dark mode Then the view uses a dark theme with text/UI contrast ratios meeting WCAG 2.1 AA (>=4.5:1 for regular text, >=3:1 for large text/icons) And the countdown and redaction remain legible in dark mode Given a small screen of 320x568 logical pixels Then all interactive targets (e.g., Reveal) are at least 44x44 dp with 8dp spacing And the code display does not overflow horizontally and is fully visible without horizontal scrolling Given a screen reader is active Then the Reveal control has an accessible name and role And countdown updates announce at most once per second via polite live region And the hidden state announces "Hidden" or equivalent And all focusable elements show a visible focus indicator with >=3:1 contrast
Redacted SMS Delivery & Preview Suppression
"As a business owner, I want SMS to show only a safe link and suppress previews so that secrets aren’t exposed in notifications or by link‑scanning bots."
Description

Ensure SMS messages never include the secret value. Use neutral copy with a secure short link. Configure the landing endpoint to suppress rich previews: return minimal Open Graph metadata, cache-control headers, and an initial lightweight response that withholds secret content from non-interactive clients. Detect known link-preview and security-scanner user agents and serve a non-redeeming placeholder (HTTP 204/preview stub) to avoid burning tokens. Keep the domain recognizable (FetchFlow-branded) while ensuring notifications show only generic text (e.g., “Tap to reveal secure code”). Validate behavior across major devices and carriers.

Acceptance Criteria
SMS Body Contains No Secrets
Given a one-time code must be delivered via SMS When the message is generated and sent Then the SMS body MUST NOT contain the secret value or any substring matching the code format And the body text uses neutral copy (e.g., "Tap to reveal secure code") without pet names or contextual hints about the secret And the message includes exactly one HTTPS short link to a FetchFlow-branded domain And no secret, token, or identifier appears in the URL path, query, or fragment And automated scans of the outbound SMS payloads report 0 occurrences of the secret or code pattern across 100 sample messages
Secure Short Link Uses FetchFlow Domain and Opaque Tokenization
Given a reveal link is created for SMS When the URL is generated Then the domain is on the approved FetchFlow-branded allowlist And the scheme is HTTPS with HSTS enabled on the domain And the URL contains only an opaque link ID; the code or redeemable token is not present client-side And guessing or incrementing the link ID does not expose any additional metadata or increase error verbosity (rate-limited and generic 404) And the link resolves successfully for valid, unexpired IDs and returns a non-revealing shell on first load
Rich Preview Metadata Suppressed for SMS Link
Given an SMS client or crawler requests Open Graph/Twitter metadata for the reveal URL When the endpoint is accessed for preview (HEAD or GET without interaction) Then the response includes minimal, generic metadata only (e.g., og:title="FetchFlow secure link"), with no secret, pet info, or dynamic content And no image or large preview assets are returned And HTTP headers include Cache-Control: no-store, private, max-age=0, must-revalidate; Pragma: no-cache; Expires: 0; X-Content-Type-Options: nosniff And the response body contains no secret value and no redeeming payload And the token redemption state remains unchanged (not marked viewed/redeemed)
Known Preview/Scanner User Agents Receive Non-Redeeming Placeholder
Given the reveal URL is requested by a known link-preview or security-scanner user agent (e.g., iOS LinkPreview, Android Messages previewer, carrier/security scanners on the documented UA list) When the request is received Then the server returns HTTP 204 No Content or a 200 preview stub with generic text only And the underlying token is not redeemed, viewed, or otherwise advanced in lifecycle And subsequent human-initiated access from a standard browser still renders the reveal experience successfully And analytics/logs record the UA classification and placeholder response without logging the secret And load tests confirm 0% unintended redemptions across 1,000 simulated preview/scanner hits
Initial Response Withholds Secret; Secret Fetched Only After User Interaction
Given a human user taps the SMS link in a standard mobile browser When the initial page is delivered Then the initial HTML/JSON contains no secret value or redeemable token And the secret is fetched only via a secondary API call triggered by explicit user interaction (e.g., tap Reveal) with CSRF and one-time token checks And both initial and API responses include no-store cache headers And the initial payload (compressed) is <= 25 KB to remain lightweight And static analysis of the page source confirms 0 occurrences of the secret prior to the user gesture
Cross-Device and Carrier Validation of Redaction and Preview Suppression
Given test devices on iOS (iMessage), Android (Google Messages), and Samsung Messages across AT&T, Verizon, and T-Mobile When SMS messages with reveal links are sent and received on each combination Then message previews show only generic text and the FetchFlow-branded domain, with no code or sensitive data displayed And link-preview fetches do not redeem tokens (confirmed via server logs and token state) And tapping the link reveals the code only within the interactive flow; previews never display it And 100% of tested combinations (minimum 12 device/carrier/app pairs) meet the above conditions
Re‑Request with Step‑Up Authentication
"As a recipient who needs to see the code again, I want to verify my identity and request another brief reveal so that I can regain access without compromising security."
Description

Allow recipients to re‑request a reveal after the first view with mandatory re‑authentication. Support methods based on context: SMS OTP to the original phone number, passcode set by the provider, or biometric via signed-in staff app. Enforce configurable limits (e.g., max 2 re‑requests, cooldown periods) and throttle by IP/device. Each re‑request issues a new single‑use token and updates audit logs. Provide clear UX in the web view and dashboard to request again, verify identity quickly, and proceed to a fresh brief reveal without exposing the code in transport or UI.

Acceptance Criteria
Re-request via SMS OTP after initial reveal
Given a recipient has already viewed a one-time reveal and the link shows a re-request option When the recipient taps Request another reveal in the web view Then the system sends a numeric OTP via SMS to the original verified phone number on file And the OTP expires in 5 minutes and allows a maximum of 3 entry attempts before temporary lockout of 15 minutes And upon correct OTP entry, a new single-use token is created and bound to this re-request And the code is displayed for 6 seconds, is non-selectable and non-copyable, then auto-hides And no code content is transmitted via SMS, stored in SMS previews, or persists in the DOM after auto-hide And the web view shows a clear confirmation and remaining re-request allowance
Re-request via Provider Passcode
Given the provider has enabled a passcode method for re-request and set a 4–8 digit passcode When the recipient selects Re-request and chooses Passcode verification Then the UI presents a masked passcode field and submit action And on correct passcode, a new single-use token is issued and the code is revealed for 6 seconds before auto-hide And on 5 consecutive failed attempts, re-request via passcode is locked for 30 minutes and an error message explains next steps without revealing the passcode And no passcode value is ever sent via SMS or displayed back to the user
Re-request via Staff App Biometric Approval
Given a staff member is signed in to the FetchFlow staff app with biometrics enabled for the same business account as the code And the recipient opens the re-request link showing Approve in staff app When the staff member receives an in-app approval prompt and approves via biometric within 60 seconds Then the re-request is authorized server-side and a new single-use token is issued to the web view session And the code is revealed for 6 seconds then auto-hides without being cached in the app or web view beyond that window And if approval times out or is denied, the web view displays a non-sensitive error and offers alternative verification methods
Enforce Re-request Limits and Cooldown with IP/Device Throttling
Given business settings specify max 2 re-requests per code within 24 hours and a 5-minute cooldown between approvals When a recipient attempts additional re-requests beyond the allowance or within the cooldown Then the server returns HTTP 429 with a JSON error code and retry-after seconds, and the UI disables the button with a countdown And per IP and per device fingerprint throttling limits requests to 3 attempts per 10 minutes; excess attempts are rejected with 429 and logged And limits and cooldowns are enforced consistently across SMS OTP, passcode, and biometric methods
Issue New Single-Use Token and Invalidate Prior Tokens
Given a re-request is successfully re-authenticated by any allowed method When the system issues a reveal Then a new unique single-use token is generated with a TTL of 120 seconds and can be redeemed exactly once And all previously issued tokens for this code are immediately invalidated And any reuse or late use of an old token returns HTTP 410 Gone and shows a non-sensitive expired message in the UI And the token value is never exposed in client-side logs or URLs beyond opaque identifiers
Comprehensive Audit Logging of Re-request Attempts and Outcomes
Given audit logging is enabled by default for secure reveals When any re-request attempt occurs (success or failure) Then an audit entry is recorded with timestamp, requester method (OTP, passcode, biometric), outcome, IP, device fingerprint, token ID, attempt count, and reason on failure And entries are immutable, time-ordered, and available in the dashboard within 5 seconds of the event And exporting the audit trail for a code includes all re-requests with consistent field names and redacts secret values
Dashboard Controls and UX for Re-request and Limits
Given a provider opens the FetchFlow dashboard for One-Time Reveal settings When they configure allowed re-auth methods, max re-requests, cooldown, and attempt limits and click Save Then the settings persist, validate with inline errors on invalid values, and apply to new re-requests within 30 seconds And the code detail view shows a Re-request panel with current allowance remaining, last attempt status, and an action to approve via staff app And error, lockout, cooldown, and success states display clear, accessible messages without exposing sensitive data
Revocation, Alerts, and Audit Trail
"As a provider, I want to see who accessed the code and revoke it if something looks suspicious so that I can stay in control and protect client security."
Description

Maintain immutable logs of link creation, views, re‑requests, revocations, IP/device fingerprints, timestamps, and appointment associations. Expose an audit view in the FetchFlow dashboard and via webhooks for downstream systems. Allow providers to revoke active tokens instantly, with optional SMS to notify the recipient of revocation and a quick re‑issue path. Trigger alerts for anomalous behavior (e.g., multiple reveal attempts from different geolocations or rapid retries) and auto‑block further attempts pending verification.

Acceptance Criteria
Immutable Token Lifecycle Logging
Given a provider creates a One‑Time Reveal link for an appointment, When the link is created, Then an audit record is appended with event_type=created, token_id, appointment_id, provider_id, recipient_id (if available), and timestamp_utc in ISO 8601. Given an authorized user attempts to update or delete any audit record via UI or API, When the request is made, Then the system returns 403 Forbidden and no changes are made to the record. Given reveal, re‑request, and revoke actions occur for a token, When each action completes, Then a corresponding audit record is appended within 1 second of the action and is retrievable in chronological order by timestamp_utc. Given the audit log is queried for a token_id, When records are fetched, Then all lifecycle events that occurred are present and none are missing or altered.
Comprehensive Audit Fields Capture
Given any token lifecycle event (created, viewed, re‑requested, revoked) occurs, When the audit record is stored, Then it includes: event_type, token_id, appointment_id, provider_id, recipient_id (if available), timestamp_utc (ISO 8601), ip, user_agent, device_fingerprint, geo_country, geo_city, and outcome (success|blocked|revoked). Given geolocation cannot be resolved for an event, When the audit record is stored, Then geo_country and geo_city are null and the record includes geo_resolution="unresolved". Given multiple events are generated from the same device within a session, When their records are inspected, Then device_fingerprint remains consistent across those events.
Dashboard Audit Trail View and Export
Given a user with Audit permission opens the Audit view for an appointment or token, When the view loads, Then events are displayed in reverse‑chronological order with columns: timestamp (localized with UTC on hover), event_type, actor, outcome, ip, device_fingerprint, geo_country/geo_city, and token_id. Given the user applies filters (date range, event_type, outcome, token status, actor), When Apply is clicked, Then the results refresh within 2 seconds and only matching events are shown. Given the user clicks Export CSV, When the file downloads, Then it contains exactly the filtered events with ISO 8601 UTC timestamps and a single header row. Given a user without Audit permission attempts to access the Audit view, When they navigate to it, Then access is denied with a 403 and no data is exposed.
Webhook Event Delivery and Idempotency
Given an audit event is created, When webhooks are enabled, Then a POST is sent to each subscribed endpoint within 5 seconds with a JSON payload containing event_id, event_type, token_id, appointment_id, timestamp_utc, ip, user_agent, device_fingerprint, geo_country, geo_city, outcome, and actor. Given a webhook delivery receives a non‑2xx response or times out, When the retry policy executes, Then the delivery is retried with exponential backoff for up to 24 hours and delivery attempts are logged. Given duplicate webhook deliveries occur, When the receiver inspects the event_id or X‑Idempotency‑Key header, Then duplicates are detectable and the payload is identical across retries. Given webhooks are configured with signing, When the receiver validates the X‑FetchFlow‑Signature using the shared secret, Then the signature matches the payload.
Instant Token Revocation with Optional Recipient Notification
Given a provider clicks Revoke on an active token, When they confirm, Then within 5 seconds the token status becomes revoked, a revoke audit record is appended, and subsequent reveal attempts return 410 Gone without revealing the code. Given Notify recipient is selected during revocation, When revocation completes, Then an SMS is sent within 10 seconds that excludes the code, references the appointment, and provides instructions or a link to re‑request; an audit record logs sms_sent=true. Given a recipient opens a revoked link, When the page loads, Then a Link disabled screen is shown with a re‑auth option and no code content is present in the response payload or DOM.
Quick Re‑Issue After Revocation
Given a token has been revoked, When the provider selects Re‑issue from the revocation confirmation or audit view, Then a new token is created and linked to the same appointment and recipient in a single action and the old token remains revoked. Given the provider opts to send the new link via SMS, When re‑issue completes, Then the SMS is delivered within 10 seconds and audit records for reissued and sms_sent events are appended. Given webhooks are enabled, When re‑issue occurs, Then a reissued event is delivered including old_token_id and new_token_id fields.
Anomaly Detection and Auto‑Block on Suspicious Reveals
Given reveal attempts for a token originate from IPs in two or more countries within any 10‑minute window, When the second distinct country is detected, Then an anomaly event is logged, the token is auto‑blocked within 2 seconds, and further reveal attempts are prevented pending verification. Given more than 5 reveal attempts occur for the same token within 60 seconds, When the threshold is exceeded, Then an anomaly event is logged, the token is auto‑blocked, and subsequent attempts return a Too Many Attempts screen without revealing the code. Given an anomaly has been triggered, When the provider opens the dashboard, Then an alert is displayed with the trigger reason and details, and notifications are sent according to preferences (e.g., SMS/email) within 15 seconds. Given the provider completes verification, When they choose Unblock or Re‑issue, Then the choice is executed immediately and a corresponding audit event (unblocked or reissued) is appended.
Business Policy Controls & Workflow Integration
"As an admin, I want to set policies and plug One‑Time Reveal into my scheduling and messaging flows so that the feature matches my risk tolerance and day‑to‑day operations."
Description

Provide admin settings to configure default reveal duration, token TTL, max re‑requests, required auth methods, and allowed recipient roles. Allow per-appointment-type overrides and templates in message composer. Integrate creation and sending of One‑Time Reveal links into the existing appointment timeline, two‑way reminders, and payment checkout steps. Offer an API and Zapier-compatible hooks to create tokens programmatically and receive events (created, viewed, expired, revoked). Support localization and ADA-compliant UI text, and log policy versions applied to each link for auditability.

Acceptance Criteria
Admin Policy Configuration & Validation
Given an admin with policy permissions When they set default revealDurationSeconds, tokenTTLMinutes, maxRerequests, requiredAuthMethods, and allowedRecipientRoles Then the system validates ranges and dependencies (tokenTTL >= ceil(revealDurationSeconds/60), maxRerequests >= 0, requiredAuthMethods subset of supported) and saves the configuration with a new policyVersion Given invalid inputs (e.g., negative values, unsupported auth method) When the admin attempts to save Then the save is blocked and specific inline error messages are displayed for each invalid field Given a saved policy When a new One‑Time Reveal link is created via any surface (timeline, reminders, checkout, API) Then the link inherits the current default policy settings and stores the applied policyVersion immutably on the link record Given requiredAuthMethods contains multiple entries When a recipient views a link Then the recipient must satisfy all configured methods to reveal the code; failure on any method prevents reveal and logs the failed attempt Given maxRerequests = 0 When a recipient attempts to re‑request a reveal Then the option is not shown in UI and API requests return 403 with reason=max_rerequests_disabled
Recipient Role Enforcement
Given allowedRecipientRoles is configured When a user attempts to send a link to a recipient whose role is not in the allowed set Then the send action is blocked in UI with an error state and in API returns 403 with reason=role_not_allowed Given a link successfully sent to a permitted role When a different role attempts to access the link URL Then access is denied, the code is not revealed, and an unauthorized_view event is emitted and logged Given role changes in the CRM occur after link creation When the recipient with a now‑disallowed role tries to view the link Then access is denied based on current policy and an event is logged referencing the policyVersion applied to the link
Appointment-Type Overrides & Composer Templates
Given an appointment type with overrides for revealDurationSeconds, tokenTTLMinutes, maxRerequests, requiredAuthMethods, and a message template When a link is created for an appointment of that type via any surface Then the override values are applied and supersede global defaults for that link and the selected template auto‑populates the composer Given both appointment-type overrides and manual per‑message edits in the composer When the user edits policy fields in the composer Then the per‑message edits take precedence for that link without changing the stored override or default policies Given a message template containing variables {pet_name}, {appointment_time}, {link} When the message is sent Then variables resolve correctly for the target appointment and pet, and the actual short‑lived link URL is inserted at send time (not at template save time) Given an appointment type without overrides When a link is created for that appointment Then global defaults apply and the generic template is available
Workflow Integration: Timeline, Reminders, and Checkout
Given an existing appointment When a user opens the appointment timeline Then a Create One‑Time Reveal action is available and, upon use, a timeline entry is added showing created status, recipient, and policyVersion Given two‑way reminder configuration When a reminder template includes the One‑Time Reveal placeholder Then at send time the system generates a fresh link that respects current policies and records a created event tied to the reminder message Given a payment checkout in progress When the user clicks Send One‑Time Reveal from the checkout screen Then the link is created and sent without blocking payment capture, and both actions are recorded in the appointment activity stream Given maxRerequests > 0 and requiredAuthMethods set When the recipient taps Re‑request in the conversation thread Then re‑auth is required and, on success, a new short‑lived reveal is issued and a rerequested event is logged; on reaching maxRerequests the link is auto‑revoked and subsequent attempts are denied
API & Zapier Event Hooks
Given API credentials with scope one_time_reveal:write When a client calls POST /api/v1/one_time_reveals with appointmentId, recipientId, and optional policy overrides Then the API returns 201 with tokenId, revealUrl, expiresAt, policyVersion, and appliedSettings; invalid inputs return 400 with field-level errors Given a configured webhook/Zapier destination When a token lifecycle event occurs (created, viewed, expired, revoked) Then an event payload containing eventType, tokenId, appointmentId, recipientId, policyVersion, occurredAt, and reason (when applicable) is delivered within 5 seconds, signed, with retry on 3xx/4xx/5xx according to backoff policy Given a token is revoked via POST /api/v1/one_time_reveals/{tokenId}/revoke with reason When a subsequent view is attempted Then the reveal is blocked, a revoked event with the provided reason is emitted, and the API/view returns 410 Gone Given excessive API requests beyond the documented rate limit When additional requests arrive Then the API responds 429 with Retry-After and no events are dropped for successful requests
Localization & ADA Compliance
Given workspace locale = es-ES When users interact with One‑Time Reveal UI and messages Then all user-facing strings are presented in Spanish, including errors, button labels, countdowns, and SMS redaction text; fallback to en-US only for missing keys Given keyboard-only navigation and a screen reader When a user reveals a code Then focus order is logical, controls are reachable via keyboard, controls have accessible names, an ARIA live region announces reveal and auto-hide countdown, and the interface meets WCAG 2.1 AA contrast (>= 4.5:1) and focus visibility Given locale changes mid-session When a new link is created or a message is sent Then the selected locale is applied consistently to the generated link page and SMS content
Auditability: Policy Versioning & Access Logs
Given policy settings are changed and saved When the admin reviews policy history Then a new immutable policyVersion is recorded with who, when, and what changed Given a link is created When viewing its audit record Then the applied policyVersion, any overrides, creator identity, appointmentId, recipient role, and timestamps are present and read-only Given any view attempt (success, unauthorized, expired) When the audit log is queried Then each attempt shows timestamp, actor/recipient, outcome, and source (UI/API) and can be exported to CSV Given compliance review When the system exports logs via API Then the export includes policyVersion references for each event, enabling end-to-end traceability

Access Ledger

Create an immutable trail of access: who revealed which code, when, where, and for which visit. Entries attach to the job and can be shared with owners, boosting trust and providing evidence alongside photos/GPS if any dispute arises.

Requirements

Append-Only Access Ledger
"As a provider, I want every code reveal to be automatically recorded with complete visit context so that I have an indisputable access trail if questions or disputes arise."
Description

Implement a write-once, append-only ledger that records each access event when a lockbox/door/garage code is revealed for a job. Every entry must include job ID, pet and client references, service type, staff identity, channel of reveal (SMS link, dashboard, mobile app), masked code identifier, server-signed timestamp, device timestamp, IP address, device fingerprint, user agent, GPS coordinates with accuracy metadata, reverse-geocoded address, and action outcome. Entries are chained with cryptographic hashes to prevent tampering; edits are prohibited and corrections are captured as compensating entries. The ledger integrates with Jobs, Messaging, Media (photos and GPS check-in/out), and Billing (e.g., no-show fees) so evidence can be surfaced alongside visit artifacts. Provide APIs and internal services to query entries by job, staff, date range, and anomaly flags, with retention policies configurable per workspace.

Acceptance Criteria
Record access event upon code reveal
Given a job with an associated access code and an authenticated staff member When the access code is revealed via SMS link, dashboard, or mobile app Then exactly one ledger entry is created for the event And the entry includes: job_id, client_id, pet_id(s), service_type, staff_id, channel, masked_code_identifier, server_signed_timestamp, device_timestamp, ip_address, device_fingerprint, user_agent, gps_lat, gps_lng, gps_accuracy_m, reverse_geocoded_address, action_outcome And masked_code_identifier does not expose the full code And the server_signed_timestamp is cryptographically signed and verifiable And the entry is retrievable by querying the job_id
Append-only immutability and cryptographic chaining
Given an existing ledger entry When any API or service attempts to update or delete it Then the operation is rejected and no mutation occurs And a compensating entry can be created referencing the original entry_id with correction_reason Given a sequence of entries in a workspace When entry_hash is recomputed and compared across the sequence Then each entry's prev_hash equals the prior entry's entry_hash and the chain validates
Accurate channel attribution and masked code visibility
Given separate reveal events initiated from SMS link, dashboard, and mobile app When the entries are created Then each entry's channel equals the originating channel And no API or UI response exposes the full access code; only masked_code_identifier is present
Query APIs by job, staff, date range, and anomaly flags
Given multiple ledger entries across different jobs, staff, and dates with various anomaly flags When the query API is called with filters for job_id, staff_id, date_range, and anomaly_flags Then only entries matching all provided filters are returned And results are scoped to the caller's workspace And results are ordered by server_signed_timestamp descending with stable pagination
Surface ledger evidence alongside job artifacts
Given a job with ledger entries, visit photos, GPS check-in/out, and billing events When the job is viewed in the internal dashboard or fetched via the evidence bundle API Then the access ledger entries are displayed alongside photos and GPS artifacts And each ledger entry includes resolvable links to the related job, media, and billing records where applicable
Retention policy enforcement per workspace
Given a workspace retention policy of N days and entries older than N days When the scheduled retention process runs Then entries older than N days are no longer returned by query APIs And a retention audit record logs the purge window and count And the remaining entries' hash chain remains valid starting from the first retained entry
Anomaly detection and flagging
Given configured anomaly rules (time drift, geo mismatch, duplicate reveal) When an access event meets any rule Then the created entry contains anomaly_flags including the matched rules And querying with anomaly_flags returns only entries containing those flags And entries without anomalies have an empty anomaly_flags set
Verified Revealer Identity
"As an operator, I want the system to verify exactly which staff member revealed a code so that accountability is clear and impersonation is prevented."
Description

Ensure the “who” in each access event is verified and unambiguous. Require authenticated identity for reveal actions via the dashboard and mobile app, and single-use, job-scoped SMS reveal links bound to the intended staff member and device. Enforce OTP or optional 2FA for high-risk reveals, bind sessions to verified phone numbers, and capture device fingerprinting to deter link forwarding. Support role attribution (staff, owner, admin) and handle shared devices with per-user sessions. Block reveals from unassigned staff unless explicitly overridden by an admin with audit notes, and log all identity assurance steps in the ledger entry.

Acceptance Criteria
Authenticated Dashboard/Mobile Reveal by Assigned Staff
Given a staff user assigned to visit J is logged into the dashboard or mobile app with a verified phone number And the active session is valid and not expired When the user selects “Reveal Access Code” for visit J Then the system validates user assignment to J and phone_verified = true And confirms session integrity (not revoked, IP/device consistent with session binding) And reveals the access code only once per action And writes an immutable ledger entry including: actor_user_id, actor_full_name, actor_role=staff, visit_id, job_id, timestamp_utc, session_id, device_fingerprint, ip_address, phone_number_hash, identity_method="dashboard-session|mobile-session" And the ledger entry becomes visible in the job timeline within 2 seconds And the owner can view the entry if job sharing is enabled
SMS Single-Use Job-Scoped Reveal Link Bound to Intended Staff and Device
Given an SMS reveal link is generated for job J, intended for staff S And the link is job-scoped, single-use, and addressed to S’s verified phone When S opens the link on a device and completes OTP verification sent to the verified phone number Then the link binds to the first verified device fingerprint and phone_number_hash used And the access code is revealed exactly once and the link is immediately invalidated And any subsequent attempt (reuse, different device, different user, expired TTL) returns an error "invalid or used link" and reveals nothing And the link expires if not used within 24 hours or after visit end time, whichever comes first And the ledger entry records intended_user_id, verified_user_id, device_fingerprint, phone_number_hash, otp_challenge_id, outcome, and failure_reason if applicable
High-Risk Reveal Requires OTP/2FA
Given job J is marked high-risk by policy or owner setting When any user (staff/owner/admin) attempts to reveal an access code for J via dashboard, mobile app, or SMS link Then an OTP challenge to the verified phone number is required and must be passed And if the user’s account has 2FA enabled (TOTP/push), a second factor is also required And failed OTP/2FA attempts are limited to 5 within 10 minutes, after which a 5-minute lockout is enforced And the ledger entry includes auth_factors_used, otp_attempt_count, 2fa_result, and lockout_applied (if any) And no code is revealed until all required factors are satisfied
Block Unassigned Staff Reveals with Admin Override and Audit Notes
Given a staff user U not assigned to visit J attempts to reveal the access code When U initiates a reveal action Then the system blocks the reveal with reason "unassigned staff" and no code is shown And an admin may override only from the dashboard by explicitly selecting an Override reason and entering an audit note of at least 20 characters And the override action requires admin re-auth (password or SSO) plus OTP if high-risk And upon override, the code is revealed and the ledger entry records: override=true, admin_user_id, override_reason, audit_note, pre-override assignees snapshot, timestamp_utc And SMS links cannot be used to perform admin overrides
Per-User Sessions on Shared Devices
Given a shared device used by multiple users When user A logs out or their session expires Then user B must authenticate with their own credentials and verified phone to perform any reveal And only one active user session is allowed per browser profile/app instance; reveals are attributed to the currently active user And reveal actions are blocked if no active authenticated session exists And each ledger entry shows the correct actor_user_id irrespective of shared device, and includes the current device_fingerprint And session switch events are logged separately and correlate to subsequent reveal entries
Identity Assurance and Role Attribution Logged in Ledger Entry
Given any access code reveal attempt (success or failure) When the system creates or updates the access ledger Then each entry must include: actor_user_id, actor_full_name, actor_role (staff|owner|admin), identity_method (dashboard-session|mobile-session|sms-otp), phone_verified (true/false), phone_number_hash, device_fingerprint, ip_address, session_id (if applicable), timestamp_utc, job_id, visit_id, result (success|failure), failure_reason (if any), auth_factors_used (password|otp|totp|push) And entries are append-only and immutable; corrections create new entries linked by prior_entry_id And entries are retrievable via API/UI within 2 seconds of event and filterable by actor_role and identity_method And PII is minimized (no full phone numbers; only hashed plus last 2 digits)
Sessions Bound to Verified Phone Numbers
Given a user logs in to the dashboard or mobile app for the first time When the user provides a phone number Then the system sends an OTP and requires successful verification before enabling reveal actions (phone_verified = true) And all sessions store phone_number_hash and phone_verified state; if the phone becomes unverified, active sessions are revoked within 60 seconds And any reveal attempt with phone_verified = false is blocked and recorded in the ledger with result=failure and reason="phone not verified" And changing the account phone number requires re-verification and generates a ledger entry noting old/new hashes and actor_user_id
Time and Location Attestation
"As a provider, I want access events to include accurate time and location so that I can confirm on-site access occurred within the scheduled window."
Description

Capture trustworthy time and place for each access event. Use server-signed timestamps synchronized via NTP and record device-local time for comparison. Collect GPS coordinates with accuracy/altitude/heading when available, fall back to Wi‑Fi/cell triangulation if GPS is unavailable, and reverse geocode to the job address. Support configurable geofences so reveals far from the scheduled address or outside the service window are flagged. Handle offline scenarios by queuing a locally signed event on-device and reconciling with server time upon sync while preserving original device evidence and a reconciliation note in the ledger.

Acceptance Criteria
Online Reveal Within Geofence and Service Window
Given a job with a configured service window and geofence radius And the worker device has data connectivity When the worker reveals the access code within the service window while within the geofence radius Then the server issues and stores an NTP-synced, server-signed timestamp in RFC 3339 format And the ledger entry stores device_local_time and computes delta_seconds = |device_local_time - server_time| And the ledger entry stores location lat, lon, accuracy_m, altitude_m (if available), heading_deg (if available), and source And the ledger entry contains no flags
Clock Drift Detection and Flagging
Given clock_drift_threshold is configured to 120 seconds When an access reveal is recorded Then delta_seconds = |device_local_time - server_time| is computed and stored And if delta_seconds > 120 then the ledger entry is flagged "ClockDrift" with delta_seconds and device_timezone recorded And if delta_seconds <= 120 then no "ClockDrift" flag is present
GPS-First Location Capture with Network Fallback
Given location permissions are granted When an access reveal is initiated Then the app attempts to acquire a GPS fix for up to 10 seconds And if GPS accuracy_m <= 50 within 10 seconds then source = "gps" and accuracy_m is stored And if no GPS fix with accuracy_m <= 50 is available within 10 seconds then source = "network" (Wi-Fi/cell) and accuracy_m is stored And if neither source provides a location, source = "unavailable" and the ledger entry is flagged "LocationUnavailable" And altitude_m and heading_deg are recorded when provided by the location provider
Reverse Geocoding and Geofence Distance Evaluation
Given the job address is geocoded to a reference point and geofence_radius_m is configured (default 100) When a reveal with a captured location is recorded Then the system reverse geocodes the coordinates to a postal address and stores it And computes geo_distance_to_job_m between the captured location and the job reference point And if geo_distance_to_job_m > geofence_radius_m then flag "OutsideGeofence"; else do not flag "OutsideGeofence" And if reverse geocoding fails, store lat/lon only, flag "AddressUnresolved", and enqueue a retry up to 3 times within 15 minutes without changing the original timestamp
Service Window Validation and Flagging
Given the job has a service window [start, end] in the job's local timezone and tolerances early_tolerance_min=0 and late_tolerance_min=0 unless configured otherwise When an access reveal is recorded Then the server compares server_time to the service window with tolerances And if server_time < start - early_tolerance_min or server_time > end + late_tolerance_min then flag "OutsideServiceWindow"; else do not flag "OutsideServiceWindow"
Offline Reveal: Local Signature and Server Reconciliation
Given the device has no network connectivity at reveal time When the worker reveals the access code Then the app creates and queues a locally signed event containing device_local_time, location payload, and a device_signature And upon connectivity, the server verifies device_signature, appends a server-signed timestamp, and stores both times And the ledger entry retains the original device evidence and adds a reconciliation_note "ReconciledOffline" with delta_seconds and sync_time And original fields are not overwritten; only append-only annotations are added
Shareable Access Report
"As a pet owner, I want a shareable access summary for each visit so that I can see how and when entry occurred alongside photos and GPS."
Description

Generate a secure, owner-shareable report per visit that summarizes how, when, and where access was obtained, linked to the job. The report presents a chronological timeline combining the access ledger entry with check-in/out GPS, photos, and notes. Share via expiring, branded links with optional passcode, track views, and allow export to PDF for dispute resolution. Provide controls to redact sensitive code values while preserving audit details and allow admins to revoke previously issued links. Surface the report directly from the job detail in the dashboard and via SMS to owners on completion.

Acceptance Criteria
Generate Report from Job Detail
Given I am an authenticated staff member with permission to view jobs And a job is marked Complete and has associated access ledger entries, check-in/out events, photos, and notes When I open the job detail and click Generate Shareable Access Report Then a report is created and linked to the job with a unique report ID And the report header shows business brand, owner name, pet name(s), job ID, service type, and service date/time And the timeline renders all available events in chronological order with precise timestamps and sources And the report generates within 3 seconds for 95% of requests
Create Expiring Branded Link with Optional Passcode
Given a report exists for a job When I choose Share and configure link settings Then the system creates a branded public link using the business logo/name And I can set an expiration between 1 hour and 30 days (default 7 days) And I can require a passcode; if enabled, a random 6-digit code is generated and shown to the staff, with option to set a custom 4–12 character code And the link does not expose PII or job identifiers in the URL And accessing the link after expiration returns an Expired page with no report content
Track Report Views
Given a shareable link for a report has been created When a recipient opens the link and successfully views the report Then the view count increments by 1 and records timestamp, user agent, and IP country And the dashboard shows total views and last viewed time And repeated opens from the same device within 5 minutes are counted as a single view And no tracking data is recorded for failed or passcode-blocked attempts
Export Report to PDF
Given a report is open in the dashboard When I click Export to PDF Then a PDF is generated matching the on-screen timeline order and content And the PDF includes business branding, job metadata, photos (with captions), and GPS coordinates with map snapshots And any active redactions are applied And the generated file size is <= 10 MB for reports with up to 20 photos And the PDF is available to download within 5 seconds for 95% of exports
Redact Sensitive Access Codes
Given the report contains access code values (e.g., door, key box, keypad) When I toggle Redact Access Codes on Then all code values in the report are masked to last 2 characters (e.g., ****23) while retaining code type and event timestamp And a Redaction Applied indicator appears on the report and export And the underlying ledger remains unchanged and fully visible to authorized internal users And shared links and PDFs always reflect the current redaction setting
Revoke Previously Issued Link
Given a shareable link for a report exists When an admin clicks Revoke Link and confirms Then the link becomes unusable immediately And future visits display a Revoked page with support contact details and no content leakage And the report’s share status updates to Revoked with timestamp and admin ID And no further views are counted after revocation
Auto-Send Report Link via SMS on Completion
Given a job is marked Complete and the owner has a verified, SMS-opted-in mobile number When the job transitions to Complete Then the system generates (or reuses) the report and sends an SMS containing the branded link and passcode if required And SMS delivery status is recorded (queued, sent, delivered, failed) And on failure, the dashboard surfaces a retry option and error details And no SMS is sent to owners who have opted out; the link remains available in the dashboard for manual sharing
Secure Code Masking and Storage
"As a business admin, I want codes stored and displayed securely so that sensitive entry details aren’t exposed while still keeping a complete audit trail."
Description

Protect sensitive entry codes while enabling full auditing. Store codes encrypted at rest using a managed KMS, restrict decryption to a dedicated service, and never transmit plaintext in notifications. Display only masked codes in UI until an authorized reveal occurs, require users to state a purpose (e.g., start visit) before reveal, and automatically log that purpose. Enforce role-based permissions, minimum access levels, and session timeouts, and provide configurable redaction rules (e.g., display last 2 digits only). Implement retention policies for code data and access entries, support secure deletion when clients revoke access, and include key rotation procedures with zero downtime.

Acceptance Criteria
Masked Display with Configurable Redaction
Given a user views a job containing an entry code and has not performed a reveal When the code is rendered in any UI or API response Then only the masked value is shown according to the active redaction rule and no plaintext is present Given the org redaction rule is set to show the last 2 digits When a code "123456" is displayed Then it renders masked showing only "**56" (pattern may vary but reveals only the last 2 digits) Given the redaction rule configuration is updated (e.g., to last 4 digits) When the UI/API next serves the resource Then masked displays reflect the new rule within the configured cache/propagation TTL without deploy Given browser/network inspection without reveal When the page and API calls load Then no plaintext code appears in HTML, JS, network payloads, or client logs Given a standard read endpoint is called without reveal authorization When the response is returned Then the plaintext field is null/absent and only the masked value plus non-sensitive metadata are returned
Authorized Reveal with Purpose Capture
Given a logged-in user with reveal permission and an active session When they click or call Reveal on a code Then they must select/enter a purpose from the configured list before proceeding Given the user selects "Start Visit" and confirms When the reveal executes Then plaintext is displayed to that user session only and is not persisted in source after reload; masked view remains the default elsewhere Given a reveal occurs When an Access Ledger entry is written Then it includes code id, job id, user id, role, purpose, timestamp (UTC), IP, device/user agent, and location context (if available) Given another user loads the same job without reveal When viewing the code Then only the masked value is shown Given an audit export is requested for the job When generated Then the export contains the reveal entry with purpose and validates against the audit schema
KMS Encryption and Service-Scoped Decryption
Given codes are stored in the database When inspecting data at rest Then the code column contains ciphertext with a key reference and no plaintext is stored Given the managed KMS key for codes When a decryption request is made by any principal other than the dedicated code-service Then KMS denies the request and records the denial in audit logs Given the code-service performs decrypt for an authorized reveal When KMS is called Then decryption succeeds using envelope encryption and meets platform SLOs for latency and error rate Given platform logs and APM When monitoring reveal operations under representative load Then no plaintext codes appear in logs, traces, or error messages and metrics remain within SLOs
Role-Based Access Control and Session Timeout
Given organizational roles (e.g., Admin, Staff, Assistant, Owner) and a configured minimum access level for reveal When a user attempts to reveal a code Then only users meeting the minimum level are authorized; others receive 403 with a non-sensitive reason and the attempt is audited Given a user without reveal permission calls the reveal API directly When the request is processed Then it is blocked, rate-limited per policy, and no partial code (masked or plaintext) is exposed beyond standard error Given a permitted user with an idle session exceeding the configured timeout When they click Reveal Then re-authentication (and MFA if enabled) is required before proceeding Given an API token lacking the "codes:reveal" scope When invoking the reveal endpoint Then the request is rejected with 401/insufficient_scope and an audit entry is created
No Plaintext in Notifications and Integrations
Given any outbound notification (SMS, email, push) or webhook for a job with an entry code When the message/payload is generated Then only the masked code per current redaction rules is included; plaintext is never included Given an admin edits message templates When inserting variables Then only masked code variables are available; plaintext code placeholders are not exposed or are blocked at save Given a third-party integration subscribes to job events When a payload is delivered Then it excludes plaintext codes and includes a flag indicating that reveal requires secure UI/action Given outbound queues and delivery logs are sampled When scanning a statistically significant sample (e.g., 1000 messages) Then 0 contain plaintext codes and any violations fail the build/checklist
Retention Policy and Secure Deletion on Revocation
Given retention policies for codes (X months) and access logs (Y months) When records exceed their configured TTL Then they are purged by scheduled jobs and deletion events are logged with counts and success/failure metrics Given a client revokes access for a property or lock When secure deletion is triggered for associated codes Then plaintext material and DEKs are destroyed/unlinked such that subsequent decrypt attempts return NotFound/AccessDenied while masked placeholders remain for historical context Given backups and replicas exist When secure deletion executes Then deletion propagates according to the data protection policy and verification reports confirm unrecoverability within the configured RPO/RTO Given a data export is requested after deletion When fulfilled Then no plaintext or ciphertext of revoked codes is included; only non-sensitive tombstone metadata is returned
Zero-Downtime Key Rotation
Given a new KMS key is provisioned for codes When rotation is initiated Then live read/write traffic continues without user-visible errors and observed latency remains within platform SLOs Given existing records encrypted under the previous key When background re-encryption runs Then it completes within the configured maintenance window and surfaces any failures with actionable reports and retries Given dual-key decrypt support during rotation When decrypting records Then both old and new key references are accepted until cutover completes Given rotation completes When validating metadata and key policies Then no records reference retired keys and decrypt is disabled on retired keys by policy
Anomaly Alerts and Flags
"As a business owner, I want to be alerted to unusual access behavior so that I can act quickly to prevent misuse and reassure clients."
Description

Detect and act on suspicious access behavior. Define rules to flag reveals that occur far from the job address, outside scheduled time windows, from unrecognized devices or IP ranges, or with excessive reveal frequency. Surface inline flags on jobs and ledger views, and notify designated recipients (owner, admin, assigned staff) via push/SMS/email according to configurable escalation policies. Provide an incident review workflow to acknowledge, comment, and resolve alerts, optionally locking further reveals for a code until reviewed. Expose alert data via API and include flags in shareable reports when relevant.

Acceptance Criteria
Geo-Distance Anomaly Flag on Code Reveal
Given a job with a geocoded service address and anomaly_distance_threshold set to 200 meters When a code reveal is recorded 650 meters from the job address Then an alert is created with type "geo_distance", severity "high", status "open", distance_m=650, and it is attached to the job and the reveal ledger entry And an inline flag indicator appears on the job and ledger views reflecting the new alert Given a job with anomaly_distance_threshold set to 200 meters When a code reveal is recorded within 150 meters of the job address Then no geo_distance alert is created Given anomaly_distance_threshold is updated to 100 meters When a code reveal is recorded 120 meters from the job address Then a geo_distance alert is created with threshold_used=100
Time-Window Anomaly Flag Outside Scheduled Visit
Given a job scheduled from 10:00 to 11:00 local time and allowed_window_buffer set to 10 minutes When a code reveal occurs at 09:45 Then a time_window alert is created with subtype "early", severity "medium", status "open" Given the same job schedule and buffer When a code reveal occurs at 11:20 Then a time_window alert is created with subtype "late", severity "medium", status "open" Given the same job schedule and buffer When a code reveal occurs at 10:15 Then no time_window alert is created
Unrecognized Device or IP Trigger
Given an assigned staff member with registered device IDs [D1, D3] and IP allowlist CIDRs [203.0.113.0/24] When a code reveal occurs from device ID D2 not in the registered list Then an alert is created with type "unrecognized_device", severity "medium", status "open", and includes device_id=D2 Given the same staff member and IP allowlist When a code reveal occurs from IP 198.51.100.25 not in the allowlist Then an alert is created with type "unrecognized_ip", severity "medium", status "open", and includes ip=198.51.100.25 Given device ID D2 is subsequently registered to the staff member When the next reveal occurs from device ID D2 Then no unrecognized_device alert is created Given IP allowlist checks are disabled in settings When a reveal occurs from any IP Then no unrecognized_ip alert is created
Excessive Reveal Frequency Spike
Given reveals_per_code_per_window is set to 3 and window_minutes is set to 15 When the 4th reveal for the same code occurs within a rolling 15-minute window Then a frequency alert is created with type "frequency", severity "medium", status "open", count=4, window_minutes=15 And no additional frequency alerts are created for the same code within the same window while the alert remains open Given the same configuration When reveals are spaced such that no 15-minute window exceeds 3 reveals Then no frequency alert is created Given reveals_per_code_per_window is updated to 5 When 4 reveals occur within 15 minutes Then no frequency alert is created
Surface and Share Flags in UI, API, and Reports
Given a job with two open alerts of types ["geo_distance", "time_window"] When viewing the job card and ledger list Then an inline flag badge displays count=2 with type indicators, and clicking the badge opens an alert panel listing each alert with type, severity, timestamp, and status Given the ledger list is filtered with "Show flagged only" When the filter is applied Then only jobs with at least one open alert are displayed Given an authenticated API client When GET /v1/alerts?job_id={job_id} is called Then the response is 200 with a paginated list of alerts including id, job_id, code_id, type, severity, triggered_at, status, assignee_id, location, device_id, ip, and metadata fields Given a shareable ledger report is generated for a date range When the report includes jobs with alerts Then the report includes a flags summary per job and an alert details section listing type, severity, and status for each alert Given an alert is resolved before report generation When the report is generated Then the alert appears with status "resolved" and resolved_at populated
Escalation Notifications for Anomalies
Given an escalation policy: immediate push to assigned staff; SMS to owner at +5 minutes if alert still open; email to admin at +10 minutes for severity "high" When a new geo_distance alert with severity "high" is created at time t0 Then a push notification is sent to the assigned staff within 30 seconds containing job_id, pet_name, alert_type, and distance_m And if the alert remains open at t0+5 minutes an SMS is sent to the owner And if the alert remains open at t0+10 minutes an email is sent to the admin Given the same alert When the alert is acknowledged before t0+5 minutes Then pending SMS and email escalations are canceled Given multiple alerts are created for the same job within 2 minutes When notifications are generated Then notifications are consolidated per channel to a single message summarizing alert types and counts Given a recipient has opted out of a channel When escalations are due for that channel Then no notification is sent for that recipient via that channel Given a notification delivery fails When retry policy is applied Then the system retries up to 3 times with exponential backoff and logs outcomes
Incident Review and Reveal Lock Workflow
Given an open alert for a job When a reviewer clicks Acknowledge Then the alert records acknowledged_by and acknowledged_at and remains open Given the same alert When a reviewer adds a comment Then the comment is appended with actor, timestamp, and content to the alert's thread Given the same alert When a reviewer clicks Resolve and selects a resolution_reason Then the alert status changes to "resolved" and resolved_at and resolution_reason are recorded Given a reviewer with lock permissions toggles "Lock further reveals for this code" When a subsequent reveal attempt is made for the same code Then the reveal is blocked with error "reveal_locked", no access code is shown, and a ledger entry of type "reveal_blocked" is added Given the alert is resolved and lock is removed When a subsequent reveal attempt is made Then the reveal proceeds normally and no reveal_locked error is returned Given any review action occurs When audit logs are queried Then an immutable record exists with actor, action, timestamp, and before/after status

Offline Unseal

If GPS or data is spotty, allow a fallback SMS challenge (owner‑defined question or manager PIN) to unseal the code. Attempts are rate‑limited and owners get a heads‑up, ensuring the walker isn’t stranded while keeping security tight.

Requirements

Offline Challenge Trigger & Flow
"As a walker, I want a reliable offline unseal path via SMS or PIN when data or GPS is spotty so that I can start my visit on time without being stranded."
Description

Detect unreliable data or GPS during check-in/unseal and present an offline path that guides the walker to complete a secure fallback via SMS or manager PIN without leaving the appointment flow. The app pre-fills a signed SMS containing appointment context (appointment ID, pet name, walker ID, timestamp, nonce) and the selected challenge method (owner security question answer or manager PIN, hashed client-side). The backend validates the challenge and returns a short-lived, single-use unseal token by SMS that the walker can enter to proceed. If data connectivity returns mid-flow, the app resumes the normal online validation seamlessly. The UI clearly explains steps, provides copy-to-clipboard for the token, and prevents accidental back navigation. All operations work on iOS and Android default SMS apps, require no special carrier features, and avoid exposing the unseal code in any notifications. Successful unseal associates to the correct appointment and starts the job timer as usual.

Acceptance Criteria
Auto-trigger offline challenge during check-in with unreliable connectivity or GPS
Given the walker is on the check-in/unseal screen for a scheduled appointment and any of the following are true: (a) no network connectivity, (b) median unseal API round-trip latency > 8 seconds over the last 3 attempts, or (c) GPS accuracy > 100 meters or no fix for > 10 seconds, When the unseal action is initiated, Then the app presents the Offline Challenge path within the same appointment flow, shows an offline banner with the detected reason, records a reason code and timestamp, and disables system back navigation except via an explicit Cancel with confirmation.
Prefilled signed SMS composition with context and hashed secret
Given the Offline Challenge path is shown and the walker selects Owner Security Question or Manager PIN, When the app opens the OS default SMS composer, Then the message is prefilled to the configured backend SMS number and the body includes appointment_id, pet_name, walker_id, timestamp (ISO-8601 UTC), nonce (128-bit), method (owner_question|manager_pin), hashed_secret computed client-side using the selected method and a salt derived from the nonce, and a signature (HMAC-SHA256) over the canonical payload; And the plaintext answer/PIN is never transmitted or stored; And the composed SMS length does not exceed 480 characters; And the app stores the nonce locally for later token verification.
SMS send via default apps across iOS and Android without special carrier features
Given the prefilled message is displayed in the OS default SMS app, When the walker taps Send, Then the SMS sends successfully on iOS (15+) and Android (10+) without requiring any special carrier features; And if the device is unable to send SMS, the app returns to the Offline Challenge screen with a clear error and retry guidance; And the FetchFlow app does not generate any local/push notification that reveals any unseal token content.
Backend validation, rate limiting, and token issuance by SMS
Given the backend receives a challenge SMS with a valid signature and an unseen nonce for the appointment, When the hashed_secret matches the stored owner answer or manager PIN policy, Then the backend replies via SMS with a 6-digit numeric unseal token within 30 seconds; And the token TTL is 5 minutes and is single-use; And per appointment rate limit is max 5 validation attempts per 15 minutes with exponential backoff; And after 3 consecutive failed validations, the pet owner receives a heads-up SMS; And all attempts are audit logged with timestamp, walker_id, and reason.
Seamless resume to online validation if connectivity returns mid-flow
Given the walker is on the Offline Challenge or token entry screen, When stable data connectivity and GPS accuracy ≤ 50 meters are detected for at least 5 seconds, Then the app offers Resume Online Validation preserving appointment context; And if accepted (or auto-resume is enabled), the app performs online unseal; And upon online success, any outstanding offline tokens are invalidated server-side and the offline flow is dismissed; And no duplicate check-in or job timer starts occur.
Token entry UX, clipboard support, and navigation safeguards
Given a token SMS has been received, When the walker returns to FetchFlow, Then the token entry accepts only 6 numeric digits, supports paste-from-clipboard, and provides a Copy button once the token is captured; And on Android, a one-tap Paste from SMS (SMS Retriever/Intent) is offered; And accidental back navigation is blocked with a confirmation dialog; And expired/invalid tokens display specific error states without revealing expected values; And after 3 invalid entries, a 2-minute lockout is enforced.
Successful unseal associates to correct appointment and starts job timer
Given a valid offline token is entered for appointment A by walker W, When the token is accepted, Then the unseal is recorded with method=offline, nonce, and timestamp against appointment A and walker W; And the appointment transitions to In Progress and the job timer starts within 1 second; And online/offline banners are cleared; And if the token is reused or does not match appointment A or the stored nonce, the unseal is rejected with an appropriate error.
Owner Heads-Up SMS Notification
"As a pet owner, I want to receive a heads-up when my walker uses an offline unseal so that I know access is happening even if systems are down."
Description

Automatically notify the pet owner via SMS whenever an offline unseal attempt occurs, including service type, walker name, approximate time window, and business name, without revealing the unseal token. Provide configurable quiet hours, opt-out compliance (STOP/HELP), and organization-level toggles to enable or suppress owner heads-up by client. Include a lightweight "Report a concern" link to route issues to the business, and log delivery status and owner responses. Notifications are triggered on attempt and on success, deduplicated to prevent spam, and localized to the owner’s language preference from their Pet Profile Smart Card.

Acceptance Criteria
Heads-Up SMS on Offline Unseal Attempt
Given a walker initiates an offline unseal attempt for a scheduled service and the owner’s client record has Heads-Up enabled, the owner has not opted out, and the current time is outside configured quiet hours When the attempt is recorded Then the system sends one SMS to the owner within 60 seconds And the SMS includes business name, service type, walker name, and the scheduled/approximate time window And the SMS excludes any unseal token, codes, or PINs And the SMS contains a “Report a concern” link that routes to the business’s configured inbox or dashboard And the message is localized to the owner’s language preference from the Pet Profile Smart Card (fallback to English if unset) And the send event is logged with message ID, booking ID, owner ID, timestamp, and trigger type = attempt
Heads-Up SMS on Successful Offline Unseal
Given an offline unseal completes successfully for a scheduled service and the owner’s client record has Heads-Up enabled, the owner has not opted out, and the current time is outside configured quiet hours When the success event is recorded Then the system sends one SMS to the owner within 60 seconds And the SMS includes business name, service type, walker name, and the actual service/unseal time expressed as an approximate window And the SMS excludes any unseal token, codes, or PINs And the SMS contains a “Report a concern” link that routes to the business’s configured inbox or dashboard And the message is localized to the owner’s language preference from the Pet Profile Smart Card (fallback to English if unset) And the send event is logged with message ID, booking ID, owner ID, timestamp, and trigger type = success
Notification Deduplication and Merge Policy
Given the deduplication window is set to 5 minutes per booking and a merge window of 2 minutes between attempt and success When multiple offline unseal attempts occur within the dedup window for the same booking Then at most one attempt heads-up SMS is sent within that window When an offline unseal success occurs within the merge window of an attempt for the same booking Then only the success heads-up SMS is sent and the attempt heads-up is suppressed And no more than one success heads-up SMS is sent per successful unseal event
Quiet Hours Suppression
Given quiet hours are configured for the client via the organization settings (e.g., 21:00–07:00 local time) When an offline unseal attempt or success occurs during quiet hours Then no heads-up SMS is sent And the suppression is logged with reason = quiet_hours, booking ID, owner ID, and timestamp
Carrier Opt-Out and HELP Compliance
Given the owner replies STOP to any FetchFlow heads-up message When the STOP message is received Then all future heads-up SMS to that phone number are blocked until re-opt-in And a one-time confirmation SMS is sent acknowledging the opt-out and providing HELP info Given the owner replies HELP Then a HELP SMS is sent including program name (FetchFlow), business name, support contact (phone/email), and STOP instructions Given the owner replies START or UNSTOP after opting out Then the number is re-subscribed and heads-up SMS resume
Organization-Level Heads-Up Toggle per Client
Given a staff member disables the Heads-Up toggle for a specific client in organization settings When an offline unseal attempt or success occurs for that client Then no heads-up SMS is sent regardless of other settings (except carrier compliance replies) And the suppression is logged with reason = org_client_disabled Given the toggle is enabled When subsequent events occur Then heads-up SMS are sent per other acceptance criteria
Delivery Status and Owner Response Logging
Given an SMS heads-up is sent When delivery webhooks from the SMS provider are received Then the system records per-message status transitions (queued, sent, delivered, failed/undelivered) with timestamps and provider error codes Given the owner replies to a heads-up (e.g., STOP, HELP, START, or submits a concern via the link) Then the reply/concern is logged and linked to the booking and client record and routed to the business’s configured channel
Manager PIN Administration
"As an operations manager, I want to manage secure offline PINs so that staff can complete visits when connectivity fails without compromising security."
Description

Provide an admin interface to create, rotate, and revoke time-bound manager PINs used for offline unseal, scoped by location/team and permissioned to admins only. Enforce PIN complexity and reuse rules, set expirations, and allow emergency one-time PINs for a specific appointment. PINs are never displayed in plaintext after creation, are stored server-side as salted hashes, and are distributed to authorized managers through a secure channel. The mobile app supports entering a manager PIN when chosen as the challenge method, transmitting only hashed material via SMS for verification, and locks the PIN entry after N failed attempts.

Acceptance Criteria
Admin creates a time-bound manager PIN with enforced complexity and reuse rules
Given an authenticated user with Admin role and a selected location/team When the admin submits a request to create a manager PIN with an explicit expiration date and time Then the system validates that the PIN meets policy (numeric, length 6–8, not sequential or repeating, not among the last 5 used for the same scope) And the system requires an expiration between 1 hour and 30 days from creation And the system persists the PIN server-side only as a salted hash with unique per-PIN salt And the UI confirms creation without displaying the plaintext PIN and shows only masked/last-2 digits reference And the event is recorded in an audit log with admin ID, scope, timestamp, and outcome
Admin rotates an active manager PIN for a location/team
Given an authenticated Admin and an existing active manager PIN for a location/team When the admin initiates a rotation Then the system generates a new compliant PIN and replaces the previous PIN immediately And the previous PIN becomes invalid at once for offline unseal And the new PIN inherits the same scope and a new expiration within policy And the plaintext of neither old nor new PIN is shown in the admin UI at any time And rotation is captured in the audit log with old→new reference (hashed identifiers) and timestamps
Admin revokes a manager PIN and propagation is immediate
Given an authenticated Admin and a specific active manager PIN When the admin revokes the PIN Then the PIN is marked invalid immediately across all services And subsequent offline unseal attempts using the revoked PIN are rejected And the revocation action records admin ID, scope, timestamp, and reason code in the audit log And any scheduled distributions for the revoked PIN are canceled
Admin scoping and access control for PIN management
Given a user without Admin role When the user attempts to view, create, rotate, or revoke manager PINs Then access is denied and the attempt is logged without exposing sensitive data Given an authenticated Admin with access limited to specific locations/teams When the admin manages PINs Then they can only manage PINs within their authorized scopes And search/list views display only masked entries within authorized scopes And all PIN management endpoints require Admin role and organization scope checks
Secure distribution of manager PINs to authorized managers
Given an authenticated Admin has created or rotated a PIN When the admin initiates distribution Then the system delivers the PIN only to selected authorized managers via a secure channel with transport encryption And no plaintext PIN appears in system logs, push previews, email/SMS bodies, or analytics And distribution artifacts expire or become inaccessible once the PIN is revoked or expires And the system records recipient IDs, channel, and delivery status in the audit log without storing the plaintext PIN
Issue an emergency one-time PIN for a specific appointment
Given an authenticated Admin and a specific appointment When the admin issues an emergency one-time PIN Then the system generates a single-use PIN scoped only to that appointment and valid for a maximum of 30 minutes And the one-time PIN cannot be reused after a successful unseal or after expiration And the PIN is stored as a salted hash and never shown in plaintext in the admin UI And distribution is limited to authorized recipients via the secure channel and is audit-logged
Mobile app offline unseal using manager PIN with hash-only SMS and lockout
Given the mobile app is offline or data is spotty and manager PIN is the selected challenge method When the walker enters a manager PIN to unseal Then the app transmits only hashed material via SMS (no plaintext PIN digits) required for server verification And the attempt is rate-limited and after 5 failed attempts within 15 minutes the PIN entry locks for 15 minutes And during lockout the UI displays remaining lockout time and blocks further submissions And upon successful verification the unseal completes and the attempt counters reset
Attempt Rate Limiting & Lockout
"As a security-conscious business owner, I want offline unseal attempts to be rate-limited and lock after too many failures so that brute-force attacks are prevented."
Description

Apply centralized rate limits on offline unseal attempts across both SMS and in-app initiation, keyed by device, phone number, appointment, and organization. Use exponential backoff and temporary lockouts after configurable thresholds (e.g., 3, 5, 8 failed attempts), with clear on-device messaging and guidance to contact a manager. Trigger alerts to managers after repeated failures and suppress further token issuance during lockout windows. All thresholds are configurable at the org level, with safe defaults, and logged for security auditing.

Acceptance Criteria
Unified Attempt Counting Across SMS and App
Given an appointment A in organization O and a walker using device D with phone number P And the walker attempts Offline Unseal via both SMS and the mobile app When failed attempts occur across either channel within a 24-hour window Then a single centralized failure counter keyed by {org: O, appointment: A, device: D, phone: P} increments by 1 per failed attempt And the combined counter is used to evaluate backoff and lockout thresholds regardless of channel And the last-attempt metadata (timestamp, channel, reason) is recorded once per attempt and visible to support tools within 10 seconds
Exponential Backoff After Consecutive Failures
Given key K = {org: O, appointment: A, device: D, phone: P} has n ≥ 1 consecutive failed attempts and no active lockout When the user initiates another attempt Then the earliest allowed retry time is lastFailureAt + min(baseDelay × 2^(n−1), maxBackoff) And attempts made before that time are blocked and do not increase n And the backoff parameters baseDelay, multiplier, and maxBackoff are configurable at the org level with safe defaults (60s, ×2, 15m) And on a successful unseal for K, the consecutive failure counter resets to 0
Threshold-Based Temporary Lockouts
Given org O has thresholds T = [3, 5, 8] and lockout durations L = [5m, 15m, 60m] and key K When the failure count for K reaches any threshold t in T Then a lockout with duration L[t] starts immediately and applies to both SMS and app And during lockout, all attempts are rejected with a lockout message including unlock time and manager contact guidance And during lockout, no challenge is issued, no token is generated, and failures are not counted And the app/API responds with HTTP 423; SMS replies with a templated lockout text
Manager Alerts on Repeated Failures
Given a lockout has started for key K in org O When the lockout is initiated or extended by a higher threshold Then an alert is sent to org O managers via SMS (and email if configured) within 30 seconds And the alert includes orgId, appointmentId, deviceId, phone, failure count, lockout duration, last channel, and a link to the incident log And duplicate alerts are suppressed to at most 1 per lockout window for K
Suppress Token Issuance During Lockout
Given a lockout is active for key K When any system component requests issuance or refresh of an Offline Unseal token for appointment A in org O Then the request is denied and no token object is created or updated And API responses are HTTP 423 with error code offline_unseal_locked; SMS replies use the lockout template And the denial event is logged with reason "lockout_active"
Org-Level Configuration and Safe Defaults
Given an org admin updates rate-limiting and lockout settings When the changes are saved Then validation enforces ascending thresholds, positive durations, baseDelay within 1–300s, maxBackoff within 5–60m, multiplier ≥ 2 And the new configuration takes effect for new attempts within 60 seconds across all services And existing lockouts are not shortened; longer durations take effect only on the next threshold event And if no custom config exists, safe defaults are applied [thresholds: 3,5,8; durations: 5m,15m,60m; baseDelay: 60s; multiplier: 2; maxBackoff: 15m]
Security Audit Logging Coverage
Given any Offline Unseal attempt, block, or lockout decision occurs for key K When the event happens Then an audit log entry is written with timestamp (UTC), orgId, appointmentId, deviceId, phone, channel, attemptId, outcome, failure count, backoff delay applied, lockout state, config version, actor identifiers, and IP (if app) And entries are immutable, tamper-evident, retained for at least 365 days, and queryable by org admins And 95% of audit queries for a single appointment return within 5 seconds
Secure SMS Gateway & Tokenization
"As a platform admin, I want the SMS processing to issue short-lived tokens only after validating signed challenges so that offline unseals remain secure and tamper-resistant."
Description

Implement a backend SMS gateway that validates signed challenge requests, verifies owner security question answers or manager PINs against hashed records, and returns a one-time, short-lived unseal token via SMS. Tokens are 6–8 character alphanumerics, single-use, expire in 5 minutes, and are bound to the appointment and, when available, the initiating device identifier to mitigate replay. The gateway enforces anti-abuse controls (HMAC on payload, nonce with replay detection, IP/ASN checks on webhooks, per-sender throttles) and integrates with the existing messaging provider (e.g., Twilio) for delivery receipts and retry policies. No PII beyond first name and last initial is echoed, and secrets are redacted in logs.

Acceptance Criteria
HMAC-Signed Challenge Request Verification
Given a POST /sms/challenge request containing appointment_id, optional device_id, nonce, and timestamp in the JSON payload And the request includes an X-Signature header computed as base64(HMAC-SHA256(shared_secret, canonical_json)) with UTF-8 and lexicographically sorted keys When the gateway validates the signature and checks the timestamp is within ±3 minutes of server time and not older than 5 minutes Then requests with a valid signature and time window are accepted with HTTP 202 and requests with missing/invalid signature or outside the time window are rejected with HTTP 401 without revealing validation details And the nonce field is present and non-empty or the request is rejected with HTTP 400 And all validation failures are recorded with redacted details and without echoing payload secrets
Owner Security Answer Verification Against Hashed Records
Given an appointment whose owner security answer is stored as a salted hash using an approved algorithm When an SMS reply to the challenge is received containing the owner’s answer Then the gateway normalizes input by trimming surrounding whitespace and performs a constant-time hash comparison to validate And on a correct answer, the request is marked Verified and proceeds to token issuance And on an incorrect answer, the attempt is counted and the response indicates failure without revealing correctness hints And after 3 failed attempts within 15 minutes per appointment per sender, further attempts return HTTP 429 for 15 minutes and the owner receives a single SMS heads-up notification about failed attempts And no portion of the submitted answer is stored or logged in plaintext; logs contain only attempt metadata
Manager PIN Verification and Attempt Throttling
Given a manager PIN is configured for the location and stored as a salted hash When a user submits a PIN via SMS in response to the challenge for a specific appointment Then the gateway validates the PIN using constant-time comparison of the hashed value And on success, the request is marked Verified and proceeds to token issuance And on failure, the attempt is counted and no correctness hints are returned And attempts are rate-limited to 3 failures per 15 minutes per appointment per sender with exponential backoff (30s, 60s, 120s) on subsequent failures; exceeding limits returns HTTP 429 And on the first lockout per 24 hours for an appointment, the owner is notified via SMS that fallback verification was attempted And PIN digits are never echoed in SMS or logs
One-Time Unseal Token Generation, Binding, and Expiry
Given a challenge request has been Verified via owner answer or manager PIN When the gateway generates an unseal token Then the token is a random 6–8 character alphanumeric (A–Z, a–z, 0–9) value with at least 64 bits of entropy and no ambiguous characters outside that set And the token is single-use, bound to the appointment_id and, if provided, the initiating device_id; it cannot be redeemed for any other appointment and is rejected with HTTP 403 if redeemed from a different device when a device_id was bound And the token expires 5 minutes after issuance; redemption after expiry returns HTTP 410 and the token is invalidated And upon successful redemption, the token is marked consumed and any further redemption attempts return HTTP 409 And the token is delivered via SMS to the requester phone number and is not persisted in logs or analytics; only masked form (last 2 chars visible) may appear in debug logs
SMS Delivery Receipts and Retry Policy Integration
Given an unseal token SMS has been queued to the messaging provider When provider webhooks for delivery status are received Then the gateway records message_id, status transitions (queued, sent, delivered, failed), and timestamps And on failure or no delivery receipt after 60 seconds, the gateway retries up to 3 times with backoff (15s, 30s, 60s) without regenerating a new token; the same token is reused within its validity window And if all retries fail within the 5-minute token lifetime, the requester is informed via SMS that delivery failed and to retry the challenge; no token value is echoed in the failure message And only first name and last initial are included in any outbound SMS content; no other PII is present
Anti-Abuse Controls: Nonce, Replay Detection, and Network Validation
Given each challenge request includes a cryptographically random nonce When the gateway receives a request Then the nonce must be unique per appointment for a rolling 10-minute window; duplicate nonces result in HTTP 409 and are recorded for abuse monitoring And the gateway maintains a replay cache keyed by signature+nonce and rejects replays with HTTP 409 And challenge request rates are throttled to at most 5 challenges per 10 minutes per sender phone number and 20 per 10 minutes per source IP; excess traffic receives HTTP 429 And provider webhook requests are accepted only from configured IP ranges/ASNs and must pass the provider’s signature verification; failing requests are rejected with HTTP 403 and do not alter message state
Privacy and Logging Redaction Compliance
Given the gateway processes challenge, verification, and token events When generating logs, metrics, or notifications Then no secrets (HMAC keys, raw answers, PINs, unmasked tokens) are written to logs or analytics; such fields are replaced with [REDACTED] or masked (e.g., token ****AB) And outbound SMS content includes at most the pet name, appointment time window, and the owner’s first name and last initial; no email, address, or full name is present And audit logs capture who/what/when (requester phone hash, appointment_id, outcome, attempt counts) without storing PII beyond first name + last initial And automated tests verify the absence of disallowed PII patterns and secrets in sampled logs and message bodies
Owner Security Question Setup
"As a pet owner, I want to set a private security question for offline access so that my home can be verified without sharing keys or codes widely."
Description

Enable owners to set an optional security question and answer within the Pet Profile Smart Card or Client Settings to be used for offline unseal. Provide curated question templates and a custom option, enforce minimum answer length, and store answers as salted, slow-hash digests. Staff can view the question but never the answer. Owners can update or disable the question at any time, with change history logged and prior answers invalidated immediately. During offline challenge, the walker is shown the owner’s question; the hashed answer is sent via SMS for backend verification, never stored on-device beyond the session.

Acceptance Criteria
Enable and set security question from Pet Profile Smart Card
Given an authenticated owner is on Pet Profile Smart Card > Security Question When the owner selects a curated template or enters a custom question and provides an answer of at least 6 characters (after trimming) And taps Save Then the security question is saved and visible to staff And the plaintext answer is never stored nor logged And the answer is hashed using a slow-hash (Argon2id or bcrypt) with a unique per-answer salt And the security question state is Enabled for the client And an audit entry is created with timestamp, actor, and question type (template ID or custom)
Minimum answer policy and validation
Given the owner inputs an answer shorter than 6 non-space characters When they attempt to save Then the save is blocked with an inline error indicating the minimum length requirement And answers consisting only of whitespace are rejected And leading/trailing spaces are ignored for validation and matching And verification is case-insensitive
Curated templates and custom question support
Given the owner opens Security Question setup When the owner expands the template list Then at least 8 curated question templates are displayed And a Custom Question option is available with a max length of 120 characters And the previously saved selection (template or custom) is pre-selected on reopen And saving a blank custom question is blocked with an inline error
Staff visibility and API redaction
Given a staff user with permission views a client profile When viewing the Security Question section Then the question text is visible And the answer is never visible in UI nor retrievable via API And API responses return the answer field as null or redacted And exports and logs never contain the plaintext answer or its hash And unauthorized attempts to access the answer are denied per RBAC (e.g., 403)
Owner update/disable with immediate invalidation and history
Given a client has an enabled security question When the owner updates the question and/or answer or disables the feature Then all prior answer digests are immediately invalidated and cannot validate future challenges And change history records action (create/update/disable), actor, timestamp, and prior question type without storing any answer content And any in-progress offline challenge started before the change fails with an Invalid/Expired response
Offline challenge UI and secure SMS verification
Given a walker initiates an offline unseal for a client with a security question enabled When the challenge screen opens Then the owner's security question is displayed and no answer is prefilled And upon entry and submission, the answer is not persisted to device storage and is cleared from memory after submission or after 5 minutes of inactivity And the app sends an SMS containing a challenge ID, client ID, timestamp, and a salted slow-hash of the entered answer signed with an app key; no plaintext answer is sent And the backend validates the hash and replies; success completes the unseal, failure shows a clear error message
Audit Trail & Reporting
"As a manager, I want a complete audit trail of offline unseal activity so that I can investigate issues and demonstrate due diligence."
Description

Record every offline unseal attempt and outcome with timestamp, user, device identifier, last-known location, method used (owner question or manager PIN), attempt count, rate-limit state, and notification events. Surface this data in the business dashboard with filters by date, staff, client, and outcome, and allow export to CSV. Provide a per-appointment activity timeline showing the offline unseal step and the token redemption. Apply data retention policies consistent with org settings and privacy requirements, and exclude secrets or token values from all logs and reports.

Acceptance Criteria
Audit Entry Contains Required Fields
Given a staff member performs an offline unseal attempt for an appointment When the attempt is processed (success, failure, or rate-limited) Then the system records exactly one audit entry containing at minimum: timestamp (ISO 8601 with timezone), staff_user_id, client_id, appointment_id, device_identifier, last_known_location (latitude, longitude, accuracy if available), method_used (owner_question|manager_pin), attempt_count_for_appointment, rate_limit_state at attempt time, outcome (success|failure|rate_limited), and references to related notification events (if any) Given GPS or data is unavailable When recording last_known_location Then the field may be null without preventing the audit entry from being saved
Notification Events Are Recorded and Linked
Given the system sends an owner heads-up SMS or a manager PIN-related notification for an offline unseal When the notification is queued, sent, delivered, or fails Then a notification event is logged with: type, recipient_masked, status (queued|sent|delivered|failed), timestamp, and a reference to the associated unseal attempt And the corresponding unseal attempt audit entry contains references to its notification events
Dashboard Filtering by Date, Staff, Client, and Outcome
Given a manager opens the Audit & Reporting view When filters for date range, staff member(s), client(s), and outcome(s) are applied Then only unseal attempts matching all active filters are displayed And clearing all filters returns all results within the user’s permitted scope And an empty state is shown when no results match the filters
CSV Export of Filtered Audit Data
Given the user has applied filters in the Audit & Reporting view When the user requests Export to CSV Then the generated UTF-8 CSV contains only the filtered rows and includes the columns: timestamp, staff_user_id, client_id, appointment_id, device_identifier, last_known_location, method_used, attempt_count_for_appointment, rate_limit_state, outcome, notification_events_count And the CSV includes a header row and uses commas as separators with values properly quoted And no secrets or token values are present in any column
Per-Appointment Activity Timeline Shows Unseal and Redemption
Given an appointment has one or more offline unseal attempts and a subsequent token redemption When a user views the appointment’s activity timeline Then each offline unseal attempt appears as a distinct event with timestamp, method_used, outcome, and attempt_count_for_appointment And the token redemption appears as a separate event in chronological order And each event links to its detailed audit record
Data Retention and Privacy Enforcement
Given an organization-level data retention period is configured When an audit entry exceeds the configured retention window Then it is purged or redacted according to the policy and is excluded from dashboards, timelines, and CSV exports And retention processing does not remove or reveal any secrets or token values at any time
No Secrets or Token Values in Logs and Reports
Given audit logs, dashboards, timelines, and CSV exports When reviewing any stored or rendered fields Then owner question text/answers, manager PIN values, unseal tokens/codes, and any secret values are never stored or displayed And only non-sensitive metadata required for auditing and reporting is present

Code Refresh

Automatically prompt owners to rotate or confirm access codes after a set number of visits, staff changes, or reported issues. A secure link collects updates, replaces the old code on the Pet Profile Smart Card, and redacts the retired code from future messages.

Requirements

Configurable Refresh Triggers
"As a business owner, I want to automatically trigger code refreshes based on visits, staff changes, or issues so that we keep client access secure without adding manual work."
Description

Provide a rules engine to initiate Code Refresh prompts based on configurable conditions: visit count thresholds per pet/household, staff roster changes (e.g., new walker assigned or staff offboarded), or staff-reported access issues. Triggers must support per-business defaults and per-client overrides, with options for minimum intervals between prompts, blackout windows (e.g., not during active appointment windows), and automatic deferral when a refresh is already pending. The system should evaluate triggers at booking creation, check-in, and completion events, and queue prompts accordingly. All evaluations and outcomes must be logged for auditability and troubleshooting.

Acceptance Criteria
Visit Count Threshold Trigger at Completion (Household Scope)
Given business default visit_threshold.scope = household and threshold = 10 completed visits And the household has 9 completed visits and no client override When the 10th visit for any pet in the household is marked Completed Then the rules engine evaluates within 60 seconds of completion And a single Code Refresh prompt is queued for the household owner And no prompt is queued before the 10th completion And the queued prompt includes trigger_type = visit_threshold, scope = household, visit_count = 10, threshold = 10
Per-Client Override Takes Precedence Over Business Default
Given business defaults: visit_threshold.scope = household, threshold = 10; min_interval = 30 days And client override for this household: threshold = 5; min_interval = 14 days And the household has 4 completed visits in the past 14 days When the 5th visit is marked Completed Then a Code Refresh prompt is queued within 60 seconds And the decision log records rule_source = client_override and values threshold = 5; min_interval = 14 days And business default values are not used in this evaluation
Roster Change Trigger on Next Evaluation Event (New Assignment or Offboarding)
Given last completed visit used staff_id = A and no pending refresh for the client And staff assignment changes to staff_id = B before the next appointment When the next booking_created event for this client occurs Then a Code Refresh prompt is queued with trigger_type = roster_change and reason = new_assignment And min_interval and blackout rules are applied to scheduling And if staff_id = A is offboarded before the next event When the next check_in or completion event occurs (whichever occurs first) Then a Code Refresh prompt is queued with trigger_type = roster_change and reason = offboarding And only one prompt is queued for the roster change until completed or expired
Staff-Reported Access Issue Triggers Prompt at Check-In
Given a staff member marks the active booking with issue_type = access_code_problem at check-in When the check_in event is processed Then a Code Refresh prompt is queued within 60 seconds with trigger_type = staff_report And the prompt is addressed to the client's primary contact And evaluation respects min_interval and blackout rules; if blocked, the prompt is scheduled for the earliest allowed time
Minimum Interval and Blackout Window Enforcement with Deferred Scheduling
Given min_interval = 30 days and a blackout window defined as any time during active appointments 08:00–18:00 local And the last Code Refresh prompt for the client was sent at T0 (20 days ago) And a trigger condition is met at time T1 that falls within a blackout window When the rules engine evaluates the event Then no immediate message is sent And a prompt is scheduled for max(T0 + 30 days, blackout_end_at(T1)) And the evaluation log records decision = deferred and next_attempt_at = that scheduled time
Pending Refresh Prevents Duplicate Prompts and Consolidates Triggers
Given the client has an outstanding Code Refresh prompt in pending status awaiting owner action And additional triggers are detected (e.g., visit_threshold and roster_change) When any evaluation occurs at booking_created, check_in, or completion Then no additional prompts are queued And the pending prompt metadata is updated to include reasons = [visit_threshold, roster_change] without altering scheduled_at or expiration And the evaluation log records decision = skipped_due_to_pending with referenced pending_prompt_id
Audit Log Captures All Evaluations and Outcomes
Given an evaluation occurs for event_type in {booking_created, check_in, completion} When the rules engine processes the event Then an immutable log record is written containing: correlation_id, timestamp, business_id, client_id, pet_ids, event_type, evaluated_triggers, rule_values_used (defaults or overrides), decision (queued|deferred|skipped|blocked), reason(s), scheduled_at (if any), pending_prompt_id (if any), actor_id (for staff reports) And logs are queryable by date range, client_id, trigger_type, decision, and correlation_id and return results within 2 seconds for up to 10,000 records And all queued prompts reference the correlation_id of the originating evaluation
Secure Owner Verification & Update Link
"As a pet owner, I want a secure, simple link to confirm or update my access code so that I can keep my home secure without lengthy back-and-forth texts."
Description

Generate and deliver time-bound, single-use secure links via SMS (with email fallback) that allow owners to confirm the current access code or submit a new one. The flow must verify identity (e.g., SMS OTP or magic-link with device fingerprinting), support optional fields like lockbox location, entry notes, and temporary codes, and clearly explain why the request was sent. Links expire after a configurable period and are invalidated upon completion. All data in transit and at rest must be encrypted, with secrets masked in logs. Collect explicit consent to store and use the access information for service delivery.

Acceptance Criteria
SMS Link Delivery with Email Fallback
Given a code refresh is triggered for an owner with a verified mobile number and email on file When the system attempts delivery Then send an SMS containing a unique, single-use, time-bound link And the SMS message includes the business name, pet name(s), and explicit reason (e.g., staff change, N visits, or reported issue) And the link TTL equals the organization's configured expiry (default 24 hours) And if SMS delivery status is Failed or no delivery receipt within 5 minutes, send the same link via email And ensure only one active link per owner-pet context exists at any time
Link Expiry and Single Use Enforcement
Given a secure link has been issued When the link is opened before expiry for the first time and verification is completed Then mark the token as consumed and invalidate further use And subsequent attempts to open the link return an "invalid or expired link" page without exposing any access details And when the link is opened after expiry, show an "expired link" page with a "request new link" CTA And invalidate any prior active link when a new link is generated
Owner Identity Verification Options
Given an owner opens the secure link When identity verification is required Then present the choice to continue via SMS OTP to the registered mobile or via magic-link with device fingerprinting And for SMS OTP: send a 6-digit OTP to the registered number, rate-limit to 5 attempts, expire OTP after 5 minutes, require exact match And for magic-link: bind the session to the first device fingerprint that opens the link and deny access from different devices And log verification method, timestamp, and outcome for audit
Explicit Consent Capture for Access Info Storage and Use
Given an owner is reviewing or editing access information When the form is presented Then display a required consent checkbox with clear language specifying storage and use of access information for service delivery, plus a link to the privacy policy And block submission until consent is checked And on submission, store consent flag, text version, timestamp, and user identifier with the update event And include a clear explanation at the top of the form stating why the request was sent
Capture Optional Entry Details
Given the owner is on the update form When entering details Then provide optional fields: lockbox location (max 120 chars), entry notes (max 500 chars), and temporary code with start/end datetime And validate that temporary code entries include both start and end times and that end is after start And allow submission with "no change" by explicitly confirming the current code is still valid And prevent prohibited characters and trim leading/trailing whitespace in text fields
Data Security and Secret Masking
Given the system processes secure link and form data When data is transmitted or stored Then enforce TLS 1.2+ for all transport and encrypt sensitive data at rest using AES-256 or equivalent via managed KMS And mask access codes and OTP values in application logs, metrics, and webhook payloads (e.g., show last 2 characters only) And ensure no retired or current access codes appear in any outbound messages or notifications And restrict access to decrypted secrets to authorized services and log access attempts
Completion Confirmation and Audit Trail
Given an owner completes verification with confirm/no-change or submits updated access details When submission succeeds Then send a confirmation SMS (and email copy if email channel was used) stating that access info was received without including the actual code And create an immutable audit record including owner identifier, verification method, device fingerprint hash, fields changed, consent metadata, and link/token ID And invalidate the token immediately and prevent re-submission And expose completion status via API and dashboard within 60 seconds of submission
Smart Card Code Versioning & Audit
"As an admin, I want versioned storage of access codes on the Smart Card so that we can track changes and prove compliance without exposing retired codes."
Description

Update the Pet Profile Smart Card with the newly confirmed/rotated code while maintaining version history, timestamps, and actor attribution (owner vs. staff). Store old codes as retired versions and prevent their display in standard views. Provide an audit trail accessible to admins for compliance, including who initiated the refresh, delivery status of the prompt, and completion details. Ensure backward compatibility with existing Smart Card fields and APIs.

Acceptance Criteria
Owner Confirms Code via Secure Link Updates Smart Card
Given an owner opens the secure link and confirms or enters a new access code When the submission is successfully completed Then the Pet Profile Smart Card displays the submitted code as the Active code with version number incremented by 1 relative to the previous version And the previous code is stored as a Retired version with retired_at timestamp set And actor attribution for the new version is recorded as owner with owner_id captured And the update is reflected in dashboard views and message templates within 30 seconds of submission
Staff-Initiated Refresh After Threshold Visits or Staff Change
Given a pet reaches the configured visit threshold or a staff change/reportable issue is recorded When a staff member initiates a code refresh from the dashboard Then an audit entry is created with initiator=staff, reason captured (threshold/staff_change/issue), and timestamp And an owner prompt is sent via the configured channel(s) with delivery status events tracked (queued, sent, delivered, failed) and timestamps And if the owner completes the refresh, the new version is attributed to owner; if staff performs an override due to non-response, the new version is attributed to staff with override_reason captured And the prior code becomes Retired and is not displayed in standard views
Redaction of Retired Codes from Standard Views and Messages
Given one or more retired code versions exist for a pet When viewing the Smart Card in the dashboard or rendering outbound SMS/email templates Then no retired code values are displayed or included; only the Active code is shown where applicable And attempts to access retired code values via standard UI components return a masked value (e.g., REDACTED) and are non-copyable And retired code raw values remain stored but are only retrievable via admin audit endpoints, not through standard views
Admin Views Compliance Audit Trail for Code Refresh
Given an admin opens the audit trail for a pet's Smart Card When filtering by date range or event type code_refresh Then the audit list includes for each event: initiator (owner/staff with id), delivery channel(s), delivery status timeline with timestamps, completion details (confirmed/rotated by who, completed_at), previous_version_id, new_version_id And each event has an immutable event_id and request_id for traceability And exporting the audit as CSV returns the same fields with consistent row counts as the on-screen results And non-admin users receive HTTP 403 when attempting to access audit endpoints
Version History Integrity and Single Active Code Enforcement
Given multiple code updates have occurred over time When retrieving the code version history via admin UI or internal API Then versions are ordered by created_at ascending with monotonically increasing version numbers starting at 1 And exactly one version is Active at any time; all others are Retired And any attempt to persist more than one Active version is rejected with a validation error and no changes saved And each version record includes created_at, retired_at (null for Active), updated_by_actor_type, and updated_by_actor_id
Backward Compatibility for Existing Smart Card Fields and APIs (v1)
Given an existing integration calls the v1 Smart Card API to fetch the access code When the code has been rotated and versions exist Then the v1 response schema remains unchanged and returns the current Active code in the original field name with HTTP 200 And v1 requests require no new parameters; version metadata is excluded unless explicitly requested via a new optional header or feature flag And existing webhooks continue to deliver payloads with unchanged top-level schema; any versioning metadata is added only under a new namespaced object And automated regression tests for v1 clients pass without code changes
Secure Storage and Access Control for Code Versions
Given code versions are stored in the database When data is at rest and in transit Then code values are encrypted at rest and never logged in plaintext And access to Active code values is limited to authorized staff roles; raw retired code values are retrievable only by admins via audit endpoints with MFA enforced And every read or write of code values generates an access log entry with user_id, timestamp, action, and version_id
Messaging Redaction & Template Replacement
"As a staff user, I want retired codes automatically removed from messages so that we don’t accidentally share outdated or sensitive information."
Description

Automatically redact retired access codes from all future outgoing messages, notes, and reminders while replacing placeholders with the current valid code where permitted. Implement a templating guard that prevents manual insertion of retired codes and flags messages containing potential secrets. Redaction must apply across SMS, push, and email channels and in staff-facing quick replies, with exceptions only for authorized roles. Provide configurable redaction patterns and a preview mode to verify outputs before sending.

Acceptance Criteria
Channel-Wide Redaction of Retired Codes
Given a pet profile has one or more retired access codes and a current valid access code And redaction patterns are configured And there exist pending and newly created outbound items (SMS, push, email, notes, reminders) that include the retired code literal When each item is generated, queued, or sent after the code is retired Then no outbound content contains the retired code value in body, subject, or metadata And all retired code matches are replaced per the configured redaction pattern And delivery succeeds with original scheduling and recipient lists unchanged
Placeholder Replacement With Current Code (Permitted Cases)
Given an outbound message template contains the {{access_code}} placeholder And the recipient and channel are permitted to receive the current access code When the message is generated Then the placeholder is replaced with the current valid code from the Pet Profile Smart Card And no retired code value appears anywhere in the output And if no current code exists, the placeholder is removed with no code shown
Staff Quick Replies Comply With Redaction
Given a staff member inserts a quick reply that contains a retired code literal or {{access_code}} When the quick reply is applied to SMS, push, email, notes, or reminders Then any retired code literal is redacted per configured pattern And {{access_code}} is replaced only when the recipient/channel is permitted And the compose preview shows the exact final content that will be sent
Templating Guard Blocks Manual Retired Code Insertion
Given a non-authorized user types a value that matches a retired access code into the compose field When they attempt to send or schedule the message Then the send action is blocked and an inline error indicates retired codes cannot be sent And the error clears immediately once the retired value is removed or masked
Secret Detection Flags Potential Secrets
Given secret-detection patterns are configured And a message body matches a secret-detection pattern not explicitly whitelisted When a non-authorized user attempts to send the message Then the system highlights the matched fragment and shows a "Potential secret detected" warning And send is blocked until the fragment is removed, masked, or whitelisted
Role-Based Exception Handling for Redaction and Guards
Given a user with an authorized role composes a message containing a retired code or a secret-detection match When they attempt to send Then the system allows an explicit, single-use override via confirmation dialog And upon override, the message is sent exactly as composed without redaction And for non-authorized roles, the override option is not available
Configurable Patterns and Preview Verification
Given an admin updates the redaction patterns and mask token, and secret-detection rules And preview mode is enabled for a draft message containing retired codes and {{access_code}} When the user opens preview for SMS, push, and email Then the preview shows channel-specific final outputs with redactions and replacements applied according to the current configuration And the sent messages match the preview exactly
Access Controls & Staff Offboarding Safeguards
"As an operations manager, I want strict access controls and automatic revocation on staff changes so that client access information remains secure."
Description

Enforce role-based access so only authorized roles can view current access codes, while most roles see masked values. On staff offboarding, immediately revoke access to current codes and require code refresh prompts for affected clients if configured. Provide configurable notifications to owners when staff changes trigger a refresh. Support activity logging, session invalidation, and exportable access reports for audits.

Acceptance Criteria
RBAC: Viewing Current Access Codes
Given a user with role in {Owner Admin, Manager} and a pet profile with a current access code When they view the Access Codes in the dashboard or request via API Then the current code displays unmasked in UI and is present in the API response as current_code Given a user with role in {Staff, Contractor, Support} When they access the same UI or API Then the code is masked as ••••XX (last 2 chars only) in UI and API returns masked_code only with current_code omitted Given a data export initiated by a non-authorized role When the export includes access-related fields Then all code values are masked as ••••XX and raw codes are omitted Given a notification preview or message history viewed by non-authorized roles When current codes would otherwise be rendered Then the values are masked as ••••XX Given a user’s role changes When permission updates are saved Then subsequent views reflect the new visibility immediately without requiring logout
Offboarding Triggers Immediate Access Revocation and Refresh
Given a staff member is marked Offboarded When the offboarding action is saved Then all of their permissions to view current access codes are revoked within 60 seconds and further attempts return 401/Forbidden Given the setting "Require code refresh on staff change" is enabled When an offboarded staff member is linked to N active clients Then a code refresh prompt is queued for those clients within 5 minutes with a secure link per client Given the setting is disabled When offboarding occurs Then no refresh prompts are sent Given the offboarding is processed When the event completes Then an audit log entry is created with actor, target_staff_id, affected_client_count, and timestamps
Owner Notifications: Configuration and Delivery for Staff-Change Refresh
Given channels {SMS, Email} and templates are configured with required placeholders When a staff-change-triggered refresh fires Then owners receive messages on enabled channels with business name, pet name, and secure link populated Given an owner has opted out of SMS When the refresh is sent Then no SMS is sent and Email is used if enabled Given the primary channel delivery fails When a fallback channel is configured Then a fallback send occurs within 10 minutes and is logged Given a rate limit of 1 prompt per owner per 24 hours When multiple staff-change events occur within the window Then only one prompt is delivered Given an admin sends a test notification from preview When the send is executed Then no real owner is contacted and the event is logged as test
Comprehensive Activity Logging for Access Code Events
Given any access code view, export, update, refresh prompt send, or secure-link open When the action occurs Then a log record is written with event_type, actor_id, actor_role, pet_id, owner_id, timestamp_utc, ip, user_agent, outcome, and reason without storing raw codes Given a non-authorized access attempt to current codes When the request is blocked Then a denied event is logged with 403 and no sensitive value Given a retention policy of 24 months When querying logs older than retention Then they are not returned and a retention notice is displayed Given an admin filters logs by date range, actor, event_type, and pet_id When exporting logs Then CSV/JSON contains exactly the filtered set and excludes raw codes for non-authorized downloaders
Session Invalidation on Staff Offboarding
Given a staff member has active sessions on web and mobile When they are offboarded Then all active sessions are invalidated within 30 seconds and subsequent API calls return 401 Unauthorized Given push logout is supported When offboarding occurs Then the user is forced to the login screen on all devices within 30 seconds Given personal access tokens or API keys exist for the user When offboarding occurs Then tokens are revoked immediately and any use attempts are logged as denied Given any outstanding password reset or magic links exist When offboarding occurs Then those links become unusable
Access Reports Export for Audits
Given an admin selects a date range and filters by role and event type When generating an Access Report Then a report is produced within 60 seconds for up to 100k events with columns: timestamp_utc, event_type, actor_id, actor_role, pet_id, owner_id, channel, outcome, redaction_applied Given a manager without code-view permission exports the report When the file is generated Then access code fields are masked or omitted and redaction_applied = true Given a report is generated When downloaded Then the file includes an integrity checksum and metadata for filters and requester_id Given a multi-location business When filtering by location Then only events for selected locations are included
Secure Link Update and Redaction of Retired Codes
Given an owner receives a secure link to confirm or rotate an access code When they open the link Then token validation is required and the link expires after first successful submission or after 72 hours, whichever comes first Given the owner confirms the existing code When the form is submitted Then the confirmation timestamp is stored on the Pet Profile Smart Card with no change to the value Given the owner enters a new code that meets policy When submitted Then the new code replaces the old on the Smart Card and the old code is marked retired Given a code is retired When future outbound messages, visit notes, or staff instructions are generated Then the retired code is not included and any references display as redacted or ••••XX Given multiple secure links are opened concurrently When a later link attempts submission after a successful earlier update Then the submission is rejected as expired with no changes made
Booking Pipeline Integration & Prompt Cadence
"As a groomer/walker, I want timely prompts tied to bookings so that I’m never stuck without a valid code when I arrive."
Description

Integrate refresh prompts with the booking lifecycle: pre-visit confirmation prompts at N visits since last confirmation, immediate prompts when a booking is assigned to a new staff member, and follow-up reminders if owners don’t respond. Support throttling (max prompts per time window), quiet hours, and smart bundling for households with multiple pets. Block or warn at check-in if the code is stale according to business policy, with override options and reason capture.

Acceptance Criteria
Pre-Visit Cadence Prompt at N Visits
Given an organization cadence setting N=5 visits since last confirmation and a pre-visit lead time of 24 hours And a household with 4 completed visits since their last code confirmation and a new booking scheduled When the booking enters the 24-hour lead time window Then the system sends exactly one code confirmation prompt for that booking to the household’s primary SMS number And if the household has fewer than 5 visits since last confirmation, no prompt is sent And upon owner confirmation (confirm unchanged or update), the household’s visit counter resets to 0 and the last-confirmed timestamp is updated
Immediate Prompt on New Staff Assignment
Given a booking is reassigned to a staff member who has not serviced the household within the last 6 months When the reassignment is saved Then the system triggers a code confirmation prompt immediately And if current time falls within quiet hours, the prompt is scheduled for the next allowable send window And the prompt is tagged with reason=new_staff for auditing And the system does not send this prompt if one for the same household is already pending or sent in the last 24 hours (throttle)
Follow-Up Reminders with Throttling and Quiet Hours
Given an initial prompt was sent and no owner response has been received When 24 hours elapse since the last prompt and the time is outside quiet hours Then the system sends a follow-up reminder And no more than 2 follow-up reminders are sent per initial prompt (total prompts per flow ≤ 3) And the system enforces a throttle of max 3 prompts per household in any rolling 24-hour window And all send times respect the household’s timezone and configured quiet hours (e.g., 21:00–08:00 local); if within quiet hours, schedule to 08:00 next day
Household-Level Smart Bundling for Multiple Pets
Given multiple bookings for pets in the same household have promptable triggers within a 6-hour bundling window When generating prompts Then the system sends a single bundled prompt covering all impacted pets/bookings for that household And the secure link presents the household address and lists all affected bookings And a single owner confirmation applies to all bundled bookings and resets the visit counter at the household level And bundling respects throttling and does not create duplicate prompts for the same household within the window
Secure Code Update Flow and Redaction
Given an owner opens the secure link from a prompt When the owner submits a new code of 4–12 characters (digits and letters only) or confirms the existing code is unchanged Then the system validates input, updates the Pet Profile Smart Card with the active code, and timestamps the confirmation And the previous code is retired and redacted from all future outbound messages and owner-facing views And an audit log entry is stored with masked old/new codes (e.g., last 2 chars only) and the prompt reason And the link is single-use and expires after 7 days or upon successful submission, whichever comes first And the owner receives a confirmation message of successful update/confirmation
Check-In Stale Code Warning/Block with Override
Given an organization policy of block_if_stale with thresholds N=5 visits or D=90 days since last confirmation And a staff member attempts to check in to a booking where the household code is stale per the policy When the check-in is initiated Then the app displays a block screen with stale reason and last confirmation date And provides an override option only to roles with permission=allow_override, requiring a typed reason (min 10 chars) And all overrides are timestamped and logged with user, booking ID, and reason And upon block (no override), the app offers to send an immediate prompt (respecting quiet hours)
Incident-Triggered Prompt on Reported Access Issue
Given a staff member files an incident on a booking with category=access_code_issue or an owner reports a security concern via SMS keyword When the incident is submitted and classified as code-related Then the system sends a code confirmation prompt immediately (or schedules for next send window if within quiet hours) And tags the prompt with reason=reported_issue and links it to the incident ID And suppresses additional incident-triggered prompts for the same household for 48 hours after a successful confirmation And all actions are captured in the incident timeline
Analytics & Policy Enforcement
"As a business owner, I want visibility into code refresh compliance and the ability to enforce policies so that we reduce failed entries and improve client trust."
Description

Provide dashboards and exports showing refresh prompt send/complete rates, time-to-confirm, visits blocked due to stale codes, and security events tied to staff changes. Allow businesses to set policies that require a valid, recently confirmed code before certain appointment types, with configurable grace periods and exceptions. Surface alerts in the main dashboard and via weekly summaries to improve compliance and reduce no-shows or failed entries.

Acceptance Criteria
KPI Dashboard for Code Refresh Compliance
Given a business user with access to Analytics and a selected date range When they open Analytics > Code Refresh Then the dashboard displays for the selected range: Prompts Sent, Prompts Completed, Completion Rate (%), Median Time-to-Confirm (hh:mm), Visits Blocked Due to Stale Codes, and Security Events from Staff Changes And the default date range is Last 30 Days in the business’s time zone And available filters include Location, Staff, Appointment Type, Prompt Status (Sent, Completed, Expired), and Channel (SMS, Email) And all metric values match backend aggregates for the same filters with a tolerance of ≤0.5% And an empty state is shown when no data matches the filters
Filtered Export Matches Dashboard
Given a filtered dashboard view When the user clicks Export and selects CSV or XLSX Then the file is generated within 30 seconds and delivered via download or emailed link if more than 50,000 rows And each row represents a single prompt, visit block, or security event with unique IDs and timestamps in the business’s time zone And sensitive access codes are never exposed; current and retired codes are masked as last 4 with asterisks And exported counts and medians equal the dashboard metrics under the same filters (±0.5% tolerance) And the export schema includes columns: Record Type, Business ID, Location ID, Staff ID, Client ID, Pet ID, Appointment Type, Prompt Status, Sent At, Completed At, Time to Confirm (seconds), Block Reason, Security Event Type And the export reflects data freshness no older than 15 minutes
Policy Enforcement on Scheduling and Check-in
Given an appointment type is configured to require a recently confirmed access code with a grace period of N days And a pet’s last confirmation is older than N days or missing When a user attempts to schedule, check-in, or start a visit for that appointment type Then the action is blocked with a message stating the reason and required next steps And the user can send a Refresh Prompt with one click without leaving the flow And users with Override permission can create a one-time exception with a required reason entry And each blocked attempt is logged as a “Visit Blocked Due to Stale Code” event tied to the visit, pet, and user
Configurable Grace Periods and Exceptions
Given a business admin configures policy rules per appointment type with a grace period in days and optional client or staff exceptions When the policy is saved Then the new rules take effect across scheduling, reminders, and check-in within 5 minutes And all policy changes are audited with actor, timestamp, and before/after values And exception lists bypass enforcement but are logged as “Policy Exception Applied” events And disabling a policy immediately removes enforcement but preserves audit history
Staff Change Triggers and Deduplication
Given the “Prompt on Staff Change” setting is enabled When a staff member is added, removed, or their access role changes Then owners with upcoming visits in the next 30 days and an on-file access code receive a refresh prompt within 15 minutes And no owner receives more than one staff-change prompt in a 24-hour period per location (deduplicated) And a Security Event is recorded linking the staff change, affected pets, and sent prompt IDs And if the staff change is reverted within 15 minutes, queued prompts are canceled and no event is sent And all events are visible in the dashboard and included in exports
Alerts and Weekly Summary for Non-Compliance
Given compliance monitoring is enabled with a threshold T for completion rate When the last 7 days’ completion rate drops below T or at least one visit was blocked Then a dashboard alert banner appears with counts and a link to a pre-filtered analytics view And the weekly summary email delivered by Monday 9:00 AM local includes: Prompts Sent, Completion Rate, Median Time-to-Confirm, Visits Blocked, Security Events, and trend vs prior week And alerts auto-resolve once the condition no longer applies And users can mute weekly emails in Notification Settings; muted users do not receive summaries
Time-to-Confirm Definition and Performance
Given prompts may be resent When calculating time-to-confirm Then each sent prompt instance is measured from Sent At to Completed At for that instance; canceled or expired prompts are excluded And Median Time-to-Confirm is computed across completed prompts in the selected range; P90 is also available And times are rounded to the nearest minute for display and kept in seconds in exports And analytics pages render within 2 seconds for ranges up to 90 days; background aggregations refresh at least hourly

Role Lock

Restrict code visibility to the assigned pro during their scheduled window. Handoffs require one‑tap manager approval and instantly revoke prior links, limiting unnecessary exposure across teams and keeping compliance simple.

Requirements

Time-Bound Code Visibility
"As an assigned pro, I want to see a client’s entry code only during my job window so that I can access the home securely without exposing the code outside my shift."
Description

Restricts client entry code visibility to the assigned pro only during the scheduled job window with a configurable buffer (e.g., 15 minutes before start to checkout plus 15 minutes). Applies across the mobile app and SMS deep links; codes are masked outside the window and access tokens auto-expire when the window ends. Prevents local caching by using ephemeral, non-shareable views and short-lived tokens. Integrates with FetchFlow scheduling so changes to assignments or times immediately recalculate visibility and invalidate stale access.

Acceptance Criteria
App Code Visible Only During Scheduled Window
Given a job assigned to Pro A with start time S, end time E, and a visibility buffer of B minutes When Pro A opens the client entry code view in the mobile app within [S - B, E + B] Then the full entry code is rendered unmasked and a short-lived access token is issued And when Pro A opens the view outside [S - B, E + B] Then the code is masked (e.g., ••••) and no token is issued And when any user other than Pro A attempts access at any time Then the code remains masked and access is denied And this behavior is consistent across iOS and Android
SMS Deep Link Time-Bound Access
Given an SMS deep link containing a single-use, short-lived token for Job J assigned to Pro A When Pro A opens the link during the valid visibility window [S - B, E + B] Then the code is displayed and the token is marked as consumed And when the link is opened outside the valid window by any user or forwarded recipient Then the code is not displayed, a "Link expired" message is shown, and HTTP 401/403 is returned for API calls And when the same link is opened again after initial use Then access is denied and no code is revealed
Access Tokens Auto-Expire at Window End
Given any active access token for Job J When the visibility window ends (E + B reached) Then the token becomes invalid within 60 seconds and further API calls using it return HTTP 401 And refresh or extension of the token beyond E + B is disallowed And any open code view auto-masks within 60 seconds of window end without user action
Immediate Recalculation on Assignment or Time Change
Given Job J is assigned to Pro A with visibility window W When the assignment changes to Pro B or the schedule times S/E/B are updated Then Pro A's active tokens and sessions are invalidated within 30 seconds and access is revoked And Pro B's access is granted per the recalculated window W' within 30 seconds And all previously issued deep links for the prior assignment/time are rendered invalid And access logs record the revocation and grant events with timestamp, actor, and job IDs
Ephemeral, Non-Shareable Code View
Given the code view is displayed in the mobile app When the app is backgrounded, the device attempts a screenshot/screen recording, or the user navigates away Then the code is immediately masked and not captured in screenshots/recordings (using platform-level secure view flags) And copy, paste, and text selection are disabled on the code value And if the device is offline or the view is reopened, the code is fetched only via a fresh validity check; no local cache is used And the code auto-hides after 60 seconds of inactivity
Audit and Masking Outside Window
Given any attempt to access a client entry code outside the valid visibility window or by an unauthorized user When the access request is made via app or SMS deep link Then only a masked placeholder is returned and no code digits are transmitted And the attempt is logged with timestamp, user identifier, job identifier, channel (app/SMS), and reason (outside_window/unauthorized) And repeated failed attempts (>5 within 10 minutes) trigger rate limiting for 15 minutes
One-Tap Manager Handoff Approval
"As a manager, I want to approve a handoff with one tap or an SMS reply so that I can quickly reassign jobs and control who can see the client’s entry code."
Description

Enables managers to approve handoffs with a single tap from push or SMS, instantly reassigning job ownership and code visibility to a new pro. Presents the pending handoff details (job, pet, time window) and captures an optional reason code. Provides an SMS fallback by allowing keyword replies (e.g., APPROVE/DECLINE) when the app is unavailable. Triggers immediate policy updates and audit entries upon approval or rejection.

Acceptance Criteria
One-Tap Approval via Push Notification
Given a pending handoff notification is delivered to an authorized manager with job ID, pet name, and time window visible When the manager taps Approve in the notification or in-app banner Then job ownership updates to the designated new pro within 5 seconds And code visibility is enabled for the new pro and disabled for the prior pro within 5 seconds And all prior pro access links are revoked immediately and show a revoked state if accessed And the manager and involved pros receive success confirmation (push or SMS) within 10 seconds
SMS Keyword Approval Fallback
Given an authorized manager receives an SMS about a pending handoff with a unique handoff token When the manager replies with APPROVE <token> (case-insensitive, extra whitespace ignored) Then the handoff is approved and ownership reassigned within 10 seconds And the system replies with a confirmation SMS including job ID and new pro first name And invalid keywords or missing/invalid token result in an error SMS with usage guidance And if the handoff is expired or already resolved, the reply returns a no longer pending message
Decline Decision via Push or SMS
Given a pending handoff is awaiting manager decision When the manager selects Decline in-app/push or replies DECLINE <token> via SMS Then job ownership and code visibility remain unchanged And the requester is notified of the decline within 10 seconds And an audit entry is recorded with decision=decline, reason (if provided), channel, and timestamp And all outstanding links/tokens for the handoff are invalidated immediately
Capture Optional Reason Code
Given an approval or decline action is being confirmed When the manager selects a reason from a predefined list or enters free text up to 200 characters Then the reason is saved with the decision and appears in audit logs and manager reports And the reason field is optional and does not block submission if omitted And the predefined reason list is configurable by org admins and includes at least 5 default options
Immediate Link Revocation and Idempotency
Given a handoff link or token has been issued for a pending handoff When any user attempts to reuse a consumed or revoked link/token Then access is denied with HTTP 410 Gone or equivalent in-app message And all previously issued links/tokens are revoked immediately upon approval/decline And pending links expire at the end of the scheduled window or after 30 minutes, whichever comes first And decision endpoints are idempotent; repeated approvals/declines return the final state without side effects
Role Lock Policy Update Propagation
Given a handoff is approved When the new pro opens the dashboard or code view within the scheduled window Then the job code is visible and usable to the new pro and hidden from others And outside the scheduled window, the code is hidden for all pros And any cached prior-pro views are invalidated; fresh requests return 403 within 5 seconds of approval And policy/permission changes propagate to all clients within 5 seconds
Comprehensive Audit Trail Entry
Given any handoff decision (approve or decline) is completed When the operation is committed Then an immutable audit record is created with: handoff ID, job ID, pet ID(s), old pro ID, new pro ID, manager ID, decision, reason (nullable), channel (push/in-app/SMS), request and decision timestamps, and IP/user agent (if available) And audit records are queryable by admins within 10 seconds of decision And audit records are append-only; corrections are new entries referencing the original
Instant Access Revocation
"As a manager, I want previous access to be revoked the moment I approve a handoff so that only the newly assigned pro can view the entry code."
Description

Immediately invalidates prior access links, tokens, and sessions for the previous assignee when a handoff is approved or the schedule changes. Propagates revocation across mobile, web, and SMS deep links with real-time token blacklisting and remote sign-out. Notifies affected users that access has changed and provides a safe error state when attempting to view revoked codes. Ensures revocation occurs within seconds even under poor connectivity with queued invalidation and retry logic.

Acceptance Criteria
Handoff Approval Triggers Revocation
Given a code is assigned to Pro A with active web, mobile, and SMS deep-link access And a handoff to Pro B is pending When the manager approves the handoff Then all tokens, links, and sessions for Pro A to that code are revoked within 5 seconds (p95) and 10 seconds max under normal connectivity And subsequent API requests from Pro A return 403 Access Changed with an empty payload And Pro B can access the code using newly issued links without delay
Schedule Change Triggers Revocation
Given Pro A is removed from a scheduled window for a code When the schedule change is saved Then Pro A’s tokens, links, and sessions for that code are revoked within 5 seconds (p95) and 10 seconds max And Pro A cannot view, search for, or export that code across any channel And unaffected codes remain accessible to Pro A
Cross-Channel Deep Link Blacklisting
Given Pro A possesses previously sent SMS, mobile universal links, and web URLs for a code When revocation occurs Then opening any prior link yields a safe error screen stating Access to this code has changed without revealing code contents And the HTTP response is 403 for API endpoints tied to those links And the same links cannot regain access after refresh or re-login
Remote Sign-Out of Active Sessions
Given Pro A is actively viewing the code on web or mobile When revocation is triggered Then the active session is terminated within 5 seconds (p95) and 10 seconds max And the UI transitions to a safe error state and clears previously displayed code details within 1 second of revocation receipt And navigation, back, or refresh does not restore access without new authorization
Revocation Under Poor Connectivity with Retry
Given transient network issues prevent immediate propagation to all clients When revocation is initiated Then server-side validation rejects revoked tokens immediately on first request after revocation And the system queues invalidation messages and retries with exponential backoff until confirmed, completing within 10 seconds of connectivity restoration (p95), 30 seconds max And offline clients block local access to the code and show a pending access change message until confirmation arrives
User Notification and Safe Error State
Given a user’s access to a code is revoked When revocation completes Then the user receives an SMS and in-app notification stating their access has changed without exposing sensitive details And attempting to view the code shows a safe error with a single action to Request Access or Return to Dashboard And no code content, notes, photos, or identifiers beyond a generic label are displayed in the error state
Ephemeral Reveal and Masking
"As a pro, I want to briefly reveal the entry code only when I need it so that I can enter quickly without leaving the code exposed."
Description

Displays entry codes in a masked state by default and reveals them only on explicit user action for a short, configurable duration (e.g., 30 seconds). Disables copy where possible, auto-clears clipboard after a brief interval, and avoids rendering codes in notifications or screenshots where device capabilities permit. Logs each reveal as an event for auditing. Provides a minimal SMS-only fallback that redacts most digits and requires manager re-auth to reveal fully.

Acceptance Criteria
Default Masking on Load
Given an assigned pro opens the appointment details during their scheduled window When the screen loads Then the entry code is masked by default with no more than the last 2 digits visible And the full code is not present in the view hierarchy or accessibility tree until explicitly revealed And the reveal control is visible but disabled for users outside the scheduled window or lacking permission
Time-Boxed Reveal with Auto-Remask and Backgrounding
Given an assigned pro within the scheduled window taps Reveal When the reveal starts Then the full code is displayed for the configured duration (default 30 seconds) And a countdown indicator shows remaining seconds And when the timer elapses the code re-masks automatically And if the app is backgrounded, the device locks, or the user navigates away during reveal the code re-masks within 1 second And each reveal start and end is logged with actor ID, appointment ID, timestamp, device identifier, and duration
Copy and Clipboard Protection and Auto-Clear
Given the code is revealed in-app When the user attempts to long-press, select, copy, or share the code Then selection is disabled and system copy/share menus do not appear And programmatic copy of the code by the app is blocked except in explicitly authorized flows And if the app ever places the code on the clipboard in an authorized flow it is automatically cleared within 15 seconds And after clearing the clipboard no longer contains the code value and an audit event is recorded
Notification, Share, and Screenshot Redaction
Given any push, SMS, or in-app notification references an entry code When the notification is delivered Then the code is masked with no more than the last 2 digits visible and no full code is present in the payload or previews And deep links navigate to the in-app reveal flow rather than exposing the full code in the notification And while a code is visible in-app screenshots and screen recordings are blocked or the code region is masked on devices that support screen security APIs And on devices that do not support screen security APIs no blocking occurs and an audit event noting protection unavailability is recorded
Role-Locked Reveal Access Window
Given a user who is not the assigned pro or is outside the scheduled window attempts to reveal the code When they press the reveal control Then access is denied with a clear reason (Not assigned or Outside scheduled window) And the reveal control remains disabled until access conditions are met And an audit event is recorded with user ID, appointment ID, and denial reason And when the assigned pro within the scheduled window presses reveal access is granted
Manager Handoff Approval and Immediate Revocation
Given a manager approves a handoff from Pro A to Pro B When the approval is executed Then Pro B gains reveal access immediately upon view refresh And Pro A's reveal access is revoked instantly and any currently visible code on Pro A's device is re-masked within 1 second And any previously issued reveal links or deep links for Pro A become invalid And an audit event captures approver ID, from-user, to-user, timestamp, and revocation outcome
SMS-Only Fallback with Manager Re-Auth
Given the recipient is in an SMS-only flow without app access When the system sends an SMS referencing the entry code Then the SMS displays a redacted code with at most the last 2 digits visible and includes a secure link to request manager-approved reveal And when the recipient requests full reveal via the link manager re-authentication within the last 5 minutes is required And if manager re-auth succeeds the full code is displayed for a single 30-second window in a secure web view and then re-masked and the link expires And if manager re-auth fails or has expired the full code is not displayed And the SMS/web view prevents copy where possible and clears any clipboard placement within 15 seconds And each reveal attempt and outcome is logged for audit
Access Audit Trail and Compliance Reporting
"As an operations lead, I want a complete audit of who accessed which codes and when so that I can prove compliance and investigate issues."
Description

Captures immutable events for code access, reveals, approvals, revocations, and assignment changes with timestamps, user IDs, job IDs, and device metadata. Provides a manager-facing report with filters (date, user, client, location) and export options (CSV/PDF). Supports retention policies and redaction on request. Surfaces exceptions (e.g., access outside window attempts) to help teams monitor compliance and investigate incidents.

Acceptance Criteria
Immutable Event Capture for Access Lifecycle
Given any of the following actions occur: code revealed, code accessed, approval granted, access revoked, or assignment changed When the action completes or is denied Then an audit event is appended with fields: event_id (UUID), event_type, outcome (success|denied), timestamp_utc (ISO 8601), actor_user_id, job_id, client_id, location_id (if available), device_id, device_os, app_version, ip_address, request_id, and for assignment changes previous_assignee_id and new_assignee_id And the event is queryable by event_id and job_id And updating or deleting audit events via any UI or API is not supported (no endpoints) And any attempt to modify an event returns 403 Forbidden and is logged as an exception
Exception Surfacing for Out-of-Window and Revoked Access Attempts
Given a pro attempts to reveal or use a code outside their scheduled window, with a revoked/expired link, or without an assignment When the attempt occurs Then the action is denied and an exception audit event is appended with fields: exception_type, reason, timestamp_utc, actor_user_id, job_id, scheduled_window_start, scheduled_window_end, attempted_time_utc, prior_revocation_event_id (if applicable) And managers can view the exception in the report Exceptions view within 60 seconds of the attempt And the exception is filterable by exception_type, user, client, and location
Manager Report with Multi-Filter and Permissions
Given a user with Manager role When they open the Access Audit report Then they can filter by date range (UTC), user(s), client(s), and location(s) in any combination and the results reflect all applied filters And results display columns: event_type, timestamp_utc, user, client, location, job_id, outcome, exception_type (if any) And for datasets up to 50,000 events the report loads in <= 3 seconds at P95 and supports pagination and sorting by timestamp asc/desc And a user without permission for a location sees no rows from that location even if filtered
CSV/PDF Export Fidelity and Metadata
Given a filtered report view When Export CSV is invoked Then a UTF-8 CSV is generated containing exactly the rows matching the filters and columns: event_id, event_type, outcome, timestamp_utc, actor_user_id, job_id, client_id, location_id, device_id, device_os, app_version, ip_address, request_id, exception_type, reason, previous_assignee_id, new_assignee_id And the file includes a header block with export_timestamp_utc and filter parameters Given the same filtered view When Export PDF is invoked Then a paginated PDF is generated with the same rows and visible columns, includes a header with export_timestamp_utc and filter parameters, and a summary with total rows exported And filenames follow pattern access_audit_YYYYMMDDThhmmssZ.csv/pdf
Retention Policy Enforcement and Redaction
Given an organization retention policy in months (default 24) is configured When an event’s age exceeds the policy Then the event is excluded from the report and exports within 24 hours and a retention_purge audit event is appended Given a verified redaction request scoped to a user and/or device metadata When the redaction is processed Then ip_address, device_id, device_os, and app_version fields are replaced with [REDACTED] for all matching events while preserving event_id, event_type, timestamp_utc, outcome, job_id And a redaction_performed event is appended with request_id, scope, timestamp_utc, actor_user_id, processor_user_id And subsequent reports and exports reflect the redactions
Handoff Approval and Instant Revocation Auditability
Given a manager approves a handoff to a new pro When approval is submitted Then an approval_granted audit event is appended with prior_assignee_id and new_assignee_id And all prior access links for the job are revoked within 5 seconds and an access_revoked event is appended for each revoked link referencing link_id(s) Given a revoked link When any user attempts to use it Then access is denied and an exception with exception_type=revoked_link_use is logged referencing the revocation event_id
Audit Trail Integrity and Immutability Verification
Given the audit trail APIs When attempting to modify or delete an existing event Then the API responds 403 Forbidden and logs a modification_attempt exception event with actor_user_id, target_event_id, and timestamp_utc Given daily integrity checks When the integrity job computes a running hash over the last 24 hours of events Then the computed hash matches the stored hash and an integrity_check event is appended with result=pass When a mismatch is detected Then an integrity_check event with result=fail and an exception entry are appended and are visible in the report within 60 seconds
Role and Permission Controls
"As an account owner, I want clear role-based controls over code visibility and handoffs so that our team can operate securely across locations."
Description

Defines role-based permissions for Managers, Pros, and Owners, including who can view codes, initiate/approve handoffs, and configure buffers or overrides. Supports team- and location-level policies and per-client exceptions. Provides a centralized settings UI and APIs to integrate with existing org structures in FetchFlow, ensuring consistent enforcement across mobile and SMS experiences.

Acceptance Criteria
Pro Code Visibility Window
Given a Pro is assigned to a client appointment with start and end times and a location policy defining pre-buffer X minutes and post-buffer Y minutes When the Pro requests the access code via mobile app or SMS link at time T Then access is granted only if start−X ≤ T ≤ end+Y And any request by non-assigned Pros during the window is denied with 403 And any request by the assigned Pro outside the window is denied with 403 and message "Access window closed" And all attempts are recorded in the audit log with user, client, channel, timestamp, outcome, and reason And time zone used for evaluation matches the appointment’s location time zone
Manager Handoff Approval and Link Revocation
Given Pro A is currently assigned to an active appointment and has an active access link And Pro B requests a handoff for the same appointment When a Manager approves the handoff in the dashboard Then Pro A’s existing links are revoked within 2 seconds and subsequent use returns 401 "Link revoked" And Pro B gains access immediately subject to the same policy window and buffers And notifications are sent to Pro A and Pro B confirming the change And an audit record captures approver, from-user, to-user, timestamp, and reason When a Manager denies the handoff Then Pro A retains access and Pro B receives a denial notification
Per-Location Policy Enforcement
Given a user belongs to multiple locations with differing role policies (visibility buffers, override allowances) When the user accesses a client’s code associated with Location L via mobile or SMS Then Location L’s policy is applied And if multiple applicable policies exist for the user, the most restrictive rule is enforced And enforcement behavior is identical across mobile app and SMS link flows And policy updates take effect within 60 seconds of saving And enforcement logs include the applied policy version identifier
Per-Client Access Exception
Given a client-specific exception is configured (e.g., extend/reduce Pro visibility by N minutes or disallow handoffs) When a Pro attempts to view a code or initiate a handoff for that client Then the client exception overrides team/location defaults for that client only And exceptions cannot grant Pros Manager/Owner capabilities (e.g., approving handoffs) And the audit log records exception ID, setter, timestamp, and scope on each enforcement And removing the exception reverts to default policy on the next request without requiring re-authentication
Centralized Role Settings UI Permission Controls
Given an Owner, a Manager, and a Pro are authenticated When each navigates to Role & Permission Settings Then Owners can view and edit all role definitions, policies, and client exceptions And Managers can edit team/location policies and approve handoff settings but cannot change Owner-only controls And Pros have read-only visibility of their assigned permissions and cannot edit any settings And all edits require confirmation, are enforced server-side, and produce immutable audit entries with before/after and actor And unauthorized edit attempts return 403 and display an error within 1 second
API Integration for Org Structures
Given an API client with scope org.permissions.write sends a PUT to /v1/orgs/{orgId}/roles with a valid idempotency-key and a payload mapping users to roles and locations When the payload is valid Then the service upserts assignments idempotently, returns 200 with summary counts, and propagates to enforcement caches within 60 seconds And invalid payloads return 400 with field-level errors; unauthorized tokens return 401; forbidden scopes return 403 And during propagation, last-known-good policy remains in effect and no user gains broader access than previously held And resulting role changes are consistently enforced across mobile and SMS within the propagation window

Product Ideas

Innovative concepts that could enhance this product's value proposition.

Gap Gobbler

Auto-build a live SMS waitlist and instantly offer canceled slots to nearby clients with one-tap claim. Priority rules favor package holders and closest distance.

Idea

Pop-Up QuickQueue

Kiosk mode for pop-ups: clients scan a QR, pick a slot, prepay a deposit, and get SMS updates. Staff closes out with one-tap rapid checkout.

Idea

TextCard Vault

Securely vault client cards via SMS link for auto-deduct at visit end. Supports pre-auth holds for no-show protection.

Idea

Vax Snap Sync

Clients text a photo of vaccine records; OCR extracts dates into Smart Cards and schedules expiry nudges. Flags missing shots before confirmation.

Idea

SplitStream Payouts

Route each payment through configurable split rules—percent or fixed—to partners and staff automatically. Exports daily split summaries for reconciliation.

Idea

Insta-SMS Bridge

Auto-reply to Instagram DMs with a branded booking link that moves the conversation to SMS. Captures handle, pet name, and consent automatically.

Idea

DoorCode Drip

Store gate and lockbox codes safely, then release them by SMS only after the walker checks in on-site. Auto-redacts codes in owner-facing messages.

Idea

Press Coverage

Imagined press coverage for this groundbreaking product concept.

Want More Amazing Product Ideas?

Subscribe to receive a fresh, AI-generated product idea in your inbox every day. It's completely free, and you might just discover your next big thing!

Product team collaborating

Transform ideas into products

Full.CX effortlessly brings product visions to life.

This product was entirely generated using our AI and advanced algorithms. When you upgrade, you'll gain access to detailed product requirements, user personas, and feature specifications just like what you see below.