Pet services scheduling and billing

PawPilot

Booked solid. Paid instantly.

PawPilot is an SMS-first scheduling, reminders, and billing platform for independent groomers, dog walkers, and sitters. It texts clients where they reply, auto-filling 83% of cancellations in minutes with a Smart Waitlist and deposit link, cutting no-shows 58%, saving 6+ admin hours weekly, and moving payment from seven days to same day.

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

PawPilot

Product Details

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

Vision & Mission

Vision
Empower every independent pet pro to run fully booked, stress-free businesses with instant payment and fiercely loyal clients.
Long Term Goal
By 2029, power 50,000 independent pet pros to run fully booked: halve no-shows, fill 10 million canceled appointments annually, and deliver same-day payment on 90% of bookings.
Impact
For independent groomers, walkers, and sitters, PawPilot reduces no-shows by 58% with deposits and timed SMS reminders, fills 83% of cancellations within 15 minutes, recovers 6+ admin hours weekly, and cuts days-to-cash from 7 to 1 through instant checkout.

Problem & Solution

Problem Statement
Independent groomers, walkers, and sitters waste hours juggling DMs, voicemails, and spreadsheets, lose revenue to no-shows, and chase late payments because salon and generic booking tools are clunky, email-centric, and ignore SMS workflows and last-minute cancellation recovery.
Solution Overview
PawPilot centralizes bookings, reminders, and payments in SMS, where pet clients actually reply. A Smart Waitlist auto-texts prequalified customers a one-tap Claim This Slot link with deposit to fill cancellations in minutes, while instant Stripe checkout collects payment immediately instead of days later.

Details & Audience

Description
PawPilot is an SMS‑first scheduling, reminders, and billing platform for pet pros. Built for independent groomers, walkers, and sitters who live in their texts. It slashes no‑shows, ends back‑and‑forth, and gets you paid instantly, reclaiming 6+ hours a week and cutting days‑to‑cash from 7 to 1. Its Smart Waitlist auto‑texts prequalified clients a one‑tap 'Claim this slot' link, filling 83% of cancellations in minutes with a deposit.
Target Audience
Independent groomers, dog walkers, and sitters (24-55) battling no-shows and late payments, text-first operators.
Inspiration
In my neighborhood groomer’s shop, clippers buzzed while she penciled names on a coffee-stained waitlist. At noon a cancellation hit; the table sat empty as she apologized into a ringing phone and fired off voicemails no one returned. Her clients were texting. The answer was simple: text the waitlist a one‑tap claim‑this‑slot link, take a deposit, fill the gap in minutes. That moment became PawPilot.

User Personas

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

P

Pop-Up Pro Piper

- Age 29–38; independent mobile groomer; urban/suburban event hubs. - 5–8 years experience; former salon pro. - Partners with two pet retailers; weekend-heavy schedule. - Income $60k–$85k; revenue spikes on event days.

Background

Started in a big-box salon, learned throughput on busy Saturdays. Shifted to mobile, then built retailer partnerships after seeing pop-up demand. Needs tools that tame surges and keep lines moving.

Needs & Pain Points

Needs

1. Instant day-of waitlist and confirmations 2. Batch booking links for pop-up days 3. On-site, same-day SMS payments

Pain Points

1. Walk-up spikes derail carefully planned slots 2. No-shows waste scarce event capacity 3. Manual checkout creates bottleneck lines

Psychographics

- Chases momentum; hates idle minutes. - Champions simple, highly visible processes. - Prioritizes customer flow over customization. - Data-minded about slot fill rate.

Channels

1. Instagram DMs - daily 2. SMS text - instant 3. Facebook Events - promotions 4. Google Business Profile - updates 5. Nextdoor - local

A

After-Hours Adapter Alex

- Age 26–45; independent walker/sitter; dense city nightlife corridor. - Works 5pm–1am; weekday-heavy with weekend spikes. - 3–6 years experience; safety-focused night routes. - Income $45k–$70k; premium after-hours rates.

Background

Started as a bar manager with a rescue dog, saw demand for after-hours care. Built a client base among service workers; needs tools that work flawlessly when most systems sleep.

Needs & Pain Points

Needs

1. Automated evening reminders and confirmations 2. Instant rescheduling via SMS templates 3. Deposits to secure late-night slots

Pain Points

1. Last-minute cancellations around shift changes 2. Clients slow to respond after midnight 3. Delayed payments from irregular schedules

Psychographics

- Reliability-obsessed, especially during late-night windows. - Prefers quiet automation over constant chatter. - Values safety checks and location clarity.

Channels

1. SMS text - primary 2. Instagram DMs - nightly 3. Reddit r/dogwalking - advice 4. Google Business Profile - reviews 5. WhatsApp - international clients

C

Compliance-Conscious Casey

- Age 32–50; boutique studio groomer; strict-ordinance city. - 7–12 years experience; insured and certified. - Serves health-sensitive breeds; appointment-only model. - Income $70k–$100k; policy-first branding.

Background

A past incident with missing rabies proof led to a costly reschedule. Since then, Casey systematized intake and compliance, hunting tools that make paperwork automatic without slowing bookings.

Needs & Pain Points

Needs

1. SMS intake with vax/waiver capture 2. Policy templating in confirmations 3. Deposits for policy-heavy services

Pain Points

1. Chasing proof of vaccinations pre-appointment 2. Policy disputes at drop-off 3. Paper forms misfiled or lost

Psychographics

- Rules-first; documentation always beats memory. - Trust is earned through transparent policies. - Loves checklists; despises operational ambiguities.

Channels

1. Google Search - policy updates 2. Email inbox - weekly 3. LinkedIn - professional 4. YouTube - compliance tips 5. Facebook Groups - local business

B

Bilingual Bridge Ben

- Age 24–42; independent walker/sitter; English–Spanish bilingual. - Mixed urban/suburban routes; family-heavy clientele. - 3–8 years experience; referrals dominate. - Income $40k–$65k; steady repeat business.

Background

Grew up translating for family businesses; built trust handling pet care for non-native speakers. Needs messaging that’s clear, consistent, and easy to duplicate in both languages.

Needs & Pain Points

Needs

1. Bilingual SMS templates for key flows 2. Auto-reminders in preferred language 3. Deposit links with clear translations

Pain Points

1. Misunderstandings about times and entrances 2. Inconsistent language in policies 3. Payment confusion across platforms

Psychographics

- Inclusivity as a competitive advantage. - Precision with wording and expectations. - Relationship-first; replies fast to every message.

Channels

1. WhatsApp Business - daily 2. SMS text - primary 3. Facebook Groups - neighborhood 4. Instagram DMs - visual proofs 5. Google Business Profile - reviews

N

New-Neighborhood Networker Nia

- Age 27–39; sitter-only; new to metro area. - 2–5 years experience; background-checked. - Flexible daytime windows; shared housing base. - Income $30k–$55k; growth-focused.

Background

Moved for a partner’s job; left a full book behind. Determined to accelerate word-of-mouth with disciplined follow-ups and fast replies to every lead.

Needs & Pain Points

Needs

1. Lead capture via SMS from all sources 2. Smart Waitlist to prove responsiveness 3. Broadcast texts for openings and promos

Pain Points

1. Empty weekdays in early months 2. Leads ghost after first inquiry 3. Scattered conversations across platforms

Psychographics

- Hustle mindset; measures weekly pipeline. - Comfortable with light promos and offers. - Optimistic, resilient, and cheerfully data-curious.

Channels

1. Nextdoor - local 2. Facebook Groups - community 3. Instagram Reels - showcase 4. Google Business Profile - discovery 5. SMS text - follow-up

M

Multi-Pet Maestro Maya

- Age 31–48; house-call sitter/walker; multi-pet specialists. - 6–10 years experience; vet-tech adjacent training. - Suburban routes; larger homes, multiple animals. - Income $55k–$80k; premium care add-ons.

Background

Started volunteering at a rescue, learned medication routines and behavior cues. Built a niche serving multi-pet families; demands airtight instructions and dependable reminders.

Needs & Pain Points

Needs

1. Per-visit care notes auto-texted 2. Staggered scheduling with buffer alerts 3. Photo check-ins bundled via SMS

Pain Points

1. Missed meds when schedules shift 2. Overlapping walks causing lateness 3. Care notes lost across apps

Psychographics

- Detail-obsessed; checklists calm the chaos. - Compassionate care powered by structure. - Prefers predictable routines to surprises.

Channels

1. SMS text - primary 2. Instagram DMs - updates 3. Reddit r/petsitting - peers 4. YouTube - how-tos 5. Google Business Profile - visibility

Product Features

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

Smart Expiry

Auto-optimizes the 10-minute hold based on slot urgency, client reliability, and time-of-day—tightening windows during peak demand and allowing brief extensions for VIPs. Maximizes fill rates while staying fair, with zero extra steps for you.

Requirements

Adaptive Hold Duration Engine
"As a groomer, I want hold times to adjust automatically based on context so that high-demand slots fill faster without manual tuning."
Description

Compute a dynamic hold window for each offer using real-time signals (slot urgency, client reliability score, time-of-day demand, service type) with configurable minimum/maximum bounds and a safe 10-minute fallback. The engine tightens holds during peak demand and short-notice slots, and allows modest flexibility off-peak, returning a single hold duration used by the offer pipeline. All calculations are deterministic, logged for auditability, and resilient to missing data. The component exposes a stateless API to the Smart Waitlist and SMS modules, supports feature flags for controlled rollout, and adheres to business hours and timezone settings.

Acceptance Criteria
Peak Demand Short-Notice Slot Tightening
Given slot_start_time is within configured short_notice_threshold_minutes and demand_index >= configured peak_threshold for the shop timezone When the engine computes hold_duration_seconds Then hold_duration_seconds <= default_fallback_seconds (600) and >= configured_min_hold_seconds And hold_duration_seconds <= configured peak_tightened_cap_seconds And applied_rules includes ["short_notice", "peak_demand"] and any service_type_weight used is reflected in applied_rules
Off-Peak VIP Extension Within Bounds
Given demand_index < configured off_peak_threshold and reliability_score >= configured vip_threshold and slot_start_time is beyond short_notice_threshold_minutes When the engine computes hold_duration_seconds Then hold_duration_seconds may be > 600 but never > configured_max_hold_seconds nor > configured vip_extension_cap_seconds And applied_rules includes ["vip_extension", "off_peak"]
Safe Fallback and Bounds Enforcement
Given any request When computation errors occur, upstream signal stores are unavailable, or inputs are out-of-range Then the engine returns hold_duration_seconds = 600, applied_rules includes ["fallback"], and error_reason_code is populated And for all successful computations, hold_duration_seconds is clamped to [configured_min_hold_seconds, configured_max_hold_seconds]
Deterministic Output and Idempotency
Given identical inputs (slot_start_time, service_type, reliability_score, demand_index, config versions, timezone, business_hours, feature_flag state) When the engine is invoked repeatedly and concurrently with the same payload 1000 times Then hold_duration_seconds is identical across all invocations (variance = 0) And no state is persisted between calls; only a structured log entry per request_id is emitted And the same request payload always yields the same response fields (excluding timestamp)
Resilience to Missing or Partial Signals
Given a request missing one or more optional signals (e.g., reliability_score, demand_index, service_type) When the engine computes hold_duration_seconds Then default values from configuration are substituted, a defaults_used array is included in the response, and a WARN log notes which defaults were applied And the engine returns HTTP 200 with hold_duration_seconds within configured bounds
Business Hours and Timezone Adherence
Given a shop with configured timezone and business hours spanning DST boundaries When requests are evaluated near opening/closing times and during DST transitions Then peak/off-peak and short_notice calculations use the shop timezone (not server time) and correctly handle DST gaps/overlaps And outside business hours the off-hours policy is applied (e.g., no VIP extension) and reflected in applied_rules
Stateless API Contract and Feature Flag Rollout
Given POST /v1/hold-duration is called by Smart Waitlist or SMS with a valid payload Then respond 200 with JSON fields: hold_duration_seconds (integer), applied_rules (array), request_id (string), defaults_used (array, optional), error_reason_code (string, optional) And invalid payloads return 400 with validation_errors; out-of-range configuration returns 422 And when feature flag adaptive_hold_duration is OFF, hold_duration_seconds = 600 and applied_rules includes ["flag_off"]; when ON, dynamic computation is used And p95 latency <= 200ms and availability >= 99.9% during rollout window
Client Reliability Scoring
"As a business owner, I want clients’ dependability quantified so that expiry windows reflect real behavior and reduce no-shows."
Description

Generate a 0–100 reliability score per client from historical behavior (response latency, confirmation rate, no-shows, late cancels, deposit completion, payment disputes) with recency weighting and cold-start defaults. Scores update nightly and on key events, are privacy-safe, and never exposed to clients. The score is consumed by the hold engine to shorten windows for low-reliability clients and allow brief flexibility for trusted ones. Includes configurable weights, decay half-life, and guardrails to prevent extreme swings from single events.

Acceptance Criteria
Score Range and Cold-Start Defaults
Rule: reliability_score is a whole number from 0 to 100 inclusive after any computation. Rule: A client with no qualifying history is assigned DEFAULT_SCORE from configuration on creation. Rule: The cold-start score remains until the client meets MIN_HISTORY_THRESHOLD (events or days) configured, after which computed score replaces it. Rule: If computation produces null/NaN due to missing inputs, system falls back to the last valid score or DEFAULT_SCORE, whichever is available.
Recency Weighting and Decay Half-Life
Given two identical event types for the same client, one 7 days ago and one 28 days ago, When scores are computed with half_life_days = 14, Then the 7-day-old event’s weight is exactly double the 28-day-old event’s weight. When half_life_days is changed to a new value, Then nightly recomputation applies the new decay to all scores and writes an audit entry per tenant. Rule: Event contribution decays exponentially by 0.5 every half_life_days.
Configurable Factor Weights and Guardrails
Rule: Administrators can set non-negative weights for factors [response_latency, confirmation_rate, no_shows, late_cancels, deposit_completion, payment_disputes] that sum to 1.0 ± 0.01; otherwise, the update is rejected and the previous weights remain active. Rule: The absolute change in reliability_score from a single event is capped at MAX_DELTA_PER_EVENT; exceeding deltas are clipped to the cap. When a weight or cap configuration is updated, Then the change is versioned with timestamp, actor, old value, and new value in configuration audit logs.
Update Cadence: Nightly Batch and Key Events
Given the nightly window 01:00–03:00 in tenant local time, When the batch job runs, Then 99% of client scores are recomputed within the window and 100% within 4 hours, with failures retried up to 3 times. When a key event occurs (confirmation, no-show, late cancel, deposit completion, payment dispute, inbound reply), Then the client’s score is recalculated within 5 seconds of event ingestion and persisted. Then the updated score is immediately available to the hold engine for any new or pending holds created after persistence.
Privacy and Access Control
Rule: reliability_score is not included in any SMS template variables or client-facing payloads. Rule: Public API responses and webhooks exclude reliability_score and any derived classifications. Rule: Only services/users with scope reliability:read can access the score; all reads and writes are logged with actor, timestamp, and purpose. When an unauthorized request for the score is made, Then the request is denied with 403 and no score value is leaked.
Hold Engine Integration and Fairness Bounds
Given baseline_hold_minutes B, min_hold_minutes MIN, max_hold_minutes MAX, low_threshold L, high_threshold H, When offering a slot to a client with score <= L, Then the hold duration is set to max(MIN, B - low_adjustment) per configuration. When offering a slot to a client with score >= H, Then the hold duration is set to min(MAX, B + high_adjustment) per configuration. Rule: The selected hold duration, client score, and adjustment reason are recorded in the offer audit log. When the client score is unavailable, Then the hold duration falls back to B and is logged as fallback_used = true.
Data Integrity: Event Deduplication and Backfill
When duplicate event payloads are received (same client, event type, and idempotency key), Then only one event is counted toward the score. When historical events are backfilled or a client profile is merged, Then the score is recomputed within the next nightly batch and the delta from the previous score is capped by MAX_DELTA_PER_BATCH. Rule: Out-of-order events are applied by event_timestamp, not arrival time.
VIP Tier & Auto-Extension Rules
"As a groomer, I want VIP clients to receive brief automatic extensions when appropriate so that loyal customers aren’t penalized during busy times."
Description

Support segment-based policies (e.g., VIP, Standard, New) that allow brief, capped extensions for high-value clients without staff intervention. Rules define per-segment max extension length, max number of extensions per hold, and daily/weekly caps to preserve fairness. Extensions are granted automatically near expiry when demand conditions permit, are fully logged, and never exceed global min/max hold bounds. Admins can map VIP status via tags, revenue thresholds, or manual designation.

Acceptance Criteria
Auto-Extension for VIP Near Expiry Under Allowable Demand
Given a hold for a VIP client with 1 minute remaining before expiry and demand state is normal, When the extension trigger window of 2 minutes before expiry is reached, Then the system automatically extends the hold by the VIP segment's configured extension length without staff action, And the new expiry time reflects the applied extension. Given a hold for a VIP client within the trigger window and demand state is peak, When the extension evaluation runs, Then no extension is granted. Given a hold for a client in a segment with zero configured extension length, When the trigger window is reached, Then no extension is granted.
Per-Segment Max Extension Length and Per-Hold Limit Enforcement
Given a segment policy with max extension length of 3 minutes and max 1 extension per hold, When an eligible hold is extended once by any amount, Then subsequent attempts to auto-extend the same hold are blocked. Given a segment policy with max extension length of 3 minutes, When an auto-extension is granted, Then the added time does not exceed 3 minutes. Given a hold with 14 minutes total duration and the global max hold is 15 minutes while the segment max extension is 3 minutes, When an auto-extension is granted, Then the added time is capped to 1 minute.
Daily and Weekly Extension Cap Enforcement per Client
Given a VIP client with a daily extension cap of 5 minutes and already received 4 minutes of extensions today, When another auto-extension of 3 minutes is due, Then the extension is capped to 1 minute. Given a VIP client who has reached the weekly extension cap, When the trigger window is reached on any subsequent holds in the same week, Then no extension is granted. Given an account timezone configured to America/New_York, When calculating daily and weekly caps, Then day and week boundaries use that timezone.
Global Min/Max Hold Duration Bounds Observed
Given global hold bounds of min 5 minutes and max 15 minutes, When any auto-extension is applied, Then the resulting total hold duration remains within 5 to 15 minutes inclusive. Given an auto-extension would push total hold duration above the global max, When evaluated, Then the extension is reduced to the remaining time up to the max or not applied if remaining is 0. Given current hold duration is below global min due to prior tightening, When evaluating auto-extension, Then no changes violate the min bound.
Admin Mapping of VIP Segment via Tag, Revenue, or Manual
Given an admin maps VIP to clients with tag named VIP, When a client record contains that tag, Then new holds for that client use the VIP segment. Given an admin sets VIP revenue threshold to 1000 in the past 6 months, When a client meets or exceeds that threshold, Then new holds for that client use the VIP segment. Given a client is manually designated as VIP in admin, When conflicts exist with other mapping rules, Then the manual designation takes precedence. Given a client segment changes, When an existing hold is already created, Then the hold retains its original segment for the purpose of extension rules.
Audit Logging of Auto-Extension Decisions
Given an auto-extension decision occurs (granted or denied), When the evaluation completes, Then an immutable audit log entry is created including timestamp, client ID, hold ID, prior expiry, new expiry (or unchanged), client segment, rule identifiers used, demand state, cap reasons, and staff involved = false. Given a user views the hold timeline in the dashboard, When the hold has extension activity, Then the audit entries are visible with precise before/after expiry times. Given an export of extension logs is requested for a date range, When generated, Then all extension decisions in that range are included and can be filtered by segment and demand state.
Expiry Nudge & Countdown SMS
"As a client, I want clear messages that show exactly how long I have to accept so that I can respond in time and not lose the slot."
Description

Send SMS offers with a live countdown indicator and a clear call-to-action, followed by a single nudge near expiry for unresponsive recipients. Message templates adapt to hold length and include short links to confirm and pay deposits. VIPs may see an optional “Need 2 more minutes?” action when rules allow. The module respects quiet hours, opt-out keywords, and deliverability constraints, with graceful fallbacks if link tracking is unavailable.

Acceptance Criteria
Initial Offer SMS: Countdown + CTA
Given a hold is created with duration D by Smart Expiry When the system prepares and sends the initial offer SMS Then the SMS is dispatched within 10 seconds of hold creation And the SMS uses the template variant that reflects D (e.g., “held for D minutes”) And the SMS contains a short link to a live countdown page for the held slot And the SMS contains a short “Confirm & Pay” link that opens the deposit flow with the slot preselected And the SMS includes a clear call-to-action to reply YES to claim And each link is shortened to ≤ 23 characters using the configured short domain And the SMS is not sent to any contact marked as opted-out or blocked
Single Nudge Near Expiry
Given the initial offer SMS was sent and delivery status is Delivered or Sent And the recipient has not replied or tapped the confirm link And the slot remains available When time remaining enters the nudge window (the final 20% of D, bounded between 30 seconds and 120 seconds) Then send exactly one nudge SMS to the recipient And include the current time remaining in the nudge content And do not send the nudge if quiet hours are in effect at the intended send time And do not send the nudge if the slot has been claimed or the recipient has replied in any way And ensure at most one nudge is sent per recipient per hold (no duplicates) And if the initial offer’s final delivery status is Undelivered or Filtered, do not send the nudge and mark the offer as not nudged due to deliverability
VIP Extension Prompt
Given the recipient is marked VIP and extension rules permit at the current demand state and time-of-day And the recipient has not yet responded When the nudge is sent with ≤ 60 seconds remaining on the hold Then include an optional “Need 2 more minutes?” action (link or keyword EXTEND) And upon tap of the link or reply EXTEND, extend the hold by exactly 2 minutes once per offer And deny the extension if the slot has been claimed, if quiet hours begin before the new expiry, or if business closing time would be exceeded And update the countdown page within 2 seconds to reflect the new expiry And record an audit log entry for the extension decision and outcome
Quiet Hours Compliance
Given quiet hours are configured for the business and the recipient’s local timezone is known When a hold is created during quiet hours Then do not send the initial offer SMS during quiet hours And if the remaining hold time at quiet-hours end would be ≥ 2 minutes, queue the offer to send at quiet-hours end; otherwise suppress it and log quiet-hours suppression And never send a nudge during quiet hours; cancel any scheduled nudge that would occur within quiet hours and log the cancellation reason
Opt-Out & Keyword Handling
Given a recipient is opted out prior to offer creation When the system attempts to send an offer or nudge Then suppress the message and log an opt-out suppression reason Given a recipient replies STOP, END, CANCEL, QUIT, or UNSUBSCRIBE to an offer or nudge When the keyword is received Then mark the contact as opted out within 5 seconds And send a single opt-out confirmation SMS (if allowed by regulations) And cancel the hold for that recipient and invalidate confirm/deposit links And suppress any further nudges or follow-ups for the offer
Link Tracking Unavailable Fallback
Given the link shortener or tracking service is degraded or unavailable per health check When composing the offer or nudge SMS Then switch to a backup short domain; if unavailable, insert the raw long URL And ensure the SMS remains ≤ 2 segments (≤ 306 GSM-7 characters) by trimming non-essential copy while preserving CTA and links And ensure the countdown page remains reachable without tracking parameters And if the countdown page is unavailable, include a static time-remaining string (e.g., “~D minutes left”) and instruct the user to reply YES to claim
Countdown and Hold Synchronization
Given a hold exists with server-side expiry time E When the recipient opens the countdown page via the SMS link Then the displayed time remaining matches server time within ±1 second And when the hold expires, the page disables claim/payment actions and displays an expired message within 2 seconds And if the slot is claimed by someone else before E, the page shows taken status within 2 seconds and all confirm/payment attempts return a 409/410 response And any confirm attempt after expiry triggers a failure response and an informational SMS to the recipient within 5 seconds And no nudge is sent after the hold is claimed or expires
Waitlist Auto-Routing on Expiry
"As a groomer, I want expired holds to pass automatically to the next qualified client with a recalculated window so that slots fill without manual work."
Description

When a hold expires or is declined, automatically release the slot and route the offer to the next best-matched waitlist client with a freshly computed hold window. The flow is idempotent, race-condition safe, and preserves fairness by preventing simultaneous holds for the same client-slot pair. Integrates with deposit collection and appointment creation, ensuring the slot is marked filled immediately upon payment to avoid double-booking.

Acceptance Criteria
Hold Expiry Auto-Reroute to Next Best Match
Given slot S (2025-08-20T10:00:00Z) is on hold H1 for Client A with expiry at 2025-08-15T10:00:00Z and Smart Expiry is enabled with minHold=5m and maxHold=15m When the expiry time passes without acceptance or payment Then H1 status changes to Expired and is timestamped And the routing engine selects the highest-ranked eligible waitlist client (Client B) at routing time And a new hold H2 is created for Client B with ttl between 5m and 15m and expiry = now + ttl (±5s) And an SMS is sent exactly once to Client B containing slot details and a deposit link And slot S remains in Held status under H2 until H2 expires, is declined, or is paid
Decline Immediately Releases and Reroutes
Given a slot S is on hold H1 for Client A When Client A declines via SMS keyword or decline link before expiry Then H1 status changes to Declined and the hold is released instantly And the routing engine immediately evaluates the waitlist and selects the next best eligible client (Client B) And a new hold H2 is created for Client B with a Smart Expiry-computed ttl within configured bounds And exactly one SMS with a deposit link is sent to Client B And Client A does not receive the same slot S again in this routing cycle
Idempotent Expiry Event Handling
Given duplicate expiry events (E1 and E1') are received for the same hold H1 within 2s When both events are processed concurrently Then H1 transitions to Expired exactly once And downstream routing is executed at most once And exactly one new hold (H2) is created or none if no candidates exist And exactly one SMS is sent for the next offer And the system records a single audit event for expiry with a deduplication marker
Concurrency and Fairness—No Simultaneous Holds for Same Client-Slot Pair
Given two routing workers attempt to create a hold for the same client-slot pair (Client B, Slot S) concurrently When both attempts try to persist a new hold Then at most one active/pending hold exists for (Client B, Slot S) And the losing attempt receives a duplicate constraint outcome and does not send an SMS And the invariant "one active hold per slot" is maintained throughout (no overlapping holds for S) And metrics record one successful hold creation and one deduplicated attempt
Deposit Payment Immediately Fills Slot and Cancels Others
Given Client B has an active hold H2 for Slot S with a deposit link When Client B pays the deposit within the hold window Then an appointment A is created for Client B at Slot S and linked to the payment transaction And Slot S status changes to Filled at payment authorization time (≤2s) And any in-flight routing jobs for Slot S are canceled and no further offers are sent And any pending/queued holds for Slot S are invalidated and affected clients (if already notified) receive a "slot filled" SMS And duplicate payment submissions within 60s are idempotently ignored after the first success
No Eligible Waitlist Candidates on Expiry
Given a hold H1 for Slot S expires or is declined and the waitlist has no eligible candidates after applying filters When auto-routing executes Then no new hold is created and no SMS offers are sent And Slot S status is set to Open/Available for manual booking or other channels And the routing attempt is logged with outcome = NoneFound and includes evaluation metrics (candidate count = 0)
Fairness & Abuse Prevention
"As a business owner, I want safeguards that prevent clients from repeatedly extending or blocking holds so that the system stays fair and efficient."
Description

Enforce system-wide guardrails to prevent hoarding and gaming: per-client limits on concurrent holds, daily extension caps, cooldowns after expiries, and denial of extensions during critical peak windows. Provide transparent internal logs and reasons for denials, and automatically downgrade policies if abnormal behavior is detected. All rules remain compliant with existing cancellation, deposit, and communication policies.

Acceptance Criteria
Concurrent Hold Limit Enforcement
Given a client has active holds equal to the configured per‑client concurrent hold limit When the client attempts to initiate or accept an additional hold via any channel (SMS, link, or staff-created) Then the system atomically rejects the new hold so the client’s total active holds never exceed the limit And the client receives the approved "hold limit reached" SMS template And an audit record is created with reason_code=HOLD_LIMIT_EXCEEDED including client_id, attempted_hold_id, active_hold_count, limit_value, timestamp, and correlation_id And existing holds remain unchanged And no staff intervention is required
Daily Extension Cap Enforcement
Given a client has reached the configured daily extension cap for the current calendar day (in the account’s timezone) When the client requests any hold extension Then the system denies the extension and preserves (or tightens per Smart Expiry) the current expiry time And the client receives the approved "extension cap reached" SMS template And an audit record is created with reason_code=EXTENSION_DAILY_CAP_REACHED including client_id, hold_id, cap_value, extensions_used_today, timestamp, and correlation_id And the client can still confirm the slot without an extension if within the current hold window And no staff intervention is required
Cooldown After Expiry Enforcement
Given a client’s hold expires at time T When the client requests any new hold extension or a new hold within the configured cooldown duration after T Then the system denies the extension requests and sets any new hold to the minimum configured hold window And an audit record is created with reason_code=COOLDOWN_ACTIVE including client_id, hold_id, expiry_time=T, cooldown_end_time, timestamp, and correlation_id And the client receives the approved cooldown notification SMS template And existing cancellation, deposit, and communication policies remain unchanged And no staff intervention is required
Peak Window Extension Denial
Given the current time is within a configured critical peak window When any client requests an extension on an active hold Then the system denies the extension regardless of client tier or VIP status And any new holds created during the peak window use the configured peak hold duration And the client receives the approved "peak window extension unavailable" SMS template And an audit record is created with reason_code=PEAK_WINDOW_RESTRICTION including client_id, hold_id, peak_window_id, configured_durations, timestamp, and correlation_id And no staff intervention is required
Transparent Internal Logs and Reason Codes
Given any fairness or abuse-prevention rule denies or modifies a client action (e.g., hold creation denial, extension denial, policy downgrade, tightened window) When the action is executed Then an audit record is written within 2 seconds And the record includes: event_type, client_id, phone, rule_id, reason_code, human_readable_reason, prior_state, new_state, effective_limits, timestamp, actor=system, correlation_id, and request_context (channel, location) And authorized staff can retrieve the record via Admin UI and API by client_id and date range filters And the Admin UI displays the reason and rule applied without exposing internal flags in client-facing SMS And logs are exportable in CSV and JSON formats
Automatic Policy Downgrade on Abnormal Behavior
Given the detection service flags a client for abnormal behavior according to configured thresholds When the flag is raised Then the client’s Smart Expiry policy is downgraded per the configured downgrade profile (e.g., max_concurrent_holds=1, extensions_disabled=true, hold_window=min) And the downgrade takes effect within 60 seconds and no later than the client’s next action And a scheduled reversal is set per configured duration or until manual clearance by authorized staff And an audit record is created with reason_code=ABNORMAL_BEHAVIOR_DOWNGRADE including detection_evidence, prior_policy, new_policy, timestamp, and correlation_id And cancellation, deposit, and communication policies remain unchanged
Policy Compliance With Cancellation, Deposit, and Communication Rules
Given any fairness or abuse-prevention action (denial, cooldown, downgrade, or tightened window) When deposits or cancellation rules apply to the booking Then deposit amounts, due times, and refund policies remain unchanged and are applied exactly once And cancellation windows, fees, and terms remain unchanged And all client-facing SMS use approved templates and do not disclose internal risk flags or rule names And no duplicate messages are sent and no double charges occur And waitlist backfill continues to operate within existing policies
Optimization Metrics & Tuning Dashboard
"As an operator, I want visibility into Smart Expiry’s impact and safe ways to tune it so that I can balance fairness and speed for my business."
Description

Expose key performance metrics for Smart Expiry (fill rate lift, average time-to-fill, acceptance latency, expiry outcomes, extension utilization, no-show impact) and allow safe tuning of parameters behind feature flags. Provide cohort views by segment, time-of-day, and service type, plus export and A/B testing hooks. Include a read-only decision trace per offer for support, enabling quick diagnosis without adding operational steps for staff.

Acceptance Criteria
Metrics Accuracy, Definitions, and Freshness
Given the account timezone is configured, When a user selects any date range, Then all metrics use account timezone day boundaries for aggregation. Given live Smart Expiry events are ingested, When new events occur, Then dashboard metrics refresh within 15 minutes of event time. Given a validation dataset with known outcomes, When metrics are computed, Then values match expected within ±0.1 percentage points for rates and ±1 second for timing medians. Given incomplete or missing event fields, When a metric cannot be computed, Then the UI shows "No Data" and an error is logged without crashing the page. Given display rules, When values are shown, Then percentages are rounded to 1 decimal and durations to the nearest second.
Cohort Filters and Combined Segmentation
Given filters for segment, time-of-day, and service type, When a single filter is applied, Then all counts and denominators reflect only matching offers. Given multiple filters are applied, When two or more filters are active, Then results equal the intersection of the selected cohorts. Given filters yield zero matching records, When the view renders, Then the dashboard displays a "No Data" state and provides a Clear Filters control. Given cohort filters change, When filters are cleared, Then metrics revert to the unfiltered baseline without a full page reload. Given an export is initiated, When filters are active, Then the exported data respects the same filters.
Safe Parameter Tuning with Feature Flags and Rollback
Given parameters (base_hold_sec, peak_tighten_sec, vip_extension_sec), When values are edited, Then changes save as Draft and do not affect production until Published. Given Publish is invoked, When toggled ON for a selected environment or cohort, Then the new configuration takes effect within 60 seconds and an audit entry is recorded with actor, timestamp, and diff. Given a published configuration, When Rollback is invoked, Then the prior configuration is restored within 60 seconds and an audit entry is recorded. Given validation constraints (base_hold_sec 60–600, vip_extension_sec 0–600, peak_tighten_sec 0–540), When inputs violate bounds or types, Then inline errors block save. Given feature flags are OFF, When staff use Smart Expiry, Then no additional prompts or steps are introduced to their workflow.
A/B Testing Hooks and Variant Attribution
Given ingested events include experiment_key and variant, When data is processed, Then the dashboard segments metrics by variant and displays control vs treatment comparisons. Given a configured 50/50 split and N ≥ 1000 offers in range, When the sample ratio check runs, Then the observed split falls within ±2% or a warning is displayed. Given no experiment_key is present in the data, When rendering the dashboard, Then A/B comparison widgets are hidden. Given a variant filter is selected, When exporting or drilling down, Then the selection is preserved in metrics and exports.
Per-Offer Read-Only Decision Trace
Given an offer_id, When a Support or Admin opens the decision trace, Then it loads in ≤ 2 seconds at p95 and is read-only (no editable controls). Given the trace view, When rendered, Then it shows inputs (slot_urgency score, client_reliability tier, time_of_day bucket), rules applied, initial hold window, extensions with reasons, final expiry time, outcome, all relevant timestamps, model/version, and active feature flags at decision time. Given a non-privileged user attempts access, When the trace URL is opened, Then access is denied and the attempt is logged. Given a trace is viewed, When the view is closed, Then no changes are made to the underlying offer or client records.
Exports for Metrics and Decision Traces
Given a date range ≤ 90 days and applied filters, When a CSV or JSON export is requested, Then the export includes headers, uses RFC 4180 for CSV, ISO-8601 timestamps in account timezone, and completes in ≤ 120 seconds for up to 50,000 offers. Given a metrics summary export is selected, When the file is generated, Then it includes fill rate lift, average time-to-fill, acceptance latency, expiry outcomes distribution, extension utilization, and no-show impact aligned with the selected filters and date range. Given a decision traces export is selected, When the file is generated, Then it contains one row per offer with the same fields shown in the read-only trace view. Given PII constraints, When exporting, Then client phone numbers are masked to last 4 digits and message bodies are excluded.
Dashboard Performance, Availability, and Access Control
Given the default 30-day view, When the dashboard loads, Then first meaningful paint occurs in ≤ 2 seconds at p90 and filter interactions complete in ≤ 1.5 seconds at p90. Given service availability targets, When measured over a calendar month, Then the dashboard achieves ≥ 99.5% uptime excluding scheduled maintenance. Given RBAC policies, When roles are enforced, Then Owners/Admins/Support can view and export; standard staff can view metrics only; unauthenticated or unauthorized users cannot access the dashboard or traces. Given maintenance mode is enabled, When users access the dashboard, Then a read-only banner is displayed and exports are disabled with clear messaging.

Tap Cascade

If Apple/Google Pay isn’t available or fails, the link seamlessly falls back to card-on-file or secure card entry—no app downloads, no friction. More deposits land on the first try, reducing manual follow-ups.

Requirements

Smart Payment Cascade Orchestrator
"As a pet care business owner, I want deposit links that automatically try the fastest available payment method and fall back seamlessly so that more clients pay on the first try without me chasing them."
Description

Implements a single, SMS-friendly deposit link that automatically detects and prioritizes Apple Pay or Google Pay via Payment Request APIs, then falls back to card-on-file (COF) if available, and finally to secure card entry—without requiring app downloads. The orchestrator evaluates device/browser capabilities, merchant configuration, and client context to choose the fastest method. It preserves session state across steps, uses idempotency to prevent duplicate charges, and supports manual method selection if auto-advance fails. This reduces friction, increases first-try deposit success, and eliminates manual follow-ups for failed payments.

Acceptance Criteria
Apple Pay Eligible Device—Successful Deposit via Payment Request
Given the client opens the SMS deposit link on iOS Safari with Apple Pay configured and the merchant is enabled for Apple Pay And a valid deposit exists with amount, currency, appointment_id, and client_id When the link loads and capability detection runs Then Apple Pay is selected via Payment Request API as the primary method And the Apple Pay sheet appears within 2 seconds of tap And the sheet shows the correct merchant name, amount, and currency, and does not request shipping When the client authorizes payment Then exactly one charge is created using an idempotency key scoped to the deposit_id And the deposit status updates to Paid within 3 seconds of authorization And an SMS receipt is sent to the client within 10 seconds And analytics record method=apple_pay, success=true, latency_ms
Android Chrome—Successful Deposit via Google Pay
Given the client opens the SMS deposit link on Android Chrome with Google Pay configured and the merchant is enabled for Google Pay And a valid deposit exists with amount, currency, appointment_id, and client_id When the link loads and capability detection runs Then Google Pay is selected via Payment Request API as the primary method And the Google Pay sheet appears within 2 seconds of tap And the sheet shows the correct merchant name, amount, and currency When the client authorizes payment Then exactly one charge is created using an idempotency key scoped to the deposit_id And the deposit status updates to Paid within 3 seconds of authorization And an SMS receipt is sent to the client within 10 seconds And analytics record method=google_pay, success=true, latency_ms
COF Fallback—Charge Saved Card When Wallet Unavailable
Given the client opens the SMS deposit link and wallet payment is unavailable, not configured, or declines with a non-retryable wallet error And a non-expired card on file exists for the client and merchant permits COF charges When capability detection completes Then the orchestrator auto-selects Card on File as the payment method And the UI displays masked card brand and last4, amount, and currency And if SCA/3DS is required, a challenge flow is presented and completed within 90 seconds When the client confirms the charge Then exactly one charge is created using an idempotency key scoped to the deposit_id And the deposit status updates to Paid within 3 seconds of processor confirmation And an SMS receipt is sent to the client within 10 seconds And analytics record method=card_on_file, success=true, wallet_unavailable_reason is populated
Secure Card Entry Fallback—No Wallet and No COF
Given the client opens the SMS deposit link and no supported wallet is available and no valid card on file exists When capability detection completes Then the orchestrator displays a PCI-compliant secure card entry form within 1.5 seconds And the form supports Visa, Mastercard, American Express, and Discover and validates inputs client-side And if SCA/3DS is required, a challenge flow is presented and completed within 90 seconds When the client submits valid card details Then the card is tokenized and exactly one charge is created using an idempotency key scoped to the deposit_id And the deposit status updates to Paid within 3 seconds of processor confirmation And an SMS receipt is sent to the client within 10 seconds And analytics record method=card_entry, success=true
Capability Detection and Manual Method Selection Fallback
Given the client opens the SMS deposit link on any device/browser And merchant configuration and client context are available When capability detection runs Then methods are prioritized Apple/Google Pay > Card on File > Secure Card Entry And if a primary method is confidently available within 1.5 seconds, the flow auto-advances to that method And if detection exceeds 1.5 seconds or is inconclusive, a method picker appears within 2 seconds showing only available methods with reasons for unavailability on others When the client manually selects a method Then the selected method is initiated without page reload and without losing context And unavailable methods are disabled with explanatory tooltips And analytics record auto_advance=true/false and selected_method
Session State Preservation Across Cascade Steps and Retries
Given the client opens the SMS deposit link containing a signed, expiring token for a specific deposit When the client navigates between methods, reloads the page, or returns via the back button within a 30-minute TTL Then appointment_id, client_id, amount, and previously entered data remain intact And capability detection does not re-request user input And only one active session exists per deposit; a new device opening the link invalidates the prior session with a non-blocking notice And if the session expires, the client sees a clear expiry message with a CTA to request a fresh link And analytics record session_id, resumed=true/false, and session_end_reason
Idempotent Charging and Duplicate Prevention Under Adverse Network Conditions
Given the client double-taps the pay button, experiences a network timeout, or opens the deposit link multiple times When the orchestrator sends charge requests Then a stable idempotency key derived from deposit_id and method is used for all retries and taps And no more than one successful charge is created for the deposit And subsequent identical attempts return the original outcome to the client without a second charge And the pay button is disabled within 100 ms after first tap and shows a progress state And on processor timeout, the orchestrator retries up to 2 times with exponential backoff while preserving idempotency And analytics record duplicate_prevented=true when applicable
Card-on-File Vault Integration
"As a returning client, I want my saved card to be charged automatically for deposits so that I don’t have to re-enter my details."
Description

Integrates with the payment processor’s customer and payment method vault to securely store, retrieve, and charge tokenized cards for returning clients. Supports multiple saved cards per client, default selection, consent capture via SMS link, and automatic handling of expired or updated cards using network token updates. Maintains PCI scope minimization by never handling raw PAN data and relying on vault tokens only. Enables one-tap deposit completion when wallets are unavailable.

Acceptance Criteria
Tokenized Card Storage and Consent Capture via SMS Link
Given a client opens the SMS payment link on a processor-hosted secure page When the client enters card details and explicitly agrees to save the card on file Then the processor returns a customer ID and payment method token to PawPilot, and PawPilot stores only token identifiers and masked card data And no raw PAN, CVV, or full expiry is stored, transmitted, or logged by PawPilot systems or clients And consent text, timestamp, client phone, and IP/user agent are recorded and associated with the token
Multiple Saved Cards and Default Selection
Given a client has two or more saved cards in the vault When PawPilot retrieves the client’s payment methods Then it returns all active tokens with masked display fields (brand, last4, exp) and exactly one default marked And when the client or groomer changes the default via the link Then the new default is persisted in the processor vault and reflected on subsequent retrievals And when a card is removed Then it is detached/disabled in the vault and no longer returned by the API
Tap Cascade Fallback to Card-on-File Charge
Given Apple/Google Pay is unavailable or the wallet authorization fails When the client taps Pay Deposit in the SMS link Then the default card-on-file is charged for the configured deposit amount using the vault token And a confirmation SMS including amount and masked last4 is delivered within 5 seconds of success And the booking transitions to Deposit Paid and stores processor transaction ID, auth code, and receipt URL
Automatic Handling of Expired or Updated Cards via Network Token Updates
Given a saved card expires or is reissued by the network When the processor sends a network token update webhook Then PawPilot updates the payment method metadata (expiry, last4, status) without client action And the next deposit charge uses the updated token and proceeds without requiring new consent And if the card becomes invalid with no available update Then the card is flagged inactive and the client receives an SMS link to update their card
Decline Handling and Secure Entry Fallback
Given a charge attempt on the default card is declined or requires unsupported customer action When the client follows the retry flow from the SMS link Then PawPilot offers other saved cards for selection and retry And if no saved card succeeds, it falls back to processor-hosted secure card entry And upon successful entry, the new card is tokenized, consent is captured, and (optionally) set as default before the deposit is charged And the client receives a success SMS and the booking is marked Deposit Paid
Idempotent Charges and Safe Retries
Given the client taps Pay multiple times or a network timeout occurs When PawPilot creates the deposit charge request Then it uses an idempotency key scoped to booking and deposit so that at most one successful charge is created And retries within the retry window do not result in duplicate charges And booking state, receipts, and internal ledger entries are written exactly once per successful charge
Secure Card Entry with SCA/3DS
"As a client, I want a quick, secure way to enter my card when wallets aren’t available so that I can pay the deposit without hassle."
Description

Provides a hosted, PCI-compliant card entry flow (e.g., Checkout or hosted fields) as the final fallback, including support for SCA/3DS via Payment Intents. Prefills known client data, uses single-use, time-bound links, and supports mobile browsers launched from SMS. Ensures accessibility, fast load times, and localized formatting for ZIP/postcode and currency. Captures billing details needed for fraud checks and downstream reconciliation.

Acceptance Criteria
Single-Use, Time-Bound Link
Given a client opens the deposit link within 30 minutes of issuance, when the link has not been used, then the hosted card entry loads and displays the correct amount and business name. Given a client opens the deposit link after 30 minutes or after successful payment, when the URL is loaded, then an expired link screen is shown with no card fields and a Request new link action. Given the same link is opened in multiple tabs, when one completes payment, then all other tabs show the already used state within 5 seconds. Given an expired or used link, when the API is called, then no new PaymentIntent is created and the attempt is logged as blocked.
SCA/3DS Challenge Flow via Payment Intents
Given a card that requires SCA, when the client submits the form, then a 3DS2 challenge is invoked and upon success the PaymentIntent transitions to requires_capture or succeeded as configured. Given a frictionless SCA outcome, when the client submits the form, then the PaymentIntent confirms without a visible challenge and the confirmation screen appears within 2 seconds. Given a failed or canceled 3DS challenge, when the flow returns, then the client sees a clear error and can retry with the same PaymentIntent without re-entering non-card fields. Given network failures during SCA, when the client resumes within 5 minutes, then the PaymentIntent state is recovered and the client is prompted to continue or retry.
Prefill and Validation of Client Details
Given a known client opens the link from SMS, when the form loads, then full name, email, phone, and billing country/postal code are prefilled from the client record and are editable. Given prefilled data is invalid, when the client submits, then inline validation shows specific messages and prevents submission until corrected. Given card number, expiry, and CVC fields, when the client interacts, then PCI data is not handled by our servers and only hosted fields tokenize the card. Given the client completes SCA and returns, when the confirmation page loads, then all non-card form inputs retain the last entered values.
Mobile Performance and Accessibility
Given a mid-tier Android device on 3G Fast, when the card entry page loads, then First Contentful Paint occurs within 1.5s and Time to Interactive within 2.5s. Given the form is navigated with keyboard and screen reader, when tabbing and reading, then elements have proper labels, focus order, and meet WCAG 2.1 AA; tap targets are at least 44x44. Given numeric inputs (card, expiry, CVC, postal code), when focused on iOS/Android, then the numeric keypad is invoked. Given error messages, when announced, then they are programmatically associated to their inputs via aria-describedby.
Localization of Currency and Address Formats
Given a US client, when the page renders, then the amount shows in USD with $ prefix, ZIP Code label, and 5-digit validation with ZIP+4 support. Given a UK client, when the page renders, then the amount shows in GBP with £ prefix, Postcode label, and UK postcode validation with uppercase formatting. Given a Canadian client, when the page renders, then the amount shows in CAD with $ prefix, Postal Code label, and A1A 1A1 validation with auto-spacing. Given a locale override is provided in the link token, when the page renders, then currency and address formats follow the override, not the device locale.
Fraud Signals and Reconciliation Metadata
Given a successful confirmation, when the PaymentIntent is created or confirmed, then billing address line 1, postal code, country, email, phone, and IP or user agent are attached for AVS and CVV checks. Given an attempt success or failure, when records are persisted, then appointment_id, client_id, business_id, and link_id metadata are stored in both the PSP object and our database. Given a decline response, when the error occurs, then the decline code and AVS or CVV result are captured and viewable in admin logs. Given a refund or cancel is initiated later, when reconciliation runs, then the original PaymentIntent or charge is uniquely linked by metadata to the appointment in reporting.
Cascade to Secure Card Entry When Wallet Fails
Given Apple Pay or Google Pay is unavailable or returns a failure, when the client taps the deposit link, then the flow automatically presents card-on-file if present; otherwise the hosted card entry appears without additional taps. Given the flow cascades to card entry, when the page loads, then the original amount, currency, and PaymentIntent are preserved and no duplicate intents are created. Given the client completes payment via card entry after a wallet failure, when the confirmation screen shows, then the end-to-end completion time is under 90 seconds for the 95th percentile.
Failure Handling and Auto-Retry UX
"As a client, I want the payment link to handle errors gracefully and try another method automatically so that I can complete payment without starting over."
Description

Delivers resilient error handling across all cascade steps, including timeouts, declines, and network errors. Shows concise, human-readable messages, auto-advances to the next available method after a short delay, and preserves any entered data to avoid rework. Implements capped retries with exponential backoff, clear recovery paths, and a manual method picker when automatic progression cannot continue. All failure reasons are captured for analytics and support.

Acceptance Criteria
Auto-Fallback from Wallet Failure to Next Method
Given a user initiates payment via Apple Pay or Google Pay and the wallet is unavailable, cancelled, or returns an error code When the failure is detected or no response is received within 10 seconds Then a banner message "Wallet unavailable—switching to another method" is shown within 500 ms And a 2-second visible countdown displays before auto-advancing to the next eligible method in the cascade (Card on File, then Secure Card Entry) And no duplicate authorization or charge is created on the same PaymentIntent And the deposit amount, currency, customer reference, and session ID persist across the transition And the transition occurs without requiring an app download or page reload And an analytics event "cascade_step_failed" is emitted with step=wallet, reason, attempt, and next_step
Timeout and Exponential Backoff on Network Errors
Given any payment-step network request (initiate wallet, confirm intent, fetch card-on-file, tokenize card) times out or returns a 5xx When performing retries for that step Then retries are attempted with exponential backoff of 1s, 2s, 4s with ±20% jitter, capped at 3 attempts per step And the UI displays a non-blocking "Retrying… (attempt x/3)" message during backoff And if all retries fail, the flow auto-advances to the next eligible method within 1 second And each retry increments an attempt counter recorded in analytics with elapsed_ms And total time spent retrying a single step does not exceed 8 seconds
Human-Readable Decline Messaging and Recovery
Given the processor returns a card decline or failure code (e.g., do_not_honor, insufficient_funds, expired_card) When rendering feedback to the user Then a friendly message is displayed explaining the issue and next steps (e.g., "Card declined. Try another card or payment method.") And no raw gateway codes or sensitive details are shown And a primary CTA "Try another method" is focused and moves to the next eligible method within 500 ms And the PaymentIntent status is updated to require_payment_method without cancellation And an analytics event "decline" captures code_family, step, and last4 (if available) without transmitting PAN or CVV
Data Preservation Across Cascade Steps (PCI-Safe)
Given the user entered details in Secure Card Entry or selected a Card on File When the flow transitions due to a failure or a user-chosen method change Then non-sensitive fields (amount, name, ZIP/postal, selected saved-card) persist and are prefilled on the next step And sensitive fields (full PAN and CVV/CVC) are never persisted; CVV is always re-entered And tokenized card data is reused when valid; if token expired, only the minimum necessary fields are re-requested And masked last4 and brand are displayed to confirm the selected card And the user is not required to retype any non-sensitive field in 90%+ of fallback transitions
Manual Method Picker When Auto-Progression Stops
Given automatic progression cannot continue (no eligible next method, retries exhausted, or user opts to choose) When the manual method picker is shown Then all eligible methods are listed in recommended order with clear labels And the picker meets accessibility requirements (ARIA roles, keyboard navigation, focus visible, contrast ≥ 4.5:1) And selecting a method loads that step within 500 ms and suppresses further auto-progression And the user can return to the picker from any step via a visible control And analytics events "method_picker_opened" and "method_selected" include prior_step and reason
Analytics: Capture and Attribute Failure Reasons
Given any failure, retry, fallback, or manual selection occurs in the cascade When emitting analytics telemetry Then each event includes link_id, session_id, hashed payment_intent_id, step, reason_code, attempt, elapsed_ms, user_agent, and network_status And personally identifiable payment data (PAN, CVV, full name) is never sent; last4 and brand only when already displayed to the user And 95% of events are transmitted within 3 seconds; unsent events are queued and retried up to 3 times with backoff And event schemas are versioned and validated client- and server-side to prevent drops
Resume and Idempotency on SMS Link Re-Entry
Given a user re-opens the same SMS payment link after a failure or abandonment When restoring the session Then the flow resumes at the next eligible method with state preserved for up to 30 minutes since last activity And the existing PaymentIntent is reused idempotently; no duplicate authorizations or charges are created And if the payment already succeeded, the link shows a confirmation screen and disables further payment actions And if the link expired, a clear message is shown with a CTA to request a new link And an event "resume" captures time_since_last_activity and resumed_step
Merchant Controls and Policy Rules
"As a groomer, I want to control how deposits are collected and which methods are used so that the process fits my business policies."
Description

Adds admin settings to configure cascade order (e.g., Wallet → COF → Card Entry), deposit amount rules (flat, percentage, minimum), capture vs authorization behavior, deposit expiry windows, and whether COF can be auto-charged. Supports per-service overrides and safe defaults. All changes are versioned and auditable to ensure predictable behavior and regulatory compliance.

Acceptance Criteria
Configurable Payment Cascade Order and Safe Defaults
Given a merchant sets a cascade sequence [Wallet, COF, Card Entry] in Settings, When a deposit is requested, Then the payment link presents and attempts methods in that order without requiring an app download. Given the first method in the sequence is unavailable or declines with a non-retryable error, When the link is used, Then the system automatically advances to the next method; And for retryable errors, it permits one retry before advancing. Given a method is disabled in Settings, When the cascade runs, Then that method is omitted from the sequence. Given a per-service override exists, When a deposit is requested for that service, Then the override sequence is used; Else the global default sequence is used. Given no configuration exists, When a deposit is requested, Then a safe default of [Wallet, COF, Card Entry] is applied. Given all methods fail, When the cascade completes, Then the payment link displays a clear failure state and records the failure reason for audit.
Deposit Amount Rules: Flat, Percentage, and Minimum
Given the rule type is Flat with amount A in merchant currency, When a deposit link is issued, Then the deposit amount equals A and is capped at the service price. Given the rule type is Percentage with rate P and minimum M, When a deposit link is issued, Then the deposit amount equals max(round_up(P% of base service price to nearest cent), M) and is capped at the service price. Given add-ons are selected at booking, When calculating percentage-based deposits, Then add-on prices are included; Taxes and tips are excluded. Given per-service overrides exist, When issuing deposits for those services, Then the override rule is applied; Else the global rule is applied. Given invalid inputs (e.g., P not in 0–100, negative amounts), When saving settings, Then validation prevents save and displays specific errors. Given a deposit amount is calculated, When the client opens the link, Then the exact deposit amount and rule basis (flat/percentage) are displayed prior to payment confirmation.
Capture vs Authorization Policy
Given the merchant selects Immediate Capture, When the client approves payment, Then the deposit is captured immediately. Given the merchant selects Authorization Only with auto-capture at service start, When the client approves payment, Then an authorization hold is placed for the deposit amount and is auto-captured at the scheduled start time in the merchant’s timezone. Given an authorization is older than the configured preauth validity window or the issuer releases the hold, When auto-capture is attempted, Then the system performs a re-authorization; If re-authorization fails, it notifies the merchant and marks the booking as Deposit Pending. Given the payment method is Wallet or COF, When applying the policy, Then the same capture/authorization behavior is enforced consistently. Given a refund is initiated before capture on an authorization, When processed, Then the authorization is voided rather than refunded.
Deposit Link Expiry Windows
Given the merchant sets a deposit link expiry duration D (range 1 hour to 14 days), When a link is created at T0, Then the link expires at T0 + D using the merchant’s timezone. Given a client opens an expired link, When the link is accessed, Then the page clearly states the link is expired and offers a CTA to request a new link; No charge attempts occur. Given the expiry duration is changed, When existing links are accessed, Then they continue to honor the expiry set at creation time (policy version pinned). Given a link expires, When the event occurs, Then an audit log entry records link ID, policy version, expiry timestamp, and user who created it.
Auto-Charge Card-on-File (COF) Consent and Controls
Given the Auto-Charge COF setting is Enabled and a valid COF token with explicit client consent timestamp exists, When a deposit is requested, Then the cascade may charge the COF at the COF step without client interaction. Given the Auto-Charge COF setting is Disabled or no consent is on record, When a deposit is requested, Then the COF step requires client confirmation via link or is skipped per cascade configuration. Given per-service overrides for auto-charge, When the deposit is for that service, Then the override value is applied; Else the global setting is applied. Given any COF charge is attempted, When recorded, Then the audit log includes token reference, consent source, consent timestamp, policy version, and outcome; No full PAN or CVV is stored. Given a COF auto-charge fails with a retryable error, When the cascade proceeds, Then it attempts the next method and notifies the merchant of the failure.
Versioning and Auditability of Policy Changes
Given a settings change is saved, When the save completes, Then a new policy version is created with version ID, changed fields (old → new), user ID, timestamp (UTC), scope (global/service), and optional reason. Given a new policy version exists, When a deposit link is created, Then the link is tagged with that policy version and will continue using it for its lifecycle. Given audit logs are requested for a date range, When exported, Then a CSV is generated containing all policy changes and key deposit events with immutable records. Given a historical policy version is viewed, When accessed, Then the system provides a read-only snapshot of settings and effective dates; Historical versions cannot be edited or deleted. Given an unauthorized user attempts to change settings, When they submit, Then the change is rejected with an audit entry for the denied attempt.
Conversion Analytics and Reporting
"As a business owner, I want visibility into which payment methods convert best so that I can optimize my deposit settings and messaging."
Description

Tracks and surfaces end-to-end funnel metrics: wallet availability rate by device/browser, success by cascade step, fallback paths taken, time-to-completion, and failure reason codes. Exposes merchant-facing dashboards and CSV export, and emits structured events to internal analytics for cohort analysis. All metrics are privacy-safe and exclude sensitive card data.

Acceptance Criteria
Wallet Availability Rate by Device/Browser
Given an end user opens a PawPilot deposit link When the page loads Then the system detects Apple Pay/Google Pay availability via platform APIs and records session_id, merchant_id, device_type (mobile/desktop), os_name/version, browser_name/version, wallet_availability (true/false), country, and timestamp And Then bot/crawler sessions are excluded using user-agent and behavioral heuristics (e.g., headless UAs, no interaction within 1s) And Then the aggregated metric wallet_availability_rate = available_sessions / eligible_sessions is computed per merchant, device_type, os_name/version, browser_name/version, and day (merchant timezone) And Then the dashboard displays counts and rate with two-decimal precision and filters for date range (up to 90 days), merchant, device_type, os, and browser And Then data freshness for this metric is <= 15 minutes at p95 And Then no PAN, CVV, or full billing address is collected or stored in any event or dashboard
Cascade Outcomes and Fallback Paths
Given a user interacts with Tap Cascade to pay a deposit When payment completes or the session times out (30 minutes of inactivity) Then the system attributes the final outcome to cascade_step in {wallet, card_on_file, secure_card_entry} and records the ordered path of attempts (e.g., wallet>card_on_file) And Then attempts, successes, success_rate, and drop-off rate are computed by cascade_step; denominators use exposures per step (users who reached that step) And Then the dashboard displays counts, success_rate, drop-off rate by step, and the top 10 fallback paths for the selected date range And Then retries within the same step are deduplicated; attempt_seq is monotonic per session And Then metrics are partitioned by merchant and device_type and become queryable within 15 minutes p95
Time-to-Completion Measurement
Given a deposit link session starts at the first page view When payment is confirmed Then time_to_completion_ms = confirmation_timestamp - first_view_timestamp is recorded using server-side timestamps And Then distributions per merchant and cascade_step compute p50, p90, p95, max, and mean, excluding sessions exceeding a 30-minute inactivity window And Then the dashboard shows these percentiles with filters for date range, device_type, browser, and cascade_step And Then 95% of measurements are accurate within ±200ms compared to server logs And Then data freshness for percentile aggregates is <= 15 minutes p95
Failure Reason Codes Normalization and Reporting
Given a payment attempt fails at any cascade step When the processor or wallet returns a raw error code/message Then the system maps it to canonical_reason in {insufficient_funds, authentication_failed, network_error, issuer_declined, expired_card, timeout, user_canceled, unknown} and stores the raw code only in internal logs And Then the dashboard reports failures by canonical_reason, cascade_step, device_type, and browser with counts and failure_rate = failures/attempts per step And Then the unknown mapping rate is <= 1% over a rolling 7-day window; if exceeded, an alert is emitted to ops And Then no sensitive card data (PAN, CVV) appears in events, dashboards, or exports
Merchant Dashboard Visualization and Filters
Given a merchant is authenticated in PawPilot When they open Conversion Analytics Then they see widgets for wallet availability rate, success by cascade step, fallback paths, time-to-completion percentiles, and failure reasons for the selected date range And Then filters include date range (last 7/30/90 days, custom), device_type, OS, browser, and cascade_step; applying filters updates all widgets consistently And Then the dashboard loads within 2 seconds at p90 for up to 50k sessions in range; individual widget queries return within 1.5 seconds at p90 And Then the dashboard persists last-used filters per user and respects the merchant’s timezone setting for date bucketing And Then empty states render with clear messaging when no data matches filters
CSV Export Parity and Performance
Given a merchant has applied filters on the Conversion Analytics dashboard When they request a CSV export Then a CSV is generated with columns: date, merchant_id, device_type, os_name, os_version, browser_name, browser_version, wallet_availability_rate, step_attempts, step_successes, step_success_rate, top_fallback_path, failures_by_canonical_reason, p50_time_ms, p90_time_ms, p95_time_ms And Then exported aggregates match on-screen values for the same filters and date range within 0.1% tolerance And Then exports up to 1,000,000 rows complete within 60 seconds at p90 and are delivered via a secure link that expires after 15 minutes And Then timestamps are ISO 8601 in the merchant’s timezone; numeric fields use dot decimal and no thousand separators And Then the CSV contains no sensitive card data or PII beyond merchant_id and high-level device/browser metadata
Internal Analytics Event Emission and Schema
Given user interactions with the Tap Cascade payment flow When key milestones occur Then events are emitted with schemas: - payment_link_opened{event_version, session_id, merchant_id, device_type, os, browser, tz, ts} - wallet_availability_detected{event_version, session_id, wallet_available:boolean, detection_method, ts} - payment_attempted{event_version, session_id, step, attempt_seq:int, ts} - payment_succeeded{event_version, session_id, step, amount_cents:int, currency:ISO4217, ts} - payment_failed{event_version, session_id, step, canonical_reason, raw_code, ts} - cascade_path_finalized{event_version, session_id, path:string, outcome:{succeeded|abandoned}, ts} And Then events are delivered to the data pipeline within 5 minutes at p95, are idempotent (dedup by session_id+event_type+attempt_seq), and validated for required fields/types And Then invalid events are rejected with error logs and metrics; retries use exponential backoff And Then no sensitive card data is included; amounts are recorded only as integer cents and ISO currency
Webhook and Appointment State Sync
"As a scheduler, I want deposits to update appointment records and notify clients automatically so that my calendar stays accurate without manual follow-up."
Description

Processes payment success, failure, and refund webhooks to update appointment records, deposit status, and waitlist moves in real time. Sends SMS receipts and confirmations, handles idempotency and late/out-of-order events, and reconciles authorization-to-capture transitions based on merchant policy. Ensures the calendar, client thread, and financial records remain consistent without manual intervention.

Acceptance Criteria
Payment Success Webhook Confirms Appointment and Sends Receipt
Given an appointment with status "Pending Deposit" and an associated deposit request And a valid payment success webhook is received for the exact appointment and amount And the webhook signature and payload schema validate When the event is processed Then the appointment status updates to "Confirmed" And depositStatus updates to "Paid" with amount, currency, paymentMethod, transactionId, and processedAt And the appointment appears as "Confirmed" on the calendar within 5 seconds of webhook receipt And a ledger entry is recorded for the deposit with transactionId and no duplicates And an SMS receipt is sent to the client within 10 seconds including amount, method (masked), appointment datetime, and remaining balance And the HTTP response to the provider is 2xx within 2 seconds And if the same eventId is reprocessed, no additional SMS, ledger entries, or state changes occur
Payment Failure Webhook Reopens Slot and Advances Waitlist
Given an offered appointment hold exists for Client A with status "Pending Deposit" and a waitlist with ranked candidates And a payment failure webhook is received referencing Client A's attempt And the webhook signature and payload schema validate When the event is processed Then the appointment hold for Client A is released and marked "Deposit Failed" And the next waitlist candidate is invited via SMS within 30 seconds with a fresh payment link And Client A receives an SMS failure notice with a retry link And calendar availability reflects the open slot within 5 seconds And processing is idempotent across duplicate failure events
Refund Webhook Updates Deposit and Financial Records
Given an appointment with depositStatus "Paid" and a ledger entry linked to transactionId And a refund webhook is received (full or partial) for that transactionId And the webhook signature and payload schema validate When the event is processed Then depositStatus updates to "Refunded" for full refunds or "Partially Refunded" with refundedAmount recorded And a refund ledger entry is created with negative amount and reference to the original transactionId And an SMS refund receipt is sent to the client within 10 seconds with refunded amount and settlement estimate And the appointment and calendar state update per merchant policy: "Canceled" if cancellation-triggered, otherwise unchanged And processing is idempotent with no duplicate refunds recorded
Out-of-Order and Late Events Apply Only Valid Forward Transitions
Given the system stores appointment state versions with updatedAt timestamps and lastProcessedEventId And events may arrive late or out of order When an event is received whose occurredAt is earlier than the current appointment updatedAt or is a stale transition (e.g., auth.created after capture.succeeded) Then the event is recorded for audit but produces no state change or SMS And a reconciliation job runs within 2 minutes to verify final state consistency using the complete event history And the provider receives 2xx to prevent retries, unless schema/signature invalid
Authorization-to-Capture Reconciliation Follows Merchant Policy
Given a merchant policy of either "Auto-Capture on Booking" or "Capture at Check-In" And an authorization webhook is received for the deposit amount And the webhook signature and schema validate When policy is "Auto-Capture on Booking" Then the system attempts capture immediately and, upon capture success, sets depositStatus "Paid"; upon capture failure or hold expiry, reverts the appointment to "Pending Deposit" and notifies client and merchant When policy is "Capture at Check-In" Then the system records depositStatus "Authorized", tracks authorization expiry, and sends a capture task to the groomer at check-in And if the authorization expires before capture, the client receives an SMS to re-authorize and the appointment returns to "Pending Deposit" And all state changes reflect on the calendar and in the client thread within 5 seconds
Idempotent Processing Ensures Exactly-Once Side Effects
Given webhook deliveries may repeat for up to 7 days And each event includes a unique eventId and payload hash When an already-processed eventId with identical hash is received Then the system responds 2xx and performs no side effects When an eventId is repeated with a different payload hash Then the event is quarantined, flagged for manual review, and no side effects occur And idempotency keys and processing audit records are retained for at least 14 days
Webhook Security: Signature Verification and Schema Validation
Given all incoming webhooks include a provider signature and timestamp When the signature is missing, invalid, or the timestamp skew exceeds 5 minutes Then the request is rejected with 4xx and no state changes, SMS, or ledger writes occur And rate limiting is applied per source IP/provider key to 60 requests per minute with a burst of 120 And all accepted events are logged with eventId, appointmentId, type, and processing outcome without storing raw PAN or sensitive card data

Waitlist Cascade

When a hold expires, PawPilot auto-texts the next best-fit waitlister in the same thread with a fresh deposit timer, cascading until filled. Cancellations convert to bookings in minutes without intervention.

Requirements

Expiry Trigger & Cascade Orchestrator
"As a groomer, I want cancellations to auto-offer the slot to the next best client so that my calendar refills without manual texting."
Description

Monitors active holds and, upon timer expiry, automatically triggers a cascade that offers the slot to the next waitlisted client in the same SMS thread with a fresh deposit timer. Manages the offer lifecycle (offer sent, accepted, expired, withdrawn), retries with backoff, and ensures idempotent state transitions to avoid double-bookings. Integrates with scheduling, waitlist, messaging, and payments services; respects quiet hours and business rules; and continues cascading until the slot is filled or candidates are exhausted.

Acceptance Criteria
Cascade starts on hold expiry
Given an active hold reaches its expiry time And the associated waitlist has at least one eligible candidate When the expiry is detected by the orchestrator Then an SMS offer is sent to the top-ranked eligible waitlister in the same SMS thread as prior communications with that client And the message includes a unique deposit link and a countdown timer set to the configured duration And the offer lifecycle state is set to Offer Sent and persisted And the slot remains temporarily reserved for the offer duration
Booking confirmed on deposit within timer
Given an outstanding offer with a valid deposit link and active timer When the waitlister completes the deposit payment before the timer expires Then the slot is booked for that waitlister in the scheduling service And the offer lifecycle state transitions to Accepted then Booked And all other outstanding offers for the slot are withdrawn and notified via SMS And the cascade process stops for this slot And payment is captured and associated to the new booking
Auto-advance on offer expiry
Given an outstanding offer with a running timer When the timer expires without deposit Then the offer lifecycle state transitions to Expired and is persisted And the deposit link becomes invalid immediately And the next eligible waitlister is offered the slot within 60 seconds with a fresh timer And the cascade history records the transition from previous candidate to next
Respect quiet hours for outbound offers
Given business quiet hours are configured for the location And an offer needs to be sent during quiet hours When a hold expires within quiet hours Then no outbound SMS is sent during quiet hours And the offer is queued to send at the first minute after quiet hours end in the business’s timezone And the deposit timer starts at actual send time, not queue time
Idempotent transitions prevent double-bookings
Given duplicate or concurrent expiry events may be received for the same slot When multiple orchestrator workers attempt to process the same transition Then at most one Offer Sent is created for the next candidate due to idempotency keys/locks And at most one Booked state is committed for the slot across services And any losing attempts are safely no-ops with clear logs And if the slot becomes booked by any other channel, all outstanding offers are withdrawn within 60 seconds and the cascade halts
Retry with exponential backoff on transient failures
Given a transient failure occurs when sending an SMS offer or updating external services When the operation is marked retriable (e.g., 5xx, timeouts) Then the orchestrator retries with exponential backoff (e.g., ~1m, ~2m, ~5m, ~10m, ~20m) up to a maximum of 5 attempts or a 30-minute TTL, whichever comes first And duplicate effects are prevented via idempotency And on exhausting retries, the candidate is marked Failed-Send, skipped, and the cascade proceeds to the next eligible candidate And all failures and retries are logged with correlation IDs
Terminate cascade when waitlist exhausted
Given all waitlisted candidates have been processed (accepted, expired, withdrawn, or failed) When no further eligible candidates remain Then the orchestrator marks the slot as Unfilled and stops the cascade And no additional SMS messages are sent for this event And the slot remains available for manual fill per business rules, with a notification recorded for staff
Best-Fit Waitlister Scoring
"As a sitter, I want the system to offer openings to the most compatible clients first so that I reduce back-and-forth and maximize successful bookings."
Description

Ranks waitlisted clients using a configurable, explainable scoring model that considers service type, pet profile and size, client availability windows, distance/zone, historical responsiveness, no-show risk, deposit compliance, and VIP status. Supports weighted attributes, service-specific rules, and deterministic tie-breakers with FIFO fallback. Exposes scoring rationale for auditability and integrates with customer data and scheduling constraints to produce a prioritized candidate list for each cascade step.

Acceptance Criteria
Same-Day Grooming Cancellation Prioritization
Given hold H1001 for service "Grooming" on 2025-08-16 14:00–16:00 in Zone A, pet size "Medium", groomer capacity supports Medium, waitlist fixture WL-GROOM-01 with 1,000 candidates, and config profile "groom-default-v1" When the scoring engine executes for cascade step 1 with seed 42 Then candidates failing hard constraints (service mismatch, zero availability overlap, pet size unsupported, outside allowed zone A) are excluded prior to scoring And the eligible candidate count equals 237 And the top 5 candidate IDs equal [C104, C087, C223, C019, C311] with scores [0.862, 0.813, 0.802, 0.784, 0.776] ±0.005 And scores are non-increasing for all ranks And p95 scoring latency is ≤ 300 ms for 1,000 candidates on standard tier
Service-Specific Weighting for Dog Walking Requests
Given a "Dog Walking" cancellation hold H1102 on 2025-08-17 11:00–11:30 in Zone B, waitlist fixture WL-WALK-02, and config profile "walk-default-v1" with weights: distance=0.30, availability_overlap=0.30, responsiveness=0.20, no_show_risk=-0.15, vip=+0.05 When the scoring engine executes for cascade step 1 with seed 7 Then the applied weight profile ID equals "walk-default-v1" And the top 3 candidate IDs equal [W055, W014, W102] And rerunning with config profile "groom-default-v1" yields a different top-3 ordering than [W055, W014, W102] And each top-3 candidate’s audit breakdown shows attribute contributions that sum to the final score within ±0.001
Deterministic Tie-Breaking with FIFO Fallback
Given fixture WL-TIE-01 where candidates T001, T002, T003 compute equal final scores to 3 decimal places under profile "groom-default-v1", and tie-break rules [VIP desc, last_response_recency desc, waitlist_join_ts asc] When scoring completes Then the ordering equals [T003, T001, T002] per the tie-break rules And rerunning with the same inputs and seed yields the same order And changing only T001.waitlist_join_ts to an earlier timestamp reorders to [T001, T003, T002] while final scores remain equal
Explainable Scoring Audit Trail Exposure
Given candidate C104 from WL-GROOM-01 and hold H1001 When requesting rationale via GET /scoring/rationale?candidateId=C104&holdId=H1001 Then the response body includes fields [candidateId, holdId, serviceProfileId, inputs, weights, contributions, exclusions, finalScore, version, computedAt] And sum(contributions.values) equals finalScore within ±0.001 And inputs include signals for service match, pet profile/size, availability overlap (minutes), distance (miles), responsiveness percentile, no-show risk percentile, deposit compliance flag, VIP status And p95 response time is ≤ 250 ms
Availability, Distance, and Scheduling Constraints Pre-Filter
Given schedule fixture CAL-01, hold H2002 for 2025-08-20 09:00–10:00, allowed zones [A, C], max_distance=8 miles, and waitlist WL-MIX-03 with 500 candidates When the pre-filter step executes prior to scoring Then only candidates with availability overlap ≥ 20 minutes are retained And only candidates within allowed zones or within 8 miles of the service location are retained And the eligible count equals 142 before scoring And eliminated candidates are logged with reasons in [NoAvailabilityOverlap, OutOfZone, PetSizeUnsupported, ServiceMismatch]
Behavioral Signals: Responsiveness, No-Show Risk, Deposit Compliance
Given waitlist WL-MIX-04 with candidates B001, B002, B003 and config profile "groom-default-v1" with behavioral weights responsiveness=0.20, no_show_risk=-0.20, deposit_compliance_penalty=-0.10 And hold H3003 requires a deposit (deposit_required=true) When scoring executes Then a candidate with responsiveness percentile 90 vs 50 gains ≥ 0.08 score advantage, all else equal And a candidate with no-show risk percentile 80 vs 20 incurs ≥ 0.12 score penalty, all else equal And a candidate with deposit delinquency in the last 90 days is excluded when deposit_required=true And the final ordering equals [B001, B003] because B002 is excluded for delinquency When rerun with deposit_required=false Then B002 is included with a -0.10 ± 0.005 score penalty and the ordering equals [B001, B002, B003]
Single-Thread SMS Messaging & Compliance
"As a client, I want to receive offers in the same text thread I already use so that it feels natural and I can reply quickly."
Description

Delivers all cascade outreach within the client’s existing SMS thread using branded, template-driven messages with merge fields (service, date/time, price, deposit amount, timer). Implements carrier compliance (10DLC/A2P), opt-in/opt-out handling (STOP/HELP), per-carrier throttling, and localized content. Tracks delivery, clicks, and replies (YES/NO) to drive state transitions, with link shortener on a verified domain to preserve deliverability and trust.

Acceptance Criteria
Cascade outreach sent in existing SMS thread
Given a hold for an appointment expires and the next best-fit waitlister is identified When PawPilot sends the cascade outreach Then the message is sent from the same business A2P 10DLC long code assigned to that client And the message appears in the client’s existing SMS thread by using the same From/To numbers And the selected branded template renders with all required merge fields: {service}, {date_time_local}, {price}, {deposit_amount}, {timer} And if any required merge field is missing, the send is blocked and an error is logged with template_id and client_id And a unique deposit/timer token is generated per offer and embedded in the link And only one outreach message is sent per waitlister per offer (idempotent by offer_id + client_id)
A2P 10DLC compliance and opt-in/opt-out handling
Given the business brand and campaign are registered for A2P 10DLC When sending a cascade message to a client who is opted in Then the message includes the brand name and opt-out disclosure at least once in the conversation window And STOP immediately unsubscribes the client, sends a one-time confirmation, and prevents further outreach except compliance confirmations And HELP returns a branded help response with support contact and data rates disclaimer And messages are not sent to clients without recorded opt-in; such attempts are blocked and logged with reason = no_opt_in
Carrier-aware throttling and retry strategy
Given multiple cascade messages are queued across carriers When dispatching messages Then per-carrier throughput limits (TPS) configured for the campaign are enforced And messages are enqueued and released in FIFO order per recipient to preserve thread sequencing And 429/rate-limit or carrier 30001/30002 errors trigger exponential backoff with jitter and up to 3 retries And messages exceeding retry policy are marked failed with final_error_code and are not retried further
Verified-domain link shortener and click tracking
Given a deposit or booking URL must be included in the cascade message When generating the message Then the URL is shortened using the verified domain shortener (e.g., https://pwp.lt/…) And the short link resolves to an HTTPS destination on a verified domain And each click is recorded with message_id, client_id, offer_id, timestamp, and user agent where available And if the shortener is unavailable, the system falls back to the full verified-domain URL and records a shortener_unavailable event
Delivery and reply tracking driving state transitions
Given a cascade message has been sent When delivery webhooks arrive from the SMS provider Then message status is updated through states: queued → sent → delivered or failed with provider error code And failures with carrier filtering are logged and the offer proceeds to the next waitlister per cascade rules When the client replies within the active timer window Then YES confirms the booking, charges/holds the deposit, sends confirmation, and ends the cascade for this offer And NO declines and immediately advances the cascade to the next waitlister And unrecognized replies trigger a clarification template without advancing the cascade
Localized content, time, and currency formatting
Given the client has a saved locale and timezone When rendering the cascade message Then the message language matches the client locale, falling back to English if unsupported And date/time is formatted in the client’s timezone and locale style And {price} and {deposit_amount} are formatted in the client’s currency with symbol and correct decimals And YES/NO parsing supports localized equivalents (e.g., SI/NO) based on locale configuration
Deposit Timer & Payment Link Generation
"As a client, I want a secure, expiring deposit link so that I can quickly confirm a spot and trust my payment is applied correctly."
Description

Generates a unique, time-bound deposit link per offer with a visible countdown and secure tokenization. Supports Apple Pay/Google Pay and card payments, immediate capture, automatic expiration, and reconciliation to the booking. On payment, updates booking state and cancels competing offers; on expiration, reclaims the slot for the next cascade step. Includes anti-abuse protections, signature validation, and webhooks for real-time status updates.

Acceptance Criteria
Unique Time-Bound Deposit Link Generation with Visible Countdown
- Given an offer is created, when a deposit link is generated, then a unique, single-use URL with a cryptographically secure token is created and bound to the offer ID and recipient phone. - Given the link is generated, then it includes an expiration timestamp equal to the configured hold duration and is not more than 1 second skewed from server time. - Given the checkout page loads, then a visible countdown timer displays remaining time and stays within +/- 2 seconds of server time over its duration. - Given a new link is generated for the same offer before expiration, when the old link is opened, then it is invalidated and returns 410 Gone or redirects to the latest valid link without exposing payment. - Given a link token is tampered with or expired, when accessed, then payment UI is not shown and a non-payable expired message is shown.
Payments with Immediate Capture (Apple Pay/Google Pay/Cards)
- Given a compatible device and a valid link, when Apple Pay or Google Pay is selected, then the native payment sheet opens and on success the charge is captured immediately (not auth-only) and a success receipt is displayed within 3 seconds of confirmation. - Given any device and a valid link, when card details are entered, then inputs are validated (Luhn, CVC, ZIP, expiry) and 3DS is performed when required; on success, funds are captured immediately and a success receipt is shown. - Given a payment attempt fails or is canceled, then a clear error is shown, no booking state changes occur, and the link remains usable until it expires.
Automatic Link Expiration and Slot Reclamation
- Given the countdown reaches zero or server time exceeds expiration, when the link is loaded or refreshed, then payment is blocked and the page states the offer has expired. - Given an offer expires without payment, then the slot is released to the cascade engine within 2 seconds and a new offer is issued to the next waitlister. - Given an expired link is accessed, then it cannot be used to pay and returns 410 Gone with no payment form.
Payment Success Updates Booking and Cancels Competing Offers
- Given a payment succeeds for an offer, when the confirmation is received, then the booking state transitions to Confirmed (or Held by Deposit per config), deposit amount is recorded, and timestamps are persisted. - Given competing active offers exist for the same slot, when the first successful payment is recorded, then all competing offers are canceled within 2 seconds, their links are invalidated, and recipients receive an unavailability notice. - Given two payments are attempted concurrently for the same slot, then only the first succeeds; subsequent attempts are declined or auto-refunded and do not change booking state.
Anti-Abuse and Signature Validation
- Given a deposit link is generated, then the token contains a signed payload (offer ID, recipient identifier, expiry) and the server validates signature and expiry on every request. - Given repeated failed payment or access attempts exceed threshold (e.g., >10/min per IP or token), then further requests are throttled with 429 and no state changes occur. - Given a token is replayed after successful payment or invalidation, then the server returns 410 Gone and no sensitive data is exposed in responses or URLs. - Given any request, then all transport is HTTPS/TLS 1.2+ and tokens are opaque and contain no PII in plaintext.
Webhooks and Reconciliation
- Given payment, cancellation, or expiration events occur, when processor webhooks arrive, then signatures are verified, idempotency is enforced, and state is updated exactly once. - Given a successful payment, then a reconciliation record links processor charge/intent ID to the booking and customer, including gross, fees, net, currency, and timestamps. - Given webhooks are delayed or missed, then a scheduled job polls the processor and backfills missing updates without duplicating records. - Given a webhook fails verification, then it is rejected, logged with error details, and an alert is raised.
Cascade Rules, Quiet Hours, and Rate Limits
"As a groomer, I want control over how aggressively the system cascades so that I respect clients and avoid over-messaging."
Description

Provides configurable business rules for cascade behavior, including max attempts, delay between attempts, quiet hours by timezone, day-of-week constraints, per-contact cooldowns, VIP prioritization, and skip lists. Supports manual pause/resume and override of existing holds. Enforces rate limits to avoid over-messaging and coordinates across simultaneous cancellations to prevent client spam and offer collisions.

Acceptance Criteria
Max Attempts and Inter-Attempt Delay
Given a cancellation hold expires and cascade rules are set to maxAttempts=3 and delayBetweenAttempts=10m And there are at least 5 eligible waitlisters When the cascade starts Then the system sends the first offer immediately And waits 10m (±30s) before sending the next attempt if the slot remains unfilled And sends no more than 3 total attempts for this cancellation And stops sending further attempts immediately when any recipient pays the deposit or the hold is manually filled And records attempt_count and timestamps for each attempt in the audit log
Quiet Hours Enforcement by Business Timezone
Given the business timezone is America/Los_Angeles and quietHours are 21:00–07:00 And the next attempt is scheduled for 22:15 local time When the scheduler evaluates the send time Then the attempt is deferred to 07:00 local time on the next calendar day And no message is sent between 21:00 and 07:00 regardless of server timezone And DST transitions are respected using the business timezone rules
Day-of-Week Messaging Constraints
Given allowedSendDays are Mon–Fri and startOfDayTime=09:00 local time And the current local day is Saturday and an attempt would otherwise be due at 10:00 Saturday When evaluating constraints Then the attempt is rescheduled to 09:00 Monday in the business timezone And the reschedule does not increment attempt_count And the UI shows the next scheduled attempt time
Per-Contact Cooldown Enforcement
Given perContactCooldown=48h And contact A received a waitlist cascade offer 12h ago And contact B received a waitlist cascade offer 49h ago When selecting candidates for the next attempt Then contact A is skipped and not messaged And contact B remains eligible And the skip is logged with reason "cooldown_active" and remaining duration
VIP Prioritization and Skip List Application
Given the candidate pool includes VIP contacts V1 and non-VIP contacts N1, N2 And skipList contains contact N2 And ties are broken by last_response_time ascending When ordering the send sequence Then VIPs are placed before non-VIPs And contacts on the skipList are excluded from all attempts And among VIPs/non-VIPs, ties are resolved by last_response_time ascending And the final ordered list with positions is persisted for auditability
Manual Pause/Resume and Hold Override Behavior
Given a cascade is in progress with the next attempt scheduled in 5 minutes When a user clicks Pause on the cancellation card Then all pending scheduled attempts are canceled and no new cascade messages are sent until Resume And the UI displays status "Paused" with the paused timestamp When the user clicks Resume Then the cascade re-schedules the next attempt respecting quiet hours, day constraints, and cooldowns And when "Override existing holds" is enabled for this cancellation Then contacts with an active hold from this business are eligible and their prior holds are immediately voided with an audit entry before sending the new offer
Global Rate Limits and Collision Coordination Across Cancellations
Given businessRateLimit=1 cascade message/second and perContactDailyLimit=2 offers/24h And two cancellations C1 and C2 start cascades at the same time with overlapping candidate pools including contact X When the system schedules outbound messages Then no more than 1 cascade message is sent per second across the business And contact X receives at most 1 active offer at a time; if selected for both C1 and C2, only the higher-priority offer is sent and the other is queued until the first offer is accepted or expires And higher priority is determined by earlier cancellation creation time; if tied, by service priority And a contact does not receive more than 2 cascade offers in any rolling 24h window And all suppressed or queued messages record reason codes (rate_limit, active_offer_collision, daily_limit) in the audit log
Auto-Confirm & Calendar Sync
"As a dog walker, I want the slot to confirm automatically once a deposit is paid so that I don’t have to intervene and can trust my schedule is accurate."
Description

On successful deposit, automatically confirms the appointment, removes the client from the waitlist, withdraws any in-flight offers to other clients, and sends confirmations to both provider and client. Updates internal schedules and syncs with external calendars (Google/Apple) and resource allocation to prevent double-booking. Triggers reminders and follow-up workflows and guarantees atomicity across systems to keep state consistent.

Acceptance Criteria
Auto-Confirm and Notifications on Successful Deposit
Given a pending waitlist hold for a specific time slot and service When the client completes the deposit and the payment provider returns a success signal Then the appointment status changes to Confirmed within 5 seconds And confirmation SMS messages are sent to the client and provider within 10 seconds including date, time, service, location, and manage link And the booking record stores the payment transaction ID and deposit amount
Waitlist Removal and In-Flight Offer Withdrawal
Given a client’s deposit initiates the confirmation for a held slot When the confirmation is committed Then the client is removed from the waitlist for that slot and service And all open offers for the same slot to other waitlisters are automatically withdrawn within 10 seconds And previously issued hold links for that slot become invalid and return an expired/withdrawn message And withdrawn recipients receive a decline SMS
Internal Schedule Update and Resource Lock
Given an appointment is confirmed via auto-confirm When internal scheduling updates execute Then the provider and required resources are locked for the appointment window And any attempt to create an overlapping booking for those resources is rejected with a clear error And the internal availability view reflects the occupied slot within 5 seconds
External Calendar Sync with Atomic Commit
Given the provider has connected Google and/or Apple calendars When the appointment confirmation process runs Then a corresponding external calendar event is created or updated within 15 seconds with accurate start/end time, title, and notes And the system captures the deposit only after external event creation succeeds And if external event creation fails after 3 retries within 60 seconds, the confirmation is aborted, the booking remains unconfirmed, any payment authorization is voided, and both client and provider are notified via SMS
Reminders and Follow-Up Workflow Scheduling
Given an appointment becomes Confirmed at time T When workflow scheduling executes Then reminder SMS jobs are scheduled at provider-configured offsets (e.g., 24h and 2h before T) and a follow-up at T+2h And these jobs appear in the job queue within 30 seconds with correct recipient, template, and send times And rescheduling or canceling the appointment automatically updates or cancels the related jobs within 15 seconds
Concurrency Control for Simultaneous Deposits
Given multiple waitlisters receive offers for the same slot When more than one client completes the deposit Then the system deterministically selects the winner by earliest successful deposit timestamp And only the winner proceeds to confirmation; other deposits are not captured and those clients receive a decline SMS And no double-booking occurs for the slot or resources
Idempotency and Duplicate Event Handling
Given the payment provider or calendar APIs send duplicate or out-of-order webhook/events When the system processes these events Then the confirmation workflow is idempotent: no duplicate confirmations, charges, SMS, or calendar events are created And the booking state remains consistent and correct And a single audit log entry with a correlation ID represents the committed transaction
Audit Trail & Performance Analytics
"As a business owner, I want to see how well the cascade performs so that I can optimize my messaging and policies."
Description

Captures an end-to-end audit log of cascade events (trigger, offers, replies, payments, expirations, withdrawals) with timestamps and reasons. Provides dashboards and exports for key KPIs such as time-to-fill, conversion per step, offer-to-pay latency, revenue recaptured, and opt-out rates. Enables A/B testing of SMS copy and timer durations, segmented by service type and client cohort, to optimize cascade performance while maintaining privacy controls.

Acceptance Criteria
End-to-End Cascade Audit Log Capture
Given a cancellation triggers a waitlist cascade, When the system processes trigger, offer_sent, reply_yes, reply_no, deposit_paid, hold_expired, client_withdrawn, and booking_confirmed events, Then each event is recorded in the audit log with fields: cascade_id, event_id (UUIDv4), event_type, event_timestamp (UTC ISO-8601), actor (system/client/staff), target_client_id (nullable), reason_code (from controlled list), waitlist_rank_at_offer (nullable), service_type, booking_id (nullable), message_template_id (if applicable), previous_event_id, and metadata_checksum. Given an SMS provider error occurs, When retries execute, Then the audit log includes an error event with provider_error_code, retry_count, final_status (sent|failed), and elapsed_ms. Given a retry or duplicate webhook is received, When an existing idempotency_key is detected, Then no duplicate audit row is created and a deduplicated flag is appended to the existing event. Given events are persisted, When timestamps are validated, Then the difference between server_received_at and event_timestamp is ≤ 200 ms for 99% of events over a 24-hour window. Given reason codes are stored, When a withdrawal or expiration occurs, Then reason_code is one of: client_changed_mind, slot_no_longer_needed, hold_expired, payment_failed, staff_canceled, or other_with_note, and other_with_note contains ≤ 140 chars.
KPI Dashboard: Time-to-Fill and Conversion Funnel
Given a user selects a date range, service type(s), and cohort filter, When the KPI dashboard loads, Then it displays cards and charts for time_to_fill (median, P90), conversion per step (offer→reply_yes, reply_yes→deposit_paid, deposit_paid→booking_confirmed), and opt_out_rate with absolute counts and percentages. Given new cascade events arrive, When the user refreshes, Then dashboard metrics reflect events within 2 minutes (P95 freshness ≤ 120 seconds) and show last_updated_at. Given a sample dataset of known outcomes, When the dashboard computes KPIs, Then values match reference SQL queries within ±0.1% relative error. Given the funnel is displayed, When the user clicks a stage, Then a drill-down table of underlying cascades appears with links to each cascade’s audit trail. Given the user hovers a chart, When tooltip appears, Then it shows numerator/denominator, filter set, and timezone.
Offer-to-Pay Latency and Revenue Recaptured Metrics
Given offers and payments occur, When calculating offer_to_pay_latency, Then latency is computed as deposit_paid.event_timestamp minus offer_sent.event_timestamp per candidate and summarized as median, P75, and P90 for the selected filters. Given bookings are filled via cascade, When calculating revenue_recaptured, Then revenue_recaptured equals the sum of paid amounts for bookings that originated from a cascade within the selected window, excluding refunds, and is broken out by service_type. Given timezones vary, When presenting distributions, Then all times are displayed in the account’s business timezone while stored in UTC; timezone used is shown in the UI. Given refunds occur after initial payment, When metrics recalculate, Then revenue_recaptured and conversion rates reflect net refunds within 15 minutes of refund event ingestion.
A/B Testing Framework by Service Type and Client Cohort
Given an experiment with variants A/B/n is configured with allocation ratios and eligibility (service types and client cohorts), When cascades trigger, Then eligible offers are randomly assigned to a variant according to ratios with per-client sticky assignment within a cascade. Given assignment occurs, When events are logged, Then audit entries include experiment_id and variant_id for exposure (offer_sent) and outcomes (reply_yes/no, deposit_paid, booking_confirmed). Given the dashboard loads, When experiment results are shown, Then per-variant conversion rates, confidence intervals (Wilson, 95%), and lift vs baseline are displayed; significance is flagged when two-proportion z-test p-value < 0.05 and min sample size per variant ≥ 200 offers. Given segmentation by service type and cohort is applied, When counts are below minimum sample size threshold (configurable), Then results are labeled “insufficient data” and significance is not computed. Given an adverse outcome, When opt_out_rate for any variant exceeds baseline by >2x with ≥ 50 opt-outs, Then the system auto-pauses that variant, sends an alert to admins, and logs a guardrail_triggered event.
Export and Data Retention with Privacy Controls
Given a user with export permission selects filters, When requesting CSV export, Then a file is generated including audit fields and KPI summaries for the selection, excluding full phone numbers and message bodies by default; client identifiers are pseudonymous (hashed) unless explicit PII access is granted. Given an export is generated, When a download link is created, Then the link expires after 24 hours, is single-use, and all accesses are logged with user_id, ip, timestamp. Given data retention policy of 24 months, When an event exceeds retention, Then PII fields (names, phone numbers) are purged or irreversibly hashed while aggregate metrics remain intact; a retention_purged flag is set on affected rows. Given a data subject deletion request, When processed, Then the subject’s PII is removed from analytics and audit exports within 7 days while preserving aggregated counts; a privacy_action event is logged. Given filters are applied, When export completes, Then exported row counts and KPI totals match dashboard values for the same filters within ±0.1%.
Role-Based Access and PII Redaction in Analytics
Given role permissions, When accessing analytics, Then Admins can view raw audit logs and exports; Staff can view dashboards with redacted PII; ReadOnly can view aggregate KPIs only; unauthorized access returns 403 and is logged. Given PII redaction settings, When viewing dashboards or drill-downs without PII permission, Then phone numbers are masked except last two digits, client names are initialized (e.g., “Sam D.”), and free-text fields are suppressed; only controlled vocabulary reason codes are shown. Given access attempts to export with PII by a user lacking permission, When the request is made, Then the export excludes PII and the user is notified in-app of applied redactions. Given an admin audit, When viewing access logs, Then the admin can filter by user_id, resource, action, date range and export the access log as CSV.

Deposit Rules

Set deposit amounts and requirements by service, price tier, client status, or time window (e.g., 50% for new clients, flat $15 for morning hotspots). Rules apply automatically in each SMS link, keeping policies consistent.

Requirements

Rule Builder UI
"As a business owner, I want an easy way to configure deposit rules without code so that I can tailor policies by service and client type while keeping them consistent."
Description

Provide an admin-facing, no-code interface to create, edit, and preview deposit rules by service, price tier, client status (e.g., new, VIP, high no-show risk), and time window (e.g., mornings, weekends, peak dates). Supports percentage or flat deposits, minimums/maximums, rounding, currency, and effective date ranges. Includes condition selectors, logical operators (AND/OR), conflict resolution order, and a live preview that shows the deposit a client would see for a selected scenario. Integrates with existing service catalogs and client profiles, persists rules securely, and validates inputs to prevent conflicting or unsafe configurations.

Acceptance Criteria
Create and save a percentage-based deposit rule with multiple conditions
Given an admin with Rule Builder access and available service catalog and client statuses When they configure a rule with Service=Grooming: Full Bath, Price Tier=Standard, Client Status=New, Time Window=Weekday mornings (08:00–12:00), Deposit Type=Percentage (50%), Effective Dates=2025-09-01 to 2025-12-31, and Priority=1 Then the Save action is enabled and the rule saves successfully, appears in the rules list with all attributes, and a confirmation is shown Given the saved rule When reopening it for edit Then all fields load with the previously saved values Given the saved rule When the current date falls within its effective range Then the rule is available for evaluation in preview and downstream application
Configure flat deposit with min/max, rounding, and currency
Given a rule configured with Deposit Type=Flat (15), Currency=USD, Rounding=Nearest $1, Minimum=$10, Maximum=$50 When previewing against a $200 service Then the previewed deposit is $15 Given the same rule When previewing against a $12 service Then the previewed deposit is $10 due to the minimum and rounding Given the same rule When previewing against a $500 service Then the previewed deposit is $50 due to the maximum Given Currency=EUR and Rounding=Nearest €0.50 increments When a computed deposit equals €14.26 Then the displayed deposit rounds to €14.50
Apply logical operators and prioritize conflict resolution order
Given two rules, R1 with (Service=Dog Walk 30m AND Client Status=New) Priority=1, and R2 with (Service=Dog Walk 30m OR Client Status=VIP) Priority=2 When previewing for a New client booking Dog Walk 30m Then R1 matches first and is applied Given the same rules When the admin drags R2 above R1 to reorder Then priorities update accordingly and preview applies R2 Given multiple matching rules with equal priority When attempting to save Then the system blocks save and requires a unique evaluation order Given a scenario where no rules match When previewing Then the UI indicates no matching rule and the deposit displays as Not set without error
Live preview reflects computed deposit for selected client/service scenario
Given a selected client, service, price tier, and appointment time When the admin modifies any rule field that affects computation Then the preview updates within 300ms with the new deposit, currency, and rounding applied Given a matched rule in preview When the preview renders Then it displays the matched rule identifier, priority, and a breakdown of base price, calculation method (percent/flat), min/max adjustments, and final amount Given required preview inputs are incomplete When previewing Then the preview prompts for missing inputs and does not display a stale amount Given a rule with a future effective date When previewing a date before the start Then the preview indicates the rule is not yet effective and does not apply it
Validation prevents unsafe or conflicting configurations
Given Deposit Type=Percentage When the percentage is set outside 0–100 inclusive Then an inline error is shown and Save is disabled Given Minimum and Maximum are configured When Minimum exceeds Maximum Then an inline error is shown and Save is disabled Given Rounding increment and Currency are configured When the increment is non-positive or the currency is unsupported Then an inline error is shown and Save is disabled Given two rules have identical conditions, overlapping effective dates, and the same priority When attempting to save Then the system blocks save and prompts to adjust priority or date ranges Given a Flat deposit is configured for specific services with known prices When the flat amount exceeds the lowest selected service price Then the system warns and requires setting a Maximum not exceeding that price or reducing the flat amount before save
Integration with service and client data in selectors
Given the service catalog and client status taxonomy are available When opening the Rule Builder Then condition selectors load options within 1 second and support search and multi-select Given a rule references an archived service When opening the rule Then the service is labeled Archived and a warning prompts updating references before save Given the client status High no-show risk exists When selecting it in the Client Status condition Then it is accepted and evaluable in preview Given the catalog service is temporarily unavailable When opening selectors Then a non-blocking error is shown with a retry option, and Save is disabled until data loads successfully
Persist rules securely and restrict access to authorized admins
Given an authenticated Admin user When saving a rule Then the request is sent over TLS, a 2xx response is returned, and the rule persists and appears after reload Given a non-admin user When attempting to create or edit rules Then access is denied and creation/edit controls are disabled or hidden Given a network interruption during save When retrying Then the rule is saved exactly once (no duplicates) and a single success confirmation is displayed Given 100 existing rules When loading the rules list Then the list loads within 2 seconds and supports pagination or search to locate a specific rule
Rule Evaluation Engine
"As a groomer using PawPilot, I want deposits to be calculated automatically and consistently so that every client sees the correct amount without manual intervention."
Description

Implement a deterministic engine that evaluates all active deposit rules at link-generation and booking time to compute the required deposit. Supports priority weights, specificity matching, and tie-breakers (e.g., most specific rule wins, then highest priority). Handles both percentage and flat amounts, with caps/floors, tax inclusion settings, and currency rounding rules. Exposes an idempotent API to other services (SMS link builder, checkout, waitlist) and returns the computed deposit plus an explanation payload for audit and UI display. Optimized for low-latency evaluation (<50ms p95) to keep SMS flows responsive. Includes fallback defaults when no rules match and safe-degrade behavior if data is missing.

Acceptance Criteria
Deterministic Rule Selection with Specificity and Priority
Given multiple active rules match the same booking context (service, client status, time window) And one rule is more specific (matches more attributes) than the others When the engine evaluates the deposit Then it selects the most specific rule And if two or more rules share the same specificity, it selects the one with the highest priority weight And the selected rule and computed amount are identical across repeated evaluations with the same inputs And the explanation payload lists all considered rule IDs with their specificity and priority and identifies the winning rule
Mixed Percentage and Flat Deposits with Caps, Floors, and Tax Inclusion
Given a rule that specifies a percentage or flat deposit with a cap and floor and a tax inclusion setting (pre-tax or post-tax) And a booking subtotal and tax amount are provided When the engine evaluates the deposit Then it calculates the base amount according to the tax inclusion setting And applies the percentage or uses the flat amount, then enforces the cap and floor And returns the final deposit amount and flags which constraints (cap/floor) were applied in the explanation payload
Currency-Aware Rounding Across Currencies
Given the engine is configured with currency rounding rules per ISO currency (e.g., USD to 2 decimals, JPY to 0 decimals) And a computed deposit has fractional minor units When the engine rounds the deposit Then it rounds according to the currency's configured rule And the returned amount respects minor unit constraints and passes downstream payment validation And the explanation payload includes currency, rounding rule, and pre/post-rounded values
Idempotent API Response with Explanation Payload
Given a caller provides an idempotency key with a request containing identical inputs When the engine is invoked multiple times with the same idempotency key Then it returns HTTP 200 with identical response payloads (amount, currency, selectedRuleId, explanation) And it creates at most one persistent evaluation record for that idempotency key And the explanation payload includes the rule evaluation path, inputs used, ruleset version, and calculation steps
Performance: p95 Evaluation Latency Under Load
Given a representative production ruleset size and warmed caches And a sustained mixed workload at target QPS When evaluating 10,000 requests Then the server-side evaluation latency is <50ms at p95 And there are zero timeouts and an error rate ≤0.1% during the test window
Fallback Default and Safe-Degrade on Missing Data
Given no active rule matches or one or more optional attributes (e.g., client status) are missing When the engine evaluates the deposit Then it applies the configured fallback default rule And returns a 200 response with a deposit amount computed from the fallback And the explanation payload includes a safe-degrade flag and a human-readable reason describing missing data or no-match
Consistent Results at Link-Generation and Booking Time
Given the same booking inputs and unchanged active ruleset version When the engine evaluates at link-generation time and again at booking time Then both evaluations return the same deposit amount and selectedRuleId And the explanation payload includes a stable ruleset version identifier across both evaluations
Client & Time Context Service
"As an operator, I want reliable client and time context available to rules so that deposits adjust correctly for new clients and peak hours."
Description

Provide the contextual data required for rule conditions, including client status (new vs returning, VIP, no-show history), service metadata (category, price tier), and temporal windows (time of day, day-of-week, blackout dates, local holidays). Normalizes and caches attributes needed by the Rule Evaluation Engine with strict SLAs and fallback values. Includes APIs for real-time lookups and nightly jobs to classify clients based on recent activity and no-show patterns. Ensures time-zone correctness and daylight-saving handling so “morning hotspots” and peak windows evaluate accurately by location.

Acceptance Criteria
Real-time Context Lookup API SLA and Payload Contract
Given a valid client_id, service_id, location_id, and timestamp When GET /context/v1 is called with those parameters and a valid auth token Then respond with HTTP 200 within p95 ≤ 200ms and p99 ≤ 500ms for warm cache, and p95 ≤ 400ms on cold cache And availability over a rolling 30 days is ≥ 99.9% And the JSON payload includes: client.status in ["new","returning"], client.vip (boolean), client.no_show_count_last_12m (integer), client.no_show_risk (boolean), service.category (string), service.price_tier in ["budget","standard","premium"], time.local_timezone (IANA), time.local_datetime (ISO-8601 with offset), time.day_of_week (1-7), time.time_of_day_segment in ["morning","afternoon","evening","overnight"], time.is_peak_window (boolean), time.is_local_holiday (boolean) And the response includes correlation_id echoing request_id and X-Context-Schema-Version = "1.0.0" And any unknown/unsupported enum inputs return HTTP 400 with a machine-readable error code
Local Timezone and Daylight Saving Accuracy
Given timestamps for 2025-03-09T08:30:00 America/Los_Angeles (DST start), 2025-11-02T08:30:00 America/Los_Angeles (DST end), and 2025-08-15T08:30:00 America/Phoenix (no DST) When context is requested with UTC timestamps and location_id mapped to those timezones Then time.local_datetime reflects the correct local wall time and UTC offset without a one-hour drift And time.time_of_day_segment = "morning" for all three cases And morning hotspot window 08:00–11:00 local sets time.is_peak_window = true for these times And duplicate hour at DST end is disambiguated via ISO-8601 offset and time.utc_offset_minutes is present And unit tests cover ≥ 10 timezones including Europe/London and Australia/Sydney with 100% pass rate
Nightly Client Classification Job
Given a nightly batch window starting 02:00 local time per tenant location When the classification job runs Then client.status = "new" if completed_bookings_lifetime ≤ 1, else "returning" And client.vip = true if LTV ≥ 1000 or completed_bookings_last_12m ≥ 12 And client.no_show_risk = true if no_shows_last_90d ≥ 2 or no_shows_last_180d ≥ 3 And the job completes within 2 hours for 1,000,000 clients with success rate ≥ 99.9% And rerunning the job with identical inputs yields identical outputs (idempotent) And an audit record per tenant includes processed_count, updated_count, and error_count ≤ 0.1% with failed items retried and DLQ drained within 24 hours
Context Caching and Invalidation
Given cached entries with TTLs: client flags 24h, service metadata 7d, holiday calendars 30d When a client completes a booking, is marked VIP, or a no-show is recorded Then cache entries for that client are invalidated within 60 seconds and subsequent lookups reflect the change And cache hit rate for repeated lookups over 15 minutes is ≥ 85% And stale-while-revalidate does not serve data older than 5 minutes beyond TTL And holiday and timezone data refresh on schedule and after manual override without serving stale data beyond TTL
Fallbacks and Degraded Mode Behavior
Given upstream systems (CRM, holiday provider, service catalog) are partially unavailable When a context lookup is requested Then the service returns HTTP 200 with structurally valid payload and flags.degraded = true And missing attributes use safe defaults: client.status = "unknown", client.vip = false, client.no_show_count_last_12m = null, time.is_local_holiday = false And an error is logged with correlation_id and metrics incremented; an alert triggers if degraded responses exceed 1% for 5 consecutive minutes And upon recovery, subsequent responses automatically include fresh data without manual intervention
Rule Engine Integration for Deposit Evaluation
Given a deposit rule set: 50% for client.status = "new"; $15 flat for time.is_peak_window = true (08:00–11:00 local); otherwise $0 When the rule engine requests context for Client A (new) at 09:00 local and Client B (returning) at 14:00 local Then the context includes client.status and time.is_peak_window sufficient to evaluate rules And the evaluated deposit for Client A at 09:00 is 50% And the evaluated deposit for Client B at 14:00 is $0 And for Client C (returning) at 09:15 local, the evaluated deposit is $15 And contract tests validate field names, enums, and types with 100% pass before deployment
SMS Link Integration
"As a client receiving a booking text, I want the deposit amount to be clearly shown and applied in the link so that I can complete my booking without confusion."
Description

Automatically inject computed deposit requirements into all outbound SMS booking and payment links, including the amount, rationale snippet (optional), and clear call-to-action. Supports deep links that carry rule evaluation tokens to prevent tampering and ensure the same deposit is shown through the flow. Handles expired links, resends, and multi-appointment scenarios. Ensures copy is localized and compliant, with dynamic templates that adapt based on rule outputs (e.g., “50% deposit required for new clients”). Tracks impressions and conversions for analytics.

Acceptance Criteria
Inject Deposit Details Into Outbound SMS Links
Given a computed deposit result (amount, currency, optional rationale, CTA type) for an outbound booking or payment message When the SMS is generated Then the SMS body includes the deposit amount formatted for the client’s currency (ISO 4217) with two decimals and locale-appropriate symbol And if the deposit is a percentage, the text includes both the percent and calculated amount (e.g., "50% ($20.00)") And the rationale snippet is included only if provided and is <= 80 characters And the SMS includes a single clear CTA starting with "Pay deposit" or "Complete booking" that references the exact amount And the SMS contains exactly one clickable link to the booking/payment flow And opening the link displays the same amount and rationale as the SMS And the total SMS body length (including link and compliance text) is <= 320 characters
Deep Link Token Integrity and Consistency
Given an outbound SMS link containing a signed rule-evaluation token with deposit amount, currency, ruleId(s), appointmentId(s), clientId, tokenId, and expiration When the recipient opens the link or navigates across booking/payment steps Then the backend validates the token signature and expiration on each request And the UI renders the amount and currency from the token for all displays And any URL parameter attempting to override amount, currency, or ruleId is ignored in favor of token values And the payment request uses the exact amount and currency from the token And the same deposit amount is shown consistently across all steps in the flow And a "token_validation" event (success/failure) is logged with tokenId
Expired Link Handling and Reissue Flow
Given a deep link whose token is expired or invalid When the link is opened Then the user sees a clear "Link expired" message with the reason (expired/invalid) And a prominent CTA to "Request a new link" is shown And selecting the CTA triggers generation of a fresh token and sends a new SMS to the client’s phone number And the expired token cannot be used to submit payment or confirm booking And an analytics "link_expired" event is logged with tokenId, clientId, and appointmentId(s)
Resend SMS Preserves Evaluated Deposit
Given an original SMS with token T evaluated at time t0 with amount A and a TTL When the business or system resends the SMS within the token TTL Then the resent SMS reuses token T and displays the same amount A and rationale And after TTL expiration, resends generate a new token T2 and may reflect updated rules And all resends include correlation metadata to the original tokenId for analytics And analytics log a "link_resent" event with the resend reason (client request, delivery failure, automatic reminder)
Multi-Appointment Deposit Aggregation and Breakdown
Given a booking flow that includes multiple appointments/services sent via a single SMS link When the link is opened Then the page displays a total deposit equal to the sum of per-appointment deposits And a breakdown lists each appointment with its individual deposit and rule label (e.g., "New client 50%") And rounding is applied per appointment before summing, with totals accurate to two decimals And the CTA references the total deposit amount And the payment request charges the exact total deposit shown
Localization and Compliance Copy in SMS and Web Views
Given a client’s language and locale preferences are stored When generating the SMS and linked page Then the system uses the locale-appropriate template and number/currency formatting And dynamic rule-based phrases (e.g., "50% deposit required for new clients") render without placeholders or untranslated strings And if the locale is unsupported, fall back to en-US templates and formatting And required SMS compliance text (e.g., "Reply STOP to unsubscribe") is preserved And the combined SMS content remains within the configured character limit (<= 320 characters)
Analytics: Impressions and Conversions Tracking
Given an outbound SMS with a deep link token is sent When the recipient opens the link Then a "deposit_link_open" event is recorded once per tokenId with tokenId, clientId, ruleId(s), appointmentId(s), timestamp, channel, and locale And when the deposit payment succeeds Then a "deposit_paid" event is recorded once per tokenId with amount, currency, paymentId, processor, and timestamp And duplicate open or paid events for the same tokenId within 5 minutes are de-duplicated And events are available in the analytics datastore within 5 minutes of occurrence
Policy Compliance, Holds, and Refunds
"As a sitter, I want deposit capture and refund rules to be enforced automatically so that cancellations are handled fairly and consistently without manual work."
Description

Support configurable behavior for when and how deposits are captured (e.g., immediate charge vs authorization hold), along with cancellation/reschedule windows that determine partial or full forfeiture. Enforce policy at checkout and during changes, with proration rules and clear client messaging. Integrate with payment processors to manage holds, captures, voids, and refunds, and record entries in PawPilot’s ledger. Provide safeguards for disputed charges and edge cases (e.g., appointment time shifts across windows) to maintain compliance and client trust.

Acceptance Criteria
Apply Hold vs Charge Based on Deposit Rule at Checkout
Given a service with a deposit rule specifying "authorization hold" or "immediate charge" And a client clicking the SMS checkout link for that service When the client enters payment and confirms booking Then the system applies the deposit as a hold or captured charge per the rule And the exact deposit amount shown in the SMS and checkout equals the amount held or captured to the cent And the receipt and ledger entry record hold vs captured state, authorization/capture IDs, timestamp, and amount And if hold is used, the authorization expiration date from the processor is stored
Cancellation Window Determines Forfeiture and Refund Proration
Given a booking with a deposit and policy windows (e.g., >24h full refund, 6–24h 50% forfeit, <6h 100% forfeit) When the client cancels at a time that falls within a specific window Then the system calculates refundable amount per the window and rounds to nearest cent And issues a partial or full refund or voids the authorization as applicable within 5 minutes And updates the ledger with original deposit, forfeit amount, refund amount, and resulting balance And sends SMS to the client stating the forfeit/refund amount and rationale
Reschedule Policy Applies Deposit Without Double Charging
Given a booking with a deposit and defined reschedule grace window When the client reschedules inside the grace window to a different time Then the deposit is transferred to the new appointment without an extra charge And any differential policy due to new time window is recalculated and disclosed before confirmation And if the new time falls into a stricter window requiring additional deposit, only the incremental amount is authorized or charged And all changes are reflected in the ledger as adjustments, not duplicate deposits
Time Shift Across Policy Windows Edge Case Handling
Given a booking where the provider modifies the appointment time And the modification moves the start time across a policy window boundary When the change is saved Then the system re-evaluates the applicable deposit/forfeiture policy based on the new start time And no additional charge or forfeiture is applied without explicit client confirmation via SMS link And the client and provider are notified of the policy change and any required action And the audit log records who changed the time, old/new times, old/new policy window, and any financial impact
Processor Operations: Holds, Captures, Voids, Refunds Are Consistent
Given integration with supported payment processors When performing hold, capture, void, partial refund, or full refund operations triggered by policy Then the processor response is validated as success before updating application state And failures are retried up to 3 times with exponential backoff and surfaced to staff if still failing And all transactions store processor IDs, status, reason codes, and are reconciled to ledger entries 1:1 And a daily reconciliation report flags any mismatches for review
Dispute Safeguards and Evidence Packaging
Given a deposit charge subject to a client dispute/chargeback webhook When the dispute is received Then the booking is flagged and any pending auto-captures are paused And evidence packet is generated within 24 hours including policy text accepted, timestamps, SMS consent logs, checkout screenshots, and message transcripts And staff are notified and can export the packet in processor-supported format And the ledger reflects the dispute hold amount and final outcome adjustments
Client Messaging Is Clear and Verifiable
Given a client opening the SMS checkout link When the deposit is presented Then the UI clearly labels whether the amount is a hold or a charge, the exact amount, and the cancellation/reschedule policy windows And the client must check a box acknowledging the policy before continuing And the acceptance is stored with timestamp, IP (if available), device info, and message version And the SMS confirmation includes a summary and link to full terms
Audit Trails and Versioning
"As an admin, I want an audit trail and versioning for deposit rules so that I can trace decisions and revert if something goes wrong."
Description

Maintain full version history of deposit rules, including author, timestamp, diffs, and publish notes. Store evaluation evidence by booking (rule IDs, inputs, result, and explanation) for transparency and dispute resolution. Provide a safe draft/publish workflow with scheduled effective dates and the ability to preview future rules against historical data. Enable quick rollback to prior versions if a misconfiguration is detected.

Acceptance Criteria
Version History: Author, Timestamp, Diffs, Publish Notes
Given an existing published deposit ruleset When a user with Manage Rules permission edits and saves changes as a draft with publish notes Then the system creates a new draft version with a unique version ID, author ID, created timestamp, and captured publish notes Given a draft is published When the system compares it to the prior published version Then a field-level diff is stored and viewable showing rule additions, deletions, and modifications with before/after values Given the ruleset history is viewed Then versions are listed in reverse chronological order with version ID, author, timestamp, and publish notes, and older versions are immutable/read-only Given a ruleset with 100 historical versions When the history view is opened Then it loads within 2 seconds at the 95th percentile
Per-Booking Evaluation Evidence Snapshot
Given a booking requires deposit evaluation When the rules engine evaluates the booking Then the booking stores an immutable evidence record containing: evaluation timestamp, ruleset version ID, ordered list of matched rule IDs, input parameters (service ID, client status, price tier, time window), computed deposit amount, currency, and a human-readable explanation referencing matched rules Given the ruleset changes after the booking evidence was stored When the booking is viewed Then the stored evidence remains unchanged and references the original ruleset version ID Given an admin views the booking When they open the Evidence tab Then the evidence record is displayed and is exportable as JSON and PDF
Draft/Publish Workflow with Scheduled Effective Dates
Given a user creates or edits deposit rules and saves as Draft Then no live evaluations use the draft version Given a draft is scheduled to publish at a future timestamp in the account’s business timezone When the effective timestamp is reached Then the version becomes the current published version and is used for all evaluations from that timestamp forward Given multiple drafts exist When one is scheduled Then the system prevents two versions from sharing the same effective timestamp and displays a Pending badge with the scheduled time Given a user publishes a draft with Publish Now Then the system records the effective timestamp as the current time and updates the current published version
Preview Future Rules Against Historical Data
Given a draft or scheduled version exists When an admin selects that version and a historical date range of bookings to preview Then the system runs a non-mutating simulation using the selected version and returns: number of bookings impacted, aggregate deposit total, delta vs current version, and per-booking simulated result with matched rule IDs and deposit amount Given a preview is executed Then no production data nor booking evidence is altered, and simulation results are clearly labeled as Preview Given a dataset of up to 10,000 historical bookings When a preview is run Then results are returned within 60 seconds and are exportable as CSV
Quick Rollback to Prior Published Version
Given a misconfiguration is detected When an admin selects a prior published version and confirms Rollback with a reason Then the selected version becomes the current published version immediately, and a new audit entry is recorded with rollback actor, timestamp, and reason Given a rollback occurred When new bookings are evaluated Then they use the restored version Given future scheduled versions exist When a rollback is performed Then the scheduled versions remain scheduled and will take effect at their scheduled times unless explicitly canceled
Audit Evidence Retrieval and Export for Disputes
Given a booking ID is provided in the Dispute view When the admin requests audit details Then the system displays the booking’s evaluation evidence, the ruleset version metadata (version ID, author, timestamp, publish notes), and the diff from the previous published version in under 2 seconds at the 95th percentile Given the admin clicks Export Bundle When the export is generated Then a read-only bundle (PDF and JSON) is produced containing the evidence snapshot, ruleset version metadata, and diffs, and the exported contents exactly match the stored records
Immutability and Append-Only Audit Records
Given a ruleset version is published or a booking evidence record is created When any user attempts to modify or delete those records via UI or API Then the operation is rejected with HTTP 403 and an audit log entry is created capturing actor, timestamp, and attempted action Given the application needs to correct an error When a change is required Then a new version or evidence addendum is appended without altering the original records, and the relationship between entries is recorded and viewable

Policy Tapback

After payment, clients receive an instant receipt plus a one-tap acknowledgment of key policies (cancellation window, lateness, refund terms). Time-stamped consent attaches to the booking, cutting disputes and chargebacks.

Requirements

Instant Receipt SMS with Policy Link
"As a client, I want an instant SMS receipt with a secure link to key policies so that I can quickly review and acknowledge the terms right after paying."
Description

After payment confirmation, automatically send an SMS receipt to the client’s booking phone number, including itemized charges, amount, date/time, business name/branding, and a secure per-transaction short link to the Policy Tapback page. Messages must deliver within 10 seconds of payment capture and support per-business templates with variables (e.g., {amount}, {appointment_time}) and localization. The link token is signed, scoped to the consent flow, device-agnostic, and expires after a configurable period. Implement carrier-compliant formatting, rate limiting, delivery status tracking, retries on transient failures, and logging for auditability.

Acceptance Criteria
Immediate SMS Receipt on Payment Capture
Given a successful payment capture for a booking with a valid SMS-enabled client number When the payment processor notifies PawPilot of the capture event Then the system enqueues the receipt SMS within 2 seconds of the capture timestamp And sends the SMS to the carrier with a "sent" status within 5 seconds of enqueue And records a "delivered" status within 10 seconds of the capture timestamp via provider callback And the SMS is addressed only to the booking phone number on record And no duplicate receipt is sent for the same payment_id (idempotent on payment_id)
Receipt Content and Branding Compliance
Given the receipt SMS is generated for a completed payment When the message body is rendered Then it includes business name/brand identifier, payment date/time (business timezone), itemized line items with per-line amounts, total amount with currency, and the secure short policy link And formatting respects carrier length limits (<=1600 chars total; segments joined in order) And currency, number, and datetime formats reflect the selected locale And the message includes a unique booking reference or last-4 of payment for client support
Secure Policy Tapback Link Generation and Expiry
Given a receipt SMS is being prepared When generating the policy link Then the link is HTTPS and shortened using the PawPilot short domain And the token is signed (e.g., JWT/HMAC), scoped to policy_tapback for the specific booking_id and payment_id, and is device-agnostic And the token cannot access any other booking/payment and cannot be used to retrieve PII beyond the consent flow And the link expires after the configured period (default 72 hours) and returns an expired state (HTTP 410 or equivalent) with a safe re-request path And the raw token is never logged; only a salted hash is stored for audit
Per-Business Templates, Variables, and Localization
Given a business has configured a custom receipt template with variables When a receipt is rendered Then all declared variables (e.g., {amount}, {appointment_time}, {business_name}) are resolved from the booking/payment context And unresolved or malformed variables cause a validation error and automatic fallback to the system default template, with an admin alert logged And the selected locale determines message language and formatting; if missing, fallback to business default, then platform default (en-US) And template rendering preserves Unicode characters and right-to-left scripts without mojibake
Carrier Compliance and Rate Limiting
Given a receipt SMS is ready to send When preparing and dispatching the message Then the sender ID/number complies with 10DLC/registered campaign requirements for transactional messaging And the message includes required identification and opt-out language where mandated by carrier/region And the recipient number is normalized to E.164 and passes basic validation before send And per-tenant rate limiting is enforced to the configured threshold (e.g., X messages/second) to prevent carrier filtering And messages exceeding limits are queued and sent in-order without loss
Delivery Status Tracking and Intelligent Retries
Given an attempt to send a receipt SMS When a transient failure is returned by the provider (e.g., 5xx, throttling, temporary unavailability) Then the system retries with exponential backoff up to 3 attempts and records each attempt with timestamps and error codes And if a permanent failure code is returned (e.g., unreachable, blocked, 30006), no retries occur and final status = failed And final delivery outcomes (queued, sent, delivered, failed) are persisted and visible in the merchant dashboard and via webhook And on repeated failures a notification is logged for merchant review
Audit Logging and Idempotency Controls
Given payment capture webhooks may be delivered more than once When duplicate events with the same payment_id or idempotency key are received within 24 hours Then only one receipt SMS is sent and subsequent events are deduplicated And the audit log stores: payment_id, booking_id, recipient number, template_id, rendered variables (keys only), message body hash, short-link token hash, send/attempt timestamps, delivery events, and actor/service IDs And audit records are immutable and retained for at least 365 days and are exportable for dispute resolution And sensitive values (full token, PAN, or PII beyond phone number and business name) are never logged
One-Tap Policy Acknowledgment UI
"As a client, I want to acknowledge the policies with one tap without logging in so that my booking is confirmed and compliant with minimal effort."
Description

Provide a mobile-first web view that loads from the SMS link, presenting a concise summary of key policies (cancellation window, lateness, refund terms) with a single, prominent Acknowledge button and an optional View Full Policy section. No login is required; identity is established from the signed link. On tap, capture a consent event with timestamp, booking ID, client identifier/phone, policy version, and minimal device metadata. Display a confirmation state, prevent duplicate submissions, and surface quick actions to contact the provider. Ensure accessibility (WCAG AA), fast load (<1.5s on 3G), localization, and graceful offline handling with queued submission.

Acceptance Criteria
Signed Link Identity Resolution (No Login)
Given a client taps the SMS link containing a signed token When the web view loads Then the session is established without login and the client is identified by phone and booking ID from the token And if the token is invalid or expired, the UI shows an error state with retry/contact options and no acknowledge action available And identity values displayed are not editable
Policy Summary & Full Policy Access
Given the view loads for a valid booking Then a concise summary displays cancellation window, lateness, and refund terms And a 'View Full Policy' control expands in-place to show full policy text without navigation And the policy version identifier is visible And content is localized to the client’s preferred language from link or device locale, with a manual language switcher And policy text is readable on small screens without horizontal scrolling
One-Tap Acknowledgment Event Capture
Given the policy is visible and the client is identified When the client taps 'Acknowledge' once Then a consent event is created containing: server timestamp (UTC), device timestamp, booking ID, client identifier and phone, policy version, and minimal device metadata (device/OS/locale or UA) And the event is persisted server-side and attached to the booking record And the API returns 201 Created with an event ID And no additional PII beyond phone and booking identifiers is included in device metadata
Confirmation State, Idempotency, and Quick Actions
Given an acknowledgment event is successfully recorded Then the UI transitions to a confirmation state showing acknowledgment date/time and policy version And the Acknowledge button is disabled or hidden to prevent resubmission And repeated taps or refreshes do not create duplicate server records via an idempotency key scoped to booking plus policy version And quick actions to contact the provider (Call and SMS) are visible and functional using configured contact details
Performance on 3G
Given a cold cache and simulated Fast 3G network on a mid-tier device When the link is opened Then the policy summary and Acknowledge button render and are interactive within 1.5 seconds And total transfer size of page resources is <= 250 KB excluding fonts And First Input Delay <= 100 ms and Cumulative Layout Shift <= 0.1
Accessibility (WCAG 2.1 AA)
Given the view is used with keyboard and screen reader Then all interactive elements are reachable via keyboard with visible focus and logical order And controls have accessible names/roles; language attribute is set; color contrast >= 4.5:1; text reflows at 200% without loss of content or functionality And automated axe-core scan reports 0 critical/serious violations; manual checks pass for focus trapping, labels, and error prevention on acknowledgments
Offline Handling with Queued Submission
Given the device is offline when 'Acknowledge' is tapped Then the UI shows an offline queued state and stores the consent payload locally with an idempotency key And the client is informed that submission will auto-send when online; no duplicate taps are allowed And on connectivity restoration, the app retries automatically with exponential backoff for up to 24 hours And upon success, the confirmation state is shown; if retries exhaust, the UI prompts the client to contact the provider And the server records device and server timestamps and stores only one consent event
Consent Attachment to Booking and Payment Records
"As a provider, I want policy consent automatically attached to bookings and payments so that I have verifiable proof in case of disputes or chargebacks."
Description

Persist consent artifacts to both booking and payment records, including ISO timestamp (UTC), policy version ID and content hash, policy summary snapshot, consent channel (SMS link), client phone, booking ID, staff member, and IP/country when available. Mark the booking with a Policy Acknowledged status and expose the consent on the booking timeline. Store artifacts immutably with encryption at rest, include audit logs, and provide a simple admin UI panel to view and export the consent details.

Acceptance Criteria
Consent Artifact Creation and Dual Attachment
Given a paid booking with bookingId and paymentId and a client receives an SMS policy acknowledgment link When the client taps the link and confirms consent Then the system persists a consent artifact containing: ISO 8601 UTC timestamp (Z), policyVersionId, policyContentHash, policySummarySnapshot (<= 2 KB), consentChannel="SMS_LINK", clientPhone in E.164, bookingId, staffMemberId, and ip/country when available And the artifact is linked to both the booking record and the payment record And a 201 Created response returns artifactId with references to bookingId and paymentId And the artifact is discoverable via booking and payment detail APIs within 3 seconds of creation
Booking Marked and Timeline Exposure
Given a booking without policy acknowledgment When a consent artifact is successfully persisted for that booking Then the booking is marked PolicyAcknowledged = true And a timeline entry "Policy acknowledged" appears with the artifact's timestamp (UTC), policyVersionId, and staffMemberId And the timeline entry is visible in the staff UI within 3 seconds and sorted chronologically by timestamp And removing the timeline entry is not permitted once written
Immutability, Encryption at Rest, and Audit Logging
Given an existing consent artifact When any user attempts to modify or delete core artifact fields (timestamp, policyVersionId, policyContentHash, policySummarySnapshot, consentChannel, clientPhone, bookingId, paymentId, staffMemberId, ip/country) Then the operation is rejected with 403 Forbidden or 409 Conflict and no changes are persisted And the artifact is stored with encryption at rest (AES-256 or cloud-managed equivalent) and a KMS keyId is recorded for the storage location And all create, read, export, and failed mutation attempts are written to an append-only audit log with actorId (or system), action, targetId, ISO UTC timestamp, and source ip
Admin UI View and Export of Consent
Given a staff user with appropriate permissions opens the Consent panel When they search by bookingId, paymentId, clientPhone, date range, or policyVersionId Then matching consent artifacts are listed with columns: timestamp (UTC), bookingId, paymentId, policyVersionId, staffMemberId, clientPhone (masked), consentChannel, ip/country And selecting a row shows the full artifact details including policySummarySnapshot and content hash And exports are available as CSV and JSON containing all artifact fields and audit log entries for the selected range And exports are limited to 10,000 rows per file, complete within 30 seconds, and produce a downloadable link that expires in 15 minutes And users without permission receive 403 and no data is returned
Idempotency and Duplicate Handling
Given the client taps the acknowledgment link multiple times or the request is retried by the network/gateway When the same acknowledgment (same bookingId, paymentId, policyVersionId, policyContentHash, clientPhone) is submitted again within 24 hours Then no new artifact record is created and the API returns 200 OK with the original artifactId And an audit event of type duplicate_acknowledgment is recorded referencing the original artifactId And concurrent submissions result in a single artifact due to idempotency key enforcement
Payment Linkage and Deferred Reconciliation
Given a booking-level consent is captured before the payment record becomes available When the payment record is later created and associated to the booking Then the system automatically links the existing consent artifact to the payment record within 10 minutes And until linkage completes, the artifact shows paymentLinkStatus = "pending" in the admin UI And if linkage fails, the system retries with exponential backoff for at least 24 hours and surfaces a visible "linking_failed" status with error details to admins
Data Validation and Policy Version Integrity
Given a consent submission payload When it is validated server-side Then timestamp must be ISO 8601 UTC (ends with Z), clientPhone must be valid E.164, policySummarySnapshot must be <= 2 KB UTF-8, and ip (if present) must be valid IPv4 or IPv6; country (if present) must be ISO 3166-1 alpha-2 And policyVersionId must exist in the policy registry and policyContentHash must match a SHA-256 hash of the stored policy snapshot And submissions failing validation return 422 Unprocessable Entity with field-level errors and no artifact is created
Policy Management and Versioning
"As a provider, I want to manage and version my policy summaries so that clients always see accurate terms and historical consents remain valid."
Description

Offer an admin interface for providers to create, preview, and publish policy summaries and full-text policies with effective dates and version history. Enforce length and clarity guidelines for SMS-friendly summaries and allow localization and branding. Maintain an immutable history of published versions; new transactions reference the current version while historical consents remain linked to their original snapshot. Provide variables (e.g., cancellation_hours) and a preview of the client-facing tapback screen prior to publish.

Acceptance Criteria
Draft, Preview, and Publish With Effective Date
Given I am a provider admin on Policy Management And I have entered a summary and full-text policy and set an effective date/time When I click Save Draft Then the draft is saved with status "Draft" and a unique draft ID When I click Preview Then I see the client-facing Tapback preview with the entered content and effective date/time When I click Publish Then a new version number is assigned incrementally (v1, v2, ...) And the version is stored with the specified effective date/time And the version status is "Active" if now >= effective date/time else "Scheduled"
Enforce SMS Summary Guidelines
Given I enter an SMS summary in the default locale When I attempt to save Then validation fails if character count after variable substitution exceeds 320 And validation fails if any placeholder variable is undefined or misspelled And validation fails if Flesch-Kincaid grade level exceeds 8.0 And validation fails if non-GSM-7 characters are present (unless the selected locale requires Unicode) And validation passes and the draft is saved when all rules are met
Client-Facing Tapback Preview
Given I have a valid draft in the selected locale When I open the Tapback preview Then it renders the summary, acknowledgment button, and link to full policy And variables render with current values And the preview shows brand logo and primary color And the locale selector switches content between available locales And the acknowledgment button is disabled until the summary is scrolled to the end if overflow occurs
Versioning and Immutability
Given a policy version is Published When I attempt to edit its summary or full-text Then the system blocks edits and shows an explanatory message When I choose "Create New Version" Then a new Draft is created cloned from the published version And the original published version remains unchanged and visible in history
Transaction Version Referencing and Consent Snapshot
Given a policy version is Active at the time of booking/payment When a client completes payment and acknowledges via Tapback Then the booking record stores policy_version_id, policy_version_hash, summary_snapshot, full_text_snapshot, and client_ack_timestamp (ISO 8601 UTC) And these stored values remain immutable regardless of later policy changes And the receipt includes the policy version label and a link to the policy snapshot
Localization and Branding
Given the organization has a default locale and optional additional locales When I create or edit a Draft Then the default locale fields (summary and full-text) are required And additional locales can be added and tracked for completeness And Publish is blocked if any selected locale is incomplete And the Tapback preview and receipts display the configured logo, brand colors, and localized business name
Version History View and Export
Given multiple policy versions exist When I open Version History Then I see version number, status (Draft/Scheduled/Active/Retired), effective date/time, published by, and changelog notes And I can filter by status and date range And I can export version history and consent records as CSV and JSON for a date range And exported records include immutable IDs, timestamps (ISO 8601 UTC), version hash, and locale
Dispute Evidence Package Generation
"As a provider, I want a ready-to-send evidence package so that I can respond to chargebacks quickly and increase my chances of winning disputes."
Description

Enable one-click generation of a consolidated evidence bundle containing the receipt, policy text and version at the time of consent, consent timestamp and identifiers, SMS delivery and click logs, booking/payment timestamps, and relevant communication history. Export as PDF and JSON with a secure share link, watermarking, and optional redaction of sensitive fields. Ensure the bundle adheres to common processor guidelines (e.g., Stripe, Square) and is retrievable within 5 seconds under typical load.

Acceptance Criteria
One-Click Generation from Booking
Given a booking with a successful payment and recorded policy consent When a staff user clicks "Generate Evidence Package" Then the system creates an evidence bundle in a single action and assigns a unique bundle ID And the UI displays options: View PDF, Download JSON, Copy Share Link And the bundle is linked to the booking, client, and payment records Given a booking without a successful payment When a staff user attempts to generate an evidence bundle Then the action is blocked with an explanatory error message and no bundle is created
Evidence Contents Completeness
Given a generated evidence bundle Then the bundle includes all of the following artifacts: payment receipt, policy text and version effective at consent time, consent timestamp and client identifiers, SMS delivery logs, link click logs, booking creation and modification timestamps, payment authorization/capture timestamps, and all booking-related communication history And every artifact is time-stamped with timezone, has a stable source identifier, and is traceable to the booking ID And if any artifact is unavailable, the bundle marks its status as "Partial" with a machine-readable missing_items list
Export Formats and Watermarking
Given a generated evidence bundle When the user selects PDF export Then a PDF is produced with a visible diagonal watermark on every page including bundle ID, booking ID, and generation timestamp And the PDF text is selectable and not a rasterized image When the user selects JSON export Then a JSON file is produced in UTF-8 with a top-level schema containing meta, receipt, policies, consent, sms_logs, click_logs, booking, communications And both exports carry matching bundle ID and a generation checksum
Secure Share Link Delivery
Given a generated evidence bundle When the system creates a share link Then the link is a signed, unguessable URL with at least 128 bits of entropy And the link expires by default in 7 days and supports a configurable expiry between 1 and 30 days And access to the link is rate limited and logged with timestamp and actor context And the link provides direct access to both PDF and JSON downloads over HTTPS only
Redaction Controls
Given a generated evidence bundle When a user enables redaction for sensitive fields Then the PDF and JSON outputs mask configured fields (phone, email, street address, card token, GPS/location, internal sensitive notes) with [REDACTED] And a Redaction Summary is appended listing each field category redacted And the unredacted bundle remains stored and is not overwritten by a redacted export
Processor Guideline Compliance
Given an evidence bundle prepared for card dispute submission When validated against Stripe and Square evidence rulesets Then the bundle passes automated checks for required categories, field presence, date formats, and file size limits And the PDF includes a cover sheet mapping artifacts to processor evidence categories And the JSON includes machine-readable fields compliant with processor schemas where available
Performance Under Typical Load
Given typical production load of up to 50 concurrent generation requests per tenant When a staff user clicks Generate Evidence Package Then the share link becomes accessible with both PDF and JSON within 5 seconds at the 95th percentile And the 99th percentile does not exceed 7 seconds And generation errors are surfaced within 5 seconds with a retriable error code
Compliance, Privacy, and Opt-Out Handling
"As a provider, I want compliance and privacy safeguards enforced automatically so that I can use Policy Tapback without risking regulatory or privacy violations."
Description

Apply TCPA/CTIA-compliant messaging with sender identification, HELP/STOP keywords, and regional quiet hours as applicable. Respect opt-outs by suppressing Policy Tapback SMS and offering alternate channels (email/in-app). Use signed tokens with short TTL for link access, encrypt PII in transit and at rest, implement data minimization, and maintain audit logs for all access to consent artifacts. Support configurable retention policies and deletion on user request, and surface privacy notices within the tapback flow.

Acceptance Criteria
HELP/STOP Keyword Handling and Sender Identification
- Given any outbound Policy Tapback SMS, Then the message includes the sender identification (business name or PawPilot short name) and the text "Reply HELP for help, STOP to opt out". - Given a recipient replies HELP, Then an automated HELP response is sent within 5 seconds including support contact and link to terms, and the event is logged with timestamp and message IDs. - Given a recipient replies any opt-out keyword (STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT; case-insensitive), Then a single opt-out confirmation SMS is sent within 5 seconds and the number is suppressed from all future SMS within 30 seconds across all services. - Given a recipient replies an opt-in keyword (START or UNSTOP), Then SMS sending is re-enabled and a confirmation SMS is sent within 5 seconds; the re-subscription is audit-logged. - Given an opted-out number, When any service attempts to send a Policy Tapback SMS, Then the message is not sent and a suppression reason is recorded; no SMS is emitted.
Regional Quiet Hours Enforcement
- Given regional quiet hours are configured per recipient region (default templates provided; tenant-configurable), When a Policy Tapback SMS would be sent during the recipient's local quiet hours, Then it is not sent and is queued for the first minute outside quiet hours. - Given the recipient time zone is known, Then it is used; When unknown, Then infer from phone number region; When still unknown, Then use the business time zone; all inferences are logged. - Given a queued message exits quiet hours, Then it is sent within 2 minutes unless the booking time has passed, in which case it is canceled with a log entry. - Given a message is suppressed due to quiet hours and an alternate channel is permitted, Then an email or in-app notification is dispatched within 2 minutes as a fallback.
Opt-Out Suppression and Alternate Channel Fallback
- Given a client has opted out of SMS, When a Policy Tapback acknowledgment is due, Then no SMS is sent. - Then an email or in-app notification with the receipt and policy summary is sent within 2 minutes using the permitted channel(s); if neither is available, Then a 'no available channel' event is logged and a dashboard alert is created for the business. - Then capturing consent via the alternate channel attaches to the booking with timestamp and channel recorded. - Then a suppression audit record is stored including booking ID, recipient ID, channel used, and timestamps.
Signed Tapback Links with Short TTL and Single Use
- Given any tapback link, Then it embeds a signed token bound to tenant, booking ID, recipient ID, and purpose, with a TTL of 15 minutes (configurable 5–60 minutes). - Then tokens are single-use; When a token is redeemed, Then subsequent requests with the same token return HTTP 410 within 1 second. - When a token is expired or invalid, Then an expiry screen is shown with a 'Request new link' action; requesting a new link sends via currently permitted channels only and invalidates outstanding tokens. - Then no PII appears in the URL path or query; only an opaque token is present; access attempts (success/failure) are audit-logged.
PII Encryption and Data Minimization
- Then all tapback web/API endpoints enforce TLS 1.2+ with HSTS; weak ciphers are disabled per security baseline. - Then PII at rest is encrypted using KMS-managed AES-256 or equivalent; keys rotate at least every 90 days; access is restricted via least-privilege roles. - Then SMS/email payloads exclude sensitive PII (e.g., full address, payment details); logs/analytics redact phone numbers (last 2 digits only) and emails (first 3 chars + domain). - Then consent artifacts store only booking ID, recipient ID, policy version, consent choice, timestamp, channel, and message IDs; raw tokens are not stored; IP/UA are hashed and retained <=30 days. - Then automated tests assert encryption in transit/at rest configs and verify no disallowed fields are persisted or logged.
Consent Artifact Audit Logging and Immutability
- Given any create, read, update, delete, or export of a consent artifact, Then an audit entry is appended with actor (user/service), action, resource ID, UTC timestamp, client IP/UA (hashed), and reason code. - Then audit logs are append-only and tamper-evident (hash-chained with daily anchor); attempts to alter entries are rejected and generate a security event. - Then authorized admins can export a booking's audit trail within 60 seconds; exports include verification hashes. - Then integration tests confirm 100% of consent artifact paths emit audit entries with required fields.
Retention Policies and Deletion Requests
- Given a tenant-configured retention period for consent artifacts (6–84 months), Then records beyond the window are purged nightly and removed from backups within 30 days. - Given a verified data deletion request, Then all associated consent artifacts and related PII are deleted or anonymized within 30 days with an auditable record of the action. - Then deletion cascades across primary DBs, search indices, analytics stores, object storage, and caches; referential integrity checks prevent orphaned references. - Then a privacy dashboard shows current retention settings and last purge status; failures emit alerts within 5 minutes.
Analytics and Monitoring
"As a provider, I want visibility into acknowledgment and dispute metrics so that I can measure effectiveness and refine my policies and operations."
Description

Track and surface key metrics including SMS delivery rate, click-through rate, consent rate, time-to-consent, dispute/chargeback rate before and after rollout, and no-show deltas. Provide cohort analysis by provider, service type, and policy version. Expose a dashboard and API endpoints, emit events to the existing analytics pipeline with PII redaction, and trigger alerts on anomalies (e.g., delivery drop by carrier, spike in declines).

Acceptance Criteria
Messaging Metrics Reporting (Delivery and CTR)
Given production SMS traffic and link tracking enabled When I view the Analytics dashboard for a selectable date range Then I see delivery rate and click-through rate visualized by hour, day, and week with drilldown by provider and carrier Given a valid API token When I GET /analytics/v1/messaging?from={ISO8601}&to={ISO8601}&group_by=provider,carrier&granularity=day Then the response contains fields sent, delivered, unique_clicks, delivery_rate, ctr and totals match the dashboard within 0.5% Given message events in the raw store When aggregating metrics Then delivery_rate = delivered/sent and ctr = unique_clicks/sent computed consistently across dashboard and API Given fewer than 30 messages in a bucket When rendering that bucket Then the dashboard displays an “insufficient data” indicator and suppresses rate trendlines for that bucket
Consent Metrics Tracking (Rate and Time-to-Consent)
Given a completed payment When a client acknowledges or declines policies via tapback Then a time-stamped consent or decline is attached to the booking and emitted as an analytics event Given bookings within a selected date range When I open the Consent analytics view Then I see consent_rate, decline_rate, median_time_to_consent, and p95_time_to_consent with filters for provider and service type Given a valid API token When I GET /analytics/v1/consent?group_by=provider&metrics=consent_rate,decline_rate,median_time,p95_time Then the response metrics match the dashboard within 0.5% for rates and ±1s for time metrics Given multiple tapbacks for the same booking When calculating metrics Then only the first definitive response counts toward consent_rate and time-to-consent
Dispute and Chargeback Rate Delta (Pre/Post Policy Tapback)
Given a configured rollout date per provider When I view the Disputes analytics Then dispute_rate and chargeback_rate per 1000 payments are shown for pre-rollout and post-rollout windows with absolute and relative deltas Given an API request to /analytics/v1/disputes?compare=pre_post&group_by=provider,policy_version When executed Then the response includes baseline_window, comparison_window, dispute_rate_per_1000, chargeback_rate_per_1000, delta_abs, and delta_pct for each cohort Given fewer than 200 payments in either comparison window for a cohort When rendering deltas Then the cohort is labeled low-confidence and relative percentages are suppressed
No-Show Rate Delta (Pre/Post Policy Tapback)
Given bookings marked attended or no-show When I select a date range that spans the rollout Then the dashboard shows no_show_rate pre vs post and the delta by provider and service type Given an API call to /analytics/v1/no_shows?compare=pre_post&group_by=provider,service_type When executed Then the response includes attended_count, no_show_count, no_show_rate, delta_abs, and delta_pct and matches the dashboard within 0.5% Given bookings rescheduled within the stated policy window When computing no_show_rate Then reschedules are excluded from no-show counts
Cohort Analysis by Provider, Service Type, and Policy Version
Given filters for provider, service type, and policy version When applied Then all charts and tables reflect only the selected cohorts and counts remain consistent across widgets Given a request to /analytics/v1/cohorts?provider_id=*&service_type=walk,groom&policy_version=v2 When executed Then all metric endpoints honor the filters and return consistent totals across endpoints Given a user exports a table as CSV When the export completes Then the CSV includes the applied filters in the header and row totals match on-screen values
Dashboard, API Exposure, and PII Redaction
Given a user with Analytics permissions When accessing the dashboard Then metrics are visible; and given a user without permissions access is denied Given unauthenticated or expired API requests When calling any /analytics/v1 endpoint Then the service returns 401 with an appropriate error code Given analytics events (sms_delivered, link_clicked, policy_tapback, dispute_opened, chargeback_received, booking_no_show) When observed in the analytics pipeline Then PII fields (name, phone, email, address, free-text notes) are excluded or irreversibly hashed with a stable salt; no raw PII is present Given dashboard views and API payloads When inspecting fields Then only non-PII identifiers (provider_id, booking_id, policy_version) are present; client identifiers are consistently redacted or hashed Given an event schema change When deployed Then schema version is incremented and remains backward compatible for at least one minor version
Anomaly Detection and Alerting for Key Metrics
Given a 7-day rolling baseline per carrier When delivery_rate drops by ≥15% for ≥30 minutes Then an alert is sent to Slack and PagerDuty with carrier, affected providers, and a dashboard link within 60 seconds of detection Given consent metrics degrade (median_time_to_consent increases by ≥50% or decline_rate doubles vs baseline for ≥30 minutes) When detected Then an alert is triggered with cohort context including provider and policy_version Given dispute_rate or chargeback_rate exceeds 3 standard deviations over the trailing 30 days When detected Then an incident is created and deduplicated with a 60-minute cooldown window Given an open alert When conditions recover to within baseline ±5% for 30 minutes Then a resolve notification is emitted and the incident is auto-closed

Gentle Nudges

If a client opens the link but pauses, timed reminders display the live countdown and offer a one-tap 5-minute extension. Recovers borderline payments while keeping waitlisters informed about availability.

Requirements

Link Open & Idle Detection
"As a groomer using PawPilot, I want the system to detect when a client opens the payment link and stalls so that timely nudges can recover the booking before the hold expires."
Description

Implement client-side and server-side event tracking to detect when a recipient opens a PawPilot payment/confirmation link and becomes idle beyond a configurable threshold (e.g., 20–45 seconds). Start a session-bound timer, persist state, and expose a webhook/event to trigger nudges. Handle multiple opens, device switches, and deep-link attribution. Ensure idempotency and resilience against flaky networks. This detection underpins timed reminders and determines when to release the hold to the waitlist if the timer lapses.

Acceptance Criteria
Start Session Timer on First Link Open
Given a recipient opens a PawPilot payment/confirmation link containing a session token When the landing page loads and tracking initializes Then start a session-bound idle timer with threshold T configurable per link within 20–45 seconds (default 30) And persist server-side state: session_id, link_type, threshold_seconds, started_at (server time), last_activity_at, user_agent, device_fingerprint (if available) And record a single "link_opened" event per session_id And update last_activity_at on qualifying activity (tap/click, scroll, keypress, input focus/change, form submit) And define idle as no qualifying activity for T consecutive seconds
Idle Threshold Reached Emits Nudge Event Webhook
Given a session has started and no qualifying activity has occurred for T seconds When the idle threshold is reached Then emit a server-side "session_idle" event within 1 second of threshold And POST a webhook to the configured endpoint containing: event_type=session_idle, session_id, threshold_seconds, idle_started_at, last_activity_at, link_type, attribution And include headers: X-PawPilot-Signature (HMAC of body), Idempotency-Key = "{session_id}:idle-1" And deliver webhooks with at-least-once semantics with up to 5 retries using exponential backoff (1s, 4s, 16s, 64s, 256s) And do not emit more than one "session_idle" event for the same session occurrence
Multiple Opens and Device Switch Deduplication
Given the same link is opened on multiple devices or tabs When a second client instance connects using the same session token Then designate the latest active instance as the owner and deactivate all other timers And ensure only one active idle timer exists per session at any time And ignore activity events from inactive instances And do not create a new session_id on reopen; reuse until completion or session expiry (30 minutes of inactivity) And ensure exactly one "link_opened" event per session and no duplicate overlapping "session_idle" emissions
Deep-Link Source Attribution in Events
Given the link contains attribution parameters (utm_source, utm_medium, utm_campaign, pp_channel, ref) or fragment identifiers When the page initializes Then capture and send attribution with the "link_opened" event And persist attribution alongside the session server-side And include the exact attribution blob in all subsequent events and webhooks ("activity", "session_idle") And if attribution is absent, include null values for those fields rather than omitting keys
Flaky Network Resilience and Event Ordering
Given intermittent connectivity causes delayed client events When connectivity is restored Then the client must flush queued events in original order with original client timestamps And the server must accept late events up to 120 seconds and compute idle using event-time with a maximum clock-skew tolerance of 5 seconds (else use server receipt time) And idle detection accuracy must be within ±3 seconds of the configured threshold under these conditions And webhook retries must not produce duplicates when the consumer returns HTTP 2xx
Timer Lapse Marks Hold Releasable with Extension Handling
Given a session has emitted "session_idle" When no qualifying interaction occurs during the configured grace period (default 5 minutes) and no extension acceptance event is received Then emit a "session_hold_releasable" event and webhook with Idempotency-Key = "{session_id}:releasable-1" And if an extension acceptance is received before grace end, extend the timer by 5 minutes and suppress the releasable event for that period And persist state transitions with timestamps: idle_started_at, grace_expires_at, extended_until (if any), releasable_at And emit only one releasable event per session unless a new hold is created
Live Hold Countdown Banner
"As a client, I want to see exactly how much time I have left to complete payment so that I can decide quickly and avoid losing my spot."
Description

Display a real-time countdown on the payment/confirmation page showing the remaining hold time for the selected slot. Sync the countdown with backend time to prevent drift, update instantly when an extension is granted, and gracefully handle reconnects. Provide accessible, mobile-first UI with clear states (active, extended, expired). On expiry, show a friendly message and redirect logic that releases the slot and informs the user of next steps (e.g., join waitlist).

Acceptance Criteria
Realtime Countdown Syncs With Server Time
Given a held slot with a server-reported remaining hold time of 10:00 at page load When the payment/confirmation page loads Then the banner displays a zero-padded countdown in mm:ss equal to server time ±1 second And the countdown decrements once per second while the tab is in focus And the displayed time never goes below 00:00 And the difference between displayed and server time remains ≤1 second over a 10-minute session via periodic resync at least every 10 seconds
Instant Update On Extension Grant
Given the countdown is active with at least 00:10 remaining and the user taps "Extend 5 minutes" When the backend confirms the extension Then the displayed countdown increases by exactly 05:00 within 500 ms of the confirmation And the banner state changes to "Extended" with a distinct style and aria-label And if the backend denies the extension, no time change occurs and an inline error is shown And if the server reports a different remaining time than the client, the banner snaps to the server value immediately
Resilience To Network Loss And Tab Backgrounding
Given the countdown is active When the device loses connectivity for up to 2 minutes and then reconnects or the tab is re-focused after being backgrounded Then the banner resynchronizes to the server-reported remaining time within 1 second of reconnect/focus And if the server indicates the hold expired during the outage, the banner shows "Expired" immediately and disables confirm/pay actions And no negative or double-decrementing time is displayed after reconnection
Accessible, Mobile-First Banner UI
Given a device viewport width of 320 px When the payment/confirmation page loads Then banner text is legible without horizontal scrolling and primary tap targets are ≥44x44 px And text-to-background color contrast is ≥4.5:1 and state colors meet WCAG 2.1 AA And countdown and state changes are announced via aria-live="polite" on initial display, on extension grant, and on expiry (not every second) And all controls are keyboard accessible with visible focus and logical order
Expiry Messaging And Redirect To Next Steps
Given the countdown reaches 00:00 or the server marks the hold expired When expiry occurs Then the banner switches to an "Expired" state with a friendly explanation that the slot was released And confirm/pay actions are disabled and the hold is released server-side And the user is presented a "Join Waitlist" action and an automatic redirect begins with a visible 5-second indicator And if the user taps the action before redirect completes, they are taken once and not redirected again
Clear Visual States: Active, Extended, Expired
Given the banner can be in Active, Extended, or Expired states When each state is triggered Then each state shows a unique label, icon, and color token per design specs And the UI never displays conflicting states simultaneously And state transitions animate in ≤300 ms and do not cause cumulative layout shift (CLS) >0.1 on mobile
Timed SMS Nudges with Deep Links
"As a groomer, I want PawPilot to automatically text a gentle reminder while a client hesitates so that more payments complete without manual follow-up."
Description

Send one or more timed SMS reminders during the idle window that include the live time remaining and a secure deep link back to the in-progress session. Respect message templates, personalization (pet name, appointment time), and localization. Support configurable cadence (e.g., T+30s, T+90s) and suppress nudges if the client resumes activity or completes payment. Ensure delivery status tracking, retries, and fallbacks if the session is no longer valid.

Acceptance Criteria
Configurable Nudge Cadence and Timing
Given an idle payment session starts at T0 with a configured cadence of [30s, 90s] and a max_nudges of 2 When the client remains inactive and the session has not expired Then send the 1st SMS at T0+30s ±2s and the 2nd at T0+90s ±2s And do not send more than 2 nudges for the session And do not schedule or send nudges after the session expiry time Te
Accurate Live Countdown in Message and Deep Link
Given a session will expire at Te When a nudge is sent at timestamp Ts Then the SMS body displays time remaining = Te − Ts using the recipient’s locale format (e.g., “1 min 30 sec”) with accuracy ±2 seconds And the deep-linked page shows a live countdown that matches remaining time within ±3 seconds of SMS And if Te − Ts ≤ 0, no nudge is sent
Template Personalization and Localization Rendering
Given a selected SMS template for locale L with placeholders {client_first_name}, {pet_name}, {appointment_time} When a nudge is generated Then all placeholders are replaced with correct values for the target appointment And {appointment_time} is formatted per L and the client’s timezone (fallback to business timezone if missing) And non-GSM characters are encoded correctly; messages exceeding 160 GSM-7 or 70 UCS-2 chars are segmented and concatenated, not exceeding the configured max segments And if a required value is missing, send is blocked and an error is logged
Secure Deep Link and Session Validity Handling
Given the nudge includes an HTTPS deep link containing a signed, single-session token that expires at or before Te When the recipient opens the link before expiry Then the session resumes without additional login and no PII is exposed in the URL And when the link is opened after expiry or token is invalid/revoked Then an “expired session” screen is shown with no payment actions enabled And upon payment completion, all outstanding tokens for the session are revoked within 5 seconds
Activity-Based Suppression and De-duplication
Given pending scheduled nudges exist for a session When the client performs qualifying activity (page view heartbeat, field input, click, or payment completion) before the next scheduled send Then cancel all pending nudges for that session within 1 second And do not send duplicate nudges if the client interacts from multiple devices; last-activity timestamp governs idleness And log suppression reason with timestamp for auditability
Delivery Tracking, Retries, and Fallback Behavior
Given a nudge is dispatched via the SMS provider When delivery callbacks occur Then record per-message status (queued, sent, delivered, failed) with provider message_id and timestamps And on transient failure, retry up to 3 times with exponential backoff (5s, 20s, 60s) unless Te − now < next backoff interval And on permanent failure or when the session becomes invalid, stop retries and send a non-link fallback template indicating the session is expired or completed And expose delivery and retry outcomes via API/UI for support teams
Opt-out Compliance for Nudge Messages
Given the recipient has an active opt-out flag or replies with a recognized opt-out keyword (e.g., STOP, UNSUBSCRIBE) When a nudge would be sent Then suppress the nudge and record suppression reason and keyword And if the recipient re-opt-ins (e.g., START), allow future nudges subject to compliance And include required brand identifier and HELP/STOP language per locale in the first nudge of any 24-hour window
One-Tap 5-Minute Extension & Limits
"As a client, I want a simple one-tap way to extend my hold briefly so that I can finish payment without losing the appointment."
Description

Offer recipients a one-tap action (from SMS or on-page) to extend their hold by five minutes. Apply configurable guardrails: maximum number of extensions per session, maximum cumulative extension time, and abuse prevention (rate limiting, signature validation). On success, instantly update the countdown UI and notify backend services. On failure or limit reached, present clear messaging and suggest alternatives (e.g., rejoin waitlist).

Acceptance Criteria
Extend Hold via SMS One-Tap
Given a recipient has an active hold and a valid, signed SMS extension link When the recipient taps the link within the hold period Then the hold expiration extends by exactly 5 minutes from the current expiration And the countdown UI updates within 500 ms to reflect the new expiry And the new expiry persists across refresh and devices within 2 seconds And concurrent or duplicate taps result in a single extension (no double-counting)
Extend Hold On-Page One-Tap
Given a recipient views the hold page with a visible countdown and an "Extend 5 minutes" button When the recipient taps the button once Then the new expiry = min(current expiry + 5 minutes, remaining allowance per guardrails) And the UI shows a success state within 500 ms and announces the update via ARIA live region And an analytics event "hold_extended" is sent with sessionId, userId, source="on_page"
Guardrails: Maximum Extensions per Session
Given maxExtensionsPerSession is configured to 3 and the session has used N extensions When the recipient requests another extension Then the request succeeds if N < 3 and fails if N >= 3 And on success, N increments atomically by 1 And on failure, response status is 429 and the UI shows "Extension limit reached" and disables the control
Guardrails: Maximum Cumulative Extension Time
Given maxCumulativeExtensionMinutes is 15 and the session has accumulated M minutes of extensions When the recipient requests another extension Then if M <= 10, add 5 minutes; if 10 < M < 15, cap at 15 minutes; if M >= 15, reject And the UI reflects the exact added time and remaining allowance immediately And attempts beyond 15 minutes return error code LIMIT_CUMULATIVE_REACHED with no state change
Abuse Prevention: Rate Limiting, Signature Validation, Replay Protection
Given each extension request includes a signed token with sessionId, userId, expiry, and nonce When the token is invalid, expired, or fails signature validation Then the request is rejected with 401 and no state changes occur And when >1 extension request is received for the same session within 1 second, excess requests are rejected with 429 And when the same nonce is reused, the request is rejected with 409 and logged as a replay
Backend Notifications & Idempotency
Given an extension is successfully applied When the new expiry is committed Then a HoldExtended event is published within 200 ms with sessionId, userId, previousExpiry, newExpiry, source, requestId And downstream subscribers receive the event at-least-once; duplicates are deduplicated via requestId And retries back off exponentially up to 3 attempts over 2 minutes on delivery failures And no duplicate state change occurs if the same request is retried And the waitlist availability service updates the hold status without emitting false availability notifications
Failure Messaging & Alternative Actions
Given an extension attempt fails due to limit, authentication, or expired hold When the failure is returned Then the countdown does not change and a reason-specific message is displayed And a "Rejoin waitlist" action is offered and enrolls the recipient within 2 seconds of tap with confirmation shown And the extension control is disabled for 5 seconds after failure to reduce repeat hammering
Waitlist Hold Sync & Notifications
"As a groomer, I want the waitlist to stay accurately informed about slot availability during holds and extensions so that openings are filled quickly without confusion."
Description

Synchronize slot hold state and extensions with the Smart Waitlist. When a hold is extended or expires, update waitlisters’ estimated availability windows and optionally send informative SMS updates without over-notifying. Ensure atomicity between slot release and waitlist auto-fill to prevent double-booking. Provide service hooks so the waitlist engine can pre-stage the next candidate while a hold is counting down.

Acceptance Criteria
Hold Extension Syncs Estimated Windows
Given a slot hold is active and at least one client is on the Smart Waitlist When the held client taps the Gentle Nudges “Extend 5 minutes” link Then the system recalculates estimated availability windows for all affected waitlisters within 5 seconds And only waitlisters whose window changes by >= 2 minutes are flagged as impacted And the new windows are persisted with a monotonically increasing version to prevent stale overwrites
Hold Expiry Triggers Atomic Release and Auto-fill
Given a slot hold is active and a next candidate is pre-staged by the waitlist engine When the hold expires (countdown reaches 0) or the client declines the slot Then the system releases the slot and creates exactly one booking using an atomic transaction And concurrent requests cannot create more than one booking (verified via idempotency keys and unique constraints) And the release + auto-fill operation completes within 2 seconds at the 95th percentile
Notification Rate Limiting and Opt-out
Given a waitlister’s notification preferences are stored (opt-in/opt-out) When multiple hold extensions or an expiry occur within a 15-minute window Then no more than 2 SMS updates are sent per waitlister per hold lifecycle And changes occurring within 60 seconds are coalesced into a single SMS with the latest window And no SMS is sent to waitlisters who are opted out, paused, or whose window change is < 2 minutes
Service Hooks for Pre-staging Next Candidate
Given a hold has T minutes remaining When T <= 3 minutes Then a pre-stage hook is emitted with slotId, candidateId, and readiness ETA, and is received by the waitlist engine within 200 ms And if the hold is extended, a pre-stage-update hook is emitted within 5 seconds with the new T And if the hold is canceled or expires, a pre-stage-cancel hook is emitted immediately
Real-time Countdown Reflected in Waitlist State
Given the Smart Waitlist is tracking a held slot When the held client opens the link and the countdown runs or is extended via Gentle Nudges Then the backend state exposes a live remaining time and recalculated windows via API within 5 seconds And any consumer (e.g., internal UI or webhook subscriber) retrieving state sees consistent countdown and windows without requiring a restart And state on refresh matches server truth (no stale countdown)
Failure Recovery and Consistency Guarantees
Given a transient failure occurs during waitlist update or SMS dispatch When retry logic executes Then waitlist updates are applied exactly once using idempotent write semantics and version checks And SMS sends are retried up to 3 times with exponential backoff and provider-level dedupe keys to avoid duplicates And queued notifications are delivered within 2 minutes after recovery And an alert is raised if end-to-end sync latency exceeds 10 seconds (p95) for 5 consecutive minutes
Compliance, Opt-Out Respect, and Nudge Guardrails
"As a business owner, I want nudges to respect consent, quiet hours, and limits so that we boost conversions without irritating clients or violating regulations."
Description

Enforce SMS compliance and customer preferences for Gentle Nudges: honor opt-in status and STOP/UNSUBSCRIBE, apply quiet hours, cap nudge frequency per session/day, and prevent reminders on sensitive flows (e.g., disputes). Centralize configuration for country-specific rules and store audit logs of consent and message metadata. Provide mechanisms to suppress nudges for VIPs or flagged clients.

Acceptance Criteria
Opt-In and STOP/UNSUBSCRIBE Enforcement
Given a client without SMS opt-in and a nudge trigger occurs, When the system evaluates eligibility, Then no SMS nudge is sent and a suppression reason "no_opt_in" is recorded. Given a client replies STOP or UNSUBSCRIBE, When the message is received, Then their opt-in status is set to opted-out within 10 seconds, all pending nudges are canceled, and future nudges are suppressed until a valid re-opt-in is received. Given a client previously opted-out replies START or UNSTOP or YES, When the message is processed, Then opt-in is restored within 10 seconds and subsequent nudges are eligible subject to all other guardrails.
Quiet Hours Suppression by Locale
Given client timezone is known and quiet hours are configured as 20:00–08:00 local, When a nudge trigger occurs at 22:15 local, Then no SMS is sent, the next eligible send is scheduled for 08:00 local, and suppression reason "quiet_hours" is recorded. Given quiet hours configuration is updated and published, When a nudge trigger occurs after publication, Then the new window is enforced without deploy or restart within 60 seconds and the applied config version is logged.
Nudge Frequency Caps per Session and per Day
Given caps are set to max 2 nudges per session and 4 per rolling 24 hours per client, When a session attempts a 3rd nudge, Then it is suppressed and suppression reason "session_cap" is recorded. Given a client has received 4 nudges in the last 24 hours, When another nudge is triggered, Then it is suppressed and suppression reason "daily_cap" is recorded. Given a client taps "Extend 5 minutes" in the link, When the countdown resets, Then no additional SMS is sent unless under caps and a scheduled reminder becomes due.
Sensitive Flow Suppression (Disputes/Legal Holds)
Given a payment dispute is open on the session, When a nudge trigger occurs, Then no SMS is sent and suppression reason "sensitive_flow:dispute_open" is recorded. Given a client account is on legal hold or chargeback watchlist, When a nudge trigger occurs, Then no SMS is sent and suppression reason "sensitive_flow:account_flag" is recorded.
Country-Specific Rule Configuration and Enforcement
Given the business country is US with STOP/START/HELP keywords enforced and quiet hours disabled, When a client sends STOP, Then opt-out is updated within 10 seconds and an opt-out confirmation is sent per US rules. Given the business country is UK with quiet hours 21:00–09:00 configured, When a nudge trigger occurs at 08:30 UK time, Then it is suppressed and scheduled for 09:00 UK time with suppression reason "quiet_hours" recorded. Given country-specific config version v2 is published centrally, When eligibility is evaluated, Then rules from v2 are applied within 60 seconds and the applied config version is logged with each decision.
Consent and Message Metadata Audit Logging
Given any send attempt (allowed or suppressed), When the event is processed, Then an immutable audit record is stored including client_id, session_id, country, timezone, consent_state, trigger_type, rule_decision (allow/suppress), reason_code, template_id, message_id (if sent), scheduled_or_send_time, carrier_status (if available), and config_version. Given a compliance audit request for a client within the last 24 months, When querying the audit log, Then up to 10,000 events are returned within 5 seconds and are exportable as CSV. Given a consent state change (opt-in or opt-out), When it is processed, Then the consent audit entry is written before any further messages can be sent.
VIP and Flagged Client Suppression with Override
Given a client has VIP tag or nudge_suppressed=true, When a nudge trigger occurs, Then the nudge is suppressed and suppression reason "vip_or_flagged" is recorded. Given a staff user with Manager role applies a one-session override with justification, When the next nudge is evaluated, Then exactly one nudge may be sent if other guardrails allow and the override details (user_id, timestamp, justification) are logged. Given the VIP list is updated via bulk import, When the import completes, Then changes take effect within 5 minutes and are reflected in subsequent eligibility decisions.
Nudge Analytics & Admin Configuration
"As a groomer, I want to configure how and when nudges are sent and see their impact so that I can maximize recovered payments without over-messaging clients."
Description

Provide an admin console to enable/disable Gentle Nudges, configure idle thresholds, nudge cadence, copy templates, and extension limits. Surface analytics including nudge send rates, extension usage, conversion uplift, time-to-payment, and impact on waitlist fill. Support segmentation by service type and cohort, export to CSV, and basic A/B toggles for future optimization.

Acceptance Criteria
Account and Service-Level Nudge Toggle
Given I am an admin with access to Gentle Nudges settings When I disable Gentle Nudges at the account level and save Then all new client sessions initiated after save send no nudges or extension prompts And service-level nudge controls are disabled and indicate inheritance from account Off And any in-flight nudge sequences are canceled within 120 seconds and logged with reason "Account toggle Off" Given account-level Gentle Nudges are On and a specific service's toggle is Off When a client in that service opens a payment link Then no nudges or extension prompts are sent And analytics label the session as "Nudges disabled (service)"
Idle Threshold and Nudge Cadence Configuration
Given I open the Gentle Nudges configuration form When I set the idle threshold to a value between 10 and 600 seconds (inclusive) Then the value is accepted and validation passes Given I enter an idle threshold outside 10–600 seconds or non-numeric When I attempt to save Then an inline error explains the allowed range and save is blocked Given I set nudge cadence to a maximum of 3 nudges with a minimum spacing of 30–300 seconds When I configure the cadence Then the UI enforces the limits and shows projected send times based on the idle threshold Given I save valid settings When clients initiate sessions after the save time Then the new settings apply only to those new sessions and not retroactively
Nudge Copy Templates and Preview
Given I edit the nudge copy templates for first, second, and final nudge When I include variables {client_first_name}, {business_name}, {countdown}, and {extension_link} Then the template validator confirms all variables are recognized and will resolve at send time Given I include an unknown or malformed variable When I attempt to save Then save is blocked and an inline error lists the invalid tokens Given I click Preview When I select a service type and example client Then the preview renders the SMS body with sample values and an updating countdown placeholder And the character count and estimated SMS segments are displayed
Extension Limits Configuration and Client Behavior
Given I set the extension limit to a value between 0 and 3 When the limit is 0 Then the extension link/button does not appear in any nudge messages Given the extension limit is greater than 0 When a client receives a nudge Then the message includes an extension button showing remaining extensions (e.g., "+5 min (2 left)") Given a client has consumed the maximum number of extensions When they attempt another extension Then the request is denied with a friendly message and no additional time is added And analytics record the total extensions used for the session
Analytics: Core Metrics and Date Range
Given I open the Nudge Analytics dashboard When I select a date range up to 90 days and apply Then the following metrics render within 5 seconds using the current filters: And Nudge send rate = sessions_with_at_least_one_nudge / eligible_sessions (displayed as % with numerator/denominator on hover) And Extension usage rate = sessions_with_extension_gt_0 / sessions_with_extensions_available (displayed as % with numerator/denominator on hover) And Conversion uplift % = ((conversion_rate_variant - conversion_rate_control) / conversion_rate_control) * 100 shown only when an A/B experiment is active; otherwise shown as N/A And Median time-to-payment (seconds) = median(payment_timestamp - link_open_timestamp) among converted sessions And Waitlist fill impact (pp) = waitlist_fill_rate_with_nudges - waitlist_fill_rate_without_nudges within 24 hours of cancellation
Segmentation by Service Type and Cohort
Given I apply filters for service type, client cohort (new vs returning), and booking channel When I update any filter Then all metrics, charts, and tables update to reflect the filtered subset And a Compare toggle allows side-by-side comparison of exactly two segments with clear labels And clearing filters resets to All and updates the URL so the current selection can be shared and reloaded
CSV Export and Basic A/B Toggle Management
Given I have any combination of filters and date range applied on Analytics When I click Export CSV Then a UTF-8 CSV downloads within 10 seconds containing one header row and data rows with columns: date_iso, service_type, cohort, booking_channel, eligible_sessions, nudges_sent, sessions_with_extensions_available, extensions_used, conversions, conversion_rate, conversion_uplift_pct, median_time_to_payment_seconds, waitlist_fill_rate, waitlist_fill_impact_pp, experiment_variant And the export is logged with my user ID, timestamp, and filter parameters Given I open the Experiments settings When I create a Basic A/B toggle for Gentle Nudges with Control and Variant Then I can set an allocation split between 1% and 99% per variant and save And parameter overrides (e.g., enable/disable, idle threshold, cadence) can be assigned per variant And users are consistently bucketed per client or session as configured, with no cross-over during the experiment And the experiment can be paused or ended, which reverts traffic to the default configuration And analytics report per-variant metrics matching the definitions above

Gap Radar

Continuously scans your day for 5–20 minute voids and upcoming slippage, then quantifies the time and income you can recover. Surfaces the next-best action as simple text prompts so you can fix gaps without opening a calendar.

Requirements

Real-time Gap Detection Engine
"As an independent groomer, I want PawPilot to automatically spot small schedule gaps in real time so that I can fill them quickly without checking my calendar."
Description

Continuously ingests day-of schedule events, travel buffers, service durations, cancellations, and late arrivals to detect 5–20 minute voids and near-term gaps without requiring the user to open a calendar. Runs on a rolling interval, evaluates provider-specific constraints, and produces structured gap objects (start, end, location, eligible services) for downstream components. Integrates with PawPilot schedule, Smart Waitlist, and SMS messaging to drive timely prompts while avoiding duplicate alerts.

Acceptance Criteria
Real-time detection of 5–20 minute gaps
Given the provider’s day-of schedule is loaded and the engine runs on a 60-second rolling interval When a cancellation, early completion, or late arrival creates an idle window between 5 and 20 minutes after accounting for pre/post-service buffers and travel time Then the engine emits a gap object within 30 seconds of the triggering change or by the next cycle, whichever is sooner And the gap start and end times match the true idle window boundaries within ±1 minute And no gap is emitted for windows shorter than 5 minutes or longer than 20 minutes
Structured gap object schema and content
Given a gap is detected Then the emitted gap object includes: gapId, providerId, start, end, durationMinutes, locationId, locationGeo, sourceEventIds[], eligibleServiceIds[], version, createdAt, updatedAt, reason And start and end are ISO 8601 timestamps in the provider’s timezone And durationMinutes equals end minus start in whole minutes And eligibleServiceIds only include services whose totalRequiredMinutes (service + setup/cleanup) ≤ durationMinutes and that meet provider constraints and minimum lead time And gapId is deterministic for (providerId, start, end, locationId) And the object validates against schema version v1.0 with no required field missing
Provider constraints respected in gap generation
Given the provider’s working hours, breaks/time-off, minimum lead time, service area, and equipment constraints are configured When computing gaps and eligible services Then no gap is produced outside working hours or during breaks/time-off And gaps that would require travel outside the provider’s service area are excluded And eligible services exclude any that violate minimum lead time or equipment constraints And all gaps preserve configured pre/post-service buffers to the adjacent appointments
Duplicate alert suppression and gap deduplication
Given a gap has been detected and an alert dispatched When the same gap is re-detected within 15 minutes with start/end shifting by less than 3 minutes Then no new alert is sent and the existing gap object is updated with version incremented and changeReason="refresh" When the gap materially changes (start or end shifts by ≥ 3 minutes) or becomes filled/canceled Then send a single consolidated update or fill confirmation and mark prior notifications as superseded And no more than one alert per gap per channel is sent within a 15-minute window
Integration with Smart Waitlist and SMS prompts
Given a new gap with at least one eligible service and matching waitlist entries exists When the gap is emitted Then the engine posts the gap payload to Smart Waitlist within 15 seconds and receives HTTP 202 And top-ranked matching clients receive at most one SMS prompt per client per gap within 30 seconds of the 202 response And on transient failure, retry with exponential backoff up to 3 attempts; if still failing, mark the gap integrationStatus="pending" and do not send duplicate messages And when the gap is filled, cancel any pending or scheduled messages for that gap
Late arrival handling and near-term slippage
Given an in-progress appointment exceeds the provider’s lateThreshold (e.g., 5 minutes) When the engine recalculates the downstream schedule Then it creates or updates a near-term gap only if an idle window of 5–20 minutes emerges within the next 90 minutes And any previously emitted gaps invalidated by the slippage are withdrawn and their alerts suppressed And recalculation completes and updates are published within 30 seconds of the lateThreshold being crossed
Travel buffers and location-aware gap computation
Given travel time estimates or configured travel buffers between consecutive appointments When computing idle windows Then gap.start equals prior appointment end plus post-service buffer plus travel time And gap.end equals next appointment start minus pre-service buffer minus travel time And no gap is produced if required travel time overlaps the candidate window And the gap location reflects the correct place context (prior or next appointment location) with geo coordinates resolved
Slippage Prediction
"As a dog walker, I want advance warning when my next few appointments are likely to run late so that I can adjust routes or slot a quick service to avoid losing income."
Description

Forecasts appointment overruns and late starts by analyzing historical service durations, client punctuality, travel times, and external signals such as traffic and weather. Flags at-risk time blocks up to two hours ahead and converts them into predicted gaps with confidence scores. Feeds predictions to Gap Radar to preemptively queue actions that prevent idle time or cascading delays.

Acceptance Criteria
Early Overrun Prediction for On‑Site Grooming
Given a scheduled grooming appointment with ≥5 historical duration records for the same service type and groomer and the start is within the next 120 minutes When the model predicts the duration will exceed the scheduled slot by ≥10 minutes with confidence ≥0.70 Then the time block is flagged as "At Risk: Overrun" and a predicted gap is created with start/end timestamps, size in minutes (integer), source appointment ID, and confidence rounded to two decimals (0.00–1.00) And no more than one alert per appointment is emitted every 10 minutes unless confidence changes by ≥0.10 or predicted gap size changes by ≥5 minutes And the prediction is published to Gap Radar within 30 seconds of calculation
Late Client Arrival Forecast Using Punctuality + Traffic
Given an in-salon appointment that requires client arrival and has ≥3 historical punctuality data points for the client or client segment and the start is within the next 120 minutes When the combined lateness model (punctuality + live travel) predicts an arrival delay ≥8 minutes with confidence ≥0.65 Then the block is flagged as "At Risk: Late Start" with predicted delay minutes, affected appointment ID, downstream impacted blocks, and confidence rounded to two decimals And if predicted delay drops below 5 minutes or confidence <0.50 for 10 consecutive minutes, the flag is auto-cleared and the resolution timestamp is recorded And the updated prediction (flag or clear) is sent to Gap Radar within 30 seconds
Travel-Time Slippage From Real‑Time Traffic Spike
Given two back-to-back mobile services with a planned travel leg and the downstream start is within the next 120 minutes When the live traffic ETA for the leg increases by ≥6 minutes versus planned with confidence ≥0.60 Then the downstream appointment block is flagged as "At Risk: Travel Slippage" and a predicted gap or delay is computed (minutes) and published to Gap Radar with confidence rounded to two decimals And end-to-end processing latency from traffic update receipt to published prediction is ≤60 seconds
Weather-Driven Duration Extension for Dog Walks
Given outdoor dog-walking appointments scheduled today within the next 120 minutes in a service area with forecast adverse weather When the weather index exceeds thresholds (rain rate ≥6 mm/hr OR heat index ≥95°F OR snowfall ≥ moderate) AND historical similar-condition walks show ≥15% duration increase Then predicted durations are adjusted accordingly, at-risk blocks are flagged, and predicted gaps with confidence ≥0.60 are created and sent to Gap Radar And if the weather index stays below thresholds for 15 consecutive minutes, active weather-driven predictions for the affected blocks are rescinded and the change is logged
Confidence Thresholding and Calibration Controls
Rule: Only create and surface predicted gaps to Gap Radar when confidence ≥0.65; predictions with confidence 0.40–0.64 are stored as insights but not surfaced Rule: Every prediction includes a confidence score (0.00–1.00, two decimals) and a list of contributing signal classes (duration history, punctuality, travel, weather) Rule: Over a rolling 30-day window with ≥200 predictions, calibration error (|predicted probability decile minus observed event rate|) is ≤0.10 in ≥7 out of 10 deciles; otherwise raise a calibration alert
Gap Radar Action Queue Feed and De‑duplication
Given a new at-risk block with a predicted gap or delay >5 minutes within the next 120 minutes When the prediction is published Then Gap Radar receives a queue item within 30 seconds containing: human-readable text prompt(s), action type, target time window, affected appointment IDs, and confidence And there is at most one active queue item per at-risk block; subsequent predictions for the same block update the existing item instead of creating duplicates And SMS prompts are generated only when within the user’s working-hours settings; otherwise the actions are queued without SMS
Revenue Recovery Calculator
"As a sitter, I want to see how much income I could recover from a small gap so that I can decide whether it’s worth messaging clients."
Description

Quantifies recoverable time and income for each detected or predicted gap by mapping eligible micro-services, add-ons, or waitlist requests to the gap duration and pricing. Calculates expected value, probability of fill, and same-day payment impact, aggregating into daily and weekly recovery metrics. Exposes values in SMS prompts and within dashboard analytics to guide the next-best action.

Acceptance Criteria
Service-to-Gap Mapping and Pricing
Given a detected or predicted gap with duration D minutes and an active service catalog When the Revenue Recovery Calculator runs Then it returns only micro-services, add-ons, or waitlist requests whose duration_minutes <= D and are marked eligible_for_gaps = true And each returned candidate includes: service_id, name, source_type (micro-service|add-on|waitlist), duration_minutes, price_cents (integer), and currency And price_cents is resolved from the active price rule for the provider’s timezone and service date And if no eligible items exist, the candidate list is empty
Expected Value and Probability of Fill Calculation
Given a list of eligible candidates with price_cents P When computing probability_of_fill Then probability_of_fill is a numeric value between 0.00 and 1.00 inclusive with two decimal places And if insufficient data exists, a provider-level default is used and default_probability_used = true When computing expected value Then expected_value_cents = round(P * probability_of_fill) using bankers’ rounding to the nearest cent And expected_value_cents is an integer >= 0
Same-Day Payment Impact
Given a candidate with expected_value_cents EV and same-day payout enabled When computing same-day payment impact Then same_day_impact_cents = EV for pay-in-full flows And if a deposit percentage d (0–1) is configured, same_day_impact_cents = round(EV * d) And if same-day payout is disabled, same_day_impact_cents = 0 And same_day_impact_cents is an integer >= 0
Daily and Weekly Recovery Aggregation
Given all unfilled gaps in a provider’s day When aggregating recovery metrics Then daily_recoverable_time_minutes = sum of the selected top expected-value candidate’s duration per gap And daily_recoverable_value_cents = sum of the selected candidate’s expected_value_cents per gap And gaps marked filled are excluded from future aggregates and recorded under realized_value_cents When aggregating weekly metrics Then weekly totals equal the sum of daily totals for Monday–Sunday in the provider’s timezone And all aggregates recompute within 60 seconds of any relevant change
SMS Prompt Exposure
Given a new eligible gap is detected or predicted When sending an SMS prompt to the provider Then the message includes the top 1–3 candidates by expected_value_cents with: name, duration, price, probability (as %), expected value (currency), and same-day impact (currency) And includes numbered quick-reply options (1–3) plus a Skip option And the SMS body length does not exceed 320 characters And if no eligible candidates exist, no SMS is sent and an audit log is recorded with reason = "no eligible candidates"
Dashboard Analytics Exposure and Consistency
Given the Recovery analytics view is opened When displaying metrics Then it shows daily and weekly: recoverable_time_minutes, recoverable_value_cents, same_day_impact_cents, gaps_analyzed_count, and projected_fill_rate And selecting a specific day lists gaps with chosen candidate, price, probability, expected value, and status (filled|unfilled) And totals match the SMS-exposed values for the same gaps with exact cent-level equality And data refresh latency from event to dashboard update is ≤ 60 seconds
SMS Next-Best Action Prompts
"As a groomer on the go, I want clear text prompts with one-tap actions so that I can fill gaps without opening my calendar."
Description

Delivers concise, actionable text messages to the provider with the single best step to fill a gap, including one-tap reply shortcuts and deep links such as deposit, confirm, and reschedule. Supports batching to avoid spam, quiet hours, and opt-in controls. Prompts are stateful and update or retract when the schedule changes to prevent conflicts, enabling gap resolution without opening a calendar.

Acceptance Criteria
Gap Detection Prompt Delivery
Given the provider has an unbooked gap between 5 and 20 minutes on today's schedule and is opted-in and outside quiet hours When Gap Radar detects the gap Then exactly one SMS next-best action prompt is sent within 30 seconds containing: a concise message (<= 2 SMS segments), at least one reply shortcut keyword, and deep links for deposit, confirm, or reschedule appropriate to the context And the SMS is addressed to the provider's verified phone number And no additional prompt is sent for the same gap unless state changes
Quiet Hours Deferral and Batch Send
Given the provider has quiet hours configured and active When a new gap is detected during quiet hours Then no prompt is sent immediately And the prompt is queued and delivered within 2 minutes after quiet hours end And any queued prompts during quiet hours are combined into a single batched SMS And the batched SMS is clearly labeled as queued and lists items in priority order
Spam-Safe Batching and Rate Limits
Given multiple prompts would be generated within a 15-minute rolling window When sending prompts Then the system sends no more than 1 outbound prompt every 10 minutes and no more than 6 per day per provider And multiple candidate prompts are consolidated into one SMS listing the highest-priority action first with a 'MORE' link or keyword for additional items And no duplicate prompts for the same gap are included
Provider Opt-In and Pause Controls
Given the provider can manage prompt preferences via settings or SMS keywords When the provider sends PAUSE, RESUME, or toggles prompt types in settings Then preferences are applied within 60 seconds and confirmed via SMS And no prompts are sent while paused except compliance-required messages And an audit log records the change with timestamp, method, and actor
Stateful Update and Retraction on Schedule Change
Given a prompt was sent for a specific gap When the gap is filled, canceled, or the schedule changes causing overlap Then any active prompt for that gap is retracted or updated within 10 seconds And previously issued deep links for the retracted prompt become invalid and return a human-readable 'No longer available' message And the system ensures at most one active prompt exists per time slot
One-Tap Actions Execute Without Calendar
Given a prompt includes reply shortcuts (e.g., CONFIRM, DEPOSIT, RESCHED) and deep links When the provider taps a deep link or replies with a valid shortcut Then the requested action completes without opening a calendar UI and a confirmation SMS is returned within 10 seconds And on failure, an error SMS with a retry path is sent and the action is not partially applied And all actions are tracked with success/failure for analytics
Recoverable Time and Income in Prompt
Given a gap is detected and a next-best action is selected When composing the prompt Then the SMS displays the recoverable minutes and estimated income for the action using the provider's pricing and buffers And the estimate is within ±10% of the billing engine's computed value And currency, time, and date formats match the provider's locale and timezone
Smart Waitlist Micro-Fill
"As a dog walker, I want PawPilot to message the best waitlist clients for tiny openings so that I can fill them fast without manual texting."
Description

Matches detected gaps with Smart Waitlist candidates based on proximity, service fit, past responsiveness, and deposit readiness. Auto-composes personalized outreach with deposit link and proposed time, throttles messages to respect client preferences, and auto-updates the schedule upon acceptance. Falls back to micro add-ons for existing nearby clients if no waitlist match is available.

Acceptance Criteria
Auto-Match Waitlist Candidate for Detected Gap
Given a gap is detected with start time, duration (5–20 min), and location When the matcher runs Then up to 3 candidates are returned whose service fits the gap, are within 2 miles or <= 6 minutes travel ETA, are deposit-ready, and have availability overlap >= gap duration And candidates contacted for the same gap today, opted out, blocked, or with unpaid balances > $0 are excluded And results are returned in <= 2 seconds And ties are broken by most recent positive engagement; if still tied, by deterministic client ID
Compose and Send Personalized Outreach with Deposit Link
Given a candidate is selected for a specific gap When composing the outreach Then the SMS includes client first name, pet name(s) if on file, service, proposed start time with timezone, duration, price, concise location cue, a deposit link, an expiry time, and clear instructions: "Reply YES to reserve, NO to skip" And total message length is <= 320 characters (excluding carrier-required STOP text) When sending the outreach Then a delivery receipt is recorded And on carrier failure, the system retries once within 60 seconds (alternate sender ID if available), else marks failed and proceeds to the next candidate
Throttling and Client Preference Respect
Given client communication preferences and quiet hours are configured (default quiet 21:00–08:00 client local time) When preparing any outreach Then messages are not sent during quiet hours and are queued until the next allowed window And frequency caps are enforced: max 2 micro-fill outreaches per client per 24 hours and min 2 hours between outreaches (unless per-client overrides allow otherwise) And clients with "no same-day offers" preference are skipped for same-day gaps with the reason logged
Auto-Update Schedule on Acceptance and Conflict Handling
Given a recipient replies YES or completes deposit via the link When acceptance is received Then the appointment is created for the proposed time and duration atomically, the deposit is captured, and a confirmation SMS is sent within 5 seconds And all other pending offers for that gap are canceled, and any already-engaged candidates receive a courteous "slot filled" notice And if deposit is not completed within 5 minutes of link open, the hold expires and the gap reopens with notifications updated And if a conflict is detected (gap no longer available), the deposit is not captured, the client is notified, and recovery options are offered
Fallback to Micro Add-ons for Nearby Existing Client
Given no qualified waitlist match is confirmed within 60 seconds of gap detection When evaluating existing appointments near the gap Then clients with appointments within ±30 minutes of the gap and within 0.25 miles are considered if their service allows add-ons And add-ons from the catalog are proposed whose combined duration fits within the gap (±1 minute tolerance) and meet or exceed a configurable revenue minimum When the client accepts Then the add-on is appended to the existing appointment atomically, pricing is updated, and a confirmation SMS is sent; rollback occurs on any failure
Candidate Ranking and Auditability
Given candidates qualify for a detected gap When scoring candidates Then a deterministic composite score is computed per candidate with weights: proximity 40%, past responsiveness (reply-within-10-min rate over last 90 days) 30%, deposit readiness recency 20%, service fit exactness 10% And the score, weight version, included/excluded reasons, and timestamps are persisted per decision And the last 100 match runs are retrievable via API filtered by gap ID or candidate ID
Duplicate Contact and Spam Safeguards
Given any set of gaps in a 24-hour period When generating outreach Then the same client receives at most 1 offer per gap and at most 3 micro-fill offers across distinct gaps in any rolling 24 hours And a global cap of 30 micro-fill sends per business per hour is enforced; additional sends are queued And STOP/UNSUBSCRIBE and "snooze" replies are honored immediately, preventing further sends and updating client status with an audit entry
Configurable Gap Rules and Quiet Hours
"As an independent sitter, I want to control what counts as a fillable gap and when I’m contacted so that prompts are useful and not distracting."
Description

Lets providers tailor gap detection and prompting behavior, including minimum and maximum gap duration (default 5–20 minutes), eligible service types, travel and buffer rules, daily hours and quiet hours, maximum prompts per day, and lead-time thresholds. Supports per-provider overrides and presets for groomers, walkers, and sitters to ensure relevance and reduce notification fatigue.

Acceptance Criteria
Gap Duration Range and Defaults Control Detection
Given a provider has not configured gap duration settings When Gap Radar scans the schedule Then it uses default min=5 minutes and max=20 minutes to detect gaps Given a provider sets Min Gap=7 minutes and Max Gap=15 minutes and saves When the day contains gaps of 4, 8, 16, and 19 minutes Then only the 8-minute gap is detected and surfaced for action Given a provider enters Min Gap greater than Max Gap When attempting to save the settings Then a validation error "Min must be less than or equal to Max" is displayed and settings are not saved
Daily Hours and Quiet Hours Control Prompt Timing
Given Daily Hours are 08:00–18:00 local time and Quiet Hours are 12:00–13:00 and 20:00–07:59 When a gap occurs at 12:10 Then no SMS prompt is sent during Quiet Hours and a single prompt is queued for 13:00 if the gap still exists Given the same settings When a gap occurs at 18:30 (outside Daily Hours) Then no prompt is sent and none is queued Given Quiet Hours are configured outside the defined Daily Hours When attempting to save Then a validation error "Quiet Hours must be within Daily Hours" is displayed and settings are not saved
Service Type Eligibility Filters Suggestions
Given eligible service types are set to Dog Walk and Drop-In Visit only When Gap Radar generates suggestions Then suggestions reference only the selected eligible service types Given Grooming is not selected as eligible When scanning for gaps Then no prompts suggest Grooming to fill the gap Given a provider attempts to save with zero eligible service types selected When saving settings Then a validation error "Select at least one eligible service type" is displayed and settings are not saved
Travel and Buffer Rules Adjust Gap Identification
Given a provider configures a 10-minute post-service buffer and 5-minute travel time between different locations When there is a 30-minute void between Appointment A (Location X) and Appointment B (Location Y) Then the effective gap is calculated as 15 minutes and is only detected if it falls within the configured Min/Max range Given two consecutive services at the same location and travel is set to 0 for same-location transitions When evaluating the gap Then travel time is not applied to the effective gap calculation Given Buffers are disabled When evaluating a void Then buffers are not deducted from the void duration
Prompt Limit and Reset Per Day
Given Max Prompts Per Day is set to 6 When the 7th prompt would be generated in the provider's local day Then the prompt is suppressed and a suppression reason "daily limit reached" is logged Given prompts are queued instead of sent during Quiet Hours When counting toward the daily limit Then queued prompts count toward Max Prompts Per Day the moment they are generated Given a new local day starts at 00:00 When the time passes midnight Then the prompt counter resets to 0
Lead-Time Threshold Gates Prompt Generation
Given Lead-Time Threshold is set to 30 minutes When the current time is 10:00 and a gap begins at 10:20 Then no prompt is generated for that gap Given the same settings When a gap begins at 10:35 Then a prompt is generated for that gap Given no Lead-Time Threshold is configured When Gap Radar runs Then a default Lead-Time Threshold of 15 minutes is applied
Presets and Per-Provider Overrides Apply and Persist
Given provider selects the "Dog Walker" preset When settings are applied Then Min/Max Gap, eligible service types, travel/buffer, hours/quiet hours, prompts/day, and lead-time values populate with the preset defaults Given the provider modifies any preset field When settings are saved Then the configuration is marked as "Overridden" and persists across sessions Given an admin sets an account-level default preset When a provider has explicit overrides Then the provider's overrides take precedence and are not overwritten by the account default Given the provider chooses "Revert to Preset" When confirmed Then all overridden fields return to the currently selected preset values
Activity Logging and Performance Analytics
"As a business owner, I want reports on how many gaps were filled and the revenue recovered so that I can see the value of Gap Radar."
Description

Captures every detected gap, prompt sent, user action, client response, and outcome to compute fill rates, time saved, cancellations backfilled, and revenue recovered. Provides daily and weekly summaries via SMS and dashboard export to CSV for bookkeeping. Data powers continuous tuning of predictions and prompt ranking.

Acceptance Criteria
Event Logging: Gaps, Prompts, Actions, Responses, Outcomes
Given Gap Radar detects a gap, When the gap is identified, Then an event is persisted with fields: event_id (UUID), account_id, provider_id, gap_id, detected_at (ISO-8601, TZ aware), gap_start, gap_end, gap_duration_minutes, detection_source, and predicted_value_cents. Given the system sends a prompt related to a gap, When the prompt is dispatched, Then a prompt_sent event is persisted with: prompt_id (UUID), gap_id, client_id, channel="SMS", template_id, template_version, rank_position, sent_at, message_sid (if Twilio or equivalent), and delivery_status. Given a user acts on a prompt, When the user accepts/declines/edits, Then a user_action event is persisted with: action_id (UUID), action_type in {accept_gap, decline, reschedule, manual_fill, cancel_prompt}, acted_at, actor_id, and references to prompt_id and gap_id. Given a client responds to a prompt, When the client replies via SMS, Then a client_response event is persisted with: response_id (UUID), prompt_id, gap_id, client_id, response_type in {yes,no,proposed_time,deposit_paid,other}, response_body (normalized), received_at, parsed_success (boolean). Given a gap transitions to a terminal state, When the slot is filled or expires, Then an outcome event is persisted with: outcome_id (UUID), gap_id, outcome_status in {filled, unfilled}, filled_by in {waitlist, direct_reply, manual}, resolved_at, and linkage to the booking_id if applicable. Given events are produced, When they are written to storage, Then writes are idempotent by their UUID, retried with exponential backoff up to 5 attempts, and achieve 99.9% successful persistence within 1 minute over a 24h window. Given events are persisted, When queried in the audit view by account and date range, Then they appear within 2 minutes (p95) and are filterable by event type, gap_id, prompt_id, and client_id.
Outcome Resolution and Metric Attribution
Given a detected gap, When a booking overlaps the gap by at least 80% of gap_duration_minutes before the gap_end, Then the gap outcome_status is set to filled and the filled_by is determined from the triggering event (waitlist/direct_reply/manual). Given a cancellation created the gap, When that cancellation’s slot is later filled, Then cancellations_backfilled count is incremented by 1 for the day of the original cancellation. Given a gap is filled, When revenue is recorded, Then revenue_recovered_cents equals the booked service price (including deposit if collected) associated with booking_id at time of fill, and is attributed to the day of resolved_at. Given time saved must be calculated, When an outcome is filled_by in {waitlist,direct_reply}, Then time_saved_minutes = configured_autofill_minutes (default 6) per filled gap; When outcome is manual but assisted by prompt (prompt_id present), Then time_saved_minutes = configured_assist_minutes (default 3); Otherwise 0. Given overlapping prompts or actions, When computing metrics, Then each gap contributes to metrics exactly once and is linked to a single outcome event; duplicate attribution is not permitted. Given metrics are computed, When daily totals are generated, Then fill_rate = filled_gaps/total_detected_gaps rounded to 1 decimal place, cancellations_backfilled is an integer count, time_saved_minutes is rounded to whole minutes, and revenue_recovered is formatted in the account currency with ISO code.
Daily SMS Performance Summary
Given an active account with at least one detected gap in the local day, When the local time reaches 7:00 PM, Then send a single SMS summary containing: fill_rate %, time_saved_minutes, cancellations_backfilled count, and revenue_recovered with currency code for the period 12:00 AM–6:00 PM local time. Given the summary is sent, When delivered, Then the SMS body includes the date (YYYY-MM-DD), account name, and a short permalink to the dashboard for details. Given no activity occurred, When the local time reaches 7:00 PM, Then send an SMS stating "No gaps detected today" and include 0 values for all metrics. Given SMS delivery may fail, When a delivery_status indicates failure, Then retry up to 3 times within 30 minutes and log a prompt_sent retry with correlation to the original message. Given user communication preferences, When an account opts out of daily summaries, Then no SMS is sent and the opt-out is recorded; metrics remain available on the dashboard. Given the summary is sent, When events are audited, Then a prompt_sent event with template_id "daily_summary" is present and linked to the day’s metrics snapshot.
Weekly SMS Performance Summary
Given an active account, When it is Sunday 7:00 PM local time, Then send a weekly SMS summary for Monday–Sunday inclusive containing: fill_rate %, time_saved_minutes, cancellations_backfilled, revenue_recovered, and week-over-week % change for each metric. Given week-over-week deltas are computed, When last week has 0 for a metric, Then display delta as N/A to avoid divide-by-zero. Given the weekly message is sent, When auditing, Then a prompt_sent event with template_id "weekly_summary" is recorded and linked to the weekly metrics snapshot. Given delivery may fail, When a failure is detected, Then retry up to 3 times within 60 minutes and record delivery_status updates. Given an account has opted out of weekly summaries, When Sunday 7:00 PM occurs, Then do not send and record the opt-out state.
Dashboard Analytics and CSV Export
Given a signed-in provider with access rights, When opening the analytics dashboard, Then display today, last 7 days, and custom date range metrics for fill_rate, time_saved_minutes, cancellations_backfilled, and revenue_recovered matching the underlying event store. Given a custom date range is selected, When applying the filter, Then totals update within 2 seconds (p95) and match CSV export totals for the same range. Given the user clicks Export CSV, When the range is <= 92 days, Then generate a CSV under 30 seconds (p95) named pawpilot_gap_analytics_{account_id}_{start_YYYYMMDD}_{end_YYYYMMDD}.csv. Given the CSV is generated, When opened, Then it contains a header row and columns: event_type, event_id, account_id, provider_id, gap_id, prompt_id, client_id, booking_id, status, detected_at, gap_start, gap_end, resolved_at, outcome_status, filled_by, response_type, revenue_recovered_cents, time_saved_minutes, currency_code, timezone, template_id, template_version. Given timezone and currency settings, When exporting, Then timestamps are ISO-8601 with timezone offset and currency_code follows ISO 4217 for the account. Given data governance, When exporting, Then no SMS body text or freeform PII is included in the CSV; identifiers are IDs only. Given access control, When a user lacks export permission, Then the Export CSV button is disabled and an explanatory tooltip is shown.
Data Timeliness and Completeness SLAs
Given events are ingested, When measuring pipeline latency, Then 95% of events are available in analytics queries within 2 minutes and 99% within 5 minutes of occurrence. Given system restarts or transient failures, When the pipeline recovers, Then no events are lost and exactly-once semantics are enforced via event_id de-duplication. Given an outage exceeding 5 minutes, When service is restored, Then the system backfills missing events and converges metrics within 10 minutes. Given monitoring is configured, When SLA breaches occur, Then alerts are emitted to on-call and a status event is logged with details of the breach window.
Model Tuning Data Feed for Predictions and Prompt Ranking
Given daily analytics snapshots, When the training data job runs at 2:00 AM local time, Then it produces a dated dataset with features: gap_duration, time_of_day, day_of_week, client_segment, prompt_template_id, rank_position, response_type, time_to_response, outcome_status, revenue_recovered_cents. Given privacy constraints, When producing the dataset, Then no phone numbers or message bodies are included; client_id is a stable hashed ID with a per-account salt. Given data sufficiency thresholds, When fewer than 100 filled gaps exist in the last 14 days, Then the ranking model is not updated and the previous version remains active. Given the model is updated, When deployed, Then the new model_version and activation_time are recorded, and a 7-day shadow evaluation is initiated. Given online performance monitoring, When conversion rate drops by more than 5% relative to the prior 14-day baseline, Then automatically roll back to the previous model_version and record a failure event.

One-Tap Shift

Texts adjacent clients smart micro-shifts (e.g., “10 min earlier?” or “Can we slide 8 min later?”) that respect preferences and quiet hours. A single YES auto-updates the route, erasing dead time with zero back-and-forth.

Requirements

Smart Eligibility Engine
"As an independent groomer or walker, I want PawPilot to automatically identify which nearby clients can shift a few minutes so that I can recover dead time without manual checking."
Description

Determine which adjacent clients are eligible for a micro-shift based on live route context, travel time, service duration, provider buffers, and client constraints. The engine evaluates earlier/later shift bounds (e.g., ±5–15 minutes), excludes opted-out clients, honors service-specific constraints (multi-pet durations, access windows, deposit/payment state), and ensures the net schedule remains feasible. It ranks candidates by expected dead-time recovered and historical acceptance propensity, then selects the optimal small set to message. Integrates with PawPilot’s schedule, mapping/ETA services, and provider availability, returning a vetted, prioritized list for messaging.

Acceptance Criteria
Eligibility Filtering Within Micro-Shift Bounds for Adjacent Clients
Given a live route with a target appointment and measured dead time When evaluating candidates Then only the immediately previous and next appointments on the same route-day are considered Given provider/service earlier/later bounds and client preference bounds When computing a proposed micro-shift Then the shift minutes fall within both sets of bounds Given provider buffers and current ETA-based travel times When simulating the micro-shift Then all affected legs satisfy drive time + buffer + service duration without overlaps Given a client is opted out of One-Tap Shift or is within quiet hours When evaluating eligibility Then the client is excluded Given service-specific constraints (multi-pet duration, access windows, access method requirements) When evaluating eligibility Then any violation excludes the client Given deposit/payment state rules When evaluating eligibility Then clients with required deposit unpaid are excluded unless provider setting allows send-with-deposit-link and this is flagged on the candidate
Net Schedule Feasibility After Hypothetical Acceptance
Given a single candidate micro-shift When simulating a YES response Then the route remains feasible end-to-end with no negative gaps or overlaps and all buffers honored Given a set of selected candidates to message When any one candidate accepts Then the resulting schedule is feasible without requiring other candidates to accept Given any two candidates in the selected set When both accept Then either their simultaneous acceptance remains feasible or the pair is not selected together Given a candidate fails feasibility simulation When evaluating Then the candidate is removed from the output with reasonCodes including FEASIBILITY_FAIL
Ranking and Deterministic Tie-Breaking
Given eligible candidates When ranking Then sort by descending expected dead-time recovered (minutes), then by descending acceptance propensity score, then by ascending recent message count to that client in last 30 days Given all prior sort keys are equal When ranking Then use a stable tiebreaker based on a hash of (routeId, appointmentId, clientId) Given a candidate lacks historical acceptance data When computing propensity Then use the service-level mean propensity; if unavailable, default to 0.5 Given ranking is computed When returning Then the output list is strictly ordered without equal ranks
Optimal Set Selection, Caps, and Anti-Spam Controls
Given a configured maxRecipientsPerShift (default 2) When selecting candidates to message Then select at most maxRecipientsPerShift Given candidates whose simultaneous acceptance would conflict When selecting Then select at most one from each conflicting group Given a client was messaged for a micro-shift within the prior 2 hours for an overlapping window When selecting Then exclude that client Given the same client appears across multiple routes/windows When selecting Then include at most one open micro-shift request per client phone number Given an org-level daily message cap per client (default 3) When selecting Then do not exceed the cap
Integration Freshness and Response Contract
Given schedule, mapping/ETA, and availability data When evaluating Then use data with freshness timestamps <= 30 seconds; otherwise abort selection and return empty candidates with reason STALE_CONTEXT Given an ETA service timeout > 1500 ms or error After one retry Then return empty candidates with reason ETA_UNAVAILABLE and do not mark any client eligible Given a successful evaluation When returning Then each candidate includes fields: clientId, appointmentId, shiftDirection, shiftMinutes, expectedDeadTimeRecoveredMinutes, acceptancePropensityScore (0–1), feasibilityPass, reasonCodes[], quietHoursRespected, depositRequired, depositSatisfied, suggestedSendWindowUTC Given response validation fails (missing required fields) When returning Then respond with 422 and a list of validation errors
Real-Time Re-evaluation and Idempotency
Given route context changes before dispatch (e.g., new booking, cancellation, ETA delta >= 2 minutes) When reevaluating within the same request lifecycle Then recompute eligibility/ranking and return the updated candidate list Given a retry with identical inputs and correlationId within 5 minutes When processing Then return an identical candidate list and order Given an outdated prior result When a newer evaluation has been produced Then mark the prior result as superseded via a new evaluationId and do not reuse it for messaging
Auditability and Performance SLAs
Given any candidate evaluated When returning Then include machine-readable reasonCodes reflecting decision path including at least ELIGIBLE, OUT_OF_BOUNDS, QUIET_HOURS, OPTED_OUT, BUFFER_CONFLICT, ACCESS_WINDOW_CONFLICT, DEPOSIT_UNPAID, FEASIBILITY_FAIL, ETA_UNAVAILABLE, STALE_CONTEXT Given debugMode=true When evaluating Then include a bounded evaluationTrace per candidate <= 5 KB with timestamps of key checks Given normal operation at up to 5 requests/sec and up to 20 candidate evaluations per request When measuring Then P95 end-to-end evaluation latency <= 400 ms and P99 <= 800 ms
Quiet Hours & Preference Compliance
"As a client, I want shift requests to respect my quiet hours and preferences so that I’m not disturbed and only asked for acceptable adjustments."
Description

Enforce per-client and per-provider preferences and legal requirements for outreach. Store and apply quiet hours by local timezone, maximum minutes willing to move earlier/later, maximum asks per period, preferred language, and contact opt-ins/opt-outs. Block shift requests during quiet hours, respect client-specific limits, and fall back to next eligible client. Capture and honor SMS keywords (e.g., STOP/START/HELP) and consent logs for compliance. Provide defaults with per-client overrides and ensure all outbound messaging for One-Tap Shift is screened through this policy layer.

Acceptance Criteria
Block One-Tap Shift during client quiet hours (local timezone & DST-aware)
Given a client with quiet hours set 20:00–08:00 in their local timezone When a One-Tap Shift message would be sent at 07:59 or 20:00 local time Then the message is not sent, the attempt is logged with reason "client_quiet_hours", and no counters are incremented. Given the same client on the day DST starts or ends When the local time is within quiet hours after applying timezone/DST rules Then the message is blocked as above and the local timestamp in the log reflects the correct offset. Given the local time is 08:01 When the message is evaluated Then it is not blocked for quiet hours (subject to other policies).
Respect per-client max earlier/later movement
Given a client with max_move_earlier=10 minutes and max_move_later=5 minutes When the required shift is earlier by 12 minutes Then the system either (a) reduces the ask to 10 minutes if it maintains route feasibility, or (b) skips this client and evaluates the next eligible adjacent client; in both cases, no ask exceeding preferences is sent. Given the required shift is later by 4 minutes When evaluated Then the ask is sent at 4 minutes later and logged with movement_delta=+4. Rule: All sent asks must include movement_delta within [-max_move_earlier, +max_move_later] inclusive.
Enforce per-client ask frequency caps
Given a client with max_asks_per_7_days=2 When two One-Tap Shift asks have been sent in the past 7 rolling days Then a third ask within that window is blocked, the attempt is logged with reason "ask_cap_reached", and the client is skipped for this opportunity. Given the 7-day window has rolled such that only one ask remains in-window When evaluated Then one additional ask may be sent and the counter reflects 2/2 after send. Rule: Only asks that are actually sent (SMS dispatched) count toward the cap; blocked evaluations do not.
Honor opt-in/opt-out and SMS keywords
Given a client is opted-out from SMS When a One-Tap Shift send is evaluated Then no message is sent, the attempt is logged with reason "opted_out", and the client is skipped. Given the system receives inbound "STOP" (case-insensitive, tolerant of punctuation/whitespace) When processed Then the client's status is set to opted-out immediately, a STOP confirmation SMS is sent once, and all future One-Tap Shift messages are blocked until re-opt-in. Given the system receives inbound "START" When processed Then the client's status changes to opted-in, the consent is logged with timestamp and source="SMS", and future messages may be sent subject to policy. Given inbound "HELP" When processed Then a HELP response is sent including support contact and opt-out instructions, localized to the client's preferred language.
Preferred language messaging and localized compliance text
Given a client with preferred_language="es" When sending a One-Tap Shift ask Then the Spanish template is used, including localized opt-out instructions and legal disclaimers. Given no template exists for the client's preferred language When evaluated Then the message is not sent in a fallback language, the attempt is logged with reason "missing_language_template", and the next eligible client is evaluated. Rule: Template selection and message content are recorded with template_id and language_code in the audit log.
Provider quiet hours, legal windows, and universal policy screening
Given provider quiet hours are set 21:00–07:00 local to the provider When a One-Tap Shift send is evaluated during provider quiet hours Then the message is blocked with reason "provider_quiet_hours". Given applicable legal outreach window is 08:00–21:00 in the recipient's local timezone When the recipient local time is outside that window Then the message is blocked with reason "legal_window" even if provider and client quiet hours would otherwise allow sending. Rule: When multiple windows apply (legal, provider, client), the most restrictive window is enforced. Rule: All One-Tap Shift outbound messages must pass through the policy screening layer; attempts to bypass (e.g., direct SMS send from the One-Tap Shift module) are prevented and logged with reason "policy_bypass_blocked"; coverage is 100% as evidenced by audit logs containing a policy_decision for every attempted send.
Comprehensive consent and messaging audit logs
Rule: For every inbound keyword (STOP/START/HELP), consent change, blocked attempt, and sent message, an immutable audit record is stored containing: event_type, timestamp (ISO 8601 with timezone), actor/source, recipient E.164, provider_id, client_id, policy_decision (allow/deny), reasons[], template_id, language_code, movement_delta, and counters snapshot. Rule: Audit records are retained for at least 4 years, are searchable by phone number and date range, and are exportable (CSV/JSON) by authorized users only. Rule: Log creation occurs synchronously with the policy decision so that no message can be sent without a corresponding log entry.
Micro-Shift Message Composer
"As a provider, I want clear, one-tap SMS shift requests auto-drafted so that clients instantly understand and can reply YES without confusion."
Description

Generate concise, personalized SMS proposals that clearly state the specific micro-shift and how to accept in one reply (e.g., “Hi Alex — can we do 10 min earlier? Reply YES to confirm.”). Support dynamic placeholders (pet name, location hints, current/new time), carrier-safe character limits, link shorteners when needed, and localization. Offer template variants for earlier vs. later asks, A/B testing, and tone controls. Ensure messages include automatic handling instructions for NO or non-YES replies and are compatible with long-code/short-code sender IDs as configured within PawPilot.

Acceptance Criteria
Earlier Micro-Shift SMS (Placeholders + CTA)
Given a scheduled appointment with old_time_local and a proposed earlier shift by delta_minutes When the composer generates the message Then the message includes client_first_name and, if available, pet_name without leaving unresolved placeholders And the message explicitly states both old_time_local and new_time_local in the recipient’s timezone And the message contains the instruction to accept with the exact phrase "Reply YES to confirm" And the message includes a clear default handling for non-YES replies (e.g., "we’ll keep your time" or "Reply NO to keep your time") And the final text contains no unresolved template tokens (e.g., "{", "}") And the final text length does not exceed the configured max_segments limit for its encoding (<=160 chars for GSM-7 per segment, <=70 for UCS-2) And no double spaces or trailing spaces are present
Later Micro-Shift SMS (Variant + Tone Controls)
Given ask_direction = later and an AB test with variants A and B and a tone parameter When the composer generates the message Then a template from the "later" set is selected And the assigned AB variant is stable for the client within the experiment window and the variant_id is emitted in metadata And tone = professional results in no emojis, no contractions, and uses "Please confirm" phrasing And tone = friendly may use contractions and optional softeners (e.g., "can we") but no emojis by default And the message includes "Reply YES to confirm" and a clear non-YES handling instruction And the composed message remains within the configured segment limits after variant and tone selection
Localization: Language and Time Format
Given a locale and a user time_format preference (12h or 24h) When the composer generates the message Then times are formatted per time_format (e.g., 12h: "2:30 PM"; 24h: "14:30") And the message language matches the locale (e.g., es-MX uses Spanish copy) And for Spanish locales the CTA uses localized wording (e.g., "Responde SÍ para confirmar") And encoding is adjusted to account for diacritics (UCS-2) with segment limits enforced And no mixed-language fragments appear in the final text
Placeholder Fallbacks and Multi-Pet Formatting
Given optional placeholders (pet_name, location_hint) may be missing and pets may be multiple When the composer generates the message Then missing optional placeholders are omitted without leaving artifacts (no extra commas, no double spaces) And for two pets, pet names are joined with " & " (e.g., "Buddy & Coco") And for more than two pets, the first two are listed followed by "+N more" (e.g., "Buddy, Coco +1 more") And no unresolved template tokens remain And the composed message stays within segment limits after fallback formatting
Link Shortening and Segment Fit
Given the message includes one or more URLs or would exceed the segment limit without shortening When the composer generates the message Then the configured PawPilot shortener is applied to all eligible URLs when needed And after shortening, the final message does not exceed the configured max_segments And the short link domain matches the configured brand domain And no raw tracking parameters are exposed in clear if the policy is to hide them (use compact tokens) And links are placed after the CTA and do not break words or tokens
Carrier-Safe Character Limits and Encoding Optimization
Given the composed text may include non-GSM characters When the composer evaluates encoding Then it detects GSM-7 vs UCS-2 and computes segment count accordingly And if normalize_to_gsm = true and transliteration is possible, the composer outputs GSM-7 only and reduces segment count when applicable And if normalize_to_gsm = false, the composer preserves characters and enforces UCS-2 segment limits And the reported segment_count and encoding are returned in metadata And messages exceeding max_segments are rejected with a clear error and suggested shorter template variant
Sender ID Compatibility (Long-Code vs Short-Code)
Given sender_id_type is configured as long_code or short_code When the composer generates the message Then for short_code, the message includes brand identification in the first sentence (e.g., "PawPilot:") And for long_code, brand identification is omitted if redundant per policy And required compliance snippets do not push the message over the configured segment limits And the final message passes the content policy ruleset associated with the sender_id_type (no disallowed keywords per config) And the selected template variant is adjusted if needed to remain within limits after compliance text is added
One-Tap Confirmation & Auto-Route Update
"As a provider, I want a client’s YES to instantly update my schedule and route so that I erase dead time without manual rescheduling."
Description

Upon receiving a YES, validate that the shift is still feasible, then atomically update the appointment time, route sequence, travel ETAs, and buffers. Send confirmations to the agreeing client with the updated time, notify the provider, and optionally notify any affected downstream client if their time shifts. Sync changes to connected calendars (Google/iCal), adjust reminders and deposit/payment timing if applicable, and emit webhooks for external systems. Handle duplicates idempotently, expire stale offers, and log a full audit trail of the change.

Acceptance Criteria
Atomic Update on YES with Feasibility Validation
Given a pending micro-shift offer with offer_id and proposed_time_delta And the provider has an active route with configured buffers and working hours When the system receives an inbound SMS containing "YES" linked to the offer_id within the offer's expiry window Then the system validates feasibility: no overlapping appointments, buffers >= provider.min_buffer_minutes, travel ETAs recomputed, and working hours respected And if feasible, it atomically commits the change: updates the agreeing appointment start time, route sequence, travel ETAs, and buffers in a single transaction And the operation returns SUCCESS with the updated appointment time and route order And if not feasible, no data is changed and the operation returns DECLINED_FEASIBILITY with reasons And the commit (success or decline) completes within 10 seconds of SMS receipt
Client Confirmation and Provider Notification Dispatch
Given an atomic route update has been committed successfully When post-commit notifications are triggered Then send an SMS confirmation to the agreeing client including the new start time and time delta in minutes And notify the provider via in-app and SMS (if enabled) including updated route order and ETAs And all messages are dispatched within 10 seconds of commit And message delivery identifiers are stored in the audit log
Downstream Client Notifications Respecting Preferences and Quiet Hours
Given any downstream appointment start time shifts by >= notify_threshold_minutes and notify_downstream is enabled When the route update is committed Then notify only clients who have shift notifications enabled and are outside their quiet hours window And include the shift amount and new start time in the notification content And if within quiet hours, queue the notification to send at the earliest allowed time respecting min_notice_minutes And do not send a notification if the shift is < notify_threshold_minutes And persist notification outcomes and timestamps in the audit log
Calendar Sync and Reminder/Deposit Adjustment
Given a successful route update When synchronizing external systems Then update the corresponding Google/iCal events to the new start time and order within 2 minutes, retrying transient failures with exponential backoff (>= 5 attempts) And reschedule all future reminders to maintain their configured offsets relative to the new start time And adjust deposit/payment due times and links to maintain policy windows without creating duplicate charges And record sync results and retry counts in the audit log
Webhook Emission and Idempotency Handling
Given a successful route update When emitting webhooks Then send signed route.updated and appointment.updated webhooks containing the idempotency_key, offer_id, previous and new times, and route order And deliver webhooks at-least-once; duplicates are detectable by consumers via idempotency_key And treat duplicate YES replies for the same offer_id as idempotent: return ALREADY_APPLIED, make no additional changes, and do not resend client/provider notifications And log all deduplication decisions with correlating message_ids
Offer Expiry and Stale Acceptance Handling
Given a micro-shift offer is expired or superseded by another change When an inbound SMS with "YES" referencing that offer_id is received Then do not modify any appointment or route data And return OFFER_EXPIRED and send a polite expiry SMS to the client And do not emit webhooks or trigger calendar sync And write an audit entry with reason expired_or_superseded
Comprehensive Audit Trail and Observability
Given any acceptance of a micro-shift (success, decline, or expiry) When processing completes Then persist an immutable audit record including actor_channel, offer_id, inbound message_id, appointment_id, provider_id, previous/new times, route order before/after, travel ETAs, buffer checks, validation result, notification IDs, calendar sync status, webhook IDs, and timestamps And ensure audit records are queryable by appointment_id, provider_id, and offer_id with response time <= 1 second And store all timestamps in UTC and include provider/client timezone offsets
Conflict Resolution & Fallback Logic
"As a provider, I want the system to avoid conflicting shifts and gracefully handle races so that my schedule stays accurate and clients aren’t confused."
Description

Prevent double-booking and race conditions by limiting concurrent outbound requests for the same slot, placing lightweight holds, and applying a first-YES-wins policy. Automatically cancel and politely close remaining pending offers once one is accepted. Re-validate feasibility after each acceptance; if conditions change (traffic spikes, cancellation), gracefully revert and notify impacted users with clear messaging. Provide manual override/undo, consistent transactional rollbacks, and safeguards for multi-staff scenarios and overlapping services.

Acceptance Criteria
First-YES-Wins with Automatic Offer Closure
Given multiple clients receive micro-shift offers for the same slot/resource When the first client replies YES Then the system applies that acceptance exactly once and updates the route And all other pending offers for that slot/resource are immediately marked closed and cannot be accepted And a polite closure SMS is sent to non-winning clients within 30 seconds And all released holds are cleared within 5 seconds and leave no residual locks And the appointment history/audit log records the winning acceptance and all auto-closures with timestamps
Lightweight Holds Prevent Double-Booking
Given a micro-shift offer is sent for a specific appointment and resource (staff/vehicle) Then a lightweight hold is created on the targeted time window and resource And the hold blocks conflicting edits/bookings (including manual changes and API calls) for the same slot/resource And the hold auto-expires on acceptance, closure, or after a configurable TTL (default 5 minutes) And on expiry the slot/resource is fully available and no ghost holds remain And hold state is persisted and visible in logs for traceability
Simultaneous YES Race Condition Handling
Given two or more YES replies are received for the same slot within a short interval When processing those replies Then the system enforces idempotency so only one acceptance mutates state And losing replies receive a clear "Sorry, that spot was just taken" SMS within 30 seconds And no duplicate route updates or double-bookings occur And the audit log shows a single winning transaction and rejected duplicates with correlation IDs
Post-Acceptance Revalidation and Graceful Revert
Given a YES was applied and the route updates When a subsequent revalidation detects infeasibility (e.g., traffic spike, upstream cancellation, preference/quiet-hours conflict) Then the system atomically reverts the change to the prior consistent state And impacted users are notified with clear reason and next-step options within 60 seconds And all related holds, offers, and calendar artifacts are rolled back consistently And the audit log records the revalidation failure, revert action, and notifications sent
Manual Override/Undo with Transactional Rollback
Given a staff user with proper permissions initiates an Undo for the last One-Tap Shift change within a configurable window (default 10 minutes) When the Undo is confirmed Then the system fully rolls back the prior change and all dependent updates atomically And all affected clients receive appropriate notifications within 60 seconds And the route, calendar, and holds return to their previous consistent state And the audit log records the actor, timestamp, and before/after states
Multi-Staff and Overlapping Services Safeguards
Given multiple staff/resources and services may overlap in time for a client or location When issuing or applying offers Then holds and conflict checks are scoped to the correct resource while also validating client-level conflicts across resources And acceptance is blocked if it would create a client, resource, or service overlap And cross-resource offers revalidate at acceptance time to ensure no double-booking arises And results are reflected consistently across all affected staff calendars
Analytics & Operator Controls
"As a business owner, I want to configure One-Tap Shift and see its impact so that I can balance client experience with efficiency and ROI."
Description

Provide a controls dashboard to configure One-Tap Shift behavior (min/max shift range, daily send caps, quiet hour defaults, template selection, experimentation flags) and visualize performance. Track minutes of dead time eliminated, acceptance rates by time of day and client segment, incremental revenue/capacity gained, declines, time-to-fill, and messaging errors. Offer exports and an audit log of offers and responses for compliance and support. Support role-based access and per-location/team settings.

Acceptance Criteria
Configure One‑Tap Shift Parameters
- Given an Admin on the Controls dashboard, when they set minimum shift to -30 and maximum to +45 minutes, then the form validates max >= min, values within -120..+120, and saves successfully. - Given invalid ranges (e.g., min -130 or max +150), when saving, then the UI blocks save and displays inline errors specifying allowed bounds. - Given quiet hours 20:00–08:00 local time are saved, when an offer would be sent at 21:30, then the system queues it until 08:00 unless the client has "allow after-hours" enabled. - Given a daily send cap of 50 for Location A, when 50 offers have been sent that day, then further offers for Location A are not sent and are logged as "skipped: cap reached". - Given a message template selection, when preview is opened, then dynamic tokens {{client_first_name}}, {{shift_minutes}}, and {{appt_time_local}} render with sample data and save is blocked if required tokens are missing.
Per‑Location and Team Overrides
- Given global defaults exist, when a Manager for Location A saves a location override for quiet hours and send cap, then Location A uses its override and other locations continue using global defaults. - Given a Team within Location A sets a team-level cap, when both location and team caps exist, then the lower cap applies to that team. - Given a location override is removed, when settings are reloaded, then the location reverts to global defaults. - Then every override change creates an audit entry with actor, timestamp (ISO 8601 with timezone), before_value, after_value, and scope (global/location/team).
Role‑Based Access Control
- Given a Staff user (role: Staff), when they open the Controls tab, then controls are read-only and save actions are disabled with tooltip "Insufficient permissions". - Given a Staff user attempts a direct API write to settings, when the request is made, then the API returns 403 with error code RBAC_DENIED and the attempt is logged. - Given an Admin edits roles for a user, when saved, then permissions take effect within 60 seconds and are reflected on next page load without re-authentication. - Given a Manager scoped to Location A, when they view analytics, then they see data only for Location A and its teams.
Performance Analytics Visualization
- Given data for the last 90 days, when the user selects a date range and filters by location, team, client segment, and time-of-day buckets, then KPIs display: minutes of dead time eliminated, acceptance rate, decline rate, time-to-fill median and p90, and messaging error count. - Then "minutes of dead time eliminated" equals sum over accepted shifts of max(0, gap_before - gap_after) in minutes, computed in the appointment's local timezone. - Then acceptance rate equals offers_accepted / offers_sent and is shown broken out by time-of-day buckets and client segment. - Given filter changes, when applied, then charts and KPIs refresh within 2 seconds and values match the detailed export within ±0.5%. - Given messaging provider errors occur, when viewing the dashboard, then errors are grouped by code with counts and a link to affected offers.
Exports: Metrics and Offer/Response Details
- Given a user with export permissions, when exporting "Offer/Response Details" with filters applied, then a CSV is generated containing at least: offer_id, appointment_id, client_id, location_id, team_id, proposed_shift_minutes, direction, template_id, sent_at, delivered_at, response_type, responded_at, accepted, decline_reason_code, error_code, time_to_fill_seconds, and actor_role. - Given a "Metrics Summary" export, when requested, then CSV includes aggregated metrics per day per location/team/segment with the same formulas as the dashboard. - Then exports respect RBAC scope and filters, generate under 60 seconds for up to 100,000 rows, and for larger jobs return a downloadable link via email within 15 minutes. - Then all timestamps are ISO 8601 with timezone offset, numeric fields are unquoted, and column headers use snake_case.
Audit Log: Offers, Responses, and Configuration Changes
- Given the Audit Log view, when a user searches by phone number, client name, offer_id, date range, or action type, then matching entries are returned with pagination (default 50 per page) and are sortable by timestamp. - Then each entry includes id, timestamp (ISO 8601 with timezone), actor (user id or system), scope (global/location/team), event_type, entity_id, before_value, after_value, and for messaging: template_id, content_hash, and quiet_hours_applied boolean. - Then the audit store is append-only; attempts to modify or delete entries are rejected and logged, and entries are retained for at least 24 months. - Then the Audit Log is exportable to CSV respecting RBAC.
Experiment Flags and Safeguards
- Given an Admin creates an experiment, when allocation percentages across variants are entered, then totals must equal 100% and save is blocked otherwise. - Given an active experiment targeting "earlier" micro-shifts for repeat clients, when offers are sent, then eligible clients are assigned deterministically to a variant and analytics report per-variant metrics (acceptance rate and dead time eliminated). - Given the global kill switch is toggled off, when pending offers exist, then new sends stop within 60 seconds and pending offers are canceled with reason "system_disabled". - Then experiment assignments, changes, and kill switch events are logged in the Audit Log.

Proximity Fill

Live geo-clustering pings nearby waitlisters and flexible regulars within your walking radius to backfill micro-gaps. Each invite includes an exact ETA window and quick-confirm link, boosting acceptance and keeping routes at capacity.

Requirements

Live Location and Radius Capture
"As a provider, I want my live location and walking radius to be accurately captured so that Proximity Fill can find nearby clients without manual input."
Description

Capture the provider’s current location (GPS via mobile app or declared starting point) and a configurable walking radius per timeslot, refreshing at defined intervals to support real-time proximity matching. Provide fallbacks when location is unavailable (e.g., last known location or home base) and guardrails like max radius, precision rounding, and opt-in controls to protect privacy. Expose normalized location/radius data to the scheduler and Proximity Fill engine via a lightweight service with low-latency reads, ensuring consistency across devices and during route changes.

Acceptance Criteria
Foreground GPS Location Capture
Given the provider is logged into the mobile app with location permissions granted and device GPS available When the app is in the foreground during an active timeslot Then the app captures current coordinates with horizontal accuracy ≤ 30 meters, UTC timestamps them, and sends an update at the configured interval (default 60s) Given the refresh interval is set between 30s and 180s When the interval is changed in settings Then subsequent updates adhere to the new interval within ±5 seconds starting with the next scheduled tick Given a 30-minute active window and a 60s interval When monitoring service receipts Then at least 28 of 30 expected updates (≥95%) are recorded without error
Fallback to Last Known or Home Base
Given location permission is denied or GPS is unavailable for > 10 seconds at timeslot start When Proximity Fill requests the provider location Then the service returns the last known location if its timestamp is ≤ 15 minutes old; otherwise it returns the home base coordinates Given a fallback location is returned When the payload is produced Then the payload includes source=fallback_last_known or source=home_base and accuracy_m is null Given permission/GPS becomes available mid-timeslot When a valid fix is obtained Then the service switches to live source within 60 seconds and subsequent reads reflect source=live_gps
Configurable Walking Radius per Timeslot
Given a provider edits a specific timeslot When they set a walking radius within 0.25–3.0 miles (or 0.4–5.0 km in metric locales) Then the value is saved for that timeslot and persisted to the service with timestamp and units Given a radius is saved When exposed to external consumers Then the radius is rounded to the nearest 0.05 miles (or 100 meters) and cannot exceed the max policy cap (3.0 miles / 5.0 km) Given no radius was explicitly set for a timeslot When the timeslot begins Then a default radius of 1.0 mile (or locale-equivalent) is applied and marked default=true
Low-Latency Location Service Reads
Given the scheduler or Proximity Fill issues GET /v1/location/{provider_id} When called under nominal load Then the service returns a normalized payload with p95 latency ≤ 100 ms and success rate ≥ 99.9% Given a sustained load of 1000 concurrent reads for 10 minutes When load testing is performed Then p99 latency ≤ 150 ms and error rate < 0.1% Given a successful write of a new location update When a subsequent read is performed Then the new data is visible within ≤ 2 seconds in 99% of cases
Route Change and Timeslot Boundary Refresh
Given a provider starts a new timeslot or marks a job complete When that event is recorded in the app Then an immediate location update is triggered and posted within 5 seconds regardless of the regular interval Given the provider edits the declared starting point for a future timeslot When the timeslot begins Then the declared starting point is used as the initial location until a live GPS fix arrives or 60 seconds elapse (whichever is first) Given the provider resumes Proximity Fill after a pause When resume is triggered Then a fresh location update is posted within 10 seconds
Privacy Controls and Guardrails
Given the provider toggles Location Sharing off When the setting is saved Then no new coordinates are exposed to scheduler/Proximity Fill, reads return sharing=false with coordinates redacted, and writes are still stored but not exposed Given Location Sharing is on When coordinates are exposed to non-owner consumers Then coordinates are rounded to the nearest 50 meters and include precision_rounding_m=50 in metadata Given a radius above the max policy is attempted When saving the value Then the request is rejected with a validation error and the value is clamped to the max in the UI
Normalized Payload Contract and Cross-Device Consistency
Given any client posts a location update When validation occurs Then the service stores provider_id, coordinates {lat,lng}, accuracy_m, radius_m, source, sharing, precision_rounding_m, timestamp_utc (ISO 8601), and schema_version, and rejects payloads missing required fields Given the provider is logged into two devices When both submit updates within a 10-second window Then the service resolves current state by latest timestamp (last-write-wins), records a conflict event, and exposes a single current record on reads Given a schema_version upgrade is rolled out When older clients submit payloads Then the service accepts backward-compatible fields and normalizes to the latest schema for all reads
Geo-Clustering and Candidate Ranking Engine
"As a provider, I want the system to identify and rank nearby eligible clients in real time so that my micro-gaps are filled efficiently."
Description

Continuously cluster nearby waitlisters and flexible regulars within the provider’s active radius, filtering by eligibility signals (service type, dog size/temperament, key/door access, duration fit, neighborhood preference, client reliability). Rank candidates by composite score combining distance/travel time, preference fit, historical acceptance rate, fairness/rotation, and estimated revenue. De-duplicate across lists, cap candidate pool size, and return a ranked list within strict latency targets to enable instant outreach. Provide tunable weights and A/B config for ranking signals.

Acceptance Criteria
Latency SLO for ranked list generation
Given a provider with an active radius and at least 50 eligible contacts across sources When the engine is invoked to produce a ranked list Then the response time is <= 250 ms at p95 and <= 500 ms at max over a 5-minute rolling window And the result contains at most max_k candidates (default 20, configurable) And service logs include request_id, candidate_count_in, candidate_count_out, and p50/p95 latency metrics
Eligibility filtering enforces constraints
Given a micro-gap slot requiring service_type=walk, duration=30m, key_access=true, dog_size<=medium, temperament in {calm, neutral}, neighborhoods_allowed={"Mission","SOMA"}, and min_client_reliability>=0.80 When the engine filters candidates Then every returned candidate satisfies all required attributes and duration fits within the slot window (±5 minutes configurable) And any excluded candidate has an exclusion reason_code recorded (e.g., wrong_service_type, size_mismatch, no_key_access, duration_mismatch, neighborhood_mismatch, low_reliability)
Geo-clustering within active radius
Given a provider current location and active_radius=1.0 mi with radius_buffer=5% When the engine clusters nearby candidates Then all included candidates are within active_radius + radius_buffer by travel-time distance from the provider’s current route And each candidate is assigned a cluster_id with intra-cluster max distance <= 0.25 mi and inter-cluster min distance >= 0.25 mi (configurable) And distance and travel-time features are computed and attached per candidate
Composite scoring with tunable weights and A/B config
Given configured weights w={distance:0.30, preference_fit:0.25, acceptance_rate:0.20, fairness:0.15, revenue:0.10} and experiment_variant=B When the engine ranks candidates Then each candidate has component scores normalized to [0,1] and a composite score=sum(w_i*component_i) And the list is strictly ordered by composite score with deterministic tiebreakers (last_invited_at asc, client_id asc) And switching to experiment_variant=A with different weights changes ordering where component differences exist And the response includes variant_id, weight_vector, and per-candidate component breakdowns for auditability
Deduplication and candidate pool cap
Given a candidate appears on multiple sources (e.g., waitlist and flexible regulars) When the engine builds the candidate set Then the candidate appears only once in the output with merged attributes favoring the most recent source And the output contains no more than max_k candidates; if more are eligible, only the top max_k by composite score are returned And deduplicated entries record deduped_source_ids for traceability
Fairness rotation and invite throttling
Given fairness_window=14 days, max_rank_repeats=3 in top_3, high_accept_threshold=0.70, and min_invite_spacing=2 hours When ranking candidates across consecutive invocations Then no single client appears in the top_3 more than max_rank_repeats within fairness_window unless their acceptance_rate >= high_accept_threshold And clients violating min_invite_spacing are excluded with reason_code=throttled And the fairness component score increases for clients with lower recent exposure and decreases for those with higher exposure
ETA Window and Route Impact Calculator
"As a provider, I want accurate ETA windows that won’t break my route so that I can accept backfills without risking late arrivals."
Description

Compute precise ETA windows using current route context, travel mode (walk/drive), real-time traffic/walking times, building entry buffers, leash-up time, dog handoff, and service duration. Validate that the insertion does not cause downstream lateness or overtime, and adjust window width dynamically based on uncertainty. Surface a go/no-go decision to the invite generator with the safest window and any constraints (e.g., latest start, hard stop). Recalculate on-the-fly if confirmations arrive or the provider moves.

Acceptance Criteria
Walking insertion between appointments with buffers and dynamic ETA window
Given current route where appointment A ends at 09:20 and next appointment B has a scheduled window 09:50–10:05 with latest allowable start 09:55 And travel mode is walk And A->C walking ETA percentiles are p10=5m, p50=6m, p90=8m And pre-service buffers are building entry=3m and leash-up=2m, and post-service handoff=1m And service duration is 10m And C->B walking ETA p90=9m When the calculator evaluates inserting service C between A and B Then start_window.start = 09:30 and start_window.end = 09:33 And expected_end_at = 09:43 And worst_case_arrival_at_B (expected_end_at + handoff + C->B p90) <= 09:55 And go_no_go = "go" And constraints.latest_start = 09:33 and constraints.hard_stop_at is null And the payload includes start_window.start, start_window.end, expected_end_at, go_no_go, and constraints
Driving mode with heavy traffic widens window and blocks unsafe insertion
Given appointment A ends at 11:00 and next appointment B has scheduled start 11:30 with latest allowable start 11:40 And travel mode is drive And A->C driving ETA percentiles are p10=12m and p90=28m And pre-service parking/entry buffer=2m and post-service handoff=1m And service duration is 20m And C->B driving ETA p90=7m When the calculator evaluates inserting service C between A and B Then start_window.start = 11:14 and start_window.end = 11:30 And window_width = 16m And worst_case_arrival_at_B = 11:30 + 20m + 1m + 7m = 11:58 > 11:40 And go_no_go = "no_go" And decision_reason includes "downstream_lateness" And the payload includes start_window, window_width, go_no_go, and decision_reason
All buffers and durations included in ETA and end-time math
Given appointment A ends at 14:00 and next appointment B has latest allowable start 14:40 And travel mode is walk And A->C ETA p50=4m And buffers are building entry=4m, leash_up=2m, handoff=1m And service duration is 5m And C->B ETA p50=6m When the calculator evaluates inserting service C between A and B Then start_window.start = 14:10 (14:00 + 4m + 4m + 2m) And expected_end_at = 14:15 (start_window.start + 5m) And expected_arrival_at_B = 14:22 (expected_end_at + 1m + 6m) And go_no_go = "go"
Hard stop and overtime guardrail respected
Given route hard_stop_at = 18:00 and overtime_limit = 5m And proposed insertion C has duration 20m and pushes projected_route_end to 18:08 When the calculator evaluates the insertion Then projected_route_end (18:08) > hard_stop_at + overtime_limit (18:05) And go_no_go = "no_go" And decision_reason includes "overtime_limit" And constraints.hard_stop_at = 18:00
On-the-fly recalculation on client confirmation locks slot and updates decisions
Given multiple candidates exist for the same micro-gap with start_window [10:10, 10:15] And waitlister X confirms at 10:05:03 And provider location is unchanged When the calculator receives the confirmation event Then recompute completes within 2s of event_time And the confirmed candidate is marked go_no_go = "go" And all competing candidates are marked go_no_go = "no_go" And updated payloads with start_window and go_no_go are surfaced to the invite generator within 2s
Provider movement triggers recalculation and safest window update
Given an active invite with start_window [13:07, 13:09] computed from A->C p10=4m, p90=6m and pre-service entry=3m And next appointment’s latest allowable start is 13:40 And at 12:55 the provider moves 120m (movement_threshold=50m) And new A->C ETA percentiles become p10=6m, p90=8m When the calculator detects movement exceeding the threshold Then recompute completes within 60s of detection And new start_window = [13:09, 13:11] And if worst_case_arrival_at_next <= 13:40 then go_no_go = "go" else go_no_go = "no_go" And updated window and decision are surfaced to the invite generator
Smart SMS Invite with Sequencing and Throttling
"As a pet parent on the waitlist, I want to receive a clear SMS invite with an ETA and one-tap confirm so that I can quickly book an opening near me."
Description

Generate personalized SMS invites containing the service type, exact ETA window, price/deposit summary, and a one-tap confirm/decline link. Support sequencing strategies (staggered waves vs. limited broadcast) with configurable batch sizes, delays, and stop rules when a slot is claimed. Enforce per-recipient frequency caps, quiet hours, timezone-aware scheduling, and template localization. Track delivery, clicks, and responses; retry on transient failures; and fall back to alternate channels (MMS/link shortener) when needed.

Acceptance Criteria
Smart Invite Content: Service, ETA Window, Price/Deposit, One-Tap Links
Given a fillable slot with defined service type, ETA window, price, and deposit And a recipient eligible for Proximity Fill When the system composes the SMS invite Then the message includes the service type, the exact ETA window localized to the recipient’s timezone, and a price/deposit summary in the recipient’s currency And the message includes two distinct one-tap links: Confirm and Decline And each link is unique per recipient and resolves with HTTP 200 within 500 ms And clicking Confirm associates the response to the correct slot and recipient and records a “confirmed” response event And clicking Decline records a “declined” response event and removes the recipient from further waves for this slot And if any required field is missing, the invite is not sent and an error is logged with the missing fields
Sequencing: Staggered Waves with Stop-on-Claim
Given a campaign configured for staggered waves with batch size B and delay D seconds and stop-on-claim enabled When wave 1 is sent Then no more than B invites are sent in wave 1 When a recipient confirms before wave 2 is due Then all unsent invites in subsequent waves are canceled within 2 seconds And the campaign is marked Filled and no further waves are sent When no confirmation occurs by the next wave time Then wave 2 sends to at most B additional recipients, continuing until the list is exhausted or the slot is filled And audit logs record wave number, send counts, cancellations, and fill timestamp
Sequencing: Limited Broadcast with Batch Throttling
Given a campaign configured as a limited broadcast with maximum recipients N and stop-on-claim enabled When the campaign starts Then no more than N invites are sent in total And the send rate does not exceed the configured provider throttle (e.g., TPS limit) When the first confirmation is received Then any remaining unsent invites are canceled within 2 seconds and the campaign is marked Filled And any subsequent confirm clicks receive an auto-reply indicating the slot has already been taken, with a link to join or remain on the waitlist
Per-Recipient Frequency Caps Enforcement
Given per-recipient caps of X invites per rolling 24 hours and Y invites per rolling 7 days When building a recipient list for a campaign Then recipients whose invite counts meet or exceed either cap are excluded from the send list And an audit entry records the cap reason and current counters for each excluded recipient And counters increment only on successful send (provider accepted) events And if a campaign is marked as cap-override, excluded recipients are reinstated and the override is recorded
Quiet Hours and Timezone-Aware Scheduling
Given each recipient’s timezone and configured quiet hours window When an immediate send is requested during a recipient’s quiet hours Then the invite is scheduled for the next available time outside quiet hours in that recipient’s local time When a campaign spans multiple timezones Then each recipient’s send time is computed in their local time respecting quiet hours When quiet hours settings change while invites are queued Then only unsent queued invites are re-evaluated against the updated settings And an audit log records any deferrals or reschedules due to quiet hours
Delivery, Click, and Response Tracking with Retry and Fallback
Given an invite is sent via SMS Then the system stores the provider message ID and updates delivery status events (queued, sent, delivered, failed) in near real time (<=10 seconds latency) And confirm/decline links are uniquely trackable and record click and outcome events When a transient send failure occurs (e.g., carrier temporary error or HTTP 5xx) Then the system retries up to R times with exponential backoff starting at 1 minute And on success after retry, tracking continues without duplication of events When retries are exhausted or a filter is detected (e.g., link domain blocked) Then the system attempts a fallback: switch to an alternate link shortener domain, or resend as MMS if message length exceeds the configured segment threshold And the final outcome is recorded as Delivered or Failed with reason codes
Template Localization and Localized Formatting
Given a recipient language/locale is available When composing the invite Then the template selected matches the recipient’s locale; otherwise the default locale template is used And date/time in the ETA window and currency in price/deposit are formatted per the recipient’s locale And all static strings are translated while dynamic variables render correctly And missing translations fall back to the default string and are logged for remediation
Quick-Confirm, Slot Hold, and Auto-Fallback
"As a provider, I want holds and auto-fallback logic so that openings are filled fast without manual juggling."
Description

Provide a lightweight confirmation page that loads instantly from SMS, pre-fills client details, and offers one-tap confirm or decline. On first confirm, place a timed hold on the micro-gap slot (e.g., 5 minutes) with visible countdown and prevent double-booking across concurrently invited candidates. If the hold expires or the client declines, automatically advance to the next ranked candidate and update all parties. Sync accepted bookings to the calendar in real time, broadcasting updated ETAs and canceling remaining pending invites.

Acceptance Criteria
Instant SMS Quick-Confirm Page
Given a recipient taps the SMS invite link on a mobile device When the Quick-Confirm page loads Then the page renders in under 1.0s on 4G and under 2.5s on 3G And the client name, pet(s), service, date, micro-gap window, location, and price are pre-filled And one-tap Confirm and Decline controls are visible and enabled And no login is required And the link is validated to the unique invite token
Atomic Hold with Visible Countdown
Given the candidate is the first to tap Confirm for the slot When the Confirm action is received by the server Then a hold for the slot is created atomically with a 5-minute TTL (configurable) And a mm:ss countdown is displayed and updates every second And the hold persists if the page is refreshed or closed/reopened during the TTL And the held slot is marked as unavailable for all other candidates during the TTL
Concurrency Guard Against Double-Booking
Given multiple invited candidates attempt to Confirm nearly simultaneously When the first server-acknowledged Confirm is processed Then only that candidate receives the hold And all other Confirm attempts receive a response within 2 seconds indicating "Slot on hold or filled" and actions are disabled And no duplicate holds or bookings are created And repeat/late Confirm requests are handled idempotently without creating new holds
Auto-Fallback on Decline or Hold Expiry
Given the current held candidate Declines or the hold TTL expires When that event occurs Then the next ranked candidate is auto-invited via SMS within 5 seconds with a fresh quick-confirm link and ETA window And the declined/expired candidate is shown a final status page indicating Declined or Expired And any prior pending invites are updated to reflect the new state (e.g., on hold, re-invited) within 5 seconds And the system continues advancing through ranked candidates until acceptance or the list is exhausted
Real-Time Calendar Sync and Broadcast
Given a candidate completes Confirm while a valid hold is active When the booking is finalized Then the job is created or updated in the calendar within 2 seconds with client, pet(s), service, location, and micro-gap time window And route ETAs are recalculated and broadcast via SMS to affected clients within 10 seconds And all remaining pending or held invites are canceled and their links invalidated within 5 seconds And the provider receives a confirmation notification with the filled slot details
Invite Link Idempotency and Expiration
Given an invite link is accessed multiple times or after the slot state changes When the link is opened Then Confirm actions are idempotent and return the same outcome without duplication And if the slot is already filled or canceled, the page shows a read-only "Slot filled" state with actions disabled And if the link is expired (e.g., 24 hours elapsed or slot filled), the page shows an "Expired" state and actions are disabled And audit events record open, confirm, decline, expiry, and fallback with timestamps for verification
Deposit and Payment on Acceptance
"As a provider, I want a deposit auto-collected on acceptance so that cancellations are reduced and no-shows are covered."
Description

Enable optional deposit or full-charge collection at the moment of acceptance via quick-pay (card on file, Apple Pay/Google Pay, or link-based checkout). Support preauth vs. immediate capture, configurable deposit amounts by service, and automatic application to the final invoice. Enforce cancellation policies and fees, issue automated receipts, and reconcile payments to the booking. Fail gracefully to non-deposit flow if payment cannot be completed, while preserving audit trails and minimizing friction.

Acceptance Criteria
Quick-Pay Card on File Capture at Acceptance
Given a client has a valid card on file and a Proximity Fill invite requires a deposit or full charge at acceptance And the service is configured for immediate capture When the client taps Quick Confirm Then the system performs a single capture for the configured amount using the card on file and returns success within 3 seconds And the booking status updates to Confirmed with payment_status = "captured" And the booking’s payment record stores transaction_id, amount, currency, and card_last4 And an itemized receipt is sent via SMS (and email if enabled) within 10 seconds And an audit event is recorded with actor, timestamp, amount, and payment method And repeated taps within 60 seconds do not create duplicate charges (idempotency enforced)
Apple Pay/Google Pay Preauthorization on Acceptance
Given the service deposit setting is "preauthorize on accept" And the client selects Apple Pay or Google Pay from the pay sheet When the client authorizes within 30 seconds Then the gateway returns an auth_id for the exact deposit amount And the booking updates to Confirmed with payment_status = "authorized" And a receipt confirming authorization (not capture) is sent within 10 seconds And the system auto-captures the authorization at job start or T-15 minutes (configurable) before ETA, whichever is earlier And if capture fails due to expired auth, the system attempts a single re-authorization; on failure, it falls back to non-deposit flow and notifies the provider And all auth, capture, and void events are recorded in the audit trail And authorizations are voided within 24 hours when cancellation occurs outside the penalty window
Service-Level Configurable Deposit Amounts
Given deposits can be configured per service as a fixed amount ($) or percentage (%) with optional min/max caps And the calculation is based on the pretax service price unless overridden by org settings When a Proximity Fill invite is generated and shown to the client Then the deposit shown equals the computed amount using service rules and rounding to 2 decimals And on acceptance the system authorizes/captures exactly that amount And for multi-line bookings, deposits are computed per line item and summed And examples validate computation: 20% of $45.00 => $9.00; 50% of $60.00 with $20 cap => $20.00; $10 fixed on $18 => $10.00
Automatic Deposit Application to Final Invoice
Given a booking with a prior deposit authorization or capture When the job is completed and the invoice is finalized Then the deposit is applied automatically as a payment/credit on the invoice And if full charge was captured at acceptance, the invoice balance is $0.00 unless adjustments are present And if the final total exceeds the deposit, the remaining balance is collected from the default payment method And if the final total is less than the captured amount, a partial refund for the difference is issued automatically (or queued for approval per setting) within 1 business day And receipts, ledger entries, and booking financials reflect the application/refund with timestamps And the booking payment_status transitions to "settled" upon successful application
Cancellation Policy Enforcement and Fee Handling
Given a booking has a configured cancellation policy (e.g., 50% inside 12h, 100% inside 2h) When the client cancels Then if inside the penalty window, the system captures up to the fee amount using any existing authorization; if insufficient or absent, it charges the default payment method immediately And the charged fee never exceeds the configured policy cap And if outside the penalty window, any open authorization is voided within 24 hours and no fee is charged And for no-shows, the system automatically charges per policy at the configured trigger time And receipts are sent and the audit trail records policy evaluation, window determination using the business timezone, and the resulting financial event
Graceful Fallback to Non-Deposit Flow on Payment Failure
Given a payment attempt on acceptance fails (e.g., decline, network error, wallet cancellation, 3DS unavailable) When the client accepts the Proximity Fill invite Then the booking is confirmed without a deposit with payment_status = "no_deposit" And the client receives a secure link to update payment or retry And the provider is notified that the booking was confirmed without a deposit And the system performs a single background retry within 30 minutes if a new method is added And any pending authorizations are voided and no duplicate or partial charges remain And an audit event records the failure reason code and fallback path
Automated Receipts and Reconciliation to Booking
Given any payment event occurs (authorization, capture, void, refund, fee charge) When the event is confirmed by the processor Then an SMS receipt is sent within 10 seconds containing merchant name, amount, currency, descriptor, last4, and booking reference And an email receipt is sent if an email is on file And a ledger entry links transaction_id to booking_id and payout batch And the booking Financials tab shows a chronological list of all payment events with timestamps and amounts And EOD exports/reporting include these transactions And idempotency ensures duplicate receipts are not sent on event replays
Compliance and Consent Management
"As a business owner, I want Proximity Fill to respect consent and messaging rules so that I stay compliant and avoid fines."
Description

Ensure explicit consent for provider location sharing and client SMS outreach, with TCPA-compliant language, opt-in records, and instant opt-out via STOP/UNSUBSCRIBE. Respect per-user quiet hours, regional regulations (e.g., US, UK, EU), and data minimization for location precision. Implement message rate limiting, audit logging for invites and responses, and configurable retention. Provide admin visibility and exports to satisfy compliance inquiries without engineering intervention.

Acceptance Criteria
Explicit SMS and Location Sharing Consent Capture (TCPA/GDPR Compliant)
Given a provider enables Proximity Fill and location sharing in their settings When the provider reviews the consent screen Then the screen explicitly states the purposes (provider location sharing and client SMS outreach), brand identity, data use, and retention, and requires an affirmative action (unchecked box or typed YES) before enabling And the system stores a consent record with: user/provider ID, phone (if applicable), purposes granted, consent text version hash, locale, region, timestamp (UTC), and IP/device fingerprint And location sharing remains disabled until an active consent record exists for that provider Given a client is being contacted for the first time via SMS by PawPilot on behalf of a provider When the system initiates outreach Then the initial message or flow includes TCPA/PECR/GDPR-compliant language (brand name, purpose, “Msg & data rates may apply”, “Reply STOP to opt out”, “HELP for help”, link to Privacy Policy) And the client must reply YES or tap an explicit opt-in to proceed And the system stores an opt-in record linked to phone+brand+purpose with the same evidence fields And any attempt to send an invite without an active opt-in is blocked and logged with reason code CONSENT_MISSING
Instant Opt-Out Handling via STOP/UNSUBSCRIBE and Re-Opt-In
Given the system receives an inbound SMS matching a recognized opt-out keyword set {STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT} When the message is processed Then the phone is immediately marked opted-out for all non-essential messaging for the associated brand And one confirmation SMS is sent (“You’re opted out. No more messages. Reply START to opt back in.”) And all pending or in-flight Proximity Fill invites to that phone are canceled within 1 second and logged with reason OPT_OUT Given the same phone later sends START or YES When the message is processed Then the opt-out suppression is lifted And a new consent record is created with timestamp and evidence fields And messaging resumes only for purposes explicitly re-consented Given an outbound send is attempted to an opted-out phone When the send is evaluated Then it is blocked, returns a non-2xx deliverability response to the caller, and creates an audit entry with reason OPT_OUT_BLOCK
Per-User Quiet Hours Enforcement for Proximity Fill Invites
Given a client has quiet hours configured (e.g., 21:00–08:00) and a stored time zone When a Proximity Fill invite is triggered during the client’s quiet hours in their local time Then the message is not sent And the event is logged with reason QUIET_HOURS_SUPPRESSION and the computed local time And, if queueing is enabled, the invite is scheduled for the first minute after quiet hours end; otherwise it is dropped Given the client’s time zone cannot be inferred from profile or E.164 country code When a send is evaluated Then the system defaults to the provider’s service region time zone and marks the log with TZ_INFERRED Given an admin toggles “Override quiet hours for emergencies” = off (default) When a high-priority invite is triggered during quiet hours Then it is still suppressed and logged; no messages are sent
Regional Compliance: US, UK, EU Messaging Rules and Content
Given a client’s phone number region is detected as US When sending the first message in a 30-day rolling window Then the message includes brand identity, purpose, HELP/STOP instructions, and TCPA disclosure; uses a registered, compliant sender type; and logs compliance_check=PASS Given a client’s region is detected as UK or EU When obtaining consent Then pre-checked boxes are not used, the purpose is specific (outreach and/or waitlist pings), and the message identifies the sender and links to the Privacy Policy And, if double opt-in is enabled for the region, a second affirmative action is required and recorded before any invites are sent Given the client’s region cannot be confidently determined When a send is evaluated Then the system applies the strictest template (EU-style consent and disclosures) and logs region=UNKNOWN_STRICT_MODE
Data Minimization for Provider Location Precision
Given Proximity Fill composes an invite When constructing the client-facing message Then it includes only an ETA window and generalized area descriptor (e.g., neighborhood or rounded radius) And no exact GPS coordinates or precise route points are exposed in the SMS body or query parameters And any confirmation links use opaque, single-use tokens without embedded location data Given provider location is used to compute proximity When storing event data Then only truncated precision (>=100m rounding or equivalent) and derived radius are persisted And raw high-precision coordinates are kept in memory only and discarded within 24 hours And the precision level is configurable (Exact, Block, Radius) with default = Block
Message Rate Limiting for Invites and Reminders
Given rate limits are configured (client_daily_invites=3, provider_burst_per_minute=30, duplicate_window_minutes=15 by default) When Proximity Fill attempts to send messages Then no client receives more than client_daily_invites per rolling 24 hours across all campaigns And no provider sends more than provider_burst_per_minute unique-recipient invites in any sliding 60-second window And duplicate invites for the same slot to the same client within duplicate_window_minutes are suppressed And suppression events are logged with reason RATE_LIMIT and counters Given limits are updated by an admin When new values are saved Then changes take effect within 60 seconds and are reflected in the audit metadata for subsequent sends
Audit Logging, Retention Policies, and Self-Serve Exports
Given any invite or response event occurs When the event is recorded Then the audit log captures: timestamp (UTC), sender ID, masked recipient, message type, content hash, consent state at send, region, quiet-hours/rate-limit checks, delivery status, and reason codes And logs are append-only with monotonically increasing IDs and clock-sync within ±100 ms Given an admin opens the Compliance console When applying filters (date range, provider, region, phone, reason code) Then matching records are displayed within 3 seconds for up to 50k rows and can be exported as CSV or JSON without engineering involvement And exports complete within 5 minutes for up to 1M rows and are access-controlled with time-limited URLs Given retention policies are configured (e.g., consent=5y, invites/responses=24m, location=30d) When the nightly purge job runs Then records older than the configured durations are deleted or aggregated as specified And a purge report with counts per category and any failures is written to the audit log and visible to admins

Buffer Guard

Protects your sanity by enforcing travel buffers, elevator/building access time, dog temperament flags, and max distance rules. Suggestions that would cause a rush or lateness are filtered out so rebalancing stays realistic.

Requirements

Travel Buffer Enforcement Engine
"As a mobile groomer or walker, I want the schedule to automatically include realistic travel and wrap-up buffers so that I’m not rushed and can arrive on time."
Description

Calculates door-to-door travel time between sequential bookings and auto-applies configurable pre- and post-visit buffers (wrap-up, cleaning, loading). Considers staffer travel mode (walking, driving, public transit), live/typical traffic, parking time, and minimum handoff overhead using a mapping service. Prevents scheduling placements that violate buffers in the calendar, quick-add, and auto-suggestion flows. Dynamically recomputes ETAs when bookings shift and surfaces the buffer math and rejection reasons in UI and SMS previews. Supports global defaults, per-staffer profiles, and per-appointment overrides; respects hard start times and service-level windows. Provides offline fallback with conservative estimates when mapping is unavailable.

Acceptance Criteria
Compute door-to-door time with buffers and traffic by mode
Given a staffer profile with travel mode and configured pre/post buffers, parking time, and handoff overhead And two sequential bookings with known coordinates and scheduled times And live traffic data is available When the engine computes travel for the time between booking A end and booking B start Then blocked time equals live-traffic ETA for the mode + parking time + handoff overhead + A post buffer + B pre buffer And the computed components and totals are stored for use by scheduling flows
Block calendar placement when buffers are violated
Given an existing booking A and an attempted placement of booking B in the calendar UI for the same staffer And staffer buffers, parking time, and handoff overhead are active When the engine calculates the required blocked time between A and B Then if B’s proposed start is earlier than A end + travel + parking + handoff + A post buffer + B pre buffer, the placement is prevented And an inline error displays missing minutes and the component breakdown And the UI offers the earliest feasible start time that satisfies buffers
Respect buffers in quick-add and auto-suggestions
Given a quick-add creation for booking B or an auto-suggestion to fill a gap When the engine evaluates fit between neighboring bookings for the assigned staffer Then any option that violates travel + parking + handoff + pre/post buffers is excluded from suggestions And at least one returned suggestion, if any exist within the service window, uses the earliest feasible start time And excluded slots include a rejection reason with component breakdown and overage minutes
Recompute ETAs and conflicts when bookings shift
Given bookings A and B are scheduled sequentially for the same staffer When booking A’s end time changes or A’s location changes Then the engine recomputes travel ETAs and buffers between A and B within 2 seconds And if B now violates buffers, B is flagged with a conflict badge and reason in calendar and quick views And auto-suggestions update to reflect the new earliest feasible start for B
Apply override hierarchy and honor hard start and service windows
Given global defaults, a staffer profile, and a per-appointment override for booking B When computing buffers and overheads Then per-appointment overrides take precedence over staffer profile, and staffer profile takes precedence over global defaults And if B has a hard start time, neighboring placements are adjusted or blocked; B’s start is not moved And if B has a service window, the chosen start falls within the window after applying all buffers and overheads
Use offline conservative estimates when mapping unavailable
Given the mapping service is unavailable or times out When the engine must compute travel between two locations Then it uses cached typical speeds by mode with a configurable worst-case multiplier to produce a conservative ETA And marks the result as offline conservative in logs and UI tooltips And scheduling blocks any placement that would violate buffers using the conservative ETA
Expose buffer math and reasons in UI and SMS previews
Given a user reviews a valid suggestion or a blocked placement When the engine presents the outcome Then the UI tooltip or modal and the SMS preview include travel ETA, parking, handoff, pre/post buffers, and net slack minutes And rejection text includes exact overage minutes and the primary contributing component And the default SMS preview remains within 160 characters while conveying the reason
Building Access Time Allocation
"As a walker, I want extra time budgeted for buildings with elevators or check-ins so that arrival and walk start times remain accurate."
Description

Adds per-client location metadata for elevator delays, concierge check-in, gate codes, and handoff time, and automatically budgets this access time into pre-arrival calculations. Incorporates time-of-day/day-of-week variations and multi-tenant complexities. Access buffers are reflected in ETA windows, scheduling feasibility checks, and SMS reminders/instructions. Captures access details during client onboarding and allows updates at booking. Provides templated entry instructions and secure storage of codes with role-based visibility.

Acceptance Criteria
Capture Access Metadata at Client Onboarding
Given I add a new client and service location When I complete the onboarding form Then the form includes fields for elevator delay (minutes), concierge check-in (minutes), gate code (masked), and handoff time (minutes) And I can define time-of-day/day-of-week variations for access minutes And I can associate the client to an existing building and unit or create a new one And minute fields accept 0–120 and reject non-numeric or negative values with inline errors And saving persists access metadata to the client location profile and returns a success message
Auto-Calculate Access Buffers in Scheduling Feasibility
Given a calendar with existing appointments, a configured travel buffer, and a client location with access minutes When I request available times for a new appointment or move an existing one Then the system adds the location’s access minutes to the pre-arrival time in the feasibility calculation And it excludes suggestions where travel + access + service duration + configured buffer would cause lateness or overlap And it returns at least three feasible slots when available, or “No feasible slots” with reason “Access buffer conflict” when none exist And the selected slot stores the calculated pre-arrival access start time with the booking
Reflect Access Buffers in ETA Windows and Client SMS
Given an upcoming appointment with access minutes configured When an ETA window is generated and the reminder SMS is sent Then the ETA window start and end reflect the added access minutes And the SMS includes templated entry instructions using variables such as {building_name}, {unit}, and {concierge_notes} And any entry codes are excluded from client-visible SMS by default unless explicitly marked “share with client” And the message renders with no unresolved placeholders; otherwise the send is blocked with a descriptive error
Update Access Details at Booking and Recompute Schedule
Given an existing booking at a client location with saved access metadata When I update access minutes, select a different unit, or edit entry instructions on the booking Then the system immediately recalculates feasibility and ETA using the updated access data And it blocks the save with a clear warning if the change would cause the provider to be late to another appointment And it updates pending reminders and instructions to reflect the new access details And it records the change in booking history with user, timestamp, and before/after values
Secure Storage and Role-Based Visibility of Entry Codes
Given a client location with stored gate/elevator/concierge codes When the data is stored, transmitted, or viewed Then codes are encrypted at rest and in transit and are masked in the UI by default And users with roles Walker/Sitter or Scheduler can reveal codes only after an explicit reveal action; Billing/Finance cannot view codes And reveal/unmask actions are logged with user, timestamp, and context And exports and client-facing SMS never include codes unless explicitly included via a “share with client” toggle
Multi-Tenant and Time-Variant Access Resolution
Given a building with multiple tenants/units and varying access rules by time When I create or edit a booking for a specific client in that building at a given date and time Then the system selects access minutes and instructions using the priority: booking override > client-unit defaults > building defaults And it applies time-of-day/day-of-week variations matching the appointment start time And it displays the resolved access minutes in the booking UI and uses them in feasibility and ETA And missing or ambiguous unit selection blocks save with a clear error and a link to add or choose the correct unit
Temperament-Aware Scheduling Rules
"As a sitter, I want the system to respect temperament constraints when suggesting times so that I can keep pets, clients, and myself safe."
Description

Stores temperament flags per pet (reactive, anxious, muzzle required, two-person assist, bite risk, elevator-averse) and enforces scheduling constraints derived from these flags. Applies minimum cool-down buffers, avoids back-to-back with other reactive pets, restricts booking to daylight or low-traffic windows, and adjusts service duration as needed. Integrates with Smart Waitlist and suggestion engine to filter out infeasible options, triggers requisite waivers/deposits, and surfaces safety alerts in calendar and SMS confirmations.

Acceptance Criteria
Enforce Cooldown Between Reactive Pets
Given org setting "Reactive Cooldown (min)" = 30 And worker W has a confirmed service for a pet with flag "reactive" from 10:00 to 10:45 on 2025-08-20 When a scheduler attempts to place any service with flag "reactive" on worker W that starts between 10:15 and 11:15 on that date Then the system blocks the placement with error code TEMPERAMENT_COOLDOWN and suggests the next available start time >= 11:15 for worker W Given org setting "Reactive Cooldown (min)" = 30 And worker W has a confirmed service for a pet with flag "reactive" from 10:00 to 10:45 When a scheduler places a service without the "reactive" flag starting at 10:30 on worker W Then the placement succeeds without error Given org setting "Reactive Cooldown (min)" = 30 And worker W has a confirmed reactive service from 10:00 to 10:45 When a scheduler places a reactive service on worker X (X ≠ W) starting at 10:30 Then the placement succeeds and no cooldown is enforced across different workers
Restrict to Daylight and Low-Traffic Windows
Given pet P has temperament flag "anxious" and org rule "Daylight Only" = true And P's service location timezone is America/Los_Angeles with sunrise at 06:20 and sunset at 19:55 on 2025-08-20 When a scheduler attempts to book P at 19:30 Then the system blocks the booking with error code TEMPERAMENT_DAYLIGHT_WINDOW and suggests times within 06:50–19:25 Given pet P has temperament flag "reactive" and org rule "Low-Traffic Window" = 10:00–15:00 Monday–Friday (local time) And today is Wednesday 2025-08-20 When the suggestion engine returns times for P Then all returned times fall within 10:00–15:00 local and within the daylight window for that date Given pet P has any of flags {anxious, reactive} and both "Daylight Only" and "Low-Traffic Window" are enabled When a user attempts a direct booking at 09:30 on a weekday Then the system blocks the booking and displays both constraints in the error message
Adjust Service Duration and Staffing Based on Flags
Given org duration increments: reactive +10 min, muzzle-required +10 min, two-person-assist +15 min (and requires min_staff = 2) And base service "Walk 30" has duration 30 min And pet P has flags {reactive, muzzle-required, two-person-assist} When a scheduler selects "Walk 30" for P Then the computed service duration is 65 min and the assignment requires at least 2 staff concurrently Given computed duration = 65 min and only one staff member is available for the time range When the user attempts to confirm the booking Then the system blocks the confirmation with error code TEMPERAMENT_STAFFING and suggests the earliest slot with 2 staff available Given pet Q has no temperament flags When a scheduler selects "Walk 30" Then the computed service duration remains 30 min and no additional staffing is required
Handle Elevator-Averse Access Constraints
Given pet P has temperament flag "elevator-averse" And the service location has elevator_required = true and stairs_available = false When a scheduler attempts to book any service for P at that location Then the system blocks the booking with error code TEMPERAMENT_ELEVATOR_ACCESS and does not offer times at that address Given pet P has temperament flag "elevator-averse" And the service location has elevator_required = true and stairs_available = true And org setting "Elevator-Averse Access Buffer (min)" = 10 When a scheduler books a 30-min service for P starting at 11:00 Then the system adds a 10-min access buffer before service (10:50–11:00) and adjusts routing/availability accordingly Given an added access buffer would push the worker past shift end When generating suggestions for P at that location Then such times are excluded from suggestions
Filter Smart Waitlist and Suggestions for Temperament Constraints
Given org setting "Reactive Cooldown (min)" = 30 And worker W has a reactive job 10:30–11:00 And Smart Waitlist contains a reactive request for 11:00 on worker W When the Smart Waitlist generates fill suggestions Then 11:00 is excluded and the first suggested time for worker W is >= 11:30 Given pet P has flag "anxious" and org rule "Daylight Only" = true And the user requests suggestions for 20:00 on a date where sunset-30 = 19:25 local When the suggestion engine computes options Then 20:00 is excluded and only times within the daylight window are returned Given pet P has flag "two-person-assist" And only one staff is available at the requested time When Smart Waitlist ranks candidates Then P is not suggested for that slot due to unmet staffing constraint
Trigger Waivers, Deposits, and Safety Alerts
Given pet P has flag "bite risk" And org policy requires waiver template "BiteRisk2025" and a $50 refundable deposit before confirmation When a booking is initiated for P Then the client receives an SMS with a waiver link and deposit link, and the booking status is "Pending-Client Action" Given the client completes the waiver and pays the $50 deposit within 4 hours When the system validates both Then the booking status auto-transitions to "Confirmed" and the calendar event unlocks for worker check-in Given the client does not complete either requirement within 4 hours When the hold window expires Then the tentative hold is released, the slot becomes available, and the client is notified via SMS of the release Given a confirmed booking for a pet with any of flags {bite risk, muzzle-required, two-person-assist} When viewing the calendar event or sending the SMS confirmation Then standardized safety alerts are surfaced: Calendar badge shows [SAFETY] with flag list; SMS includes a safety summary (e.g., "Muzzle required; two-person assist scheduled")
Max Distance & Coverage Constraints
"As an independent walker, I want to cap my service area by day so that my routes stay realistic and profitable."
Description

Configures per-staffer service areas, daily max travel distance, and no-go zones with a chosen home base for each shift. Uses road-network distance and typical transit times to score and reject suggestions that exceed limits or create downstream lateness risk. Supports exception tagging for specific clients/zip codes. Provides a map overlay in the scheduler and applies constraints consistently across manual booking, auto-rebalance, and route optimization.

Acceptance Criteria
Service Area Enforcement for Manual Booking
Given a staffer has a defined polygonal service area and a shift home base And a client service address geocodes outside the polygon And the client/ZIP has no coverage exception for that staffer When a dispatcher attempts to save a manual booking to that staffer Then the save is blocked with reason code COVERAGE_OUT_OF_AREA And the decision uses road-network distance (not straight-line) to determine inclusion And the decision and reason code return within 800 ms And the attempt is logged in the audit trail with stafferId, addressId, and reason code And if the address is inside the polygon, the save succeeds and logs COVERAGE_CHECK_PASSED
Daily Max Travel Distance Limit in Auto-Rebalance
Given a staffer has a configured daily max travel distance D (miles/km) And road-network distance is computed across the proposed day’s route from the shift home base through all assigned visits When auto-rebalance proposes assignments whose cumulative distance would exceed D by > 0.1 units Then the engine rejects only the violating assignments with reason DISTANCE_LIMIT_EXCEEDED And exposes cumulative_distance and limit in the response payload And when cumulative distance ≤ D, proposals are allowed and tagged DISTANCE_CHECK_PASSED And decisions compute in ≤ 2 seconds for up to 50 visits per staffer
No-Go Zones with Client/ZIP Exceptions
Given one or more no-go zone polygons are defined for a staffer And a booking target address lies within a no-go zone When creating or suggesting an assignment for that staffer Then the action is blocked with reason NO_GO_ZONE And if the client or ZIP code carries an explicit exception tag for that staffer Then the action is allowed and annotated EXCEPTION_APPLIED And the exception does not bypass other constraints (distance, lateness) And all allow/block decisions are reflected consistently in API and UI within 800 ms
Lateness Risk Filter Using Typical Transit Times and Buffers
Given a staffer has a travel buffer B (minutes) And typical transit times by day-of-week and hour-of-day are available from the road network When a new assignment or swap is evaluated And the predicted arrival time would be later than the appointment start or leave < B minutes of buffer to the next job Then the suggestion is filtered out with reason LATENESS_RISK and includes predicted_lateness_minutes or remaining_buffer_minutes And when predicted arrival is on time and buffer ≥ B, the suggestion passes with LATENESS_CHECK_PASSED And calculations use road-network travel time (not straight-line) and the active shift home base And the scoring completes in ≤ 1 second for up to 20 candidate insertions
Constraint Consistency Across Manual, Rebalance, and Optimization
Given identical inputs (stops, times, staffer constraints, home base) for a scenario When validating via (a) manual booking save, (b) auto-rebalance, and (c) route optimization Then each flow returns the same allow/block decision for each candidate assignment And the same reason code from {COVERAGE_OUT_OF_AREA, NO_GO_ZONE, DISTANCE_LIMIT_EXCEEDED, LATENESS_RISK} And exposes a constraint_violations array with matching entries across flows And parity is verified by automated tests covering at least 20 representative cases
Scheduler Map Overlay Rendering and Interactions
Given the scheduler map overlay is toggled on for a selected staffer and shift When the map loads Then it renders the service area polygon, no-go zones, and the shift’s home base marker with a visible legend And exception-tagged clients/ZIPs are indicated with a distinct badge/outline And clicking any polygon/marker shows a tooltip with type, name/ZIP, last updated, and who configured it And initial overlay rendering completes within 800 ms and panning/zooming maintains ≥ 45 FPS on the reference device
Shift-Specific Home Base in Distance/Time Calculations
Given a staffer has distinct home bases configured for AM and PM shifts When evaluating distances and transit times for visits within each shift Then the origin/return legs use the active shift’s home base in all calculations And changing a shift’s home base triggers recomputation of scores and constraint checks within 5 seconds And UI badges and API responses include home_base_id used for the decision
Smart Waitlist Feasibility Filter
"As a groomer, I want the waitlist to only offer fill-ins I can realistically make so that I avoid rushing and late arrivals."
Description

Evaluates waitlist candidates against travel buffers, building access time, temperament rules, and coverage constraints when filling cancellations. Only feasible candidates are texted with accurate arrival windows and deposit links. Includes reason codes for exclusions, reply-driven holds to prevent double-booking, and real-time availability updates. Targets sub-5-second candidate generation with graceful degradation under API latency. Ensures SMS content remains concise and compliant while conveying constraints.

Acceptance Criteria
Feasible Candidate Generation Under Buffer Guard Constraints
Given a cancellation slot with prior and next jobs scheduled and Buffer Guard rules enabled (travel buffer, building access time, temperament flags, max distance, coverage constraints) When the Smart Waitlist Feasibility Filter runs to generate candidates Then every returned candidate satisfies all active rules at the moment of evaluation (no violations detected by an independent rule checker) And the candidate list is empty if no candidates meet all rules (no SMS invites are sent in this case) And generation completes in ≤ 5,000 ms p95 and ≤ 7,000 ms p99 over 200 runs with waitlists up to 500 candidates And each returned candidate includes a computed arrival window and cost-of-fit score for downstream messaging prioritization
Exclusion Reason Codes Are Complete and Actionable
Given a waitlist containing candidates that each violate at least one Buffer Guard rule When the feasibility filter evaluates the list Then each excluded candidate is annotated with one or more machine-readable reason codes from the allowed set {TRAVEL_BUFFER_VIOLATION, ACCESS_TIME_VIOLATION, TEMPERAMENT_CONFLICT, MAX_DISTANCE_EXCEEDED, COVERAGE_CONFLICT, OTHER} And a primary reason is identified when multiple apply And a human-readable explanation is generated for UI/support use And 100% of exclusions have at least one reason code (no null or missing codes) And unknown conditions are mapped to OTHER with contextual metadata (source, timestamp)
Accurate Arrival Window and Deposit Link in SMS Invite
Given a feasible candidate selected for invitation and a configured deposit policy When the system composes the SMS invite Then the arrival window is computed from the current schedule, travel buffers, and access time in the client’s local timezone And the window start/end are within ±2 minutes of the schedule engine’s canonical computation and rounded to the nearest 5 minutes for display And the SMS includes a unique deposit link tied to the candidate and slot (tokenized), which returns HTTP 200 within 1 second and expires after hold_timeout + grace And no SMS is sent if either the arrival window or deposit link cannot be computed/validated
Reply-Driven Hold Prevents Double-Booking
Given SMS invites are sent to one or more feasible candidates for the same slot When any invited candidate replies with an affirmative keyword (e.g., "Y") within the configured window Then the slot is placed on a soft hold immediately for the configured duration (default 5 minutes) And exactly one candidate can be confirmed; concurrent affirmative replies result in one success and deterministic declines to others with an automatic "Sorry, filled" SMS And duplicate affirmative replies from the same candidate are idempotently acknowledged without altering the hold state And the hold auto-releases on timeout or explicit decline/cancel, after which the next candidate(s) may be invited And all state transitions (invited, held, confirmed, released) are audit-logged with timestamps and actor/source
Real-Time Availability Recalculation on State Changes
Given pending invitations and an active schedule When any of the following events occur: new booking, cancellation, coverage change, temperament update, building access update, or travel-time data refresh Then feasibility is recomputed within 2 seconds of the event (or next scheduled evaluation tick, whichever is sooner) And pending invites that become infeasible are auto-withdrawn and notified via SMS with a concise explanation And pending invites whose arrival window shifts by ≥ 5 minutes are updated via follow-up SMS with the new window And no new invites are sent based on stale feasibility snapshots older than 60 seconds
Graceful Degradation Under External API Latency
Given external travel-time services exceed 1.5 seconds latency or return errors during candidate generation When the filter runs under these conditions Then it falls back to cached travel times not older than 15 minutes; if none exist, it applies conservative defaults (adds ≥ 20% buffer or ≥ 5 minutes, whichever is greater) And candidate generation still completes in ≤ 5,000 ms p95 and ≤ 7,000 ms p99 And invites sent under degraded conditions are flagged internally as DEGRADED with the degradation reason recorded And no invites are sent if feasibility cannot be determined without violating safety constraints (e.g., missing access-time data)
SMS Content Conciseness and Compliance
Given the SMS invite template and brand settings When the message is rendered for a candidate Then the message length is ≤ 320 GSM-7 characters (or ≤ 160 UCS-2 when non-GSM characters are present) And the message includes business name, service date, arrival window, clear reply keywords (e.g., Y/N), and the deposit amount/link And the content indicates constraints succinctly (e.g., "includes travel/access buffers") without exceeding the length limit And opt-out language (e.g., "Reply STOP to opt out") is included at least once in the conversation within the past 30 days and added if missing And links use an approved domain and pass compliance checks (no prohibited content/phrasing)
Override & Risk Acknowledgement Workflow
"As a business owner, I want a controlled way to override constraints with client communication so that I can handle exceptions without breaking trust."
Description

Allows staff to override rejected suggestions with explicit confirmation and optional manager approval. Captures reason codes, logs audit trails, and auto-notifies affected clients with updated arrival windows and a clear lateness risk disclaimer. Highlights overridden bookings in the calendar and feeds analytics to recommend buffer tuning when frequent overrides occur. Supports configurable daily override limits and post-visit prompts to recalibrate buffers.

Acceptance Criteria
Override of Rejected Suggestion with Explicit Confirmation
Given a scheduling suggestion is rejected by Buffer Guard due to rule violations When a staff member selects Override on the suggestion Then an override modal displays violated rules, projected lateness in minutes, affected bookings count, and requires a reason code selection Given the override modal is displayed When the staff member checks I acknowledge the lateness risk and selects a reason code Then the Confirm Override button becomes enabled Given organization policy does not require manager approval for the violated rule(s) When the staff member confirms the override Then the booking is created or updated, the override flag is set, and the suggestion is removed from the rejected list Given organization policy requires manager approval for the violated rule(s) When the staff member submits the override Then an approval request is sent to the assigned manager, the override status is set to Pending Approval, and no client notifications are sent until approval Given a pending override request is approved within 30 minutes When the approval is recorded Then the booking is finalized and the override timestamp, approver ID, and reason code are persisted Given a pending override request is rejected or not approved within 30 minutes When the rejection is recorded or the SLA expires Then the override is canceled and the original schedule state is preserved
Immutable Audit Trail for Overrides
Given an override is confirmed or approved When the system writes the audit log Then the entry includes override ID, staff ID, approver ID (if any), timestamp, violated rules, before/after arrival windows, reason code, free-text note, affected client IDs, and request/approval latency in seconds Given an audit log entry exists When a user with audit-view permission retrieves the override details Then the system returns the full immutable record and indicates any redacted PII fields per role Given an audit log entry exists When a user attempts to edit or delete it via UI or API Then the system prevents the change and records an additional audit event noting the attempted modification Given reporting is requested for a date range When an audit export is generated Then all override entries in range are included in a CSV with the above fields and time-zone normalized to the organization time zone
Automated Client Notification with Lateness Disclaimer
Given an override that impacts one or more client arrival windows is finalized When the booking is saved Then an SMS is sent to each affected client within 60 seconds containing the updated arrival window and a clear lateness risk disclaimer Given an SMS message is sent When delivery status is received from the provider Then the system records Delivered, Failed, or Unknown per client and shows a retry action for Failed Given a client receives the SMS When the client replies with RESCHEDULE or taps the reschedule link Then a reschedule flow is initiated and the assigned staff is notified Given a client replies STOP When the system processes the reply Then the client is opted out per compliance rules and the attempt to message is logged
Calendar Highlighting of Overridden Bookings
Given a booking was created or updated via override When the calendar is rendered in day, week, or agenda view Then the booking displays an override badge/icon and a distinct color that contrasts with non-overridden bookings Given a user hovers or taps the override badge When the tooltip or popup appears Then it shows the override reason code, approval status, and violated rules Given calendar filters are applied When the user selects Show overridden only Then only bookings with the override flag are displayed Given accessibility requirements When navigating via keyboard or screen reader Then the override badge is focusable and announces "Overridden booking" with the reason
Configurable Daily Override Limits and Enforcement
Given a daily override limit is set for a role or user When a staff member attempts an override that would exceed their limit for the current day Then the system blocks the action and displays an error showing used, limit, and remaining counts Given a soft threshold warning is configured at 80% of the limit When a staff member reaches or exceeds the threshold Then a non-blocking warning is shown before proceeding with further overrides Given a manager role has bypass permissions When a manager exceeds the limit Then the override is allowed and the exceedance is logged for analytics Given the day rolls over in the organization time zone When the first override of the new day occurs Then the tally resets to zero before enforcement
Analytics and Buffer Tuning Recommendations
Given overrides are recorded with violated rule types and timestamps When at least 10 overrides of the same rule occur within a rolling 14-day window for a service area Then the system generates a buffer tuning recommendation Given a recommendation is generated When a manager views the Buffer Guard analytics dashboard Then the recommendation shows the suggested buffer adjustment in minutes with supporting metrics (count, median lateness delta, acceptance rate) Given a recommendation is accepted by a manager When the change is applied Then the buffer configuration updates, an audit entry is created, and subsequent recommendations for that rule are suppressed for 14 days Given a recommendation is dismissed When the manager provides a dismissal reason Then it is logged and the recommendation is snoozed for 7 days
Post-Visit Variance Prompt for Recalibration
Given a booking was overridden and later marked as completed When the completion is recorded Then the assigned staff receives a prompt within 15 minutes to confirm actual arrival and departure times and any delays encountered Given the staff submits the post-visit data When variance from planned arrival exceeds the buffer by 10 or more minutes Then the data is flagged for analytics and contributes to buffer recalibration Given no response to the prompt within 24 hours When the response window expires Then the prompt is closed, marked as No Response, and the visit is excluded from recalibration calculations Given three or more prompts from the same staff are flagged within a 7-day period When analytics run Then the system surfaces a coaching insight to the manager

Live Reflow

As clients confirm shifts or new fills, the route auto-recalculates and texts updated ETAs to anyone affected. Clear accept/decline options maintain trust while you keep moving—no manual re-planning needed.

Requirements

Real-time Route Reflow Engine
"As an independent walker, I want my route to automatically re-optimize when a client confirms or cancels so that I stay on time without stopping to re-plan."
Description

Core service that listens to schedule events (client confirmations, cancellations, new waitlist fills, reschedules) and recalculates the active route in real time. Applies travel-time data, appointment durations, buffers, time windows, service areas, and priority rules to minimize drive time and lateness while preserving existing commitments. Produces an updated stop sequence, start/end times, ETAs, and a change diff per stop. Concurrency-safe and idempotent to handle simultaneous events, with versioned route plans and rollback on failure. Meets performance targets (e.g., P95 < 2 seconds for typical day routes) and integrates with PawPilot scheduling, Smart Waitlist, messaging, and billing modules via events.

Acceptance Criteria
Client Confirmation Reflows Route & Notifies Affected Clients
Given an active route for today with <= 25 stops and one pending shift awaiting confirmation And the current stop is in progress and marked locked When a client confirms the pending shift and a confirmation event is received Then the engine recalculates the route using travel-time data, appointment durations, buffers, time windows, service areas, and priority rules And completes the computation P95 <= 2,000 ms for routes with <= 25 stops And returns an updated stop sequence and per-stop start/end times and ETAs And does not reorder or time-shift the in-progress or locked stops And publishes a per-stop change diff with before/after times and reason codes And publishes "eta_updated" events only for stops with |ETA delta| >= 5 minutes And includes accept/decline tokens and the committed route version in each event
Cancellation Auto-Fill from Waitlist Triggers Reflow
Given an active route with a confirmed stop scheduled later today When the client cancels and a cancellation event is received Then the engine publishes a "slot_available" event with slot window, service area, and constraints And upon receiving a "waitlist_fill_confirmed" event, inserts the fill and recomputes the route And completes each reflow operation P95 <= 2,000 ms (<= 25 stops) and commits a new route version And no remaining confirmed stop is scheduled outside its hard time window; any soft-window breach is flagged "at-risk" with predicted lateness minutes And affected clients (|ETA delta| >= 5 minutes) receive "eta_updated" events referencing the new version And if no feasible placement exists, the engine leaves the existing version unchanged and emits a "placement_unfeasible" event with reason codes
Constraint Compliance: Time Windows, Buffers, Service Areas
Given any reflow request on an active route When constraints are evaluated Then hard constraints (service area boundaries, hard time windows, and required buffers) are never violated in the committed plan And appointment durations are preserved for all stops And the in-progress stop and any stops marked "locked" keep their sequence and times within +/- 0 minutes And soft windows may be exceeded only when allowed by priority rules and must be annotated with "at-risk" and delta minutes in the diff And if the travel-time provider is degraded, a fallback provider is used and the plan metadata includes provider="fallback"
Concurrency Safety and Idempotency
Given three events (confirmation, cancellation, reschedule) arriving within 200 ms for the same route When the engine processes the events concurrently Then exactly one committed route version is produced per causal batch, with monotonically increasing version numbers And stale writes are rejected via version check and retried on the latest version And duplicate events with the same event_id are ignored without producing additional route versions or duplicate outbound events And all published events include the committed route_version and idempotency_key to ensure at-most-once delivery to downstream consumers
Rollback on Failure with Versioned Plans and Audit Log
Given a reflow computation that has produced a tentative plan When committing the plan causes any downstream integration failure (e.g., messaging publish, billing update) or the transaction fails Then the previous committed route version is restored atomically And the tentative version is marked "aborted" with failure_reason and timestamps in the audit log And no client-facing notifications reference the aborted version And a compensating "rollback_completed" event is published with pointers to the failed attempt
Performance and Scalability Targets
Given a test workload of 1,000 reflow operations on routes with up to 25 stops executed during peak hours When measured end-to-end from event receipt to committed version Then P95 latency <= 2,000 ms and P99 latency <= 5,000 ms And error rate due to timeouts <= 0.5% And the worker queue wait time P95 <= 200 ms at an arrival rate of 2 events/sec/worker And horizontal scaling to 10 workers does not degrade P95 by more than 10%
Change Diff Completeness and Accuracy API
Given a committed reflow resulting in a new route version When retrieving the change diff for that version via the API Then each stop in the route includes stop_id, old/new sequence, old/new start_time, old/new end_time, old/new ETA, delta_minutes, status_change, and reason_code And the diff includes aggregate metrics: total_drive_time_before/after, total_lateness_minutes, affected_stops_count And the diff is internally consistent with the committed plan (0 mismatches across 100 sampled routes) And the API returns within P95 <= 300 ms for routes up to 25 stops
ETA Impact Detection & Audience Selection
"As a sitter, I want only clients whose arrival time meaningfully changes to be notified so that I don’t spam unaffected clients."
Description

Detects which appointments are materially affected by a reflow by comparing old vs. new ETAs and applying configurable thresholds (e.g., notify if delta > 5 minutes or crosses a time window). Classifies changes (earlier/later) and reason codes, generates per-stop impact payloads, and selects recipients accordingly. Respects client notification preferences, quiet hours, do-not-disturb windows, and local time zones. Outputs ready-to-send data (new ETA, delta, reason, template variables) for the messaging service. Integrates with client profiles and the notification policy engine.

Acceptance Criteria
Material Impact by Minute Delta Threshold
Given a route with old ETAs and a configured minute-delta threshold of 5 When a reflow produces new ETAs with deltas of -2, +5, +6, and -7 minutes Then only the stops with absolute delta > 5 minutes (+6 and -7) are marked as impacted And each impacted stop is classified as 'earlier' (negative) or 'later' (positive) And non-impacted stops are excluded from the audience selection
Window Crossing Trigger Overrides Minute Threshold
Given a stop with a promised window of 13:00–15:00 local and a minute-delta threshold of 10 When the new ETA moves from 14:58 to 15:03 local (delta +5) Then the stop is marked impacted with reason_code='window_crossed' and classification='later' And crossing the promised window boundary triggers impact regardless of minute-delta And if a new ETA remains within the window and absolute delta <= threshold, the stop is not impacted
Recipient Selection Respects Preferences, Quiet Hours, DND
Given client A allows SMS and is outside quiet hours and DND And client B has SMS opt-out And client C is within quiet hours 21:00–08:00 local And client D is within a do-not-disturb window 12:00–13:00 local When audience selection runs at 07:30 local for each client's location Then only client A is included as a recipient And clients B, C, and D are excluded with suppression_reason values 'opt_out', 'quiet_hours', and 'dnd' respectively And evaluations use each client's local time zone
Per-Stop Impact Payload Fields and Validation
Given an impacted stop When generating the impact payload Then the payload includes stop_id, client_id, old_eta_utc, new_eta_utc, old_eta_local, new_eta_local, delta_minutes (signed), classification ('earlier'|'later'), reason_code, window_crossed (boolean), local_time_zone, and template_variables And template_variables include client_first_name, stop_address_short, old_eta_local_text, new_eta_local_text, delta_text, appointment_date, and route_name And all fields are non-null and conform to schema types (ISO 8601 timestamps with zone; integer minutes; allowed enumerations) And the payload passes schema validation; otherwise the stop is excluded with suppression_reason='invalid_payload'
Local Time Zone Computation for Multi-Zone Routes
Given a route with stops across different time zones When impact detection and audience selection run at 10:00 UTC Then each stop's quiet hours, promised windows, and local ETA strings are evaluated using that stop's local time zone And new_eta_local reflects the correct zone for each stop And no checks rely on server or driver time zone
Integration with Notification Policy Engine
Given impacted stops and associated client profiles When the notification policy engine returns a decision ('allow' or 'suppress') with a policy_id for each stop Then the audience includes only stops with decision='allow' And each per-stop payload includes policy_decision and policy_id And suppressed stops carry suppression_reason reflecting the policy outcome
No-Op Reflow Produces No Audience
Given a reflow where all ETA changes are within the minute-delta threshold and no promised window boundaries are crossed When impact detection runs Then zero stops are marked as impacted And no audience or messaging payloads are produced
SMS ETA Update with Accept/Decline CTAs
"As a client, I want a clear text with the new ETA and simple accept/decline options so that I can quickly confirm or ask to reschedule."
Description

Generates branded SMS messages that clearly present the updated ETA with one-tap accept/decline links and reply keyword options. Supports localization and templating, includes contextual details (arrival window, handler name), and deep-links to payment/deposit or rescheduling when decline is chosen. Tracks delivery, clicks, and replies; honors opt-in/opt-out (STOP/HELP), rate limits, and compliance. Integrates with PawPilot’s messaging gateway, short-link service, client profiles, and billing to update deposit flows as needed.

Acceptance Criteria
ETA SMS Content and Branding
Given a client with SMS opt-in is affected by a route reflow When a new ETA is calculated Then send exactly one SMS within 60 seconds containing: account brand name, updated ETA with time zone, arrival window string, handler name, localized job date, and a unique short link And the message uses the client's locale template; if missing, it falls back to the account default And all template variables render with no missing or placeholder tokens
Accept/Decline CTAs and Keywords
Given the ETA SMS is sent Then include two unique, signed links: Accept and Decline, scoped to the client and job, expiring at job end or 6 hours (whichever is sooner) And include reply keyword options in the message body: ACCEPT/YES/Y and DECLINE/NO/N (case-insensitive) When the client accepts via link or keyword Then mark the job as Confirmed, log the timestamp, and send a confirmation SMS When the client declines via link or keyword Then open a deep link offering reschedule and (if configured) deposit/payment, log the declination, and suppress further ETA updates for that job unless rescheduled And duplicate clicks/replies are idempotent and do not create duplicate events
Delivery, Click, and Reply Tracking
Given any ETA SMS is sent Then store message ID, client ID, job ID, body hash, and send timestamp When a delivery receipt arrives Then record delivery status (delivered/failed), provider code, and timestamp When a short link is clicked Then record click with URL ID, client, job, and timestamp (and device type if available) When a reply is received Then parse and classify (accept/decline/help/stop/other), link it to the originating message, and store timestamp and raw text And 99% of events are available in analytics within 5 minutes; duplicates are de-duplicated by message ID plus event type
Opt-in/Opt-out and Compliance Handling
Given a client is unsubscribed or has replied STOP When a route reflow occurs Then no ETA SMS is sent to that number When HELP is received Then send a HELP response containing business contact info and "Reply STOP to opt out" within 60 seconds When START/UNSTOP is received Then re-subscribe the client and confirm via SMS And all outbound ETA SMS include opt-out/help notice per locale where required And sending honors per-country regulations and internal do-not-contact lists
Rate Limiting and Reflow Deduplication
Given multiple reflows occur within a short period When ETAs change repeatedly for the same client Then send at most one update per client per 5 minutes, always with the latest ETA, canceling queued older updates And adhere to provider throughput limits (configurable TPS) by queuing; maintain >=99% successful sends with automatic retries up to 3 attempts on transient errors And message ordering per client is preserved and duplicates are not sent
Localization and Template Fallback
Given a client's locale (e.g., en-US, es-ES) and available templates When generating the ETA SMS Then select the template matching the client's locale; if unavailable, use the account default template And format date/time per locale conventions (12h vs 24h, day/month order) And Accept/Decline keywords are localized and recognized in addition to links And right-to-left rendering is supported for RTL locales
Billing/Deposit Deep Link on Decline
Given the account requires a deposit or payment for rescheduling/holding When the client chooses Decline Then generate a signed, client+job-specific deep link that pre-fills job details And if a deposit is required and unpaid, route to deposit checkout; upon successful payment, update billing state and job status within 60 seconds and send confirmation And if a deposit has already been paid, do not request an additional deposit and carry forward or credit the existing payment per policy And the deep link expires at job end or after 24 hours (whichever is sooner) and shows an informative expired page thereafter
Response Processing & Reflow Loop
"As a groomer, I want client responses to automatically adjust the schedule and route so that I don’t have to micromanage changes during the day."
Description

Parses client replies and link actions, validates intent, and applies business rules: accept locks the slot; decline triggers waitlist search, deposit refund/transfer rules, and a new reflow; no response escalates to reminder or default outcome after a timeout. Prevents oscillations with locking/versioning and resolves conflicts deterministically when multiple changes occur simultaneously. Updates the schedule, sends confirmations, and writes an auditable event trail. Provides staff notifications and manual override controls via ops tools.

Acceptance Criteria
Accept Reply Locks Slot and Triggers ETA Reflow
Given an offer for a specific slot is pending response and the client has a valid hold/deposit When the client replies with an accepted intent via SMS or link (e.g., "accept", "yes", "y", Accept button) Then the system validates the intent and locks the slot to that client And the route is recalculated within <= reflow_sla_seconds and ETAs for all impacted appointments are updated And impacted clients each receive exactly one SMS with the new ETA and clear Accept/Decline options And all competing holds/offers for that slot are canceled and notified And the accepting client receives a confirmation SMS including date/time and deposit status And an auditable event is recorded linking reply -> schedule change -> notifications with a shared correlation_id
Decline Reply Triggers Waitlist Fill and Deposit Handling
Given a booked or held slot exists with an associated deposit per policy When the client replies with a declined intent via SMS or link (e.g., "decline", "no", Decline button) Then the system frees the slot and applies the configured deposit_policy (refund, retain, or transfer) and posts the transaction And the waitlist is searched using the configured selection rules; the top-ranked candidate is offered the slot And upon a new fill, the route reflows and impacted clients receive updated ETAs via SMS And the declining client receives a confirmation SMS including deposit outcome And the event trail records decline -> deposit action -> waitlist selection (with candidate score) -> notifications
No Response Timeout and Escalation
Given an offer is pending client response When reminder_timeout_minutes elapse without a valid intent detected Then send exactly one reminder SMS with Accept/Decline options and preserve the offer And when default_timeout_minutes elapse after the reminder without valid intent Then apply the configured default_outcome (e.g., auto-decline), update the schedule, and stop waiting for the client And notify staff via the ops channel with a summary and link to the offer And the event trail records reminder sent, timeout reached, default outcome applied with timestamps
Simultaneous Changes Conflict Resolution and Oscillation Prevention
Given two or more replies/link actions affecting overlapping slots are received within the concurrency window When the system processes these actions Then optimistic locking with schedule_version ensures only one change commits; others receive a conflict and are retried against the latest state And conflicts are resolved deterministically by received_at timestamp then by client_id lexical order for ties And each committed change triggers at most one route reflow; no oscillation (no >1 reflows for the same schedule_version increment) And clients receive no more than one ETA update per committed change And the event trail shows the processing order and conflict outcomes
Ambiguous or Unsupported Reply Handling
Given a client message that does not unambiguously map to Accept or Decline (e.g., free text like "maybe", emojis) When the parser evaluates the message Then no schedule changes are applied And the client receives a clarification prompt with explicit Accept/Decline buttons/links And if ambiguity persists after max_clarification_attempts, notify staff via ops tools and mark the thread for manual review And log the ambiguous content, parser confidence, and prompts in the event trail
Auditable Event Trail Completeness and Accessibility
Given any state transition or external communication in the reflow loop When the transition occurs Then an immutable event record is written containing timestamp_utc, actor (client/system/staff), prior_state, new_state, schedule_version, correlation_id, reason_code, and message/template identifiers where applicable And events are queryable by job_id or correlation_id within <= event_query_sla_ms via ops tools And staff can view a chronological timeline that reconstructs the decision path and notifications And all records meet the configured retention_policy_days
Staff Manual Override Controls Respect and Propagation
Given a pending offer or in-progress reflow When an authorized staff user performs an override in ops tools (lock/unlock slot, select waitlist candidate, cancel offer, force reflow) Then automated actions on the affected slot are paused, the override is applied, and the system resumes from the overridden state And downstream notifications (confirmations, updated ETAs) are sent accordingly And automated retries do not revert or overwrite the manual decision And the override event includes user_id, reason, affected entities, and correlation_id in the event trail
Smart Waitlist Auto-Fill Integration
"As a busy walker, I want dropped slots to be auto-filled from my waitlist so that my day stays full without manual outreach."
Description

When declines or cancellations occur during Live Reflow, queries the Smart Waitlist by proximity, service fit, timing flexibility, and deposit-ready status to auto-fill the opening. Issues timeboxed offers, manages cascading offers, and captures deposits if required. On successful fill, triggers another reflow and sends targeted ETA updates. Ensures consistency with existing fill-rate metrics, no-show reduction logic, and billing rules.

Acceptance Criteria
Auto-Fill Trigger on Live Reflow Cancellation/Decline
Given Live Reflow is active for the provider's route and a scheduled job is declined or canceled When the job status changes to Declined or Canceled Then the system initiates a Smart Waitlist search for that opening within 10 seconds And the search parameters (service type, location, duration, time window, deposit requirement) match the canceled/declined job And a traceable event is recorded indicating the auto-fill attempt has started
Eligibility and Ranking by Proximity/Service Fit/Timing/Deposit-Ready
Given organization-level configurations for proximity radius (R), timing flexibility window (W), and maximum candidates (N) When the Smart Waitlist search runs Then only candidates offering the required service, within R of the job location, and available within W of the opening time are eligible And if the job requires a deposit, only candidates flagged as deposit-ready are eligible And eligible candidates are ranked by distance (ascending), then by earliest availability overlap, then by waitlist signup timestamp (oldest first) And the system selects up to N candidates in rank order And if no candidates are eligible, no offers are issued and the provider is notified that the slot remains unfilled
Timeboxed Offer Issuance and Expiry
Given a ranked list of eligible candidates exists When offers are issued Then each candidate receives an SMS containing service details, date/time window, estimated price, deposit requirement (if any), and Accept/Decline links/buttons And each offer has a configured TTL countdown visible in the message link flow And an offer automatically expires at TTL and cannot be accepted thereafter And (if configured) a single reminder is sent at 50% of TTL to candidates who have not responded And expired or declined offers are marked as such with timestamped events
Cascading Offers and Double-Booking Prevention
Given multiple offers may be outstanding across ranked candidates When an offer is declined or expires Then the next eligible candidate in rank order is sent an offer within 30 seconds until the list is exhausted or the slot is filled When any candidate accepts Then all other outstanding offers for that opening are immediately withdrawn and links disabled, and recipients are notified the slot is no longer available And at acceptance time, availability is revalidated to prevent overlaps or double-bookings; if a conflict is detected, the acceptance is rejected and cascading continues
Deposit Capture and Booking Confirmation
Given the opening requires a deposit per billing rules When a candidate taps Accept Then the candidate is routed to payment and the required deposit is authorized or captured successfully before confirming the booking And if payment fails or is not completed within the payment TTL, the acceptance is voided and cascading continues automatically Given the opening does not require a deposit When a candidate taps Accept Then the booking is confirmed immediately without payment And on successful confirmation, the candidate and provider receive confirmation SMS with booking details and any deposit receipt per billing rules
Post-Fill Reflow and Targeted ETA Updates
Given a booking is confirmed via Smart Waitlist auto-fill When confirmation completes Then the route is recalculated and ETAs are updated And only clients whose ETAs changed receive an updated ETA SMS containing clear Accept/Decline options And the provider's route view reflects the new sequence and ETAs within 15 seconds of confirmation And any Decline responses to ETA updates trigger another reflow and, if needed, a new Smart Waitlist search for the newly opened slot
Metrics, No-Show Logic, and Auditability
Given Smart Waitlist auto-fill is used When an auto-fill attempt completes (filled or unfilled) Then fill-rate metrics attribute the outcome to Smart Waitlist Auto-Fill and align with existing metric definitions And no-show reduction logic and risk flags are applied consistently to the newly filled booking And all key actions (trigger, filter, ranking, offer send, reminder, accept/decline, deposit outcome, confirmation, reflow, ETA notifications) are logged with timestamps and identifiers for audit And billing records (deposit amounts, fees, receipts) match configured rules with no duplicate charges across cascading attempts
Observability, SLAs, and Fallbacks
"As an operator, I want visibility and safe fallbacks for Live Reflow so that I can maintain service quality during spikes or outages."
Description

Defines and monitors reliability/performance targets for Live Reflow (e.g., P95 recalculation < 2s; SMS send latency < 30s from event). Implements structured logs, tracing across optimization and messaging, dashboards, and alerting. Provides safe-degraded modes (pause reflow, notify-only, manual-resume) and automatic rollback to last stable route on error. Adds replay tooling and idempotent event handling for incident analysis and recovery.

Acceptance Criteria
P95 Reflow Compute Latency Under Operational Load
- Given a reflow-triggering event is ingested, when the optimizer runs, then 95% of events complete reflow in <= 2,000 ms over a rolling 15-minute window and 100% in <= 10,000 ms, measured from event_ingested_at to reflow_completed_at. - Metric reflow_latency_ms is emitted per event with tags: route_id, account_id, event_id, environment, version; exported to the metrics backend at 1-minute resolution. - A distributed trace span "reflow.optimize" is recorded per event with duration matching reflow_latency_ms within ±50 ms, and is linked to the parent event ingestion span via trace_id.
SMS Notification Latency After Reflow
- Given a successful reflow affecting one or more clients, when SMS notifications are sent, then 95% of messages are provider-accepted within <= 30 seconds and 99% within <= 45 seconds from event_ingested_at. - Provider acceptance or callback timestamps are recorded per message; metric sms_send_latency_ms is emitted with tags: route_id, account_id, event_id, message_id, provider. - Each SMS log entry and trace span includes correlation_id linking to the reflow event; if no provider callback is received within 60 seconds, the message is marked timeout and a warning metric is emitted.
Structured Logs and End-to-End Trace Correlation
- 100% of reflow executions emit structured JSON logs at key stages: event_ingested, optimize_started, optimize_succeeded|failed, messaging_enqueued, messaging_provider_accepted|failed, rollback_started|completed, degraded_mode_changed. - Each log contains: timestamp, level, account_id, route_id, event_id, correlation_id (trace_id), span_id, mode, attempt, latency_ms (where applicable), error_code; PII (names, phone numbers, addresses) is masked or excluded. - 99% of reflows have a single distributed trace with connected spans across optimizer and messaging sharing the same trace_id; sampling >= 90% for non-error transactions and 100% for error transactions in production.
SLA Dashboards and Proactive Alerting
- Dashboards display SLIs: reflow latency (P50/P95/P99), SMS send latency (P50/P95/P99), error rate, queue depth, retry rate, degraded mode status, and incidents in last 24h; data updates at least every 60 seconds. - SLOs enforced: reflow P95 <= 2s; SMS P95 <= 30s; error rate <= 1% over 15 minutes. Alerts: warn on breach for 2 consecutive minutes; critical on breach for 5 consecutive minutes; route to on-call with runbook links. - Synthetic end-to-end canaries run every 5 minutes; failure of 2 consecutive canaries triggers a warning alert; failure of 5 triggers a critical alert. Alert acknowledgments and resolutions are auto-logged with user, timestamp, and incident ID.
Degraded Modes and Automatic Rollback
- Three controllable modes exist per account via UI and API with audit logging: Pause Reflow, Notify-Only, Manual Resume. - Pause Reflow: events are persisted but not optimized; no client notifications sent; an alert is emitted if paused > 15 minutes during business hours. - Notify-Only: optimizer computes ETAs but does not reorder routes; clients receive update-only SMS; the previous stable route remains authoritative. - On optimization/messaging failure that would change route order, rollback to the last stable route completes within <= 5 seconds; a rollback event with reason and trace_id is logged; clients do not receive conflicting messages. - Manual Resume drains queued events in order with rate limit <= 5 messages/sec per account, maintaining idempotency.
Idempotent Event Handling and Replay Tooling
- Event handling is idempotent: re-ingesting the same event_id within 24 hours produces no duplicate optimizations, route changes, or SMS; dedup decisions are logged with reason=duplicate. - Out-of-order events that would regress route version are ignored safely and logged with reason=stale; no client messages are sent for stale events. - Replay tool supports dry-run (no side effects) and live-run (with side effects) for a selected account and time window, preserving original event ordering; traces include replay=true attribute. - Replay throughput: dry-run >= 50 events/sec; live-run >= 10 events/sec without breaching active SLOs; auto-abort and alert on deviation. - Replay generates an exportable report summarizing counts, successes, failures, and links to traces.

Block Optimizer

For fixed time blocks, simulates micro-swaps and sequence tweaks to compress dead time and increase walk density. One tap applies the highest-scoring plan, showing minutes saved and added revenue for the block.

Requirements

Block Constraint Modeling
"As an independent walker, I want my schedule optimizer to respect fixed appointments, time windows, and pet-specific needs so that any changes it suggests are realistic and safe to execute."
Description

Model fixed-time blocks with service constraints (duration, earliest/latest windows, buffers, pet-specific requirements), immovable appointments, and provider preferences. Pulls customer, service, and address data from PawPilot’s schedule and CRM. Ensures any sequence tweak respects hard constraints (medication times, key/access notes, repeat cadence) while allowing soft-flex items to move within tolerance. Exposes a constraint API the optimizer uses to validate candidates and prevents proposals that break commitments or violate business rules.

Acceptance Criteria
Import schedule and CRM data into constraint model
Given a fixed-time block exists for a provider on a specific date And the PawPilot schedule and CRM contain customer, service, address, key/access, medication, repeat cadence, and preference records for the block’s appointments When the constraint model is initialized for that block Then it loads and normalizes all required fields for 100% of appointments in the block And geocodes all addresses; for valid addresses, success rate >= 99%, and any unresolvable address is flagged as a HARD violation with code ADDRESS_INVALID and the specific appointment_id And each appointment in the model includes duration, earliest/latest windows, pre/post buffers, pet-specific requirements, key/access notes, medication windows, repeat cadence, and provider preference flags
Enforce hard constraints (medication windows, key/access, immovable)
Given a candidate sequence for the block When any appointment’s scheduled time falls outside its medication earliest/latest window Then validation returns verdict=fail with violation code MED_WINDOW, appointment_id, scheduled_time, required_window When any appointment requires key/access ordering (e.g., key pickup before walk) and the sequence violates the required order Then validation returns verdict=fail with violation code KEY_ORDER and the involved appointment_ids When an appointment is marked immovable and its start time differs from the original by any amount Then validation returns verdict=fail with violation code IMMUTABLE and delta_minutes When two appointments overlap due to duration+buffers Then validation returns verdict=fail with violation code OVERLAP and the conflicting appointment_ids
Apply soft-flex tolerances for movable items
Given an appointment marked soft-flex with a move_tolerance_minutes value When the candidate sequence shifts its start time by an amount <= move_tolerance_minutes and within the appointment’s earliest/latest window Then validation does not fail and records an adjustment note with code SOFT_MOVE and delta_minutes When the shift exceeds move_tolerance_minutes or exits the earliest/latest window Then validation returns verdict=fail with violation code SOFT_TOL_EXCEEDED and delta_minutes When multiple soft-flex moves are present Then cumulative moves are evaluated per appointment independently; no aggregate masking is allowed
Compute and honor durations, buffers, and travel times
Given each appointment has a service duration and pre/post buffer minutes And travel time between sequential stops is computed using the configured mode (walk/drive) and current traffic profile When the model evaluates a candidate sequence Then the scheduled start times ensure duration + buffers + travel do not cause overlaps or block overrun And total block time does not exceed the fixed block window And travel time estimation uses the latest distance matrix cache no older than 15 minutes; if unavailable, falls back to baseline speeds and marks estimate_source accordingly And if required travel or buffers cannot fit, validation returns verdict=fail with violation code INFEASIBLE_TIMING
Constraint validation API returns deterministic verdicts with reasons
Given a POST /v1/constraints/validate request with provider_id, block_id, candidate_sequence, and model_version When the payload is well-formed Then the API responds 200 within p95 300ms and p99 600ms for blocks with <=12 appointments And returns a body containing verdict ("pass" or "fail"), violations[], normalized_schedule[], minutes_saved, revenue_delta, and model_version And for identical inputs (including model_version), repeated requests return identical outputs And invalid payloads yield 400 with machine-readable errors; unexpected server issues yield 503 with retryable flag
Optimizer prevents applying violating plans and surfaces violations
Given the optimizer generates a candidate plan and calls the constraint API When the API verdict is "fail" Then the Apply action is disabled and a summary of violations by code and count is shown; tapping reveals the per-appointment details And no changes are persisted to the schedule When the API verdict is "pass" Then Apply is enabled and shows minutes_saved and revenue_delta consistent with the API (tolerance: ±1 minute, ±$1) And upon Apply, the schedule updates reflect the validated sequence exactly
Respect repeat cadence and provider preference rules during tweaks
Given an appointment tagged with a repeat cadence (e.g., weekly Tue 1–3pm window) When a candidate sequence moves it outside its cadence day or window Then validation returns verdict=fail with violation code REPEAT_CADENCE and details Given provider preferences are configured with rule_type "hard" or "soft" When a candidate violates a hard preference (e.g., avoid Zone A 3–4pm, max_consecutive_large_dogs=2) Then validation returns verdict=fail with violation code PREF_HARD and rule_id When a candidate violates a soft preference Then validation does not fail and records a penalty note with code PREF_SOFT to be used for scoring
Travel Time & Density Estimator
"As a walker operating in dense neighborhoods, I want accurate travel and buffer estimates so that the optimizer’s swaps meaningfully reduce dead time and increase how many pets I can see."
Description

Estimate inter-visit travel times and walking distances to quantify dead time and route density. Combines mapping API estimates (walk/drive modes) with historical actuals to calibrate local accuracy. Supports city-specific travel modes, traffic/time-of-day adjustments, building entry overhead, and parking/pet pickup buffers. Provides a fast callable service returning time/distance matrices for a block to feed into scoring, with caching and fallback when maps are unavailable.

Acceptance Criteria
Fast Time/Distance Matrix Generation for a 30-Visit Block
- Given a block with 30 visits each having valid lat/lon and planned start timestamps, When the service is called with mode=auto, Then it returns time_seconds and distance_meters NxN matrices with diagonal=0 and asymmetry allowed within 800 ms p95 and 500 ms p50. - Given any matrix response, When validated, Then all time values are non-negative integers in seconds and all distances are non-negative integers in meters. - Given a request with request_id and block_id, When the response is returned, Then it includes computation_metadata with source_per_leg, calibration_version, computed_at, and units="seconds|meters". - Given 10 concurrent requests of size N<=30, When executed, Then each completes with HTTP 200 and latency p95 <= 1200 ms and no more than 1% throttled retries. - Given N<2 visits, When requested, Then the service returns HTTP 200 with empty matrices and reason="INSUFFICIENT_VISITS".
City-Specific Mode Selection and Time-of-Day Traffic Adjustments
- Given city_config default_mode=walking, When a block in that city is requested without per-leg overrides, Then all legs use walking estimates with time-of-day speed/traffic adjustments applied. - Given city_config default_mode=driving and traffic_model=best_guess, When the request includes leg departure times, Then driving ETAs reflect traffic for those departure times as returned by the mapping API and calibrated factors. - Given a leg with per-visit override requires_vehicle=true, When the city default is walking, Then that leg is computed with driving mode. - Given a leg crossing city boundaries with different configs, When computed, Then mode selection uses the origin city’s rules unless a per-leg override is present. - Given a request with mode=explicit(walking|driving), When submitted, Then all legs use the explicit mode regardless of city defaults and the mode_used is recorded per leg.
Nightly Calibration Using Historical Actuals
- Given at least 30 days of historical actual travel times per city/mode/time-bucket, When the nightly calibration job runs, Then it computes correction factors per (city, mode, 15-min bucket) and stores calibration_version T+1. - Given a held-out validation set from the last 7 days, When evaluated, Then median absolute percentage error (MdAPE) <= 12% and P90 APE <= 25%; otherwise the new calibration is rejected and the previous version remains active. - Given a live request after successful calibration, When times are returned, Then source_per_leg reflects "maps_calibrated" and includes calibration_version. - Given insufficient historical data (fewer than 100 legs in a bucket), When calibrating, Then the system backs off to parent time bucket or city-wide factor and marks backoff_level in metadata. - Given a calibration version change, When subsequent requests are served, Then cache entries tagged with the old version are bypassed or revalidated.
Application of Entry, Parking, and Pet Pickup Buffers
- Given per-visit settings entry_overhead_sec, parking_buffer_sec, and pickup_buffer_sec, When computing A->B, Then time_seconds(A,B) includes A.parking_buffer_sec + B.entry_overhead_sec + B.pickup_buffer_sec in addition to travel time. - Given city_defaults for any missing buffer values, When a visit omits a value, Then the default is applied and marked defaulted=true in leg metadata. - Given buffer values are updated, When the same block is recomputed, Then the returned leg times reflect the new buffers within one request and cache entries keyed with old buffer signatures are not reused. - Given a request with include_buffers=false, When computed, Then buffers are excluded and metadata includes buffers_applied=false.
Caching with Time-Bucketed Keys and Invalidation
- Given a request for leg O->D with mode=M and departure time t, When computed, Then the result is cached with a key on geohash6(O), geohash6(D), mode, calibration_version, and 15-min time bucket of t. - Given a subsequent identical request within TTL, When served, Then the service returns from cache with cache_hit=true and latency reduced by at least 30% compared to a cold call. - Given TTL=24h and calibration_version changes, When a cached entry is stale by version, Then it is not returned and a fresh value is computed and stored. - Given city_config affecting mode selection is updated, When the next request arrives, Then cache entries with the prior config_hash are invalidated. - Given geohash rounding introduces small spatial error, When distance between true coords and geohash center exceeds 250 m, Then the cache is bypassed and a fresh compute occurs.
Fallback Behavior When Mapping API Is Unavailable
- Given the mapping API returns 5xx, 429, or times out (>1200 ms), When computing a leg, Then the service first attempts cache; if miss, it computes haversine_distance and converts to time via calibrated speed for (city, mode, 15-min bucket). - Given fallback is used, When the response is returned, Then source_per_leg="fallback" and includes fallback_reason and speed_profile_id. - Given partial API failures on some legs, When building the matrices, Then successful legs use calibrated map results and failed legs use fallback without failing the entire request (HTTP 200). - Given a full API outage, When serving a 20-leg block, Then the request still completes with p95 latency <= 800 ms using cache and heuristic fallback. - Given both cache and heuristic are unavailable (missing calibration), When serving, Then the service returns HTTP 503 with error_code="NO_ESTIMATE_AVAILABLE" and no matrices.
API Contract, Validation, and Response Schema
- Given a request payload, When visits lack lat/lon but provide an address, Then the service geocodes addresses; if geocoding fails for any visit, the service returns HTTP 400 with error_code="INVALID_LOCATION" and details of offending visit_ids. - Given a valid request, When processed, Then the response includes: time_seconds_matrix, distance_meters_matrix, mode_used_per_leg, source_per_leg, calibration_version, computed_at (UTC ISO-8601), units, and warnings[]. - Given any visit has a hard window end before start, When validated, Then the service returns HTTP 400 with error_code="INVALID_TIME_WINDOW" and does not compute matrices. - Given coordinates outside supported countries, When requested, Then the service returns HTTP 422 with error_code="UNSUPPORTED_REGION". - Given request_id is supplied, When the service logs and responds, Then the same request_id is echoed and trace_id is generated and present in response headers.
Micro-swap Optimization Engine
"As a busy groomer/walker, I want the system to quickly try small schedule tweaks for me so that I can improve my block without manual reshuffling."
Description

Generate and evaluate micro-swaps and slight start-time nudges within a block to minimize idle minutes and maximize walk density while honoring constraints. Uses heuristics with bounded search (e.g., local swaps, 2-opt/3-opt, insertion) and a multi-factor score function (dead time reduction, distance reduction, lateness penalties, customer priority). Stops on time/iteration limits and returns the top-scoring plan candidates with confidence metrics. Designed for sub-second responses on mobile.

Acceptance Criteria
Sub-second Mobile Response Under Default Limits
Given a sample set of 100 representative blocks (6–12 walks, realistic travel times) and engine config timeLimit=800ms, iterationLimit=10000, K=3 on a target mid-tier mobile device, When the engine optimizes each block, Then the 95th percentile end-to-end latency per block is <= 800 ms and the 99th percentile is <= 1000 ms, And at least one candidate is returned for 100% of runs.
Hard Constraints Preservation
Given a block with appointments that include fixed durations, location coordinates, time windows, walker availability, and a latenessTolerance, When the engine returns candidates, Then for every candidate: (a) no appointment starts before its windowStart, (b) no appointment ends after windowEnd + latenessTolerance, (c) service durations are unchanged, (d) the walker’s availability window is not exceeded, (e) no overlaps exist between appointments, (f) travel time between consecutive appointments is non-negative and computed using the configured distance metric, and (g) no appointment is moved outside the block’s start/end boundaries.
Search Bounding and Early Stop Reporting
Given timeLimit and iterationLimit are set, When the engine runs, Then it never exceeds the smaller of the two limits, And it returns searchStats including elapsedMs, iterationsCompleted, and searchTruncated (true if a limit was hit), And it always returns the best-so-far candidate with score >= baseline.score.
Candidate Output Completeness and Ordering
Given K=3 and a valid block, When the engine completes, Then it returns between 1 and 3 candidates sorted by score descending, And candidate[0] is the baseline with isBaseline=true, And each candidate includes: sequence (ordered appointmentIds), startTimes (ISO8601), score (numeric), confidence (0.0–1.0), minutesSaved, addedRevenue, deadTimeMinutes, distanceKm, latenessMinutes, and all metrics are computed relative to the baseline where applicable, And ties in score are broken deterministically by lexicographic order of sequence.
Multi-factor Scoring Correctness
Given two plans that are identical except deadTimeMinutes, When compared, Then the plan with lower deadTimeMinutes has a higher score; Given two plans identical except distanceKm, Then the plan with lower distanceKm has a higher score; Given two plans identical except latenessMinutes (within tolerance), Then the plan with lower latenessMinutes has a higher score; Given two plans identical except customerPriority satisfaction (higher priority served earlier without worsening other metrics), Then the plan favoring the higher-priority customer has a higher score; Given custom weight configuration is supplied, Then the score ordering reflects those weights and differs from defaults when weights change.
Micro-swap and Nudge Scope Enforcement
Given the engine is configured with default operators {swap, twoOpt, threeOpt, insertion, nudge} and nudgeMax=5 minutes, When candidates are generated, Then each candidate’s per-appointment start time change relative to baseline is within ±5 minutes unless required by travel propagation, And no appointment’s duration changes, And no appointment is moved outside the block, And each candidate includes an auditTrail listing only operators from the allowed set used to produce that candidate.
No-Improvement Fallback Behavior
Given a block and minImprovementThreshold=0, When no candidate plan improves the baseline score, Then the engine returns a single baseline candidate with isBaseline=true, reason="no-better-plan", confidence=1.0, minutesSaved=0, addedRevenue=0, and searchStats present; Given minImprovementThreshold>0 and the best candidate’s score improvement is below the threshold, Then only the baseline is returned with the same fields and reason="below-threshold".
Smart Waitlist Fit & Revenue Injection
"As a sitter using PawPilot’s waitlist, I want eligible clients auto-suggested into new openings so that I can turn saved minutes into same-day revenue."
Description

Integrate the Smart Waitlist to fill reclaimed gaps inside the optimized block. Filters waitlisted clients by location proximity, service length, deposit rules, and customer preferences, then simulates insertion impact on the score and projected revenue. Offers one-tap outreach via SMS with deposit link and auto-confirms successful inserts back into the block. Shows incremental revenue and utilization changes attributable to the insertion.

Acceptance Criteria
Eligibility Filtering for Waitlist Fit
Given a reclaimed gap exists inside an optimized block When the user taps "Find Fits" Then only waitlist candidates are returned who meet all of the following: And Travel time to and from adjacent appointments is <= MaxTravelMinutesGapFit (default 12, configurable 5–20) using the block’s selected travel mode And Service duration plus required buffer fits fully within the gap without violating neighbor arrival windows And Deposit policy for the client/service is satisfied (deposit required and client not flagged "no deposit") And Customer and provider preferences are honored (e.g., solo-walk-only, access method, pet compatibility, handler preference) And Candidate availability overlaps the gap’s time window And Candidate has no overlapping appointments in the system during the gap
Insertion Simulation and Score Impact
Given I select a candidate from the filtered list When the system simulates inserting that candidate into the gap Then the simulation completes within 2 seconds for blocks with ≤ 20 appointments And It displays: new block score vs baseline, dead-time reduction (minutes), walk density change, and projected added revenue (currency) And It shows start-time adjustments for any affected appointments (min/max deltas) And Projected added revenue equals candidate price + applicable fees/adjustments − discounts − displaced revenue (should be 0 for gap) to the cent And Results are ranked so the highest-scoring plan is first
One-Tap Outreach with Deposit Link and Hold Window
Given a candidate is selected When the user taps "Offer Slot" Then an SMS is sent to the candidate’s opted-in number containing date, time window, price, required deposit amount, hold-expiry timestamp, and a unique payment link And The payment link encodes the slot ID and expires at the hold-expiry; post-expiry payments are rejected And The system places a hold on the gap for HoldWindowMinutes (default 15, configurable 5–60) preventing other offers/inserts And SMS delivery status is tracked; on failure, one retry is attempted and the failure is surfaced to the user And Deposit amount is computed per rule (percentage or fixed) and rendered in the message And If payment succeeds within the hold window, the candidate auto-confirms; if not, the offer auto-expires and the hold is released
Auto-Insert and Block Update on Confirmation
Given the candidate pays the required deposit within the hold window When payment confirmation is received by the system Then the appointment is inserted into the block at the simulated position without overlap, honoring buffers and travel times And The block schedule is recalculated and affected appointment start times are updated And Any affected clients receive an SMS with their new time if the delta is ≥ 5 minutes And No appointment is shifted by more than BlockMaxShiftMinutes (default 10); if exceeded, the insertion is rolled back and the payment is voided/refunded And Concurrency is handled: if the block changed since simulation, revalidate; on invalidation, rollback and notify the user with reason
Incremental Revenue and Utilization Reporting
Given an insertion has been confirmed When viewing the block summary Then the UI displays incremental revenue attributable to this insertion and the new total block revenue And It displays utilization before vs after (booked minutes / total block minutes) and the percentage delta And It displays minutes of dead time reduced for the block And Displayed values match the simulation within ±1 minute and ±$0.01 And An audit log entry is recorded with timestamp, user, client, insertion method, simulation vs actual metrics, and is exportable
No Eligible Candidates and Fallback Handling
Given a reclaimed gap has no eligible waitlist candidates When "Find Fits" completes Then an empty state is shown with the top three disqualification reason counts (e.g., too far, duration too long, deposit policy, availability) And A "Relax Filters" option allows toggling proximity (+5 minutes), buffer (−5 minutes), and non-safety preferences to preview updated counts without sending outreach And No offers are sent and no holds are created during preview And The block remains unchanged and the attempt is recorded in the audit trail
One-tap Apply, Undo, and Audit Log
"As a solo business owner, I want to apply the best plan in one tap and easily undo it so that I can move fast without risking mistakes."
Description

Provide a one-tap action to apply the highest-scoring plan to the live schedule, updating appointments, buffers, and travel expectations. Display a clear diff of changes (before/after times, sequence, added/removed visits). Include instant undo/redo, conflict detection, and atomic updates across devices. Persist an audit trail with user, timestamp, chosen candidate, and rationale for compliance and support.

Acceptance Criteria
One-tap Apply of Highest-Scoring Plan to Live Schedule
Given a fixed-time block has a computed highest-scoring candidate and no blocking conflicts When the user taps "Apply Plan" Then the system atomically updates all affected visits’ start/end times, buffers, and travel ETAs and returns success within 3 seconds And then the UI displays minutes saved and revenue delta for the block And then the apply button is disabled during processing and re-enabled only after completion or error
Change Diff Display After Apply
Given a plan has been applied to a block When the user opens the changes diff Then for each affected visit the diff shows before/after start and end times, new sequence position, and an added/removed tag if applicable And then aggregate metrics display total travel time change, total buffer time change, minutes saved, and revenue delta And then no unaffected visits appear in the diff And then the diff values match the persisted schedule state (no discrepancies)
Instant Undo/Redo of Applied Plan
Given a plan was applied to a block When the user taps "Undo" Then the system reverts all affected appointments, buffers, and travel ETAs atomically within 2 seconds and confirms success And then a "Redo" action becomes available that reapplies the exact same changes within 2 seconds And then the undo/redo stack preserves order across sessions for at least the last 10 optimization actions on that block
Conflict Detection on Apply Attempt
Given the highest-scoring candidate introduces conflicts (e.g., overlapping visits, double-booked resource, travel infeasible under constraints) When the user taps "Apply Plan" Then the system aborts the apply with no partial changes and presents a list of blocking conflicts including visit identifiers and reasons And then the user is offered actions to view the next-best candidate or refresh candidates And then an event is logged that the apply was blocked by conflicts
Atomic Multi-Device Consistency Under Concurrency
Given another device modifies any visit in the same block after the candidate was computed When the user taps "Apply Plan" on their device Then the system performs a version check, revalidates against the latest state, and either applies successfully or aborts with a stale-state message And then in either outcome there are no partial updates And then upon a successful apply, all signed-in devices reflect the new schedule within 5 seconds
Audit Trail Persistence for Apply/Undo/Redo
Given any apply, undo, or redo completes When the system records the action Then an immutable audit entry is persisted with user ID, timestamp (UTC), action type, block ID, candidate ID, candidate score, minutes saved, revenue delta, and a diff summary hash And then the audit entry includes rationale (optimizer summary and optional user note if provided) And then admins can retrieve audit entries filtered by date range, user, and block ID within 2 seconds per query
Impact Metrics & Explanation UI
"As a walker on the go, I want clear metrics and a short explanation of changes so that I trust the optimizer and can communicate updates to clients if needed."
Description

Surface minutes saved, added revenue, total distance reduced, and visits added/retained for the selected plan. Provide a brief explanation of why the plan scores highest (e.g., swapped A↔B to avoid cross-town walk at rush hour). Visualize the sequence and gaps before/after and flag any trade-offs (tight buffers, customer window shifts within tolerance). Optimized for small-screen readability inside PawPilot’s mobile-first UX.

Acceptance Criteria
Impact Metrics Display for Selected Plan
Given a block is loaded and a plan is selected When the plan detail sheet is opened Then the UI shows metrics: Minutes Saved, Added Revenue, Distance Reduced, Visits Added/Retained And Minutes Saved equals the difference (baseline_total_minutes - plan_total_minutes) rounded to the nearest whole minute; values <= 0 display as 0 And Added Revenue equals (plan_total_revenue - baseline_total_revenue) in account currency with 2 decimals; values <= 0 display as 0.00 And Distance Reduced equals the difference (baseline_total_distance - plan_total_distance) in the user’s unit (mi/km) rounded to 0.1; values <= 0 display as 0.0 And Visits Added/Retained equals (plan_total_visits - baseline_total_visits) + retained_cancellations, shown as an integer; values < 0 display as 0 And each metric provides an info affordance that reveals the baseline and plan values used in the calculation
Highest-Scoring Plan Explanation
Given the highest-scoring plan is selected When the plan detail sheet is opened Then a single-line explanation (<= 160 characters) is displayed directly under the metrics And the explanation includes at least one concrete action (e.g., swap, move, cluster) and one reason (e.g., avoid traffic, shorten travel) And the explanation includes a numeric impact when available (e.g., minutes saved or added revenue) And only customer-friendly names are used; no internal IDs or jargon appear
Before/After Route Visualization
Given a baseline sequence and a selected plan sequence exist When the user views the route visualization Then the user can switch between Before and After states via a clearly labeled toggle And each state lists visits in order with start time, client name/label, and gap/travel duration between visits And gaps are explicitly labeled (e.g., "12 min gap") and visually distinct from visit blocks And the visualization fits a 360px-wide screen without horizontal scrolling
Trade-off Flags: Buffers and Window Shifts
Given the selected plan introduces tight buffers or time-window shifts When the plan detail sheet is opened Then tight buffers below the account buffer threshold are flagged with an icon and the exact buffer minutes And time-window shifts within the account tolerance are flagged with the shift minutes and a note that they remain within tolerance And tapping a flag reveals the impacted visits with exact before/after times And if no trade-offs exist, a "No trade-offs" indicator is displayed and no flags are shown
Mobile Readability and Tap Target Standards
Given the plan detail sheet is displayed on a small screen (≤ 360px width) When the metrics and visualization render Then primary metric numbers are at least 20px font size and labels at least 14px And text and icon contrast meets WCAG AA (≥ 4.5:1) And all interactive controls (toggle, info icons, flags) have a minimum touch target of 44x44px And no horizontal scrolling occurs; overflow text truncates with ellipses and reveals full content on tap
Performance and Loading Feedback
Given a block with up to 20 visits and a selected plan When the user selects the plan Then metrics and explanation render within 700ms at P75 on a mid-tier mobile device And the full before/after visualization renders within 2s at P95, with a skeleton loader or spinner shown after 300ms if still loading And interactions remain responsive with first input delay under 100ms during loading
Edge Cases: Zero Change and Missing Data Handling
Given a selected plan yields no improvements or has incomplete data When the plan detail sheet is opened Then metrics with no improvement display as 0 (e.g., 0 min, 0.00, 0.0 mi/km, 0 visits) And the explanation reads "No changes to route" when there are no swaps or timing tweaks And if distance cannot be computed, Distance Reduced shows "—" with an info tooltip "Distance data unavailable" And the UI renders without errors; available metrics and visualizations still display

Microburst Radar

Hyperlocal weather intelligence that scans the next few hours at street level, tags at‑risk appointments, and auto-splits your map into affected vs. safe zones. You get early, confident signals and zero guessing—so you can act before storms hit and avoid wasted trips.

Requirements

Hyperlocal Nowcast Aggregation
"As a dog walker, I want reliable hyperlocal forecasts for my service area so that I can make proactive scheduling decisions before storms hit."
Description

Ingests and fuses minute-by-minute, street‑level weather data (precipitation intensity, wind gusts, lightning proximity, hail/microburst risk) from multiple providers into a unified 0–3 hour nowcast for all active service areas. Implements geospatial tiling at sub-kilometer resolution and temporal updates every 5 minutes with confidence weighting, provider failover, and quality scoring. Normalizes units and time zones, caches results with short TTLs to balance freshness and cost, and exposes an internal API for downstream consumers (risk tagging, mapping, messaging). Includes rate limiting, retry/backoff, and health checks, with audit logs for data provenance and model versions to support explainability and postmortems.

Acceptance Criteria
Multi-Provider Ingestion & Normalization
Given at least three weather providers are configured and reachable When minute-level feeds for precipitation intensity, wind gust, lightning proximity, and hail/microburst risk are published over a continuous 60-minute test window Then >=99% of valid payloads are ingested and transformed to normalized units (mm/hr, m/s, km) with UTC ISO-8601 timestamps and provider_id recorded Given payloads arrive late or out-of-order When a payload is >2 minutes older than the latest frame for its provider Then it is rejected, logged with reason=stale_out_of_order, and excluded from fusion Given ingestion receives an invalid or schema-incompatible payload When validation runs Then the record is quarantined, an error log including provider_id, source_ts, received_ts, and schema_version is written within 5 seconds, and processing continues for other providers
Sub-Kilometer Tiling & 5-Minute Fusion Cadence
Given active service area polygons are configured When the tiling process executes Then a grid with tile edge length <=500 meters covers 100% of the active polygons with no gaps and with stable tile IDs across deployments Given normalized provider data are available When the fusion scheduler triggers every 5 minutes on hh:mm:00 Then fused nowcasts for 0–180 minutes at 5-minute intervals (37 timestamps including t0) are produced and published within 90 seconds of the trigger time Given continuous operation over 24 hours When measuring served fused dataset timestamps Then the P95 data age at serve time is <=7 minutes and there are no update gaps >10 minutes
Confidence Weighting, Quality Score, and Provider Failover
Given provider reliability weights are configured When the fusion algorithm runs Then each tile/timestep output includes confidence in [0,1] and quality_score in [0,100] derived from provider agreement, recency, and coverage Given one provider returns 5xx or timeouts for >=2 consecutive fetch cycles When the next fusion cycle runs Then that provider is excluded from fusion within 10 minutes, confidence is recomputed from remaining providers, and an alert event provider_failover is emitted with details Given any tile/timestep has quality_score < 40 When API responses are generated Then the record is flagged quality="low" and includes contributing_providers for explainability
Caching, Freshness, and Cost Controls
Given fused results are cached per tile_id and timestamp When a downstream consumer requests the same tile/timestamp Then the cached object with TTL=5 minutes is returned if fresh, and cache metadata includes created_at and ttl_remaining Given a cached object is expired (age >5 minutes) When a request arrives Then the system refreshes the fused data and serves the fresh object with added latency <=2 seconds at P95, and updates the cache Given provider request quotas are configured When operating under typical load for 60 minutes Then upstream requests per provider do not exceed 90% of the configured quota, and backoff is applied when projections indicate a breach Given cache metrics are collected over 24 hours When analyzing read patterns Then cache hit ratio for tile/timestep reads is >=70% without violating the freshness targets above
Internal API for Downstream Consumers
Given a valid internal API key When GET /internal/nowcast?lat={lat}&lon={lon} is called Then HTTP 200 is returned with 37 timestamps (0–180 minutes, 5-minute step), normalized units (mm/hr, m/s, km), UTC times, confidence, and quality_score fields conforming to the published JSON schema Given sustained load of 100 RPS for 5 minutes When querying /internal/nowcast endpoints Then P99 latency <=400 ms and 5xx error rate <=0.5% Given a client exceeds 50 RPS per API key When additional requests are made Then HTTP 429 is returned with a Retry-After header and the event is logged as rate_limit_exceeded Given a malformed request (e.g., invalid lat/lon or polygon) When the API is called Then HTTP 400 is returned with error_code, message, and details fields
Resilience, Health Checks, and Audit Logging
Given upstream 5xx responses or network timeouts occur When fetching provider data Then retries use exponential backoff starting at 1s, factor 2, jitter 0–250ms, up to 4 retries, and each attempt is logged with outcome Given the health endpoint /internal/health is polled every 60 seconds When any provider has no successful fetch in >10 minutes or the fusion job has not published in >10 minutes Then the health status returns degraded with component details and an alert is emitted within 60 seconds Given any fused value is served to a consumer When querying the audit log by request_id Then an entry exists containing request_id, tile_id, timestamp, provider_ids, source checksums, model_version, fusion_version, and processing_duration, retained for >=30 days Given a replay is initiated for a historical time window When re-running fusion using recorded inputs Then the recomputed fused output checksum matches the original within 1% tolerance due to floating-point variance
Appointment Risk Scoring & Tagging
"As a scheduler, I want appointments automatically tagged with weather risk and reasons so that I can quickly see which visits need action."
Description

Calculates a per‑appointment weather risk score by intersecting each appointment’s geocoded location and time window with the nowcast. Applies configurable thresholds to assign statuses (Safe, Monitor, At Risk) and attaches reason codes (e.g., heavy rain > X mm/hr, wind gusts > Y mph, lightning within Z miles, microburst probability > threshold). Updates run continuously as forecasts refresh, with debounced changes to reduce churn. Tags are visible in calendar, list, and job detail views; are filterable/searchable; and are available to automation workflows (messaging, routing, waitlist). Persists historical risk states for reporting and accuracy tracking.

Acceptance Criteria
Compute and Assign Risk Tags Based on Nowcast and Thresholds
Given an appointment with geocoded latitude/longitude and a scheduled time window And organization-level thresholds are configured: Safe < 30, Monitor 30–59, At Risk ≥ 60; heavy_rain ≥ 6 mm/hr; wind_gusts ≥ 35 mph; lightning_within ≤ 8 miles; microburst_prob ≥ 0.35 When the system intersects the appointment’s location/time window with the latest nowcast signals Then it calculates a riskScore in the range 0–100 And assigns a tag of Safe, Monitor, or At Risk based on the configured thresholds And attaches all applicable reason codes for any triggers met within the appointment window And persists the tag, riskScore, and reason codes atomically on the appointment record
Debounced Tag Updates During Forecast Refresh
Given a forecast refresh cadence of ≤ 1 minute And a configurable debounce interval D = 5 minutes When the computed tag for an appointment crosses thresholds multiple times within D Then no more than one outward-visible tag change is applied to that appointment within any D-minute window And the published tag at the end of D reflects the latest computed state as of the last change within D And an audit log entry records each applied tag change with previous→new status, riskScore, reason codes, and timestamp
Tag Visibility in Calendar, List, and Job Detail Views
Given an appointment with a computed risk tag, score, and reason codes When viewing the Calendar view Then the appointment displays a colored status pill (Safe/Monitor/At Risk), the integer riskScore, and the primary reason icon And hovering/tapping reveals the full set of reason codes and the last updated timestamp When viewing the List view Then columns for Status, Score, and Reasons are present and populated for each appointment When viewing the Job Detail view Then a Weather Risk section shows status, score, full reason list with trigger values, and last update timestamp
Filtering and Search by Risk Status and Reason Codes
Given appointments with mixed statuses and reason codes When the user filters Status = "At Risk" and Reason includes "lightning" Then only appointments tagged At Risk with a lightning reason code are returned When the user applies a combined filter Status ∈ {Monitor, At Risk} and Time Range = next 3 hours Then only appointments in that time range with those statuses are returned When the user searches by address substring while filters are active Then results match both the search query and the active risk filters When filters are cleared Then all appointments are shown regardless of risk status or reasons
Automation Workflows Consume Risk Tags
Given automation rules for messaging, routing, and waitlist are enabled When an appointment’s risk.status becomes "At Risk" within the next 3 hours Then the messaging workflow can trigger a weather_alert template using tokens {risk.status, risk.score, risk.reasons, appointment.time} When routing optimization runs with the setting "Exclude affected zones" Then appointments with risk.status ∈ {Monitor, At Risk} are provided as the affected set and Safe as the safe set for route splitting When the waitlist workflow evaluates open slots Then it can include/exclude candidates based on risk.status and reason codes (e.g., include only Safe) And all workflows receive the current {risk.status, risk.score, risk.reasons[]} in their event payloads
Persist Historical Risk States for Reporting and Accuracy Tracking
Given risk tags may change over time before an appointment starts and during its window When a tag or reason code changes for an appointment Then a snapshot is appended with {appointmentId, timestamp, risk.status, risk.score, risk.reasons[]} And while the appointment is active, if no changes occur, a heartbeat snapshot is recorded at least every 15 minutes And historical records are retained for at least 90 days (configurable) And reporting can compute time_in_status per appointment and export CSV for a selected date range
Reason Codes and Threshold Configuration Management
Given an admin opens Weather Risk Settings When they set status thresholds and signal triggers (rain mm/hr, wind gust mph, lightning radius miles, microburst probability) Then inputs are validated (numeric, non-negative, Monitor range does not overlap Safe/At Risk, lightning radius > 0) And changes are versioned and take effect on the next forecast refresh cycle And default thresholds can be restored with one action And the active threshold version is included in risk snapshot records for auditability
Auto Zone Split & Map Overlay
"As a field operator, I want my service map split into affected and safe zones so that I can reroute and staff efficiently without guessing."
Description

Automatically partitions the operating area into affected and safe zones based on forecasted conditions and severity thresholds, minimizing fragmentation while preserving street‑level accuracy. Renders a real‑time map overlay with clear color coding, legends, and timestamps, plus list filters that mirror the map state. Refreshes at 5–10 minute intervals, supports mobile‑first interaction (pinch/zoom, tap to inspect), and offers offline‑tolerant snapshots for field use. Provides accessibility features (high‑contrast palette, ARIA labels) and exports static images/links for sharing with staff.

Acceptance Criteria
Hyperlocal Zone Split with Thresholds
- Given a 100 m forecast grid and severity threshold set to Moderate+, when the zone split runs, then the Affected zone F1-score is >= 0.90 against ground truth and the Safe zone precision is >= 0.95. - And the number of Affected polygons is <= 1.2x the number of connected components in the ground truth (rounded up). - And the boundary median deviation from ground truth is <= 50 m and the 95th percentile deviation is <= 150 m. - And events below the configured threshold are not marked Affected.
Real-Time Overlay Rendering & Legend
- Given live data, when the map loads on a mid-range mobile device, then initial overlay render completes in <= 2.0 s on 4G and <= 1.0 s on Wi-Fi. - And the overlay color palette meets WCAG 2.1 AA contrast ratio >= 4.5:1 against the base map in both light and dark themes. - And the legend displays color-to-status mapping and a Last Updated timestamp in the user's local timezone that matches backend payload time within <= 60 s. - And all interactive controls and zone overlays expose ARIA roles/labels; screen readers announce "Zone: {Affected|Safe}, Severity {level}, Updated {time}" when focused; keyboard navigation can reach legend, layer toggles, and export in a logical order. - And when Reduced Motion is enabled at OS level, overlay animations are disabled.
Auto-Refresh Cadence & Data Freshness
- Given the app is foregrounded with the overlay visible, when time since last successful fetch reaches the refresh window, then a refresh occurs with randomized jitter uniformly distributed in [5,10] minutes. - And a manual Refresh action triggers an immediate fetch and resets the auto-refresh timer. - And if the server responds with unchanged ETag/Last-Modified, the overlay is not re-rendered, Last Checked updates, and additional network payload is <= 5 KB. - And on fetch failure, retries back off exponentially up to 15 minutes, and a non-blocking banner indicates "Using data from {timestamp}".
Mobile Pinch/Zoom & Tap Inspect
- Given a supported mobile device, when the user performs pinch/zoom, then average frame rate is >= 45 FPS and gesture latency is <= 100 ms. - And tapping a zone opens a details sheet showing zone type (Affected/Safe), severity, last updated, and impacted appointment count; the sheet appears in <= 300 ms. - And interactive targets have a minimum touch area of 44x44 px. - And tapping outside the sheet dismisses it without changing the overlay state.
List Filters Mirror Map State
- Given "Mirror Map" is enabled, when the Affected layer is toggled off on the map, then the appointments list excludes all appointments located inside Affected zones, and the list count exactly matches the number of Safe appointments in the current map viewport. - And when the user pans/zooms the map, the appointments list updates to the new viewport within <= 500 ms. - And selecting an appointment in the list highlights its pin on the map within <= 300 ms and auto-centers if off-screen. - And a copied deep link to the current view restores the same layers, filters, and viewport when opened.
Offline Snapshot for Field Use
- Given the overlay was successfully loaded, when the device goes offline, then the last overlay and its metadata are cached and remain viewable for at least 12 hours. - And an "Offline Snapshot" banner is shown with the snapshot timestamp, and no network requests are attempted while offline. - And tapping zones continues to show cached details; returning to the app after <= 30 minutes in background preserves the snapshot. - And upon reconnection, the overlay auto-refreshes within <= 60 s and the offline banner is removed.
Export Static Images and Shareable Links
- Given a current map view, when Export is selected, then a PNG is produced at >= 1080p on the longest side including overlay, legend, and timestamp; file size is <= 1 MB with readable text (x-height >= 10 px). - And a shareable link is generated that preserves viewport, layers, and filters; the token expires in 24 hours and returns HTTP 401 after expiry. - And exported visuals match in-app colors with color difference DeltaE < 2 across web and mobile renderers. - And export completes in <= 3 s on Wi-Fi and <= 6 s on 4G.
Proactive Client Messaging & Rescheduling
"As a business owner, I want at-risk clients to receive clear, timely texts with reschedule options and deposit links so that I reduce no-shows and avoid wasted trips."
Description

Generates and sends targeted SMS messages to clients with at‑risk appointments, including dynamic tokens (client/pet name, time, location, weather reason) and one‑tap links to reschedule and pay deposits. Supports batch sending with throttling, quiet hours, and opt‑out compliance, plus A/B templates to optimize uptake. Integrates with Smart Waitlist to auto‑propose safe‑zone slots and backfill cancellations. Tracks delivery, response, and conversion metrics, and writes outcomes back to the appointment and billing records.

Acceptance Criteria
At‑Risk Appointment Targeting by Zone and Time Window
Given Microburst Radar has tagged appointments as At-Risk within the configured lookahead window And the map is auto-split into affected and safe zones And the user selects a time window and eligible services to notify When the system generates a send list Then only appointments in affected zones within the selected window are included And appointments in safe zones or without At-Risk tags are excluded And the UI shows the recipient count and a preview with client name, pet name(s), local time, short address, and weather reason And the user can deselect or re-include individual recipients before sending And each appointment appears at most once in the send list
Dynamic Token Rendering and Personalized Links
Given a message template contains {client_first_name}, {pet_name}, {appointment_time_local}, {appointment_address_short}, {weather_reason}, {reschedule_link}, {deposit_link} And an at-risk appointment has corresponding data When a preview is generated and messages are sent Then all tokens are replaced with accurate, localized values from the appointment And {pet_name} resolves to the pet(s) on the appointment And links are unique per appointment and deep-link the client without requiring login And if any field is missing, defined fallbacks are applied without exposing raw tokens And no message fails due to token substitution or invalid link generation
Batch Sending with Throttling, Quiet Hours, and Opt-Out Compliance
Given a selected recipient list for proactive messaging And quiet hours are configured in the business’s local timezone And a send rate limit is configured When the user initiates Send Then messages that fall within quiet hours are queued and automatically sent when quiet hours end And the system enforces the configured throttle rate without dropping messages And STOP/UNSUBSCRIBE replies immediately opt the client out and prevent future sends, with audit logged And HELP replies return the configured help text and support contact And START/UNSTOP re-subscribes previously opted-out clients with audit logged And all compliance events are recorded per recipient
A/B Template Experimentation and Performance Reporting
Given two active SMS templates (Variant A and Variant B) for the same campaign When A/B testing is enabled for a batch Then recipients are allocated evenly between variants (±5%) And the system tracks per-variant metrics: deliveries, replies, link clicks, reschedule completions, deposits paid And the dashboard displays variant performance, rates, and relative lift And the user can select a winning variant for subsequent sends
One‑Tap Rescheduling with Integrated Deposit Collection
Given a client receives a proactive SMS with a reschedule link and, if required, a deposit link And Smart Waitlist has safe‑zone slots that match service, duration, and location constraints When the client taps the link Then a pre‑authenticated page loads with proposed safe‑zone times and an option to view more And upon selecting a slot, any required deposit can be paid within the same flow And on successful payment (if required), the booking is confirmed, a confirmation SMS is sent, and the original appointment status is updated And if the client abandons or payment fails, no changes are applied and the failure is logged
Automatic Smart Waitlist Backfill After Cancellation
Given an at‑risk appointment is cancelled by the business or client via the provided flows When the cancellation is confirmed Then Smart Waitlist automatically offers the vacated slot to matching waitlist clients And upon acceptance, a new booking is created and the slot is marked filled And the original appointment is updated with outcome = Cancelled (Weather) and source = Proactive Messaging And all related actions are logged on the appointment timeline
Event Tracking, Conversion Metrics, and Record Writeback
Given a proactive messaging batch is sent When delivery, reply, click, reschedule, and payment events occur Then each event is captured and associated to the correct appointment, client, and billing records And appointment and billing records reflect final outcomes with timestamps and source (e.g., Rescheduled, Cancelled, Deposit Paid) And batch and per-recipient metrics are visible in reporting and exportable And reported totals reconcile with the sum of per-recipient outcomes
Lead Time Alerts & Confidence Controls
"As an owner, I want adjustable lead times and confidence thresholds so that I get early, actionable alerts without false alarms."
Description

Provides configurable lead times and confidence thresholds that govern when heads‑up versus action‑required alerts are generated for staff. Surfaces alerts via in‑app notifications, SMS, and email with clear severity indicators, ETAs, and rationale. Includes suppression rules and digesting to reduce alert fatigue, plus per‑service and per‑region overrides. Exposes historical accuracy metrics and allows tuning of thresholds to balance early warnings against false positives.

Acceptance Criteria
Heads-Up Alert at Configured Lead Time
Given an appointment scheduled within the next L_heads minutes and the forecast hazard probability P for its geo-path is >= T_heads When current time reaches the configured heads-up lead time L_heads before the appointment start Then generate one Heads-Up alert for that appointment and deliver it in-app within 60 seconds and via SMS/email (if enabled) within 2 minutes And the alert content includes severity "Heads-Up", an ETA impact window (start and end), hazard type, and confidence as a percentage And the alert is sent at or after L_heads and no later than L_heads + 5 minutes after the threshold condition is met And the appointment is auto-tagged "At Risk (Heads-Up)"
Action-Required Alert at Confidence Threshold
Given an appointment scheduled within the next L_action minutes and the forecast hazard probability P for its geo-path is >= T_action When this condition is detected Then generate one Action Required alert and deliver it in-app within 60 seconds and via SMS/email (if enabled) within 2 minutes And the alert includes severity "Action Required", a clear recommended action, a deep link to view affected vs safe zones, hazard type, ETA, and confidence percentage And if both Heads-Up and Action conditions are simultaneously true, only the Action Required alert is sent; Heads-Up is suppressed And the appointment is auto-tagged "At Risk (Action)"
Alert Suppression and Digesting
Given suppression window W minutes and digest window D minutes are configured When multiple forecast updates for the same appointment and hazard occur within W minutes without a severity change and with ETA shift < 15 minutes Then do not send a new standalone alert; update the existing in-app alert and include the change in the next digest And per channel (in-app, SMS, email) send at most one digest per D-minute window per staff member summarizing count, earliest ETA, and affected appointments And if severity escalates (Heads-Up -> Action) or ETA shift >= 15 minutes, send a new alert immediately and reset suppression for that appointment And all suppressed and digested events are recorded in delivery logs with correlation to the original alert
Per-Service and Per-Region Overrides Precedence
Given global defaults and configured overrides per service and per region When evaluating thresholds and lead times for an appointment Then apply rule precedence: service override first, else region override, else global default And the applied rule source (service/region/global) is displayed in the alert rationale and stored in the alert record And validation enforces: 0 <= T_heads < T_action <= 100; 5 <= L_action <= 120 minutes; 15 <= L_heads <= 240 minutes And updates to overrides take effect within 5 minutes for new alerts and are audit logged with user, timestamp, and before/after values
Historical Accuracy Metrics and Threshold Tuning
Given the last 30 days of alert outcomes are stored When a user opens the accuracy dashboard Then display per-severity metrics: precision, false positive rate, false negative rate, alert count, and average lead time, with last-updated timestamp And when the user adjusts T_heads, T_action, L_heads, or L_action in a tuning UI Then present a backtest preview using the last 30 days showing projected alert volume change and precision change before saving And upon save, persist the new thresholds, audit log the change, and apply them to new alerts within 5 minutes
Multi-Channel Delivery and Content Standards
Given staff channel preferences are configured When an alert is generated Then always send an in-app notification; send SMS and email if enabled for the staff member And ensure content parity across channels including: [Severity], [ETA window], [Hazard type], [Confidence%], [Rationale], [Manage Preferences link], and a unique [Alert ID] And constrain SMS body to <= 320 characters with a shortened URL; email subject format is "Microburst Radar: {Severity} for {Appointment/Zone} ETA {Time}" And achieve delivery success >= 98% within 2 minutes for SMS and email under normal conditions; capture bounces/failures, retry up to 2 times, and surface delivery status in logs And include a deduplication key per alert to prevent duplicate deliveries across channels
Weather‑Adaptive Routing & Slot Rebalancing
"As a dispatcher, I want routes and time slots to auto-adjust around storms so that I minimize travel risk and keep utilization high."
Description

Optimizes routes and appointment sequences to avoid forecasted storm windows while honoring travel times, service durations, client availability, and deposit policies. Proposes alternative times and safe‑zone swaps, highlights conflicts, and lets users accept/apply with one action. Syncs changes to the calendar, updates client messaging, and supports export to navigation apps. Includes manual override and rollback, with an audit trail of changes and measured impact on on‑time arrival and utilization.

Acceptance Criteria
Storm-Window Avoidance Re-Routing
Given a routed schedule of up to 25 appointments with travel times and service durations And street-level storm windows and intensities are available from Microburst Radar When the user runs Rebalance Then the produced route contains no travel or service overlapping any storm window plus a configurable buffer (default 15 minutes) And total travel time respects live traffic estimates within ±10% And all appointments remain within each client’s availability window And service durations are unchanged unless the “allow flex duration” option is enabled And deposit-required bookings are not moved in ways that violate deposit policies And a solution (or explicit unsolvable notice) is returned within 30 seconds
Safe-Zone Swap Suggestions and Conflict Highlighting
Given appointments tagged At-Risk and clusters designated as Safe-Zones When the system generates swap/alternative-time suggestions Then each suggestion keeps drive time per leg within the route SLA (default ≤25 minutes) and avoids storm windows And suggestions maintain same-day service unless user enables “cross-day moves” And each suggestion includes a confidence score and rationale (e.g., avoids 18:20–19:05 storm on Elm St) And conflicts are explicitly flagged for client availability, deposit forfeiture risk, travel-time violation, or overlapping storm And at least the top 3 suggestions are shown when available, capped at 10 And suggestions never exceed daily capacity or staff limits
One-Click Accept & Apply With Constraint Validation
Given the user has selected one or more proposals When the user clicks Accept & Apply Then the system revalidates against latest weather, calendar changes, travel times, and policies And applies all changes atomically; if any item fails, no changes are committed And the user sees a clear error list for any failures with actionable reasons And deposits/holds are preserved or re-collected per policy with links included where required And the plan version increments with timestamp and author, and a summary diff (moved/swapped/deferred) is displayed
Calendar Sync and Client Messaging Updates
Given changes have been applied to the schedule When the system syncs updates Then calendar entries update times, assigned staff, route order, and statuses consistently across web and mobile And only affected clients receive SMS with the correct template (reschedule/ETA update/cancel) in their local time And opt-outs, quiet hours, and message rate limits are honored And deposit links are included when required and prevent duplicate charges And message delivery status (queued/sent/failed) is logged per client And no duplicate notifications are sent for the same change event
Navigation Export With Accurate Timing
Given an updated route is active When the user exports to a navigation app (Google Maps, Apple Maps, or Waze) Then the app opens with the correct stop sequence and precise addresses And geocoding succeeds for ≥99% of stops; any unresolved addresses are flagged before export And stop labels include service duration notes where supported And the first-leg ETA in the nav app is within ±3 minutes of the internal estimate And multi-stop waypoints are preserved on both iOS and Android
Manual Override and Rollback With Audit Trail
Given the user manually adjusts appointment times or sequence When the user saves overrides Then constraints are rechecked in real time and any violations are highlighted And the user may force-save only where policy permits and must acknowledge violations explicitly And the user can rollback to the previous baseline plan in one action within 24 hours And an audit log records who, what, when, why, and before/after values for each change And audit entries are immutable and exportable as CSV
Impact Measurement: On-Time Arrival and Utilization
Given a day where rebalancing was applied When the service day completes Then the system calculates on-time arrival rate and utilization versus the pre-rebalance baseline And displays absolute and percentage deltas with linkage to the applied plan version And stores the metrics for reporting and trend analysis And provides an exportable report for the day/week And shows a provisional impact estimate immediately after apply using forecasted ETAs

Safe-Window Picker

For any flagged booking, PawPilot computes the three safest reschedule windows that fit your buffers, travel time, building access, and client preferences. Clients get a one-tap SMS choice with live timers, turning weather chaos into quick, confirmed rebooks without back-and-forth.

Requirements

Safe Window Computation Engine
"As an independent provider, I want PawPilot to automatically surface the three safest reschedule times that fit my constraints so that I can quickly rebook without manual coordination."
Description

Implements a scoring engine that evaluates and ranks candidate reschedule windows, returning the top three options that satisfy provider buffers, travel time, building access windows, service durations, daylight/safety constraints, and client preference windows. The engine ingests real-time and historical signals (e.g., weather severity, traffic ETAs, pet/venue notes, prior no-show patterns) and assigns a risk score to each candidate. Outputs include window start/end, confidence/risk, and justification text for client messaging. Integrates with PawPilot’s scheduling core, waitlist, and messaging services, exposing deterministic, replayable results for consistency and debuggability.

Acceptance Criteria
Top Three Safe Windows Ranked by Risk
Given a flagged booking and full scheduling context When the engine evaluates candidate reschedule windows Then it returns up to three windows sorted by ascending risk_score And each returned window satisfies all hard constraints (provider buffers, travel time, building access, daylight, service duration, and existing bookings) And ties in risk_score are broken by earlier start_time, then by ascending window_id deterministically And if no candidate meets constraints, response.windows = [] and response.fallback_reason is present
Deterministic, Replayable Results
Given identical inputs (booking_id, constraints, signal snapshots, engine_version) When compute is invoked multiple times or replay is requested with the same request_id Then the same windows are returned with identical ordering, window_id, risk_score, and justification_text And the response includes request_id, engine_version, and determinism_seed And GET /safe-windows/{request_id} returns an identical payload to the original compute
Constraint Compliance (Buffers, Travel, Access, Duration, Daylight)
For any returned window w: w.start >= prev_end + provider_buffer + travel_eta_to_w And next_start >= w.end + provider_buffer + travel_eta_to_next And [w.start, w.end) lies entirely within allowed building access windows And service_duration fits wholly inside w after applying buffers and access limits And if provider.daylight_only = true, both w.start and w.end occur between local civil dawn and civil dusk And no returned window overlaps existing confirmed or held bookings And all time calculations use the service location's timezone
Signal Ingestion and Risk Scoring Monotonicity
The engine ingests weather severity, traffic ETA reliability, pet/venue notes, and client no-show patterns with timestamps at compute_time And signals older than 10 minutes are marked stale and reduce confidence_level by one tier And risk_score is a float in [0,100] where higher = higher risk And worsening any single signal while others are constant must not decrease risk_score And any hard-blocker signal (e.g., access closed, severe_weather_level >= 80, client_do_not_schedule = true) excludes the candidate And confidence_level maps: 0–25 = high, >25–60 = medium, >60–100 = low And the top-ranked window has the lowest risk_score among all eligible candidates
Output Payload Completeness for Messaging
Each returned window includes: window_id (ULID), start and end (RFC3339 with timezone), risk_score (two decimals), confidence_level (high|medium|low), rank (1..3), justification_text (<= 240 chars), and reason_codes (non-empty list) And justification_text mentions at least one dynamic factor influencing the score (e.g., weather, travel, access, preference) And identifiers (window_id, request_id) are stable across replay for the same inputs And the response includes booking_id, compute_timestamp (RFC3339), and expires_at/hold_expires_at when holds are placed
Performance and Throughput SLOs
For candidate sets up to 200 windows, end-to-end compute latency <= 1500 ms at p95 and <= 3000 ms at p99 under production-like load And sustained 50 requests/second for 60 seconds yields error_rate <= 0.1% and timeout_rate <= 0.1% And if any external signal provider exceeds 400 ms timeout, the engine returns results using last-known-good data and downgrades confidence_level by one tier without failing the request
Integration Contracts with Scheduling Core, Waitlist, and Messaging
POST /safe-windows (compute) and GET /safe-windows/{request_id} (replay) conform to the published JSON schema; changes are backward-compatible (additive) And for each returned window, a soft hold is created via Scheduling Core with TTL = 10 minutes; response includes hold_id and hold_expires_at; holds auto-release on TTL expiry And when a window aligns with an active Smart Waitlist opportunity, response includes waitlist_opportunity_id; otherwise null And a safe_windows.computed event is emitted with booking_id, request_id, and candidates (or empty) without PII beyond schema And all integration calls are idempotent when retried with the same request_id
Real-time Constraints Sync
"As a provider, I want suggested windows to reflect live traffic, weather, and building access changes so that clients only choose times I can actually fulfill."
Description

Continuously synchronizes availability, travel time, access constraints, and client preferences to keep suggested windows valid. Pulls live calendar state, service buffers, blackout periods, and per-client preferences from PawPilot; fetches ETAs from mapping APIs and gate/concierge access windows; re-evaluates options when any input changes. Provides atomic locking when a client selects a window to prevent race conditions. Implements graceful degradation and caching to keep response time under 2 seconds, with fallback heuristics if third-party APIs fail.

Acceptance Criteria
Real-time Calendar/Buffer/Blackout Sync Maintains Valid Suggestions
Given a flagged booking and the current calendar state, service buffers, and blackout periods are available When any of these inputs change (new/updated booking, buffer change, blackout change) Then the system re-evaluates the top 3 safe windows and updates the suggestion set within 1 second of change detection And no suggested window overlaps a blackout period And all suggested windows respect pre- and post-service buffers And suggestions exclude times already occupied by other bookings at the time of evaluation
Live ETA Integration with Sub-2s Fallback
Given mapping APIs respond within SLA When computing suggested windows Then travel time between adjacent jobs uses live ETA values for the selected service type and mode And the end-to-end response time to compute and return suggestions is <= 2,000 ms at p95 And if mapping APIs time out or error, cached ETAs not older than 30 minutes are used with a conservative 20% padding, still meeting the <= 2,000 ms response target And fallback usage is flagged in logs and internal telemetry for the request
Access Windows and Client Preferences Are Enforced
Given gate/concierge access windows and per-client preferences are retrievable When generating suggestions Then every suggested window lies within the intersection of access windows and the client's allowed days/times And suggestions violating either constraint are not returned And if zero windows remain, a no-availability state is returned with a reason code "CONSTRAINTS_CONFLICT"
Client-Facing Suggestions Update on Constraint Change
Given an SMS with a one-tap choice has been sent When an upstream constraint change invalidates one or more suggested windows before the client selects Then tapping an invalidated window returns a "no longer available" message within 500 ms and presents the newly computed top 3 windows And the timers shown reflect the recomputed holds based on the latest constraints And no stale window can be booked successfully
Atomic Lock on Selection Prevents Double-Booking
Given two clients attempt to claim the same underlying time/window concurrently When the first valid selection request is processed Then an atomic lock with compare-and-set semantics is acquired and held for the booking, succeeding for exactly one requester And concurrent requests receive a "window taken" response and the latest top 3 alternatives And lock TTL is 60 seconds, with idempotent retry using a requestId preventing duplicate bookings And orphaned locks are automatically released on TTL expiry
Graceful Degradation and Caching During Third-Party Failures
Given third-party APIs (mapping or access) are degraded or unreachable When computing suggestions Then cached constraint data not older than 15 minutes is used where applicable; otherwise conservative heuristics are applied (e.g., max travel time for zone) And the system still returns a response within 2,000 ms at p95 And the returned suggestions are labeled internally as "estimated" with a degradation cause And a retry/backfill job refreshes stale caches within 5 minutes
One-tap SMS Selection with Timed Holds
"As a client, I want to pick a new time from an SMS in one tap with a brief hold timer so that I can secure a slot quickly without back-and-forth."
Description

Generates an SMS with three selectable options, each backed by a unique, expiring deep link and visible countdown timer. Selecting an option temporarily holds the slot, confirms booking on final tap, and auto-releases on expiry or cancellation. Supports numeric reply fallback (1/2/3) for devices without link support, plus confirmation and error handling flows. Prevents double-booking via transactional holds in the scheduler. Sends success/expiry notifications, updates the calendar, and triggers downstream reminders and route updates.

Acceptance Criteria
SMS Options With Expiring Deep Links
Given a flagged booking with three computed safe windows When the system composes the outbound message Then the client receives a single SMS listing exactly 3 options labeled 1, 2, and 3 with human-readable date/time and duration And each option includes a unique deep link containing a single-use expiring token And opening any deep link shows a landing screen with a live countdown timer reflecting remaining validity And the token expires at the configured selection expiry time and is rejected after expiry And the SMS body includes a numeric reply hint such as "Reply 1/2/3" as fallback
Temporary Hold on First Selection
Given an option is still available When the client first taps its deep link or replies with the corresponding number Then the scheduler creates a transactional hold on that slot and resource within 1 second And the hold duration equals the configured hold window and stores client identifier, slot id, and expiry timestamp And the client is shown (or sent) a confirmation prompt displaying slot details and a countdown matching the hold expiry (±1 second) And competing requests see the slot as unavailable until the hold is confirmed or released And if the slot is already held or booked, the client is notified it is unavailable and receives refreshed alternatives
Final Confirmation via Deep Link
Given an active hold created via deep link When the client taps Confirm before the countdown reaches zero Then the booking is confirmed atomically and the hold is cleared And the deep link becomes a non-actionable status page showing Confirmed details And a success notification is sent to the client within 5 seconds And repeated taps or refreshes are idempotent and do not create duplicate bookings And if confirmation is attempted after expiry, an Expired state is shown and new options are sent via SMS
Numeric Fallback Reply (1/2/3) Confirmation
Given a client replies to the SMS with a single digit 1, 2, or 3 When the referenced option is available Then a hold is created as per hold rules and the client receives an SMS prompt to confirm: "Reply Y to confirm or C to cancel" with remaining time indicated And if the client replies Y before expiry, the booking is confirmed and a success SMS is sent And if the client replies C or the hold expires, the hold is released and an appropriate SMS is sent with refreshed options And invalid inputs (e.g., 0, 4, text) receive a single error help SMS without creating a hold, preserving rate limits
Auto-Release on Expiry or Cancellation
Given an active hold exists When the countdown reaches zero or the client cancels Then the hold is released within 1 second and the slot returns to availability And any subsequent confirm attempts using the link or SMS are rejected with an Expired/Canceled message And an expiry/cancellation notification is sent to the client And an audit log entry records reason and timestamps for hold release
Double-Booking Prevention and Concurrency
Given two or more clients attempt to select the same slot concurrently When their selections arrive within overlapping intervals Then exactly one hold is granted and others receive immediate Unavailable responses with alternative options And no two holds exist simultaneously for the same slot and resource And if two confirmations race, only one booking is confirmed; the others receive Unavailable with refreshed options And all operations use idempotency to ensure retries do not create duplicate holds or bookings
Calendar, Notifications, and Downstream Triggers
Given a booking is confirmed via link or numeric confirmation When confirmation completes Then the calendar is updated within 2 seconds to reflect the new appointment state And success notifications are sent and downstream reminders and route updates are scheduled/updated And if any downstream task fails, the booking remains confirmed and tasks are retried per policy with a surfaced non-blocking alert And all updates are traceable via logs including correlation to the originating SMS/selection
Configurable Safety Rules and Overrides
"As a business owner, I want to configure safety rules and override suggestions so that Safe-Window Picker reflects how my operation runs."
Description

Provides a settings surface for owners to define safety buffers (pre/post), travel padding, daylight-only constraints, building access windows, default hold durations, and client preference weighting. Supports per-service and per-client overrides (e.g., senior dogs need longer buffers), and manual override of suggested windows by staff. Includes rule versioning and validation, with immediate propagation to the computation engine and audit logs of changes.

Acceptance Criteria
Configure global and per-service safety buffers with validation
Given I am an Owner on Settings > Safety Rules When I set global pre-buffer to 30 minutes, post-buffer to 20 minutes, and travel padding to 10 minutes Then the Save action is enabled and the values persist to the rules API and UI on reload When I enter non-numeric or out-of-range values (e.g., -5, 9999) Then inline validation prevents save and shows error messages specifying allowed range (0–240 minutes) When I create a Grooming service override with pre-buffer 45 minutes and save Then the Grooming service shows an Override tag and its effective buffers equal 45/20/10 while other services inherit global values
Apply daylight-only and building access constraints to suggested windows
Given Daylight-only is enabled and the service location is 94107 on 2025-09-01 When safe windows are computed Then each window's start and end fall between local sunrise and sunset for that date Given the client has building access windows of 09:00–11:00 and 14:00–18:00 local time When safe windows are computed for the same date Then the top three windows, if any, fall within the intersection of daylight and access windows and respect configured buffers and travel padding When no windows satisfy the constraints Then the system returns zero windows with reason code NO_SAFE_WINDOW and displays a staff-facing explanation
Enforce default and per-service hold durations on offers
Given a global default hold duration of 20 minutes and a Grooming service override of 45 minutes When three windows are sent for a Grooming booking Then each hold expires exactly 45 minutes after offer creation and the countdown shown on the offer link matches server time within ±2 seconds When a Walking booking offer is sent with no per-service override Then the hold expires exactly 20 minutes after offer creation When a hold expires Then the slot is released to the Smart Waitlist, the client link shows Expired, and an audit entry records expiration with window ID and rule version
Weight client preferences in window ranking
Given preference weighting is set to 0 (disabled) When windows are computed for a client who prefers mornings Then preference matches do not affect ranking; windows are ordered by safety score only Given preference weighting is set to 1.0 (max) When two candidate windows are otherwise equal but only one matches the client's preferred time/day Then the matching window ranks above the non-matching window When weights are changed from 0.2 to 0.8 Then a recompute changes the order in favor of preference-matching windows, and the decision rationale includes a weight component in the debug payload
Resolve effective values with client > service > global precedence
Given global pre-buffer=20, service(Grooming) pre-buffer=30, and client(Spot) pre-buffer=45 When computing windows for Spot's Grooming booking Then the effective pre-buffer is 45 minutes When the client override is removed Then the effective pre-buffer becomes 30 minutes (service override) When both client and service overrides are absent Then the effective pre-buffer is 20 minutes (global default) And the API response for effective rules includes source=client/service/global for each value
Allow staff to manually override suggested windows with justification
Given a staff user with OverrideSafeWindow permission views a flagged booking with no safe windows When the user selects a custom start time outside constraints and provides a required reason of at least 10 characters Then the system confirms the booking, annotates it as Manual Override, and records user, timestamp, reason, and referenced rule version in the audit log Given a staff user without OverrideSafeWindow permission When they attempt the same action Then the attempt is blocked and a permission error is shown; no booking change or audit entry is created When a manual override conflicts with building access or daylight Then the client SMS copy includes a safety disclaimer tag and internal dashboard shows a warning badge on the booking
Version rules, propagate immediately, and audit all changes
Given current rule version is 12 When the owner updates the global travel padding and saves successfully Then a new rule version 13 is created with a diff of changed fields and the change is recorded with who, when, scope, and before/after values When a safe-window computation is triggered after the save Then the computation engine uses version 13 within 5 seconds of save, and any requests during the window return a consistent version When a save fails validation Then no new version is created, the effective version remains 12, and the user sees inline errors When querying the audit log by date range and scope Then entries can be filtered by user, scope (global/service/client), and version, and results include detailed before/after values
Payment and Deposit Carryover
"As a provider, I want deposits to carry over or be collected automatically when rescheduling so that my revenue and policies are enforced without manual work."
Description

Ensures reschedules maintain revenue integrity by carrying over existing deposits, collecting new deposits when policy requires, and adjusting invoices and due dates. Applies cancellation/waiver rules based on move timing and risk reason, supports partial refunds or credits, and embeds payment links in the SMS flow when needed. Syncs with PawPilot’s billing and external processors, and updates payment status on booking confirmation.

Acceptance Criteria
Carry Over Existing Deposit on Like-for-Like Reschedule
Given a flagged booking with an existing paid deposit D and a reschedule to an equal-priced service and risk-waived window When the client confirms a Safe-Window option that does not increase required deposit per policy Then the original deposit D is applied to the new booking without additional collection And the invoice for the new booking reflects D as a payment on account and shows $0 additional deposit due And the payment status remains "Deposit Paid" And the SMS confirmation omits any payment link and states "deposit carried over" And an audit log captures carryover with previous/new booking IDs, user/channel (SMS), and timestamp And all updates complete within 3 seconds of client confirmation
Collect Incremental Deposit When Policy Requires
Given a flagged booking with existing deposit D and policy-required deposit P for the selected Safe-Window When P > D and the client taps a Safe-Window option Then the system generates a secure payment link for (P − D), embeds it in the SMS, and starts a 15-minute hold timer on the slot And the original booking remains active until payment succeeds And on successful payment within the timer, the reschedule confirms, payment status becomes "Deposit Paid", and the hold converts to a booking And on timer expiry without payment, the hold is released, the waitlist is pinged, and the client receives an SMS that the offer expired with a new link to retry And duplicate taps or webhook retries are idempotent and do not create duplicate charges And events are synced to billing and external processor within 10 seconds of payment
Apply Cancellation/Waiver Rules by Timing and Risk Reason
Given a reschedule initiated at time T relative to the current appointment and a captured risk reason (e.g., weather, building access, client-initiated) When the rules engine evaluates policy matrices Then inside-window moves (e.g., <24h) apply the defined fee unless the risk reason qualifies for waiver (weather/operator safety) And outside-window moves apply no fee and carry over deposit by default And if a fee applies, the deposit is converted to fee up to the configured cap; any remainder is carried or collected as incremental deposit And the applied rule ID, inputs (T, risk reason), and computed outcomes (fee, carryover, incremental) are persisted to the booking ledger And evaluation completes within 500 ms and the SMS copy reflects the fee/waiver outcome before sending
Handle Partial Refunds or Credits After Reschedule
Given the reschedule results in an overpayment or policy grants a refund amount R When the reschedule is confirmed Then if the processor supports immediate refund, initiate refund of R to the original payment method within 1 minute and mark invoice "Refund Initiated" And if refund cannot be issued (window closed or failure), create an account credit of R applied to the client profile and update invoice accordingly And send an SMS receipt with refund/credit details and reference IDs And track refund state transitions (Initiated → Succeeded/Failed) via webhooks; on failure, retry with exponential backoff for up to 24 hours and alert ops after 3 failures And ledger entries remain balanced with clear links between original charge, refund, and booking IDs
Adjust Invoice and Due Dates on Reschedule
Given a booking is rescheduled to datetime T_new When the reschedule confirms Then the invoice issue date updates to the confirmation timestamp, and the due date updates to T_new at 23:59 local unless policy requires same-day payment And existing reminder schedules realign to the new due date and appointment time And any late fees on the original schedule are removed or recomputed per policy and logged And the invoice number remains stable; a revision is created with versioning and diff of financial fields And the client portal and SMS summary reflect the new due date within 5 seconds
Embed Payment Links and Live Timers in SMS Flow
Given a reschedule option requires payment (new or incremental deposit or balance) When composing the Safe-Window SMS Then include a single-tap secure payment URL per option with prefilled amount, Apple Pay/Google Pay enabled, and PCI-safe tokenization And display a live mm:ss timer in the SMS/landing page; upon expiry, the link returns an "Offer expired" state with a CTA to request a new window And payment link deep-links to the client’s stored card if available; otherwise supports guest checkout And click, view, and payment events are tracked and associated with the booking And accessibility meets WCAG AA for the landing page
Sync and Reconcile with External Processors and Billing
Given payments may complete via external processors (e.g., Stripe, Square) When a payment succeeds or fails Then verify webhooks (signature + idempotency key) and update booking payment status within 10 seconds And if a webhook is not received within 60 seconds but the client completed checkout, poll the processor and reconcile And store external charge/refund IDs and expose them in the booking ledger and audit trail And nightly reconciliation cross-checks settlements; discrepancies open an ops task with details And booking confirmation is sent only after status is "Deposit Paid" or "Paid in Full" and invoice reflects the transaction
Flag Triggering and Auto-Reschedule Flow
"As a provider, I want risky bookings to be auto-flagged and start the Safe-Window process so that I can rebook proactively during disruptions."
Description

Defines and detects conditions that flag a booking (e.g., severe weather alerts by service area, provider illness, unsafe daylight windows, road closures), then initiates the Safe-Window flow. Supports configurable triggers, throttling to avoid spam, and timeboxing (e.g., only for services within the next 72 hours). Handles timezone-aware scheduling, consolidates multiple triggers, and respects do-not-disturb windows for clients.

Acceptance Criteria
Severe Weather Auto-Reschedule Within 72 Hours
Given a booking in a defined service area starts within the next 72 hours and the SevereWeather trigger is enabled and a severe weather alert overlaps the booking’s scheduled time When the system ingests the alert Then the booking is flagged with reason "SevereWeather" and the Safe-Window flow starts and three reschedule windows are computed honoring buffers, travel time, building access, and client preferences and an SMS with one-tap options and live timers is sent to the client and the action is audit logged
Trigger Configuration Controls
Given tenant A, service area SA has SevereWeather enabled and RoadClosure disabled and a booking within the next 72 hours exists When both a severe weather alert and a road closure alert are received for the booking Then only one flag is created from SevereWeather and no flag is created from RoadClosure and only one Safe-Window SMS is sent and the audit log records that RoadClosure was disabled Given the RoadClosure trigger is enabled via settings without restart When the next road closure alert is received for an in-scope booking Then the road closure trigger is processed and the Safe-Window flow initiates and the event is audit logged
Multiple Triggers Consolidated Into Single Event
Given the consolidation window is configured to 10 minutes for a booking within the next 72 hours When a severe weather alert is received and within 10 minutes an unsafe daylight trigger and a road closure alert are also received Then a single consolidated flag event is stored with reasons [SevereWeather, UnsafeDaylight, RoadClosure] and only one Safe-Window SMS is sent and the SMS reflects the latest recomputed windows and duplicate alerts within the window are ignored and logged
Client Do-Not-Disturb Deferral
Given the client’s Do-Not-Disturb window is 21:00–08:00 in the client’s local timezone and a booking within the next 72 hours is flagged at 22:15 local time When the system prepares the Safe-Window SMS Then the SMS is not sent during DND and is queued to send at 08:00 client local time and the countdown timers start from actual send time and the deferment is audit logged
Throttling of Notifications Per Booking
Given the Safe-Window notification throttle window per booking is configured to 30 minutes When multiple eligible trigger updates occur for the same booking within 30 minutes Then at most one Safe-Window SMS is sent in that 30-minute window and subsequent attempts are suppressed and logged and after the throttle window expires a new SMS may be sent upon the next eligible trigger
Timezone-Aware Timeboxing and Messaging
Given the service area timezone is America/Chicago and the client timezone is America/New_York and the booking starts 36 hours from now in the service area timezone When the booking is flagged by any enabled trigger Then the 72-hour timebox is evaluated in the service area timezone and Safe-Window candidates use service area local constraints for daylight and building access and the SMS shows options and countdown timers in the client’s local timezone and all stored timestamps include timezone offsets
Provider Illness Manual Flag Initiation
Given a provider marks themselves ill and selects bookings occurring within the next 72 hours When the provider confirms "Flag and Auto-Reschedule" Then each selected booking is flagged with reason "ProviderIllness" and the Safe-Window flow is initiated per booking and Do-Not-Disturb and throttling rules are respected and one SMS per booking is queued or sent accordingly and an audit entry lists all impacted bookings
Delivery Receipts, Audit Trail, and Metrics
"As an owner, I want a complete, exportable record of messages, choices, and outcomes so that I can audit issues and measure the feature’s impact."
Description

Captures end-to-end telemetry: options generated, scores, SMS content and delivery receipts, client selections, hold/confirm timestamps, payment outcomes, and final schedule updates. Exposes an audit log per booking and aggregate dashboards (e.g., rebook rate, time-to-rebook, no-show reduction, deposit recovery). Supports export for disputes and compliance, with configurable data retention and privacy controls.

Acceptance Criteria
Telemetry Capture for Flagged Reschedule Flow
Given a booking is flagged and Safe-Window Picker generates options When options are computed and an SMS is sent to the client Then a telemetry record is stored with fields: booking_id, run_id, option_ids (3), option_scores, computation_ts (UTC ISO8601), sms_message_ids, sms_body_hash, sms_language, client_timezone, buffers_applied, travel_time_estimate And Given the client interacts via SMS or link When a selection, hold, confirm, decline, or timeout occurs Then events are logged with event_type ∈ {option_viewed, option_selected, hold_started, hold_expired, confirmed, declined, timeout}, event_ts (UTC), actor, correlation_id And Given a deposit is requested When payment is authorized, captured, failed, or canceled Then payment_outcome with provider_txn_id, amount, currency, and ts is recorded And When the booking calendar is updated Then final_schedule_update with start_ts, end_ts, staff_id, and source=auto_reschedule is recorded And p95 write latency ≤ 3s with ≥ 99.9% successful writes over 24h; schema validation rejects malformed entries with errors logged
Per-Booking Audit Log Accessibility and Integrity
Given a permitted user opens the booking’s audit log (UI or API) When the log is retrieved Then entries appear in strict chronological order with UTC timestamps, event_type, actor, delta summary, and reference IDs And entries are immutable (append-only) with a tamper-evident hash chain per booking And filters by event_type and time range are available; pagination is 50 entries/page And PII redaction behavior: Staff=redacted by default; Owner=full; Support=redacted with one-time reveal requiring justification that is logged And a "Copy as JSON" action returns raw events for the selected range And performance: first 200 entries load within 2s p95
Delivery Receipts Tracking and Reconciliation
Given an SMS is sent for a flagged booking When carriers return delivery webhooks Then statuses are normalized to {queued, sent, delivered, failed, undelivered, unknown, read_if_supported} with provider codes mapped And multiple updates per message are consolidated into final_status with latest_ts and displayed per-recipient in the audit log And retries are linked to the original sms_message_id with attempt numbers; booking-level final outcome is computed And discrepancies (e.g., provider timeout > 2m) are flagged with status=unknown and an alert metric emitted And receipt ingestion success rate ≥ 99.9% daily; p95 status resolution time ≤ 90s
Aggregate Metrics Dashboards for Rebooking Performance
Given data exists for the selected date range When viewing Safe-Window Picker dashboards Then the following metrics are shown with definitions: rebook_rate, time_to_rebook (median, p95), no_show_rate_delta, deposit_recovery_rate, deposit_recovered_amount, option_rank_conversion (1/2/3), SMS_delivery_success_rate And filters for date range, staff, service type, location, and weather_flag can be combined And charts/tables refresh within 5s p95 after filter change and reflect data up to the last 5 minutes And tooltips display metric definitions and query time; Download CSV exports exactly the visible slice And dashboard totals match underlying event counts within ±0.5%
Evidence Export for Disputes and Compliance
Given an authorized user requests an export for a booking When "Export Evidence" is initiated (UI or API) Then a bundle is generated within 60s containing: audit_log.json, messages.csv (content, timestamps, delivery statuses), payment.pdf (receipt), schedule_changes.csv, consent.txt, summary.pdf And the bundle is PII-redacted by default with an option to include full PII; the selection is logged And the export is a single ZIP with SHA-256 checksum, delivered via a signed URL valid 72h; stored encrypted at rest (AES-256) and transferred over TLS 1.2+ And an export manifest is appended to the audit trail And if size > 200MB, the user is prompted to narrow scope or the system splits into multiple parts
Configurable Data Retention and Privacy Controls
Given an owner configures retention When setting retention by data class (telemetry, SMS content, delivery receipts, payments, audit logs) Then defaults are 24 months; configurable range is 1–60 months; separate toggle anonymizes SMS bodies while retaining metadata And a daily purge job deletes/anonymizes expired records; results (counts, errors) are logged; failures are retried with backoff And a legal hold can be applied per booking to suspend purges; reason and user are logged And a GDPR/CCPA erase action removes/anonymizes client PII within 30 days and logs completion And analytics opt-out flags exclude client data from aggregate dashboards within 24h
Access Control and Privacy by Role and Scope
Given role-based access controls are defined When users access audit logs, exports, or dashboards Then permissions are enforced: Owner=full; Staff=view redacted logs only for their bookings; Support=view redacted logs; exports require Owner approval; API scopes restrict endpoints And unauthorized requests return 403 with no data leakage; all access attempts log user_id, ip, and endpoint And rate limits apply: 60 audit log reads/min/user; 10 exports/day/account And field-level redaction masks phone number, email, payment tokens unless scope includes pii:read; redaction is visible as masked tokens And security testing shows no open high-severity findings on these endpoints

CarryForward Credit

Automatically carries deposits to the new slot during weather moves, with rules you control (full credit, partial, expiry dates, cross-service allowed). Clients see clear SMS receipts and policy notes, cutting disputes and awkward follow-ups while keeping cash flow steady.

Requirements

Configurable Credit Rules
"As an owner, I want to configure how deposits carry forward during weather reschedules so that credits are consistent with my policy and fair to clients."
Description

Provide an admin settings module to define carry-forward credit rules for weather-related reschedules, including full or partial credit percentages, flat caps, expiry windows, and cross-service eligibility. Support per-service overrides, client segment exceptions (e.g., VIPs), minimum/maximum credit amounts, rounding rules, and inclusion/exclusion of tips and taxes. Include a policy preview and validator, versioned rule sets with effective dates, and safe defaults to prevent accidental revenue leakage. Ensure compatibility with existing deposit, pricing, and service catalogs within PawPilot.

Acceptance Criteria
Default Credit Rule Configuration and Validation
Given I am an admin on the CarryForward Credit settings page When I configure default rules and attempt to save Then I can set a credit percentage between 0 and 100 inclusive And I can set an optional flat cap >= 0, and min/max credit amounts with min <= max And I can choose rounding mode (up, down, nearest) and precision (0.01, 0.05, 1.00) And I can toggle inclusion of tips and taxes in the credit base calculation And the policy preview updates to reflect all selections in plain language And the validator blocks save on invalid combinations with specific error messages And the calculated credit never exceeds the configured deposit base And a safe default preset is available that does not exceed the original deposit and requires an expiry
Per-Service Overrides with Effective Dates
Given default carry-forward rules exist When I create a service override for Service A with an effective date range and save Then the override is versioned with an ID, start date, and optional end date And overlapping effective date ranges for the same service are rejected with an error And weather reschedules for Service A within the range use the override; outside the range use the applicable non-override rules
Client Segment Exceptions (VIP) Precedence
Given a VIP client and a service override both exist When a weather reschedule is processed Then the VIP exception rules are applied in precedence over service overrides and defaults And the precedence order (segment > service > default) is shown in settings and used in calculation And if no segment exception exists, the next applicable rule is applied
Weather-Triggered Application Only
Given a booking is cancelled or rescheduled When the reason is marked as Weather (manual selection or system weather flag) Then carry-forward credit is calculated and applied per the active rule set And when the reason is client-initiated, no-show, or other non-weather reasons, no carry-forward credit is applied And the audit log records the triggering reason, applied rule set version, and calculation details
Cross-Service Eligibility Enforcement
Given cross-service eligibility is configured as Allowed or Not Allowed When a weather reschedule moves from Service A to Service B Then if Allowed, the credit applies per the rule set and any remaining balance is computed by the pricing engine And if Not Allowed, the system blocks automatic credit and prompts for compliant options And the policy preview text includes the cross-service rule And price differences do not alter the credit beyond the calculated amount and no double-charge occurs
Expiry Window Enforcement
Given an expiry window (in days) is configured When a credit is created and a new appointment is scheduled Then the credit applies only if the new appointment date is on or before the expiry date And attempts beyond the expiry date are blocked or require admin override with reason And the expiry date is stored and visible on the booking and in client-facing receipts
Rule Set Versioning and Effective Dates
Given a new rule set is drafted with a future effective date When I publish the rule set Then reschedules on or after the effective date use the new version; earlier reschedules use the prior version And existing credits retain and display the rule version under which they were created And settings display Active, Scheduled, and Past versions with timestamps
Automatic Credit Application on Reschedule
"As a groomer, I want deposits to automatically carry to the new slot when I move an appointment for weather so that I don’t spend time recalculating balances or chasing payment."
Description

When a booking is moved with a weather reason code, automatically locate the associated deposit, calculate eligible credit per configured rules, and apply it to the new appointment. Adjust the new balance accordingly, handle price differences (collect additional payment via SMS link or retain residual as credit), and prevent duplicate application with idempotent operations. Support multi-pet or multi-line bookings, cross-service carry-forward when allowed, and same-day settlement updates to keep cash flow steady. Integrate with the scheduler, billing, and notifications subsystems.

Acceptance Criteria
Auto-apply Deposit Credit on Weather Reschedule
Given a confirmed appointment with an associated deposit and a Weather-category reschedule reason code is selected When the appointment is moved to a new time slot and the reschedule is saved Then the system locates the most recent unused deposit linked to the original appointment And applies the eligible credit to the new appointment before finalizing the reschedule And updates the new appointment balance to reflect the applied credit And marks the original appointment’s deposit record as carried_forward with linkage to the new appointment And writes an audit log containing deposit_id, from_appointment_id, to_appointment_id, credit_applied, user_id, and timestamp And returns success to the scheduler within 2 seconds
Credit Calculation per Configured Rules (Percent/Flat/Expiry)
Given carry-forward rules define amount_type (full|percentage|flat), min_carry, max_carry, expiry_days, and currency rounding When computing eligible credit for a deposit on a weather reschedule Then if the deposit is expired as of reschedule date, eligible_credit = 0 with reason = expired And if amount_type = percentage, eligible_credit = round(currency, deposit_amount * percentage) And if amount_type = flat, eligible_credit = round(currency, min(flat_amount, deposit_amount)) And if amount_type = full, eligible_credit = round(currency, min(deposit_amount, new_appointment_total)) And enforce min_carry and max_carry bounds after calculation And persist calculated fields (eligible_credit, non_carried_amount, reason_code) for receipt rendering and audit
Cross-Service Carry-Forward Control
Given a reschedule from Service A to Service B with cross_service_carry_forward configuration When applying credit during a weather reschedule Then if cross_service_carry_forward = allowed for A→B, apply eligible credit to the new appointment And if disallowed, apply zero credit and retain the original deposit per policy (e.g., service-restricted credit) And notify the user in the scheduler UI/API response that cross-service carry-forward is blocked by policy And send the client an SMS with a payment link for the full new appointment amount and a policy note explaining the restriction
Multi-Line/Multi-Pet Allocation
Given a booking containing multiple line items (e.g., multiple pets/services) with a single deposit When the booking is weather-rescheduled in whole or in part Then allocate eligible credit only to the line items that moved And apportion the credit proportionally to each moved line’s pre-tax total (default) unless a line-level mapping is configured And round per line to currency precision ensuring the sum equals total eligible credit (resolve rounding to the highest-value line) And persist applied_credit per line item for billing and receipt detail
Idempotent Credit Application and Concurrency Safety
Given reschedule operations may be retried or delivered multiple times When the same reschedule is processed with an identical idempotency key derived from (from_appointment_id, to_appointment_id, deposit_id) Then no additional credit is applied after the first successful application And the system returns the original application result and transaction identifiers And database constraints or locks prevent duplicate credit rows for the same idempotency key And concurrent requests (>=2) result in exactly one credit application in load tests
Price Difference Handling (Top-up or Residual Credit)
Given the new appointment total may differ from the original When eligible credit is applied Then if new_total > eligible_credit, set outstanding_balance = new_total − eligible_credit and generate an SMS payment link for that amount And if new_total = eligible_credit, mark the appointment as paid_in_full and generate a zero-due receipt And if new_total < eligible_credit, retain residual as customer_credit with configured expiry and link it to the client account And post corresponding ledger entries the same day to reflect applied credit, outstanding balance, or residual credit
Notifications and Receipts via SMS
Given a successful weather reschedule with credit computation results When notifications are triggered Then send an SMS to the client within 60 seconds containing original deposit, eligible credit applied, remaining balance or residual credit, expiry (if any), and a brief policy note And include a secure payment link if there is an outstanding balance And record SMS delivery status and message_id for audit And expose the receipt details via API for the scheduler and in-app billing views
Client SMS Receipt with Policy Notes
"As a client, I want a clear text confirming how my deposit was used and any remaining balance or expiry so that I don’t need to dispute charges or contact the business for clarification."
Description

Send an immediate SMS receipt after a weather move that clearly shows original deposit, credit applied, new appointment details, remaining balance or remaining credit, and credit expiry date. Include concise policy notes and a link to the full policy for transparency. Localize currency and time formats, ensure message idempotency to avoid duplicates, and provide a deep link to pay any balance. Log delivery status and escalate to alternative contact if SMS fails.

Acceptance Criteria
SMS Receipt Content for Weather Move
Given a weather move is confirmed for an appointment with an existing deposit When the system generates the SMS receipt Then the SMS is queued for sending within 5 seconds of the move being saved And the SMS includes: original deposit amount, credit applied amount, new appointment service/date/time, remaining balance OR remaining credit (not both), and credit expiry date if applicable And all monetary amounts reflect active CarryForward rules and the client’s currency And if no remaining balance or credit exists, the corresponding value is shown as 0 in the correct currency And the appointment time reflects the rescheduled slot
Policy Notes and Full Policy Link in Receipt
Given policy notes and a full policy URL are configured for the business When the SMS receipt is generated Then the SMS includes up to two concise policy note lines totaling ≤160 characters And the SMS includes an HTTPS link to the full policy And the policy link resolves with HTTP 200 within 2 seconds TTFB from primary regions And the total message length is ≤3 SMS segments (≤480 GSM-7 chars or ≤335 UCS-2)
Localized Currency and Time Formatting
Given the client’s locale and timezone are known When the SMS receipt is generated Then currency values are formatted per client locale (e.g., en-US $1,234.56; en-GB £1,234.56; de-DE 1.234,56 €) And appointment date/time is formatted per client locale and shown in the client’s timezone And if locale or timezone is unknown, the business defaults are used and explicitly indicated (e.g., “Times shown in Pacific Time”)
Deep Link to Pay Remaining Balance
Given the remaining balance after credit application is greater than 0 When the client taps the pay link in the SMS Then a secure HTTPS payment page opens prefilled with the appointment and the exact remaining balance And the link is tokenized, single-use, and expires at appointment start or upon successful payment, whichever occurs first And expired or invalid links show a friendly error with an option to request a fresh link And successful payment sets remaining balance to 0 and triggers a confirmation SMS within 60 seconds
Idempotent Receipt Sending and Retry Suppression
Given each weather move has a unique Weather Move ID When the receipt send job is retried due to timeouts or manual requeue Then no more than one receipt SMS is sent per Weather Move ID per client within a 24-hour window And duplicate attempts are logged and suppressed with reason “duplicate” And a manual “Force send” explicitly overrides idempotency and logs the actor, timestamp, and reason
Delivery Status Logging and Visibility
Given an SMS receipt is sent via the provider When delivery status callbacks are received Then the system logs statuses (queued, sent, delivered, failed) with timestamps and provider message ID And the status history is visible on the appointment timeline and client conversation thread And if no callback is received within 10 minutes, the status is marked unknown and a retry policy is evaluated
Escalation Workflow on SMS Failure
Given the SMS receipt status becomes failed or undeliverable When escalation rules are executed Then an email receipt with identical content/links is sent to the client’s email on file within 2 minutes And if no email exists, an SMS is attempted to an alternate verified phone number And if no alternate channel is available, a dashboard task is created and the provider is notified And all escalation actions are logged and linked to the original Weather Move ID
Admin Override and Manual Adjustments
"As a shop manager, I want to override credit outcomes when exceptions arise so that I can resolve customer issues without breaking overall policy consistency."
Description

Provide a secure admin interface for case-by-case overrides of auto-applied credits: adjust credit amount, extend or shorten expiry, toggle cross-service eligibility, convert credit to refundable balance, or void a credit. Enforce role-based permissions, require reason codes and notes, and show the rule version that would have applied. Apply changes atomically with immediate recalculation of balances and updated receipts to maintain accuracy.

Acceptance Criteria
Adjust Credit Amount with Atomic Update & Receipt
Given an auto-applied credit exists for a rescheduled appointment and the admin has the Credit Override permission When the admin opens the override screen, enters a new non-negative credit amount, selects a reason code, and adds a note, then confirms Then the system applies the new amount atomically (all-or-nothing), recalculates the client’s credit balance and any linked invoice totals immediately, and updates the credit ledger with before/after values And an SMS receipt with the updated credit amount and policy notes is sent to the client within 30 seconds And an immutable audit record stores user ID, timestamp, reason code, note, and value changes And the UI reflects the updated amount within 1 second of save
Extend or Shorten Credit Expiry with Reason Capture
Given a credit has an existing expiry date and the admin has the Credit Override permission When the admin sets a new expiry date (future or past), selects a reason code, and adds a note, then confirms Then the system validates the date format and timezone, applies the new expiry atomically, and updates credit status if the new date is in the past And any downstream eligibility checks immediately reference the new expiry And an SMS receipt including the new expiry and policy notes is sent to the client within 30 seconds And an audit record captures old/new expiry, user, timestamp, reason code, and note
Toggle Cross-Service Eligibility and Reflect in Quoting
Given a credit with a cross-service eligibility flag and the admin has the Credit Override permission When the admin toggles the cross-service eligibility, selects a reason code, and adds a note, then confirms Then the system applies the flag change atomically and updates the rules used by quoting and checkout to include or exclude the credit for other services immediately And the client’s next quote/checkout request uses the updated eligibility without manual refresh And an SMS receipt reflects the eligibility change with clear policy notes And an audit record captures the flag change, user, timestamp, reason code, and note
Convert Credit to Refundable Balance with Ledger Update
Given an active credit exists and the admin has the Credit Override permission When the admin chooses Convert to Refundable Balance, selects a reason code, adds a note, and confirms Then the system atomically closes the original credit (remaining amount set to 0), creates a corresponding refundable balance entry for the same amount, and updates any linked invoices to reference the new balance type And the client receives an SMS receipt describing the conversion and next steps for refund or application within 30 seconds And the ledger shows two entries: credit closed and refundable balance created, with IDs linked in the audit trail And an audit record captures conversion details, user, timestamp, reason code, and note
Void Credit and Recalculate Balances Safely
Given an active or pending credit exists and the admin has the Credit Override permission When the admin selects Void Credit, provides a reason code and note, and confirms Then the system performs a version-checked, atomic void that reverts any applied portions, recalculates client balances and linked invoices, and prevents future application of the voided credit And if a concurrent change is detected, the operation is aborted with a conflict error and no partial updates are saved And an SMS receipt is sent to the client explaining the void with policy notes within 30 seconds And an audit record captures pre-void state, post-void state, user, timestamp, reason code, and note
Role-Based Permissions and Access Control on Overrides
Given multiple user roles exist with and without the Credit Override permission When a user without permission attempts to access the override UI or submit an override Then the system blocks the action with a 403-style response and logs the attempt in the audit trail without exposing sensitive fields When a user with permission accesses the override UI Then override actions are enabled only after a reason code is selected and a note is entered And all successful overrides are tied to the acting user’s role and ID in the audit log
Display Applicable Rule Version in Override UI
Given the organization has CarryForward Credit rules with versioning When an admin opens the override screen for a specific credit Then the UI displays the rule name and version that would have auto-applied (including parameters: credit percentage, expiry policy, cross-service setting) for the current context And the rule version identifier and retrieval timestamp are included in the audit record And if rules are updated globally while the screen is open, the UI indicates a newer version is available without altering the displayed would-have-applied version until refresh or explicit re-evaluation
Credit Ledger, Reconciliation, and Audit Trail
"As an owner, I want a transparent audit trail of all credit events so that I can reconcile accounts and resolve disputes quickly with clear evidence."
Description

Maintain a detailed ledger for each client and booking capturing deposit creation, credit issuance, application, adjustment, expiry, refund, and associated actors and timestamps. Provide reconciliation views and exports, and generate dispute-ready evidence packs containing SMS receipts and policy snapshots. Ensure consistency with payment processor records and guard against double-counting with transactional integrity and locks.

Acceptance Criteria
Atomic Ledger Entries and Balance Integrity
Given a successful deposit payment for booking B123 by client C456 When the processor confirms payment with txn_id T789 Then create ledger entry deposit_created linked to B123 and T789 with amount, currency, actor=system, and timestamp_utc within 5s of confirmation And do not change credit_balance Given a weather move that triggers carryforward with rule R1 When applying credit Then create credit_issued entry linked to the original deposit and rule R1 with the computed amount And atomically update credit_balance by +amount together with the booking reschedule in a single transaction Given a credit application to a new slot When applying credit to invoice I222 Then create credit_applied entry linked to the credit source and I222 And decrease credit_balance by the applied amount with balance_after = previous_balance - amount rounded to 2 decimals Given an adjustment by a user with role=Owner When an adjustment is submitted with a non-empty reason Then create credit_adjusted entry with reason, actor metadata, and reference to the affected entry_id And reject any direct edits to existing ledger entries with 403 Forbidden Given two concurrent requests to apply the same credit with the same idempotency_key When processed by the system Then exactly one request succeeds (200) and one is rejected with 409 Conflict And only a single ledger entry is created Given system time configuration When recording any ledger entry Then store timestamp as ISO 8601 UTC with millisecond precision
Processor Reconciliation and Variance Reporting
Given a date range D1-D2 When nightly reconciliation runs Then fetch processor transactions for D1-D2 and match to ledger entries of types deposit_created and refund_issued by processor_txn_id and amount Then totals per currency and counts per event_type match processor totals with variance=0 And if variance != 0 then mark run status=Failed and generate a variance report Then unmatched ledger entries are labeled Unmatched-Ledger with reason codes in {MissingProcessorTxn, AmountMismatch, CurrencyMismatch} And unmatched processor transactions are labeled Unmatched-Processor with reason codes in {MissingLedgerEntry, DuplicateProcessorTxn} Then reconciliation persists run_id, started_at_utc, finished_at_utc, totals, variances, and unmatched counts And completes within 15 minutes for <= 10,000 transactions
Reconciliation Views and CSV Export
Given a user with role=Manager When opening the Ledger view and filtering by date range, client, booking, event_type, and status Then results return within 2 seconds for <= 5,000 rows And columns include ledger_event_id, client_id, client_name, booking_id, event_type, amount, currency, balance_after, actor_type, actor_id, timestamp_utc, processor_txn_id, rule_id, expiry_at, memo When Export CSV is requested Then a CSV is generated within 60 seconds with the same columns and ordering as the on-screen view And row count equals the number of rows in the view And filename follows pattern pawpilot_ledger_{D1}_{D2}_{run_id}.csv When no data matches the filters Then the view shows "No results" And Export CSV is disabled
Dispute-Ready Evidence Pack Generation
Given a ledger_event_id or booking_id When Generate Evidence Pack is requested by a user with role=Manager Then produce a ZIP within 30 seconds containing: PDF of SMS receipts for related transactions, policy snapshots as of each event timestamp, full ledger history for the client and booking, payment processor receipt(s), and reconciliation status for related events Then include a JSON manifest with pack_id, generated_at_utc, requester, scope, and SHA-256 checksum for each artifact Then redact PII to last4 for phone and card numbers while including client names; exclude full PANs and raw phone numbers Then record audit event evidence_pack_generated with requester, scope, and download_at_utc When required artifacts are missing Then return HTTP 422 with a list of missing artifacts and do not produce a partial pack
Audit Trail Immutability and Role-Based Access
Given any existing ledger entry When a user attempts to update or delete it via API or UI Then respond 403 Forbidden and persist no changes; ledger is append-only Given a correction is needed When a user with role=Owner submits an adjustment Then create a new credit_adjusted entry referencing prior entry_id with mandatory reason (min length 10) and actor metadata Given any ledger read or write action When performed by a user or system process Then record an audit_log entry with actor, action, target_id, timestamp_utc, and ip/user_agent when available Given role permissions When role=Agent accesses the ledger Then read-only access and evidence pack generation are allowed and adjustments/refunds are blocked with 403
Credit Rule Enforcement (Amount, Expiry, Cross-Service)
Given CarryForward rules R specify full_or_partial, expiry_days, and cross_service_allowed When a weather move is initiated Then credit_issued amount equals deposit_amount × rule percentage And expiry_at equals move_date + expiry_days at 23:59:59 in business timezone And cross-service flag is stored on the credit source Given cross_service_allowed = false When attempting to apply credit to a different service Then block the application with error code SERVICE_MISMATCH and do not create a ledger entry Given a credit past its expiry_at When attempting to apply it Then block with error code CREDIT_EXPIRED and create a credit_expired entry that sets available amount to 0 Given a partial application within limits When applying credit to a new invoice Then create credit_applied for the used amount and leave the remainder available And send SMS receipt to the client including applied amount, remaining balance, and expiry date Given a daily expiry job at 02:00 local business time When it runs Then expire all credits with expiry_at < now and create corresponding credit_expired entries And record metrics expired_count and duration
Credit Reporting and Expiry Alerts
"As a business owner, I want visibility into credit usage and proactive alerts before credits expire so that I can protect cash flow and reduce write-offs."
Description

Deliver dashboards and alerts for credit health: outstanding credits liability, utilization rate, average days to use, expiry pipeline, and dispute rate. Surface client-level insights and cohort trends. Automatically remind clients via SMS before credit expiry per configurable cadence, and notify staff of high-value credits nearing expiry. Allow CSV export and schedule periodic email summaries.

Acceptance Criteria
Credit Health Dashboard Metrics and Filters
- Dashboard loads with selected date range and filters (service, staff, location, client segment) and renders metrics within ≤2 seconds with a Last Refreshed timestamp ≤5 minutes old. - Outstanding Credits Liability equals the sum of all non-expired, unconsumed credit balances as of the end of the selected date range, displayed in the account currency. - Utilization Rate (%) equals (value of credits applied to invoices during the selected date range) ÷ (value of credits issued during the selected date range), rounded to one decimal. - Average Days to Use equals the mean days between issuance and full consumption for credits that reached zero balance during the selected date range; if sample size <20, show "Not enough data". - Expiry Pipeline shows count and value of credits expiring in the next 7, 14, 30, and 60 days from today; each segment opens a list of underlying credits. - Dispute Rate (%) equals (value of credits marked Disputed that were issued in the selected date range) ÷ (value of credits issued in the selected date range), rounded to one decimal. - All metrics honor applied filters, account timezone, and currency; zero and empty states render without error.
Client-Level Credit Insights View
- Client profile includes a Credits tab listing all credits with columns: Status (Active, Used, Expired, Disputed), Original Amount, Remaining Balance, Issued At, Expires At, Source (service/appointment/staff), Policy Rule Applied; table loads in ≤1.5 seconds for up to 500 credit events. - Each credit shows a usage history with timestamp, amount, invoice ID, and staff for every application; totals reconcile to original amount. - Rollups display Total Outstanding, Last Used At, and Average Days to Use (last 12 months) for the client. - Deep links to SMS receipts and policy notes for the originating deposit open in a new window and require authorization; links resolve within ≤1 second. - Permissions: Owner/Manager can view all clients; Staff can view only assigned clients; unauthorized access returns HTTP 403 and no data. - Actions available: Export this client's credit history to CSV; Send manual expiry reminder; all actions are audit-logged with user, timestamp, and client ID.
Cohort Trends Reporting
- User can select a cohort dimension: First Appointment Month, Service Type, or Acquisition Source; charts and tables render only after a selection is made. - For each cohort, compute: Utilization within 30/60/90 days of issuance (%), Dispute Rate (%), and Average Credit Amount; cohorts with <30 issued credits show a low-sample indicator. - Display a 12-month trend chart for Utilization within 90 days; hover reveals exact values and cohort definitions. - Filters (service, location, staff, date range) constrain issuance events used in cohort calculations; tooltips document metric definitions. - Cohort table and chart export to CSV reflect current filters and account timezone. - Rendering completes within ≤3 seconds for datasets up to 100k issuance events.
Configurable SMS Credit Expiry Reminders
- Admin can configure per-policy reminder cadence (e.g., 14, 7, 3, 1 days before expiry) and quiet hours (start–end) in the account timezone. - Reminder templates are editable with variables {client_first_name}, {credit_amount_remaining}, {expiry_date}, {policy_link}; saving validates variables and shows a live preview. - System schedules one reminder per credit per configured offset; if balance reaches zero or expiry date changes, future reminders for that credit are canceled or rescheduled accordingly. - Messages are not sent during quiet hours; queued messages send at the next allowed window; send time uses client timezone when available, else account timezone. - Compliance: Numbers with STOP/DND status are skipped; HELP/STOP behavior follows carrier rules; all compliance decisions are logged. - Delivery: ≥95% of reminders enqueue within 60 seconds of trigger; failed sends retry up to 3 times with exponential backoff; final status (Sent/Failed) and provider response are visible in the message log. - Duplicate suppression prevents more than one reminder for the same credit from being sent to a client within any 24-hour period.
Staff Notifications for High-Value Credits Nearing Expiry
- Admin can set a monetary threshold and days-before-expiry window (e.g., ≥$100 within 7 days) and choose notification channels (In-App, Email, SMS). - Eligible credits are evaluated daily at 08:00 in the account timezone; notifications are generated once per credit per day and deduplicated across channels. - Recipients default to Owner and Manager; optional inclusion of originating staff; recipient list is editable and saved per account. - Each notification includes client name, remaining credit amount, expiry date, last appointment date, and a CTA to "Message Client" that opens the SMS composer prefilled with a template. - Notifications respect channel opt-outs (SMS/email) and appear in an in-app notification center with unread counts; users can mark notifications as done. - All notifications are audit-logged with timestamp, recipients, channel, and credit ID.
CSV Export of Credit Reports
- From Dashboard, Client Insights, and Cohort views, users can export CSV with columns: Credit ID, Client ID, Client Name, Issued At, Expires At, Original Amount, Remaining Balance, Status, Source Service, Staff, Location, Dispute Status, Last Used At. - Exports reflect current filters and date range; timestamps are ISO 8601 with timezone offset; currency values include currency code and two decimals. - Exports >50,000 rows run as async jobs; users receive a downloadable link in-app and via email; links are pre-signed and expire after 24 hours. - Only Owner/Manager roles can export; each export is audit-logged with user ID, filter parameters, row count, and checksum. - Export jobs complete within ≤10 minutes for up to 1 million rows or provide progress updates at least every 30 seconds.
Scheduled Email Summaries
- Admin can schedule weekly or monthly email summaries, selecting day, time, and timezone; multiple recipients supported with verification. - Summary content includes: Outstanding Credits Liability (as of send time), Utilization Rate (prior period), Average Days to Use (prior period), Expiry Pipeline (next 7/14/30 days), Dispute Rate (prior period), and Top 10 clients by outstanding value; values match the dashboard under identical filters. - Optional CSV attachment for the expiry pipeline is included when ≤10 MB; otherwise a secure download link is provided. - Emails are responsive and authenticated (SPF/DKIM aligned); delivery status is tracked; bounces/complaints remove the recipient and log the event. - Recipients can unsubscribe from summaries via link; unsubscribe affects summaries only and not transactional alerts.

DryZone Broadcast

When weather clears a slot, PawPilot auto-broadcasts it to waitlisters in nearby unaffected zones using dynamic radius and geo-clustering. Invites include precise ETAs and safe-window tags, keeping your day full even when one part of town is rained out.

Requirements

Weather-Cleared Slot Detection
"As an independent groomer, I want the system to detect when weather has cleared a previously blocked slot so that I can automatically refill my schedule without manual checks."
Description

Continuously monitor scheduled work blocks and local weather conditions to detect when precipitation or unsafe conditions have lifted, automatically marking impacted time slots as available. Integrate calendar events with a weather provider to determine start/stop times for safe operations in each service area, compute a safety window, and emit a standardized "cleared slot" event that triggers the DryZone Broadcast pipeline. Include guardrails such as minimum lead time, service daylight constraints, and operator-configurable thresholds to avoid false positives.

Acceptance Criteria
Clearance Detected Marks Slot Available
Given a scheduled slot W [start, end] in service area A with state = Weather Hold and configured safety_window_minutes S and min_overlap_minutes M When the weather provider indicates conditions in A are below configured thresholds for the forecast window [safe_from, safe_until] and [start, end] overlaps [safe_from, safe_until] by at least M minutes Then set W.state = Available, set W.available_from = max(Now, safe_from), persist safety_window = [safe_from, safe_until], and write an audit record with reason_code = "WEATHER_CLEARED" and provider_observation_at
Minimum Lead Time Guardrail
Given a cleared candidate slot W with start time Tstart and configured min_lead_time_minutes L When (Tstart - Now) < L Then do not emit a ClearedSlot event for W, set W.broadcast_eligible = false with block_reason = "LEAD_TIME", and enqueue reevaluation at (Tstart - L) if that time is in the future
Daylight Constraint Enforcement
Given service area A daylight window [daylight_start, daylight_end] (with configured offsets) and a cleared window [safe_from, safe_until] for slot W [start, end] When max(safe_from, start) is outside [daylight_start, daylight_end] Then suppress ClearedSlot event, set W.broadcast_eligible = false with block_reason = "DAYLIGHT" And if safe_until enters daylight with at least M minutes of overlap with [start, end], schedule reevaluation at daylight_start
Operator-Configurable Safety Thresholds
Rule: Safety thresholds (e.g., precip_intensity_max, wind_speed_max, visibility_min) are configurable per service area A and versioned Given thresholds for A are updated at time Tc When detection runs after Tc Then new thresholds are applied within 2 minutes and all Weather Hold slots in A are re-evaluated using the updated version Given a previously cleared slot W now violates updated thresholds before service start When reevaluation detects violation Then set W.state = Weather Hold and append an audit record with reason_code = "THRESHOLD_CHANGE"
Standardized Cleared Slot Event Emission
Given slot W is cleared and passes all guardrails (lead time, daylight, thresholds) When emitting the ClearedSlot event Then the payload validates against schema v1 with fields: event_type="ClearedSlot", event_id (UUIDv4), occurred_at (ISO-8601), slot_id, service_area_id, start, end, safe_from, safe_until, clearance_confidence (0..1), weather_provider, provider_observation_at, thresholds_version, idempotency_key, timezone, geo_centroid And the event is published to topic "dryzone.cleared_slots" within 5 seconds of clearance and acknowledged by the broker And duplicate submissions with the same idempotency_key are not re-published Given the broker is unavailable When publish fails Then retry up to 5 attempts with exponential backoff starting at 2s; on exhaustion, persist to outbox, emit an alert (severity=High), and continue retrying asynchronously until delivered
Weather Provider Degradation Handling
Given weather_data_ttl_minutes = T and provider_error_budget = E% When the latest weather datum for service area A is older than T minutes or error rate > E for 5 consecutive minutes Then pause clearance detection for A, emit no ClearedSlot events, maintain or set slots to Weather Hold, set system_status(A) = "DEGRADED", and send an operator alert And resume detection automatically when fresh data (age <= T) is observed for 2 consecutive polling intervals
Dynamic Radius & Geo-Clustering
"As a dog walker on the move, I want the broadcast to target only nearby unaffected areas so that I can fill openings without excessive driving or weather risk."
Description

For each cleared slot, compute a dynamic targeting radius based on travel time budget, current route position, service duration, historical acceptance rates, and pet density. Partition nearby demand using a geo-index (e.g., geohash/H3) and merge cells into clusters that are outside active weather alerts. Rank clusters by fill probability and drive time, outputting an ordered list of "unaffected zones" for broadcast. Provide safeguards to exclude congested or risky corridors and cap maximum expansion radius.

Acceptance Criteria
Compute Dynamic Radius From Operational Inputs
Given a cleared slot with travel_time_budget_minutes, current_route_position, service_duration_minutes, acceptance_rate_by_radius, and pet_density_index When the dynamic radius is computed Then the output radius_km: - uses all provided inputs in the calculation - is <= max_expansion_radius_km and >= min_radius_km - increases or stays the same when acceptance_rate_by_radius and pet_density_index increase, holding other inputs constant - decreases or stays the same when travel_time_budget_minutes decreases, holding other inputs constant - is deterministic for identical inputs within the same time window - is captured in an audit log with the full input vector and resulting radius_km
Geo-Index Partitioning and Weather-Safe Clustering
Given an H3 geo-index resolution and active weather alert polygons When partitioning nearby demand and merging cells into clusters Then: - include only cells whose centroids are outside active weather alert polygons - produce clusters that are contiguous under H3 neighbor rules - each cluster includes centroid, cell_ids, and total_pet_density - record excluded cells with reason = 'weather' - unit tests confirm zero inclusion of any cell with centroid inside an active alert polygon
Cluster Ranking by Fill Probability and Drive Time
Given for each cluster: fill_probability in [0,1], drive_time_minutes > 0, and configured weights w_prob and w_time (defaults 0.7 and 0.3) When ranking clusters for broadcast Then: - compute composite_score = w_prob*fill_probability - w_time*(drive_time_minutes / max_drive_time_minutes) - sort clusters strictly descending by composite_score - break ties by lower drive_time_minutes, then higher fill_probability, then shorter route deviation distance_km - ranking output is deterministic for identical inputs
Safeguard Exclusions for Congested and Risky Corridors
Given live congestion data with predicted_delay_minutes per segment and a risk_score per segment When evaluating a path from the current route to a candidate cluster Then: - exclude the cluster if predicted_delay_minutes > max_delay_fraction * slot_slack_minutes or risk_score >= risk_threshold - record exclusions with segment_ids and reason in audit logs - if an alternate route exists that meets both thresholds, retain the cluster with updated drive_time_minutes - unit tests verify exclusion and fallback behavior using mocked feeds
Cap Maximum Expansion by Distance and Time
Given configured caps max_expansion_radius_km and max_expansion_drive_minutes When generating eligible cells and clusters Then: - exclude any cluster whose haversine_distance_km from current position > max_expansion_radius_km - exclude any cluster with drive_time_minutes > max_expansion_drive_minutes - emit a 'cap_applied' audit event with counts of clusters excluded by each cap
Ordered Unaffected Zones Output Contract and Latency
Given a cleared slot ready for DryZone Broadcast When producing the ordered list of unaffected zones Then: - return an array ordered by rank where each element includes: cluster_id, centroid_lat, centroid_lng, cell_ids, eta_minutes_range_min, eta_minutes_range_max, safe_window_tag = 'weather-clear', fill_probability, drive_time_minutes, reasons_excluded, computed_at_iso8601, ttl_seconds - ensure each zone contains only cells outside active weather alerts - achieve P95 compute latency <= 1200 ms for up to 5000 candidate cells and P50 <= 400 ms - pass JSON schema validation for the response
Waitlist Targeting & Eligibility Rules
"As a business owner, I want waitlist invites to go only to eligible, opted-in clients in a fair rotation so that we maximize conversions without spamming customers."
Description

Select recipient candidates from the Smart Waitlist using explicit eligibility criteria: DryZone opt-in status, service compatibility, pet attributes, historical reliability, deposit readiness, travel constraints, and quiet-hour compliance. Deduplicate across overlapping lists, enforce fairness and rotation to prevent over-contacting the same clients, and apply per-broadcast caps and throttles. Produce a ranked, auditable recipient list per cluster with reason codes for inclusion or exclusion.

Acceptance Criteria
DryZone Opt‑In and Quiet‑Hour Compliance
Given a DryZone Broadcast is triggered at timestamp T (per recipient’s local timezone) When evaluating Smart Waitlist candidates Then include only clients where dryzone_opt_in = true AND T ∉ [quiet_hours.start, quiet_hours.end) And exclude clients with dryzone_opt_in = false with reason code OPTOUT_DRYZONE And exclude clients where T ∈ [quiet_hours.start, quiet_hours.end) with reason code QUIETHOURS_BLOCK And treat missing quiet hours as no block And record inclusion/exclusion with timestamp, client_id, and reason codes; no SMS is attempted for excluded clients
Service and Pet Attribute Compatibility
Given an open slot with service_type, duration, and required pet attributes (size, coat, temperament, special_handling) When evaluating Smart Waitlist candidates Then include only clients whose preferred services include service_type and who have at least one pet meeting all required attributes And exclude candidates with non-matching service_type with reason code SERVICE_MISMATCH And exclude candidates where no pet meets attributes with reason code PET_ATTR_MISMATCH And log matched_pet_id(s) for included candidates
Deposit Readiness Eligibility
Given business settings and slot rules indicating whether a deposit is required When evaluating Smart Waitlist candidates Then if deposit is required, include only clients with valid_payment_method = true AND deposit_consent = true And exclude clients failing either condition with reason code DEPOSIT_NOT_READY And if deposit is not required, do not filter by deposit readiness And persist the evaluated deposit_required flag and readiness state for audit
Travel Constraints and Dynamic Radius Eligibility
Given a geo-cluster centroid, a computed dynamic radius R, and unaffected zone boundaries And each candidate’s home_base and travel preferences (max_travel_time_minutes and/or max_travel_distance_km) When evaluating eligibility Then include only candidates located within R OR with estimated_travel_time <= max_travel_time_minutes (fallback: distance_km <= max_travel_distance_km) And exclude candidates in affected zones with reason code IN_AFFECTED_ZONE And exclude candidates failing time/distance constraints with reason code OUTSIDE_RADIUS And if an ETA is computed, ensure ETA falls within candidate’s safe_window tags; otherwise exclude with reason code SAFE_WINDOW_MISMATCH And record geocalc method (time|distance), values, and zone status per candidate
Deduplication and Fair Rotation Across Overlapping Waitlists
Given candidates sourced from multiple overlapping waitlists and segments When generating the recipient pool Then deduplicate by client_id so each client appears at most once with reason code DEDUPED_OVERLAP applied to removed duplicates And sort primarily by last_broadcast_contacted_at ascending (never-contacted first), then by reliability_score descending, then by client_id ascending for stability And exclude clients with an active cooldown window (e.g., contacted_in_last_hours >= cooldown_hours) with reason code COOLDOWN_ACTIVE And persist rotation_index and last_broadcast_contacted_at used for ordering
Per‑Broadcast Caps, Throttles, and Cool‑downs
Given configuration values: max_recipients_per_broadcast (default 20), send_rate_per_minute (default 30), per_client_daily_cap (default 2), per_client_weekly_cap (default 5) When preparing and dispatching invitations Then select at most max_recipients_per_broadcast from the ranked pool And throttle dispatch to not exceed send_rate_per_minute; additional messages are queued with status THROTTLED And exclude clients exceeding daily or weekly caps with reason codes PER_CLIENT_DAILY_CAP_REACHED or PER_CLIENT_WEEKLY_CAP_REACHED And record cap/throttle decisions and queue positions for audit
Ranked, Auditable Recipient List with Reliability Weighting and Reason Codes
Given the filtered candidate pool per cluster When producing the final recipient list Then compute a deterministic score per candidate using documented weights (e.g., reliability_score, rotation_priority, proximity) and exclude candidates with reliability_score < threshold with reason code RELIABILITY_BELOW_THRESHOLD And output a ranked list (1..N) limited by caps, including for every evaluated candidate: cluster_id, slot_id, client_id, include_flag, rank (if included), score, reason_codes[], evaluated_at (ISO8601), criteria_version And ensure identical inputs yield identical ordering (stable tie-break by client_id) And provide export as JSON and CSV and store the artifact for at least 30 days
Personalized SMS Invite Composer
"As a client, I want an invite that clearly states when you can arrive and how to claim the spot so that I can decide quickly and book with confidence."
Description

Generate per-recipient SMS invitations containing a precise ETA window, a "safe-window" tag, service summary, and a single-tap claim link with deposit capture. Support template variables, localization of times, branded short links, opt-out language, and reply-based actions (e.g., Y to claim, N to skip). Enforce carrier compliance, quiet hours, and rate limiting while instrumenting delivery, click, and reply events for downstream analytics.

Acceptance Criteria
Per-Recipient Invite Composition
Given a cleared slot and a targeted waitlister with known service, locale, and timezone When the system composes the SMS invite Then the SMS body includes all of: a localized ETA window (start–end), a [SAFE] safe-window tag, a service summary (service name, duration, price), and a single-tap claim link that initiates deposit capture And the ETA window width is between 30 and 90 minutes inclusive and rounded to the nearest 15 minutes And the safe-window tag reflects the recipient’s zone-specific safe window for the date And the claim link token is unique per recipient and slot and has at least 128 bits of entropy And the composed message contains no unreplaced template variables or placeholders
Template Variables and Time Localization
Given a message template containing variables {first_name}, {service_name}, {eta_start}, {eta_end}, {safe_window}, {claim_url} When rendering for a recipient with profile and slot data Then all variables resolve to concrete values with no remaining curly-brace tokens And date/time variables are formatted in the recipient’s locale and timezone (e.g., 12h vs 24h, correct DST) And if a required variable is missing, composition fails, the invite is not sent, and an error is logged with variable name and recipientId And optional variables use configured defaults without breaking the message layout And a preview endpoint returns the fully rendered SMS within 300 ms at p95
Branded Short Link Generation and Behavior
Given a long, recipient-bound claim URL When the system generates a short link Then the link domain matches the configured branded domain and serves over HTTPS with a valid certificate And the short link redirects to the long URL within 300 ms at p95 And the link expires at the earlier of slot start time or configured TTL, returning an "Offer expired" page thereafter And if the shortener service is unavailable, the system falls back to the long URL and records a shortening_fallback event And each issued link is single-use; subsequent attempts display an "Already claimed" state without re-booking
Reply-Based Actions (Y to Claim, N to Skip)
Given an invite has been sent to a recipient When the recipient replies with Y or YES (case- and whitespace-insensitive) Then the system places a 5-minute soft hold on the slot, sends a single-use payment link for the deposit, and upon successful deposit confirms the booking via SMS And if the slot is already claimed before deposit completion, the recipient receives an unavailable message and remains on the waitlist When the recipient replies with N or NO Then the system records a decline, sends an acknowledgement, and suppresses further notifications for that slot When the recipient sends an unrecognized reply Then the system sends a help message describing valid options without changing booking state And duplicate replies within 5 minutes are handled idempotently without creating duplicate holds or charges
Carrier Compliance and Opt-Out Handling
Given an SMS invite is ready to send When validating for carrier compliance Then the message starts with the brand name, includes "Reply STOP to opt out" and "HELP for help", and contains no prohibited content And messages to opted-out numbers are blocked pre-send with a compliance violation logged When the recipient replies STOP/UNSUB/QUIT/CANCEL/END Then the number is immediately opted out, a confirmation is sent within 60 seconds, and the event is recorded with timestamp and source When the recipient replies HELP Then a help message with support contact is sent and logged And message length/segmentation and URL count meet provider policy; non-compliant messages are rejected pre-dispatch with an actionable error
Quiet Hours Enforcement and Rate Limiting
Given quiet hours are configured in the recipient’s local timezone When an invite becomes eligible during quiet hours Then the message is queued and automatically released at the next allowed send time; any manual override is audited with actor and reason Given a broadcast to multiple recipients When dispatching SMS Then throughput respects a configurable rate limit per sender ID with jitter and backoff on 429/5xx And throttling-related send failures are < 1% at p95 and do not exceed provider limits And dispatch ordering maintains fairness across recipient groups and zones
Event Instrumentation for Delivery, Click, and Reply
Given an invite is sent and the recipient interacts When a delivery receipt is received from the provider Then a delivery event (recipientId, messageId, providerStatus, timestamp) is persisted and published to analytics within 2 minutes at p95 When the recipient clicks the claim link Then a click event (recipientId, messageId, linkId, timestamp) is captured at redirect and de-duplicated by (recipientId, linkId) over 24 hours When the recipient replies Then a reply event (recipientId, messageId, normalizedContent, intent=CLAIM|DECLINE|OTHER, timestamp) is persisted and linked to the invite And the ingestion pipeline provides at-least-once delivery with idempotent storage and 99.9% monthly availability And no sensitive payment data is stored in events; only tokens and metadata required for analytics
Real-time ETA & Safe-Window Engine
"As a groomer, I want accurate, continuously updated ETAs and safe windows so that I don't overpromise arrival times during changing weather."
Description

Calculate and continuously update arrival estimates using current route context, traffic and travel time APIs, service duration by pet and service type, buffers, and the current weather clearance window. Provide fallbacks when external data is unavailable and propagate updated ETAs to in-flight broadcasts and booking flows. Validate that accepted bookings remain within the safe-window and automatically adjust or cancel invites if conditions change.

Acceptance Criteria
Initial ETA and Safe-Window Tag on Broadcast Invite
Given a newly cleared slot and eligible waitlist recipients in unaffected zones And travel time APIs are responsive When DryZone Broadcast is prepared Then the system computes an ETA with minute precision using current start location, route context, live traffic data, per-pet/service duration, and configured buffers And the computed ETA falls within the current weather safe-window [start,end] And the SMS/booking payload includes ETA (local time), safe-window tag, and invite expiry timestamp And if the computed ETA is outside the safe-window, no invite is sent to that recipient
Real-time ETA Propagation to In-Flight Broadcasts
Given a broadcast invite has been sent and is not yet accepted And a change in traffic, route context, or provider calendar alters ETA by ≥ 5 minutes When the change is detected Then the system recomputes ETAs and updates pending invites and booking flows within 15 seconds And sends an SMS update if the delta is ≥ 5 minutes, otherwise updates only in the booking UI And enforces a maximum of 2 ETA update SMS per recipient per 30 minutes And if the updated ETA falls outside the safe-window, the invite is withdrawn and the recipient is notified
Fallback Behavior When External Data Is Unavailable
Given travel or traffic APIs error or exceed 1500 ms response time When preparing or updating an invite ETA Then the system uses the last known valid travel time and historical average for the segment, plus configured service durations and buffers And marks the ETA as approximate in the payload And proceeds without blocking the broadcast And retries external calls with exponential backoff starting at 1 second for up to 3 attempts And if no fallback data is available, the recipient is skipped and the event is logged with reason "insufficient data"
Validation of Accepted Booking Against Safe-Window
Given a recipient accepts an invite via SMS or link When the acceptance is received Then the system revalidates ETA against the current safe-window and buffers within 5 seconds And confirms the booking only if ETA lies within the safe-window And otherwise cancels the acceptance, notifies the recipient with reason, and offers the next feasible ETA within the same day if available And no booking is stored in a confirmed state outside the safe-window
Service Duration Calculation by Pet and Service Type
Given a provider profile with service duration rules and buffers When computing ETA for a job with N pets and specified services Then duration equals the sum of per-pet base, add-ons, setup/cleanup buffers, and travel buffer rules And conflicting rules resolve by most specific match (pet breed > size > default) And missing values default to organization-level defaults And the final duration and rule IDs used are recorded in the audit log
Weather Window Change Handling for Active Invites
Given active invites exist for a cleared slot And the weather safe-window shrinks or shifts based on new data When the engine receives the updated window Then it recomputes ETAs within 15 seconds And auto-cancels invites whose ETAs are now out of window and notifies recipients And updates remaining invites with new ETAs and safe-window tags And prevents any further accepts outside the updated safe-window
First-Claim Booking & Conflict Resolution
"As a sitter, I want first-come booking with automatic holds and payment capture so that confirmed slots don't double-book and I get paid promptly."
Description

Implement a first-come-first-served claim flow that places a short hold on the slot upon affirmative reply or link click, validates eligibility, and captures the required deposit. Apply atomic calendar locks to prevent double-booking across SMS, web, and manual channels, with automated timeout and re-broadcast on no-payment or decline. Provide clear confirmation and graceful decline messages and record full audit logs of claim attempts and outcomes.

Acceptance Criteria
Hold on Affirmative Claim (SMS or Link)
Given an open slot is broadcast and a waitlisted client receives the invite When the client replies YES via SMS or clicks the claim link Then the system starts a 5-minute hold on the slot and sends "Holding your spot until {HH:MM} — pay deposit to confirm" within 2 seconds And given multiple messages or clicks from the same client within 30 seconds When duplicates arrive Then they are de-duplicated to a single active hold
Atomic Calendar Lock Across Channels
Given a hold is active for a slot Then the slot is marked RESERVED across SMS, web booking, and staff calendar within 1 second, preventing any new bookings or holds When 100 concurrent booking attempts are initiated across channels Then exactly one succeeds (the held claimant upon deposit) and all others fail with a "just claimed by another client" message or UI notice When the hold expires or is converted to a confirmed booking Then the lock is removed or transitioned atomically with no intermediate double-bookable state
Eligibility Validation Before Hold Confirmation
Given a client triggers a claim When eligibility checks run Then the system verifies: (a) client is in the targeted DryZone Broadcast cohort (zone and service match), (b) no overlapping appointment exists for the client or assigned staff, and (c) the client is on the waitlist for the advertised service When any check fails Then the hold is immediately released, the client receives a decline SMS with a specific reason code, and the slot is re-queued for broadcast When all checks pass Then the hold remains active and the client is prompted for deposit
Deposit Capture Within Hold Window
Given a hold is active When the client opens the deposit link Then the deposit amount equals the slot’s configured deposit and accepted payment methods are presented When payment is successfully authorized within the 5-minute hold Then the booking is confirmed atomically, the calendar entry is created, and a confirmation SMS with date/time, ETA, safe-window tag, and receipt amount is sent within 10 seconds of payment confirmation When payment fails or is abandoned Then the hold remains until expiry; no partial bookings are created; no charge is captured
Hold Timeout and Automated Re-broadcast
Given a hold is active with no payment received When the 5-minute hold window elapses Then the hold and lock are released within 1 second Then the slot is automatically re-broadcast to remaining eligible waitlisters in the same zones per the original DryZone Broadcast parameters, excluding clients who explicitly declined this slot Then the original claimant receives an SMS that the hold expired with a link to rejoin the waitlist
Simultaneous Claims Conflict Resolution Across Channels
Given simultaneous claims from different clients via SMS, web, or staff UI within 500 ms When processed by the server Then the claim with the earliest server-received timestamp obtains the hold; others receive a "just claimed" message and no hold When a staff member attempts to manually book the held slot Then the UI blocks the save with a conflict notice and no override is allowed Then all outcomes are consistent across replicas even under network partition; no double-booking occurs
Audit Logging and Messaging Outcomes
Given any claim attempt When it is processed Then an immutable audit record is stored including: attempt_id, slot_id, broadcast_id, client_id, channel, timestamps (received, hold_started, validation_result_at, payment_result_at, confirmed_at or expired_at), outcome code, failure reason (if any), lock_id, and payment transaction_id (if any) Then audit records are queryable by slot_id and exportable as CSV in admin, and retained for at least 365 days When confirmation or decline messages are sent Then their message_id, content template ID, and send timestamp are linked to the attempt’s audit record and are delivered (provider accepted) within 10 seconds
Operator Controls & Performance Analytics
"As an owner, I want controls and analytics for DryZone Broadcast so that I can fine-tune performance and understand its impact on revenue and costs."
Description

Expose admin controls to enable DryZone Broadcast, adjust max radius, quiet hours, throttles, exclusion zones, and message templates. Provide a live dashboard with metrics such as time-to-fill, fill rate, conversion by cluster, opt-out rate, SMS spend, and incremental revenue, along with exportable logs for compliance. Include per-event override, preview, and manual re-broadcast capabilities, and alert operators on repeated failures or unusually low fill performance.

Acceptance Criteria
Global Toggle and Per-Event Override
Given DryZone Broadcast is globally enabled for the account When a weather-clear event creates an open slot Then the system auto-queues a broadcast to eligible waitlisters Given DryZone Broadcast is globally disabled When a weather-clear event occurs Then no auto-broadcast is sent unless the event is set to Force broadcast Given a specific event is set to Suppress broadcast via per-event override When the broadcast trigger condition is met Then no messages are sent for that event Given conflicting settings between global toggle and per-event override When evaluating whether to send Then the per-event override takes precedence Given an operator updates the global toggle or per-event override When the change is saved Then the setting persists, is effective immediately, and an audit log entry is recorded
Geo Targeting: Dynamic Radius, Clustering, and Exclusion Zones
Given max radius R and one or more exclusion zones are configured When a broadcast runs Then only waitlisters with last-known location within R and outside all exclusion zones are targeted Given initial radius r0 < R and the slot remains unfilled When the expansion interval i elapses Then the radius increases by step s up to R until the slot is filled Given geo-cluster distance threshold k meters When multiple candidates fall within k of each other Then invites are allocated round-robin across clusters to avoid saturating a single cluster Given an operator changes max radius R or exclusion zones When the next broadcast or preview is generated Then the updated geo parameters are applied and reflected in the candidate list
Quiet Hours and Send Throttling
Given quiet hours are set to 21:00–07:00 in the account timezone When a broadcast would send during quiet hours Then messages are queued and first sends resume at 07:00 local time Given a per-event override to ignore quiet hours is enabled When a broadcast occurs during quiet hours Then messages are sent immediately for that event only Given a throttle T messages per minute is configured When broadcasting to M recipients Then the send rate never exceeds T and total send duration is approximately ceil(M/T) minutes Given a recipient has opted out prior to send When the broadcast executes Then no message is sent to that recipient and the suppression is logged Given a daylight saving time change occurs When evaluating quiet hours boundaries Then local-time boundaries are honored without sending inside the quiet window
Message Templates and Preview
Given an operator edits the DryZone Broadcast template with required variables {pet_name}, {slot_time}, {eta}, and {deposit_link} When attempting to save with any required variable missing Then validation fails and indicates the missing variables Given a valid template is saved When previewing for a specific event and sample recipient Then variables resolve to concrete values and a character/segment counter with estimated SMS cost is shown Given language-specific templates exist When the recipient has a language tag Then the matching language template is used; otherwise the default template is used Given A/B template variants A and B are configured at 70/30 weighting When a broadcast sends N messages Then the distribution of A vs B is within ±5 percentage points of the configured weights
Manual Re-Broadcast and Recipient Preview
Given a slot remains unfilled after the initial broadcast When the operator selects Manual Re-broadcast Then the system displays the current eligible recipient list with inclusion/exclusion reasons and estimated reach Given the operator adjusts per-event radius, throttle, or quiet-hour override in the re-broadcast modal When the re-broadcast is confirmed Then those overrides apply only to that event instance and do not change global settings Given a recipient was invited within the cooldown window C minutes When the re-broadcast executes Then the recipient is not re-invited and the suppression reason is shown Given some recipients previously failed due to carrier errors When the operator chooses Retry failed only Then only those failed recipients are retried
Live Metrics Dashboard and Anomaly Alerts
Given the dashboard time filter is set (e.g., Last 7 days) When viewing DryZone metrics Then the following are computed for the selected window: median and p90 time-to-fill, fill rate %, conversion by cluster, opt-out rate %, SMS spend $, incremental revenue $ Given a slot fills or a broadcast completes When up to 60 seconds have elapsed Then dashboard metrics update to reflect the latest data Given the operator clicks a cluster on the dashboard When drill-down is requested Then broadcasts and outcomes for that cluster are displayed with counts and timestamps Given alert thresholds are configured (e.g., fill rate < 20% over last 3 broadcasts or median time-to-fill > 2x baseline) When thresholds are breached within a rolling 2-hour window Then an anomaly alert is issued via in-app notification and email/SMS with context (affected events, clusters, recent metrics) Given an operator acknowledges or snoozes an alert When subsequent identical conditions occur within the snooze window Then duplicate alerts are suppressed until the snooze expires
Compliance Logs Export and Auditing
Given a compliance export is requested with date range and filters When the export is initiated Then a CSV is generated within 2 minutes containing: broadcast ID, timestamp, sender, template ID, message body, recipient ID, opt-in status at send time, delivery status, cost, geo parameters (radius, exclusions), overrides used, and operator ID Given an export exceeds 100,000 rows When the request is made Then it is processed as a background job and a secure download link is emailed upon completion; the link expires after 7 days Given any admin control is changed (toggle, radius, quiet hours, throttles, exclusion zones, templates) When the change is saved Then an immutable audit log entry is created recording before/after values, user, timestamp, and IP address Given a download completes When the CSV is provided Then a SHA-256 checksum accompanies the file so integrity can be verified

Storm Reflow

Re-sequences your day around incoming weather, compressing unaffected bookings and sliding at-risk ones into safer windows. Automatic ETA texts keep everyone aligned, while buffer and distance rules ensure the plan is realistic—less scrambling, more completed appointments.

Requirements

Weather Risk Scoring
"As a dog walker, I want the system to flag time slots at risk due to incoming weather so that my schedule can be adjusted proactively and safely."
Description

Continuously ingests hyperlocal forecast and nowcast data to score weather risk for each appointment’s time window and route segment. Uses provider location, client geos, service type (e.g., outdoor walk vs. indoor groom), pet tolerance tags, and forecast severity (precipitation, lightning, wind, heat/cold index) to determine which bookings are at risk. Flags thresholds that should trigger reflow, updates scores at configurable intervals, and provides fallbacks when a weather API is unavailable. Persists risk state per job to power downstream decisions, minimizing false positives and ensuring only materially impacted appointments are moved.

Acceptance Criteria
Hyperlocal Score Computation for Appointment Window
- Given an appointment within the next 2 hours and provider/client coordinates, When the risk engine runs, Then it fetches nowcast data at ≤5-minute resolution for both coordinates, computes a risk_score in [0,100], and writes job.risk.score and job.risk.inputs within 2s P95. - Given an appointment scheduled >2 hours from now, When the risk engine runs, Then it fetches forecast data at ≥30-minute resolution and computes/stores risk_score in [0,100]. - Given both nowcast and forecast are available for the window, When the window overlaps both horizons, Then the computation blends them by nearest time slice without gaps >10 minutes.
Risk Thresholds Trigger Reflow and Notifications
- Given a job’s risk_score ≥ 70 or lightning probability ≥ 20% within the service window, When scoring completes, Then job.risk.status = "at_risk", job.reflow.suggested = true, and an event "storm_reflow_candidate" is emitted once per threshold crossing. - Given a job’s risk_score < 50 for 30 consecutive minutes with no hazard flags, When scoring completes, Then job.risk.status = "clear" and job.reflow.suggested = false. - Given a score crosses from below to above the threshold, When the next run completes, Then exactly one event is emitted within 1 minute of the crossing.
Update Cadence and Convergence
- Given configuration update_interval = 10 minutes, When the system is healthy, Then each active job’s risk is recalculated at least once every 10 minutes with jitter ±1 minute. - Given a job transitions to completed or canceled, When the next scheduler tick occurs, Then no further risk recalculations occur for that job within 1 minute. - Given identical weather inputs across consecutive runs, When the engine re-scores, Then the risk_score variance is ≤ ±1 point (idempotent behavior).
Service Type and Pet Tolerance Modifiers
- Given service_type = "outdoor_walk", When base risk is computed with precipitation ≥ 1.0 mm/hr, Then add +20 to the risk_score; if thunderstorm alert present, Then add +30. - Given service_type = "indoor_groom", When only precipitation and wind hazards are present, Then subtract 25 from the base risk_score (floor at 0). - Given pet_tolerance = "heat_sensitive" and heat index ≥ 32°C during the window, Then add +25 to risk_score; given pet_tolerance = "cold_sensitive" and wind chill ≤ -5°C, Then add +25. - Given any modifiers are applied, When finalizing, Then clamp risk_score to [0,100] and record applied modifiers in job.risk.inputs.modifiers.
Weather API Outage Fallback
- Given the primary weather API times out (>1500 ms) or returns 5xx, When fetching data, Then retry once and fall back to a secondary provider; if both fail, use last-known-good data ≤ 60 minutes old and set job.risk.source = "stale". - Given stale data is used, When scoring completes, Then retain the previous risk_score, emit metric weather_data_stale = 1, and schedule a retry within 5 minutes. - Given no prior data exists and both providers fail, When scoring runs, Then set job.risk.status = "unknown", do not set job.reflow.suggested, and enqueue a retry within 5 minutes.
Risk Persistence and Auditability
- Given a risk update occurs, When persisting, Then store timestamped score, inputs (metrics, modifiers), provider source(s), window coverage, and rule reasons, retrievable for 30 days. - Given a change in reflow suggestion state, When writing history, Then persist a log with before/after scores and the rule that triggered the change. - Given a job is hard-deleted, When cleanup runs, Then risk history is soft-deleted and excluded from analytics within 24 hours.
Route Segment Risk Integration
- Given a job with route segments between prior stop and client, When scoring, Then compute segment_risk_score per segment using segment midpoint weather and travel time, and set job.risk.score = max(window_risk, max(segment_risk_score)). - Given any segment_risk_score ≥ 80 due to lightning or flood alerts, When scoring completes, Then set job.reflow.suggested = true regardless of window_risk. - Given no route segments are provided, When scoring, Then compute risk from the appointment window only and record route_evaluated = false.
Constraint-Aware Re-sequencing Engine
"As an independent groomer, I want my day’s appointments re-sequenced automatically within realistic travel and buffer constraints so that I can complete more jobs with less scrambling."
Description

Automatically computes an optimized day plan when risk thresholds are met, compressing unaffected bookings and sliding at-risk ones into safer windows. Obeys hard constraints (service durations, provider working hours, travel time estimates, deposit/consent status) and soft constraints (client priority, pet needs, preferred time ranges) using a cost function that minimizes disruption. Produces a new timeline with start/end times, travel legs, and buffers that remain realistic under weather conditions. Integrates with PawPilot’s scheduler and route model, supports multi-pet/stacked services, and ensures the plan remains feasible if any single booking declines the change.

Acceptance Criteria
Weather Risk Trigger Generates Optimized Day Plan
Given a provider's active daily schedule and a forecasted weather risk at or above the configured threshold for any booking window or travel segment When the re-sequencing engine runs Then it generates a draft optimized plan within 60 seconds containing for every booking: start time, end time, travel legs, and buffer durations And unaffected bookings are compressed to close gaps without reducing required buffers And at-risk bookings are moved only into time windows whose risk is below the threshold And the output includes a per-booking change-set (old/new times, impact reason) and an overall disruption cost score And the draft plan is versioned and linked to the original plan
Hard Constraints Are Never Violated
Given service durations, provider working hours, travel time estimates adjusted for weather, deposit/consent status, and distance/buffer rules When validating any proposed plan Then no booking overlaps another for the same provider And no booking starts before working hours or ends after working hours And scheduled duration equals service duration plus mandated buffers And per-leg travel time is greater than or equal to the route model estimate under current weather And max distance/time between adjacent stops does not exceed configured rules And any booking missing required deposit or consent is not rescheduled
Soft Constraints Minimize Disruption
Given client priority tiers, pet needs constraints, and preferred time ranges with weights When computing the plan Then the plan's weighted disruption cost is less than or equal to the cost of the pre-change plan And P1 clients are shifted by 30 minutes or less unless their original window exceeds the risk threshold And at least 80% of bookings remain within their preferred time ranges And pet-specific care windows (e.g., medication/feeding) are not violated
Single-Decline Feasibility Preservation
Given a proposed re-sequenced plan with impacted bookings When any single impacted booking declines the change and is locked to its original time Then the engine recomputes a feasible plan within 60 seconds without violating hard constraints And the recomputed plan increases disruption cost by no more than 20% compared to the declined proposal And if no feasible plan exists, the system identifies conflicting bookings and reverts to the last feasible plan while preserving unaffected compressions
Scheduler and Route Model Integration Is Atomic and Idempotent
Given provider approval to apply the draft plan When committing the changes Then calendar bookings and route legs update atomically in a single transaction And no double-bookings or ghost bookings exist post-commit And reapplying the same plan produces no further changes (idempotent) And an audit log records old/new times, travel legs, buffers, and the disruption cost score
Multi‑Pet and Stacked Services Are Kept Contiguous
Given bookings with multiple pets or stacked services When re-sequencing the day Then all services within a package remain contiguous without intervening travel And the total block duration equals the sum of services plus any shared buffers And no service within a package is split across non-contiguous windows or locations And travel legs are computed for the package as a single stop
Weather-Adjusted Travel and Buffer Realism
Given current and forecast weather conditions When computing travel and buffer times Then per-leg travel time applies the route model's weather adjustment factor And the buffer between bookings is the maximum of provider default buffer and weather-derived buffer And the plan never requires average speed exceeding the configured safe-speed threshold And any leg violating these rules causes the plan to be rejected and recomputed
Dynamic ETA and Reschedule SMS
"As a sitter, I want clients to receive automatic, accurate ETA and reschedule texts when weather reshapes my route so that expectations stay aligned and I avoid manual texting."
Description

Generates and sends automatic SMS messages with updated ETAs or reschedule proposals when a reflow is prepared or published. Uses merge fields (client name, new window, deposit link, reply options) and respects quiet hours, opt-out status, and locale. Supports two-way replies (confirm, propose alternate, cancel) to immediately update the schedule, and throttling to avoid message fatigue. Falls back to a single consolidated update per client if multiple changes occur in a short period. Logs message outcomes and delivery for audit, aligning with PawPilot’s SMS-first communication model.

Acceptance Criteria
Auto-ETA SMS on Reflow Prepare/Publish
Given a reflow transitions to Prepared or Published And an affected appointment’s ETA or service window changes When the transition occurs Then an ETA update SMS is sent to the client within 60 seconds And the SMS uses the correct template with merge fields {ClientFirstName}, {NewWindowStart}-{NewWindowEnd}, {ReplyOptions} And the appointment timeline records an "ETA SMS sent" event with timestamp and messageId
Reschedule Proposal SMS with Merge Fields and Deposit Link
Given a reflow flags an appointment as at-risk and proposes a new window When the proposal is created at Prepared or Published stage Then a reschedule proposal SMS is sent within 60 seconds And the SMS includes {ClientFirstName}, {ProposedWindowStart}-{ProposedWindowEnd}, {DepositLink} when deposit is required, and {ReplyOptions} And the {DepositLink} resolves to an appointment-bound, client-bound URL active for at least 24 hours And the appointment status becomes "Pending Client" until a reply is processed
Quiet Hours and Opt-Out Compliance
Given a client has an active SMS opt-out When a reflow attempts to send any message Then the message is not sent And an audit entry records "Suppressed: Opted Out" Given current local time for the client is within configured quiet hours When a message would be sent Then the message is queued for the first minute after quiet hours end And the queued state and scheduled send time are visible on the appointment Given a client replies STOP When received Then opt-out is recorded immediately and a compliance confirmation SMS is sent And no further outbound messages are sent except compliance-required responses Given a client replies START or UNSTOP When received Then opt-out is lifted and future messages may be sent
Locale, Timezone, and Formatting Accuracy
Given a client profile includes locale and timezone When composing any ETA or reschedule SMS Then all date/time values are rendered in the client’s timezone And date/time formatting matches the client’s locale conventions (e.g., 12h vs 24h) And the language template matches the client’s locale, else falls back to the org default And currency/number formats in any linked pages (e.g., deposit) follow the same locale
Two-Way Reply Handling and Immediate Schedule Updates
Given the system sends an ETA or reschedule SMS with reply options When the client replies CONFIRM, YES, Y, or 👍 within 24 hours Then the appointment status updates to Confirmed within 30 seconds And an acknowledgement SMS is sent And the timeline records "Client confirmed via SMS" Given the client replies CANCEL or NO When received Then the appointment is canceled per policy within 30 seconds And a cancellation acknowledgement SMS is sent And the slot is released to the Smart Waitlist Given the client replies with an alternate time in a recognizable format (e.g., "ALT 2:30pm" or "tomorrow 9-11") When received Then the system parses the time, verifies feasibility, holds the slot for 15 minutes, updates status to "Client Proposed Alternate," and sends an acknowledgement with the held window Given an unrecognized reply When received Then the system sends a help SMS listing valid reply options and routes the thread to the team inbox
Throttling and Consolidation of Multiple Changes
Given two or more ETA or reschedule changes for the same client occur within a 10-minute window When preparing outbound SMS Then only one consolidated SMS is sent containing the latest plan and a note "Multiple updates combined" And any additional messages for that client are suppressed for 10 minutes after the consolidated send Given per-client outbound volume would exceed 3 reflow SMS in a rolling 6-hour window When preparing outbound SMS Then further messages are consolidated into the next allowed send And suppression is logged with reason "Fatigue Throttle" Given a message is consolidated or suppressed When processed Then an audit entry records the action, original triggering events, and resulting messageId (if sent)
Delivery and Outcome Logging for Audit
Given any SMS is sent or suppressed When the event occurs Then the audit log stores clientId, appointmentId, messageType (ETA|Proposal|Consolidated|Help|Ack), templateId, renderedBody hash, sendAt, provider messageId (if sent), deliveryStatus, deliveredAt (if available), failureCode/reason (if any), suppressionReason (if any), and actor (system/user) Given the SMS provider posts delivery updates When received Then deliveryStatus is updated within 2 minutes of webhook receipt And the timeline reflects the latest delivery state Given a delivery fails with a permanent carrier error When detected Then deliveryStatus is set to Failed and an alert is added to the appointment timeline
Buffer and Travel Rule Configurator
"As a service provider, I want configurable buffers and travel rules that adapt to weather severity so that the reflowed plan remains feasible and safe for pets."
Description

Provides a configuration layer for providers to define buffer minima, maximum compression percentages, and travel distance/time caps per service type and weather severity. Allows per-provider and global defaults, with presets for common conditions (e.g., rain, heat advisory, lightning). Integrates with mapping and travel time estimation to ensure reflow outputs remain feasible, humane for pets, and compliant with safety practices. Changes are versioned and auditable so that reflow results are explainable and repeatable.

Acceptance Criteria
Per-Service Buffer Minimums by Weather Severity
Given a provider selects a service type in the Buffer & Travel Rule Configurator When the provider sets buffer minimums for Normal=10, Rain=20, Heat Advisory=30, Lightning=45 minutes and saves Then the configuration persists and is retrievable via API and UI for that provider and service type And subsequent reflow runs apply the minimum buffer for each appointment gap based on the forecasted severity active during that gap And no gap for the selected service type is shorter than the configured minimum for the applicable severity And inputs outside 0–180 minutes or non-numeric values are rejected with a validation error and save is blocked And for services marked Outdoor under Heat Advisory, the enforced buffer is not less than the configured cooldown buffer value
Maximum Compression Percentage Enforcement
Given a provider configures maximum compression percentages per severity (e.g., Normal=20%, Rain=35%, Heat Advisory=25%, Lightning=0%) When Storm Reflow executes for a day with baseline idle time I0 under severity S Then the resulting idle time I1 satisfies (I0 - I1) / I0 <= configured C(S) + 1% tolerance And service durations remain unchanged by reflow And start/end of day constraints are not violated when compression is applied And inputs outside 0–80% are rejected with a validation error and save is blocked
Travel Time and Distance Caps Integration
Given a provider sets per-service, per-severity travel caps (e.g., max leg time=25 minutes, max leg distance=12 miles) When Storm Reflow computes routes using the mapping service for the affected day Then every inter-appointment leg satisfies estimated travel time <= capTime AND distance <= capDistance when both are set And if either estimate exceeds its cap, that placement is rejected from the candidate plan and the appointment is left or moved to a compliant slot And if no compliant slot exists within working hours, the appointment is flagged as unschedulable with reason "Travel cap exceeded" And units (minutes, miles/km) are consistently displayed and stored per provider locale settings
Provider Overrides vs Global Defaults Precedence
Given global default rules exist for buffer minima, compression, and travel caps And a provider has defined overrides for some of those values When reflow requests effective rules for that provider, service type, and severity Then provider overrides take precedence over global defaults only for the fields explicitly overridden And fields not overridden by the provider inherit from the global defaults And resetting a field to "Inherit" removes the override and reverts the effective value to the global default And the effective rule set returned by the API includes the source for each field as either "provider" or "global"
Weather Preset Templates Apply and Edit
Given the system offers presets for Rain, Heat Advisory, and Lightning containing buffer, compression, and travel cap values When a provider applies the "Rain" preset to a specific service type and reviews the values Then the configurator pre-populates the fields with the preset values without saving And the provider can edit any field before saving And on save, the preset (with edits) is stored as the provider’s current rules for that service type and severity And choosing "Restore" reverts the unsaved form back to the last-saved values
Versioning, Audit Trail, and Explainability
Given a provider updates any rule and saves When the save completes Then a new immutable version is created with incrementing version ID, editor identity, UTC timestamp, and a diff of changed fields And the audit log API can return all versions for a provider with filters by date range and editor And Storm Reflow records the rules version used in each run, and the resulting plan includes reasons referencing the rule fields and version (e.g., "Lightning buffer=45m (v12)") And re-running Storm Reflow with identical inputs and the same rules version produces an identical plan (route, timings, flags)
Waitlist Auto-Fill for Weather Gaps
"As a business owner, I want weather-created gaps to auto-fill from my waitlist with proximity-aware offers so that I protect revenue and reduce idle time."
Description

Automatically targets Smart Waitlist candidates to fill openings created by weather-driven shifts or cancellations. Ranks waitlist entries by proximity, service compatibility, deposit readiness, and response likelihood, then sends time-bound SMS offers. Respects the reflowed route and buffer rules, confirming the first eligible responder and updating payment/deposit links accordingly. Reduces idle time and protects revenue while keeping the plan realistic under the day’s weather constraints.

Acceptance Criteria
Weather Gap Detection and Auto-Fill Kickoff
Given the day’s schedule has been reflowed by Storm Reflow and a gap is created or widened due to weather And the gap duration is >= the shortest eligible service duration for the business When the gap is recorded with gap_id, start_time, end_time, and gap_location Then Waitlist Auto-Fill is initiated within 30 seconds of gap detection And the gap is tagged cause=weather and source=storm_reflow or cancellation And an audit log entry is created with gap_id, timestamps, and initiating user=system
Candidate Eligibility and Ranking Rules
Rule: A candidate is eligible only if their requested service matches the gap’s allowed services and the duration fits fully within the gap. Rule: Travel time from prior stop to candidate and from candidate to next stop must keep buffers >= configured minimum and total route distance <= configured daily maximum. Rule: Opted-out numbers, blocked clients, or time overlaps with existing appointments disqualify a candidate. Rule: Each candidate receives a composite score = 0.35*proximity + 0.25*service_match + 0.20*deposit_ready + 0.20*response_likelihood (all normalized 0–1). Rule: Candidates with travel_distance_to_gap > configured max_km or score < 0.50 are excluded. Rule: Up to 5 top-ranked candidates are selected per gap, with at most 2 concurrent offers active at any time.
Time-Bound SMS Offer Delivery and Content
Given a candidate is selected for an offer When the offer is enqueued Then the SMS is queued within 5 seconds and contains: date, start–end window, service name, price, deposit amount (if any), location area, reply keywords (YES to accept, NO to decline), expiry time, and a unique trackable payment/deposit link And the offer TTL is 15 minutes from send time And replies STOP/HELP are honored per SMS compliance and no further offers are sent to opted-out numbers And upon TTL expiry, the offer status becomes expired and the next candidate is offered automatically if available.
First-Responder Confirmation and Concurrency Control
Given multiple candidates may reply YES When the first eligible YES is received before TTL and does not violate route/buffer constraints And (if a deposit is required) the deposit is successfully completed within the TTL Then that candidate is confirmed and all other active offers for the gap are canceled within 5 seconds with a “slot filled” SMS And confirmation includes appointment time and an automatic ETA text And slot locking is atomic and idempotent so no double booking occurs.
Route, Buffer, and Distance Compliance on Acceptance
Given a YES reply is received When the system simulates the acceptance in the reflowed route Then minimum buffer between adjacent stops remains >= the configured minutes And travel time and distance limits remain within configured thresholds And if any constraint would be violated, the acceptance is rejected, the candidate is notified, and the next candidate continues automatically.
Deposit and Payment Handling on Acceptance
Rule: If the service requires a deposit, the offer includes a unique link prefilled to the reflowed time and service; the payment processor must return success/fail via webhook. Rule: Confirmation is issued only after deposit success within the offer TTL; on failure or timeout, the offer is withdrawn and the next candidate is considered. Rule: Deposit receipts and an updated invoice are attached to the appointment; any prior deposit links for the candidate are invalidated. Rule: For no-deposit services, confirmation occurs immediately on YES and a same-day payment link is attached to the appointment.
No Eligible Candidates and Fallback Notifications
Given all selected candidates have declined, expired, or been disqualified Or 30 minutes have elapsed since gap detection, or the current time is within 20 minutes of the gap start When no candidate has been confirmed Then the gap is marked unfilled with a reason code And all pending offers are canceled And a provider notification (SMS/app) is sent with gap details and a manual fill shortcut And an audit log entry is recorded.
Provider Preview and Override Controls
"As a provider, I want to preview and override the proposed storm reflow before it goes live so that I maintain control and can handle edge cases."
Description

Presents a side-by-side preview of the current plan vs. the proposed storm reflow with clear diffs (time shifts, route changes, SMS to be sent). Enables one-click publish, selective acceptance per appointment, manual drag-and-drop adjustments with conflict checks, and undo history. Shows risk scores and reasoning for each move to build trust. Only publishes client communications once the provider confirms, ensuring human control over sensitive changes.

Acceptance Criteria
Side-by-Side Plan Preview with Clear Diffs
Given a day has a proposed Storm Reflow, When the provider opens the preview, Then two columns labeled Current Plan and Proposed Reflow are displayed with the same set of appointments in the same sort order by client name. Then each appointment row displays diffs: start time delta in minutes (e.g., +15), route sequence change indicator, travel distance delta (per account units), and buffer change. Then a Changes Summary shows totals: appointments shifted, total drive time delta, idle time delta, and number of client messages pending. Then all times and formats respect the provider’s timezone and time-format settings. Then the preview loads within 2 seconds for up to 40 appointments on a standard broadband connection.
Selective Acceptance and Partial Publish
Given a proposed change for an appointment, When the provider clicks Accept for that appointment, Then it is marked Accepted and incorporated into the working plan preview. When the provider clicks Reject for that appointment, Then it remains at its original time/route and is excluded from the pending publish set. Then buffers and travel times are recalculated within 500 ms after each accept/reject, and any new conflicts are highlighted inline. Then the UI displays an Accepted/Total counter and the Publish action reflects only Accepted items.
Drag-and-Drop Adjustments with Real-Time Conflict Checks
Given the Proposed Reflow column, When the provider drags an appointment to a new position/time, Then valid drop zones are indicated and snapping enforces service duration + minimum travel time + buffer constraints. When the provider drops into an invalid slot, Then the move is blocked and an inline message lists the violated constraints with the two nearest valid alternatives. After a valid drop, Then route order and ETAs recalculate immediately and the diffs update in under 500 ms. Then drops across off-hours, time-off blocks, or overlapping hard commitments are blocked.
Undo/Redo History for Provider Overrides
Given any accept, reject, or drag action has occurred, When the provider clicks Undo, Then the last change is reverted and the preview and diffs update accordingly. When the provider clicks Redo, Then the reverted change is re-applied with identical results. Then the history supports at least 20 steps per session and is cleared only upon successful publish or explicit discard. Then undo/redo never alters a published schedule; after publish, new history starts from the published state.
Risk Scores and Reasoning Visibility per Move
Given a proposed move exists, Then a per-appointment risk score from 0 to 100 is displayed with a label (Low ≤33, Medium 34–66, High ≥67). Then an expandable explanation lists the top contributing factors (e.g., weather window, client flexibility, travel risk) with weights totaling 100%. When inputs remain unchanged, Then the same appointment shows the same risk score across refreshes. When the provider applies a risk filter High, Then only High-risk items remain visible in the list and sorting by risk is available.
One-Click Publish with Deferred Client Messaging and Audit Log
Given at least one Accepted change and no unresolved hard conflicts exist, Then the Publish button is enabled. When the provider clicks Publish, Then a confirmation modal lists affected appointments with before/after times, route KPIs, and the SMS messages slated to be sent per client. When the provider confirms, Then the schedule changes are persisted, an audit log entry (timestamp, provider ID, change set, SMS plan) is recorded, and client messages are queued/sent according to the plan. Then no client messages are sent prior to confirmation; canceling the modal results in no changes and no messages. If publish fails at any step, Then no partial client messages are sent, the schedule rolls back to the pre-publish preview state, and an error is displayed.
ETA Auto-Text Preview and Per-Client Suppression
Given the preview includes affected appointments, Then the exact SMS body per client is shown with merged tokens (client name, updated ETA, deposit link) and scheduled send time. Then any unresolved token renders as a highlighted error and blocks Publish until corrected or that SMS is suppressed. When the provider toggles off SMS for a specific appointment, Then that message is excluded from the send plan and the audit log records the suppression. Then SMS preview respects template language and time format settings from the provider account.

Weather Grace

Applies weather-specific policy softeners—late-cancel forgiveness, fee waivers, and brief hold extensions—based on severity and client reliability. A one-tap SMS acknowledgment records the terms, preserving goodwill and documentation without manual exceptions.

Requirements

Weather Severity Inference Service
"As a business owner, I want objective weather severity scoring per appointment so that grace policies are applied consistently and defensibly."
Description

Integrate with multiple weather providers to fetch real-time and forecasted conditions at the service location and appointment window, normalize signals into a standardized severity score, and persist event metadata for reuse. Run evaluations at booking and scheduled intervals (e.g., T-24h, T-3h, T-1h) and on-demand, with caching, rate limiting, and provider failover. Expose a deterministic severity level (e.g., None, Mild, Moderate, Severe) via an internal API for use by policy rules, messaging templates, and UI badges. Store versioned inputs and outputs to ensure transparency and reproducibility of decisions.

Acceptance Criteria
Multi-Provider Fetch and Normalization Determinism
Given at least two configured weather providers and a valid service location and appointment window When the service requests weather data for the window Then it fetches from a minimum of 2 providers in parallel And normalizes responses into a unified schema {precip_type, precip_intensity, accumulation, wind_speed, visibility, temperature, alerts[], start_at, end_at, source_timestamp} And computes a numeric severity score (0-100) and deterministically maps it to a level in [None, Mild, Moderate, Severe] using mapping_version=N And repeated requests with identical inputs and mapping_version return the exact same score and level And the API response includes scoring_version and mapping_version
Scheduled and On-Demand Evaluation Triggers
Given an appointment is booked at T0 for time window Tw When booking is confirmed Then the service schedules evaluations at T0, Tw-24h±5m, Tw-3h±5m, and Tw-1h±5m And on-demand evaluations run immediately when triggered via API, independent of the schedule And the job queue de-duplicates identical triggers for the same appointment within a 2-minute window And each execution persists an evaluation record with trigger_type, scheduled_at, started_at, finished_at, and outcome
Caching, Staleness, and Rate Limiting
Given repeated requests for the same appointment/window within cache_TTL=10m and provider data freshness <=30m When the internal API is called Then the response is served from cache and no external provider calls are made And each cache entry includes created_at and expires_at and is keyed by {location precision=~100m, window, provider set, mapping_version} And provider calls are rate limited per provider at a default of 60 req/min using token-bucket When a provider limit would be exceeded Then the service serves from cache or defers to secondary providers without erroring the request
Provider Failover and Last-Known-Data Use
Given the primary provider returns HTTP 5xx, times out >2s, or yields data older than 30m When an evaluation runs Then the service marks the primary unhealthy and fails over to a secondary within 500ms And merges multi-source data preferring the freshest source per field And if all providers fail and no cache <=60m exists, the API returns a severity level with data_fresh=false and provenance="unavailable" while logging a recoverable error And provider health is rechecked every 60s before restoring the primary to rotation
Internal Severity API Contract and Performance
Given an authorized internal client with a valid service token When it calls GET /internal/severity?appointment_id=ID or POST /internal/severity with {lat, lon, window} Then the API responds 200 with p95<=700ms and p99<=1500ms And the response payload includes {level in [None,Mild,Moderate,Severe], score 0-100, sources[], effective_window, mapping_version, scoring_version, data_fresh boolean, evaluated_at, cache_hit boolean, request_id} When the token is missing or invalid Then the API responds 401 When request parameters are invalid Then the API responds 422 with error details And the API is idempotent for the same request_id for 5 minutes, returning the same result
Versioned Persistence and Reproducibility
Given any evaluation completes Then the service persists: raw provider payloads, normalized snapshot, computed score and level, mapping_version, scoring_version, query params (lat, lon, window, appointment_id), provider health status, and trigger metadata And records are immutable, append-only, and addressable by evaluation_id When a replay is requested with evaluation_id Then the service reproduces the original score and level exactly using the stored versions and inputs And retention is at least 180 days And an audit trail links evaluations to appointments and the triggering user/service
Geo/Time Window Accuracy and Timezone Handling
Given an appointment location (lat, lon) and a window that may cross timezones or DST changes When the service queries providers Then it uses the location’s correct local time offset for the window and encodes queries accordingly And rounds the window to provider granularity (e.g., 15m) without exceeding ±5m from the requested window And minor location changes within 100m during cache_TTL do not trigger new provider calls unless they would change the severity level by ≥1 step And severity is calculated using only weather data intersecting the appointment window
Reliability-Based Eligibility Rules
"As a groomer, I want eligibility to factor in client reliability so that grace is generous to trustworthy clients without encouraging abuse."
Description

Compute a client reliability score using historical attendance, late cancellations, no-shows, timely payments, deposit behavior, and prior Weather Grace usage. Provide configurable thresholds and lookback windows to determine eligibility tiers that interact with weather severity to decide which policy softeners can apply. Support per-client overrides, business-hour constraints, and exclusion conditions (e.g., first-time clients, outstanding balances). Expose eligibility outcomes and contributing factors to staff for review while keeping the scoring model explainable and auditable.

Acceptance Criteria
Reliability Score Computation with Configurable Lookback
Given a scoring configuration with a 180-day lookback and weights: completed_visit=+5, late_cancel=-10, no_show=-20, on_time_payment=+3, deposit_paid_on_request=+4, prior_weather_grace_use=-5 And a client with events within 180 days: 4 completed visits, 1 late cancel, 0 no-shows, 4 on-time payments, 3 deposits paid, 1 prior Weather Grace use When the reliability scoring job runs Then the client score is calculated as 29 on a 0–100 scale and persisted with timestamp and parameters used And the score is visible in staff view alongside the factor breakdown counts Given the lookback is changed to 365 days and the same weights apply And the client has 2 additional completed visits and 1 additional on-time payment between 181–365 days ago When the scoring job re-runs Then the score remains 29 because only events within the active 180-day lookback are included Given a client has fewer than the configured minimum 3 events within the lookback When scoring runs Then the score is calculated from available data, flagged as low-data=true, and the default tier fallback is marked in the result
Eligibility Tier Determination by Weather Severity Matrix
Given tier thresholds are configured as Bronze=0–49, Silver=50–79, Gold=80–100 And a severity matrix for Moderate is configured as: Bronze=none, Silver=hold_extension_12h, Gold=late_cancel_forgiveness + hold_extension_24h (fee_waiver not allowed) And a client has a reliability score of 82 When a Moderate severity weather event triggers evaluation Then the client is assigned tier=Gold and allowed_softeners=[late_cancel_forgiveness, hold_extension_24h] and denied_softeners=[fee_waiver] with reasons Given a client has a reliability score of 45 When a Moderate severity weather event triggers evaluation Then the client is assigned tier=Bronze and allowed_softeners=[] and denied_softeners=[late_cancel_forgiveness, fee_waiver, hold_extension_12h, hold_extension_24h] with reasons
Per-Client Overrides Precedence and Effective Dates
Given a client has an active override ForceTier=Silver effective 2025-08-01 to 2025-08-31 And their computed score would place them in Gold When evaluation occurs on 2025-08-15 Then the system applies tier=Silver due to override and records override_id and precedence path Given the same client also has an active override DisableWeatherGrace=true for 2025-08-10 to 2025-08-12 When evaluation occurs on 2025-08-11 Then no softeners are allowed regardless of score or tier and staff view shows reason=Override:Disabled Given multiple overrides overlap When evaluation runs Then precedence is applied as DisableWeatherGrace > ForceTier > AllowSpecificSoftener and the audit log includes the evaluated order and the winning override
Exclusion Conditions: First-Time, Outstanding Balance, and Flagged Accounts
Given a client with zero completed visits When a weather grace evaluation is triggered Then eligibility_status=Excluded and exclusion_reason=First-Time Client and no softeners are applied Given a client with an outstanding balance greater than $0 When evaluation runs Then eligibility_status=Excluded and exclusion_reason=Outstanding Balance and the balance amount is displayed to staff Given a client is marked account_flag=WeatherGraceAbuse When evaluation runs Then eligibility_status=Excluded and exclusion_reason=Flagged Account: WeatherGraceAbuse and no softeners are applied Given exclusions are cleared (balance=$0 and flag removed) and the client completes their first visit When evaluation runs again Then normal eligibility computation and tiering are applied
Business Hours and Timezone Constraints for Applying Softeners
Given organization timezone=America/Chicago and business hours=Mon–Fri 09:00–18:00 And severity_snapshot_policy=trigger_time When a weather grace evaluation is triggered at 20:15 on Friday Then the decision is queued and applied at 09:00 on the next business day (Monday) using the severity recorded at 20:15 Friday Given a weather grace evaluation is triggered at 10:30 on Tuesday When evaluation runs Then the decision is applied immediately and the action timestamp is within the business hours window Given the organization is closed on weekends When an evaluation is triggered on Saturday at 10:00 Then the decision is queued and applied at 09:00 on Monday with queued=true indicated in the audit record
Explainability and Audit Trail Visibility to Staff
Given staff opens the eligibility details for a specific client and event When the details panel loads Then it displays: score value, lookback window, counts by factor (completed visits, late cancels, no-shows, on-time payments, deposits, prior Weather Grace uses), factor weights, computed tier, weather severity, allowed/denied softeners with reasons, any overrides/exclusions applied, and the scoring model version Given an eligibility decision is made (allow or deny) When the audit record is created Then it immutably stores: timestamp, actor (system/user), parameters (weights, thresholds, lookback, business-hour policy), raw event IDs used in scoring, computed score, tier, severity, outcome, overrides evaluated and precedence, and a hash/signature for tamper detection Given staff filters the audit log by client and date range When results are returned Then all matching decisions are listed with pagination and can be exported as CSV and JSON including the fields above (excluding raw PII beyond IDs)
Configurable Policy Softeners & Caps
"As an operations manager, I want to configure which softeners apply under defined conditions so that we balance customer goodwill with revenue protection."
Description

Provide a rule engine that maps weather severity and client eligibility to specific policy softeners, including late-cancel forgiveness, fee waivers (fixed or percentage), deposit forgiveness or conversion to credit, and brief hold extensions. Allow configuration of caps by client and by weather event (e.g., max two waivers per storm), precedence handling across overlapping rules, and a simulation mode to preview impacts before activation. Persist a structured grace decision record per appointment containing rule version, inputs, actions, and expirations for downstream systems.

Acceptance Criteria
Apply softeners based on severity and client reliability
Given configured rules mapping severity levels [Advisory, Watch, Warning] and client reliability bands [A, B, C] to actions And an appointment with weather_event_id=S123, severity=Warning, client_reliability=A, and eligibility gates met When the rule engine evaluates the appointment Then it returns an action set exactly matching the (Warning, A) rule configuration And each action includes type, parameters (amount/percent/minutes), currency where applicable, and expiry/validity if applicable And if eligibility gates fail, no actions are returned and the decision includes reason_code=INELIGIBLE
Enforce caps per client and per weather event
Given caps configured: per_client.max_waivers_30d=1 and per_event.max_waivers=2 for weather_event_id=S123 And client_id=C456 has waivers_used_30d=1 and waivers_used_in_event[S123]=2 When evaluating a new appointment under S123 Then no waiver-type actions are emitted And the decision includes caps_applied with details {type:"per_event.max_waivers", limit:2, used:2} and {type:"per_client.max_waivers_30d", limit:1, used:1} And non-waiver actions (e.g., hold extension) still apply if not capped
Resolve overlapping rules by precedence
Given two rules R1 and R2 both match an appointment And precedence order is [R1, R2] And R1 defines fee_waiver_percent=50 and R2 defines fee_waiver_fixed=10 When precedence_mode="first_wins" Then the resulting action set includes only fee_waiver_percent=50 from R1 When precedence_mode="merge" and conflict_rule="higher_monetary_benefit" And invoice_total=40 Then the resulting action set includes a single fee_waiver of $20 (from 50%) and excludes R2 And all actions include source_rule_id
Run simulation mode to preview impacts
Given simulation_mode=ON for weather_event_id=S123 and date_range=2025-08-15..2025-08-16 When the engine runs a simulation Then it returns a preview containing totals (appointments_evaluated, appointments_impacted, total_waived_amount, total_deposits_converted, total_hold_minutes) And no appointment, payment, or credit records are created or modified And each appointment in the preview includes a non-persisted decision payload with simulation=true And enabling simulation_mode does not emit downstream events
Calculate fee waivers and deposit conversions accurately
Given rule supports fee_waiver_fixed and fee_waiver_percent with optional max_amount and rounding=nearest_cent and base=invoice_total|service_subtotal And invoice has service_subtotal=$80, tax=$6.40, deposit_collected=$25 When applying fee_waiver_percent=25 with base=service_subtotal and max_amount=$30 Then waiver amount is $20.00 and never exceeds $30 and rounds to nearest cent When applying fee_waiver_fixed=$15 Then waiver amount is $15.00 When applying deposit_conversion_to_credit with expiration_days=30 Then a client credit of $25.00 is created with expiration in 30 days and invoice remains settled for the original deposit And total payable never goes below $0.00
Apply brief hold extensions within limits
Given original hold_expires_at=10:30 and configured hold_extension_minutes=10 with per_appointment.max_extension_minutes=20 and per_event.max_extension_minutes=45 When applying the hold extension Then new hold_expires_at=10:40 and does not exceed either max And the action includes {type:"hold_extension", minutes:10, new_expires_at, source_rule_id} And if applying an extension would exceed a cap, the extension is reduced to the maximum allowable and a cap notice is recorded
Persist structured grace decision record per appointment
Given a real evaluation (simulation_mode=OFF) produces actions for appointment_id=A789 When persisting the decision Then a GraceDecision record is stored linked to A789 containing: rule_version, weather_event_id, severity, client_id, eligibility_inputs (reliability_band, waiver_counts), actions[], caps_applied[], precedence_mode, evaluated_at, expirations, actor="system" And the record is immutable and retrievable via API GET /appointments/A789/grace-decision returning HTTP 200 within 300ms And a downstream event "grace.decision.created" is published with the record id and appointment id
One-Tap SMS Acknowledgment Flow
"As a client, I want to accept revised terms in one tap via SMS so that I know what to expect without calling or emailing."
Description

Generate templated SMS messages with dynamic variables that present the applicable Weather Grace terms and a one-tap acceptance link or reply keyword. On acceptance, record timestamp, channel identifiers, and terms version, then auto-send a confirmation message. Support expiration windows, reminders, and fallback to standard policy if not accepted in time. Log consent artifacts to the appointment and client record, and expose a simple staff view to resend or revoke offers. Ensure messages comply with carrier and regional messaging guidelines.

Acceptance Criteria
Weather Grace Offer SMS Generation with Dynamic Variables
Given a qualifying appointment is eligible for Weather Grace and acceptance_mode is set to link+keyword and expiration_window is 2 hours When the system generates and sends the offer Then the SMS body is rendered from the selected template with variables populated: client_name, service_date (client local), severity_label, grace_terms_summary, expiration_at (client local), accept_link, reply_keyword And an offer record is persisted with offer_id, appointment_id, client_id, template_id, acceptance_mode, expiration_at, status='Sent' And the outbound provider message_id and from/to MSISDNs are logged and associated to offer_id And the message is queued and dispatched successfully with delivery status tracked (Queued|Sent|Delivered|Failed)
One-Tap Acceptance Recording and Confirmation
Given a Weather Grace offer with status='Sent' or 'Reminded' When the client taps the accept_link before expiration Then the offer status updates to 'Accepted' with accepted_at (UTC), source='link', source_event_id (click_id), terms_version, and originating message_id recorded And a confirmation SMS is auto-sent within 10 seconds summarizing applied terms and including HELP/STOP language And the appointment record reflects the applied grace flags (e.g., late-cancel fee waived, hold extended) and captures the applied terms_version And a consent artifact snapshot (offer text, template_id, terms_version, timestamps, from/to MSISDNs, client_id, appointment_id) is created and stored immutably Given the client instead replies with the configured reply_keyword before expiration When the keyword is received from the same MSISDN Then the same acceptance, confirmation, appointment update, and artifact behaviors occur with source='keyword' and source_event_id=provider_message_id
Offer Expiration, Reminder, and Fallback to Standard Policy
Given a Weather Grace offer with expiration_window=2 hours and reminder_offset=60 minutes When no acceptance is recorded by 60 minutes before expiration Then one reminder SMS is sent containing the expiration time and accept options, and offer status updates to 'Reminded' When the expiration time passes without acceptance Then the offer status updates to 'Expired', the accept_link and keyword are invalidated, and attempts to accept return an 'Offer expired' response SMS And the appointment reverts to standard policy with no grace applied, and staff are notified in the staff view of the expiration And no further reminders are sent after expiration
Consent Artifact Logging on Appointment and Client Records
Given a Weather Grace offer is accepted (via link or keyword) When the system persists the consent artifact Then the artifact includes: offer_id, appointment_id, client_id, terms_version, template_id, offer text snapshot, originating and confirmation message_ids, from/to MSISDNs, accepted_at (UTC), source, source_event_id, and a content hash And the artifact is linked to both the appointment record and the client record and is immutable (write-once) And the artifact can be retrieved in staff view and exported as PDF and JSON with identical hash And retrieving by appointment_id or client_id within a date range returns the artifact in under 2 seconds for datasets up to 10k artifacts
Staff View: Resend and Revoke Offers
Given a pending Weather Grace offer with status='Sent' or 'Reminded' When a staff user selects 'Resend' Then a new offer is created with a new offer_id, fresh expiration, and a new SMS is sent referencing the updated expiration; the prior offer is marked 'Superseded' and its link/keyword disabled And an audit entry records user_id, action='Resend', timestamp (UTC), prior_offer_id, new_offer_id Given a pending Weather Grace offer When a staff user selects 'Revoke' Then the offer status updates to 'Revoked', the accept_link/keyword are immediately disabled, and client attempts to accept receive an 'Offer revoked' response SMS And an optional client notification template can be sent if enabled, and an audit entry records user_id, action='Revoke', and timestamp
Messaging Compliance: Carrier and Regional Guidelines
Given any Weather Grace outbound SMS (offer, reminder, confirmation) When the message is composed and dispatched Then the SMS includes business identification and opt-out instructions (e.g., 'Reply STOP to opt out, HELP for help') And links use a registered, carrier-compliant branded domain (no public URL shorteners) and map to the approved 10DLC campaign/brand where applicable And regional quiet hours are enforced (e.g., US recipients do not receive reminders between 9pm–8am local) unless explicitly overridden by an admin setting captured in the audit log And recipients who reply STOP are immediately marked opted-out; all pending offers to that MSISDN are canceled, no further reminders are sent, and an internal alert is logged And rate limits are respected to stay within carrier thresholds, with any throttling or failures logged and visible in delivery status for the offer
Billing and Deposit Adjustment Sync
"As a bookkeeper, I want fee waivers and deposit changes to flow automatically to billing so that our records are accurate without manual exceptions."
Description

Automatically apply approved softeners to invoices and deposits by adjusting line items, applying credits, or issuing partial refunds through the connected payment processor. Reflect changes in the client ledger with clear memo lines referencing the weather event and policy rule. Recalculate taxes and fees as needed, regenerate receipts, and prevent manual discrepancies by enforcing a single source of truth. Provide reconciliation reports showing waived amounts, recovered revenue, and deposit treatments over time.

Acceptance Criteria
Auto-Apply Weather Softener to Invoice Line Items
Given an approved Weather Grace softener for Appointment A with Invoice I and ruleId WG-LATE-CANCEL And Invoice I contains a "Late Cancellation Fee" line of amount $X When the Billing and Deposit Adjustment Sync runs Then the system adjusts Invoice I by setting the "Late Cancellation Fee" line to $0 OR adding a discount line equal to -$X per configuration And the invoice subtotal, tax, and total are recalculated And the adjustment is tagged with reason code "WeatherGrace:WG-LATE-CANCEL" and the weatherEventId And the invoice payment state (Paid/Partially Paid/Unpaid) remains accurate after recalculation And the change is applied exactly once even if the sync is invoked multiple times for the same softener
Deposit Adjustment via Connected Processor
Given Appointment A had a deposit of $Y captured via Processor P with transactionId T And the approved softener requires a partial refund of $R where 0 < R ≤ Y When the sync executes Then Processor P receives a partial refund request for $R referencing transactionId T And the system records refundId R_ID and links it to Invoice I and Client C And the client ledger shows a "Deposit Partial Refund" entry for $R with memo "WeatherGrace:[ruleId]" and weatherEventId And no duplicate refund is issued if the operation is retried
Tax/Fee Recalculation and Receipt Regeneration
Given a taxable invoice I with jurisdiction J and receipt version v When Weather Grace adjustments modify invoice amounts Then taxes are recalculated per jurisdiction J rules on the adjusted taxable base And platform/service fees are recomputed according to policy after discounts And monetary rounding uses half-up to the nearest $0.01 And a new receipt version v+1 is generated with updated totals and a note that it supersedes v And the previous receipt remains accessible but marked void/superseded And the client receives an updated receipt link via SMS in the existing thread
Client Ledger Memo and Reference Integrity
Given Weather Grace adjustments have been applied to Client C When viewing C's ledger Then each related entry contains memo "WeatherGrace:[ruleId] [eventDate] [severity]" and the weatherEventId And entries include references to invoiceId and processor transactionId where applicable And the ledger running balance equals the arithmetic sum of entries within ±$0.01 And searching by weatherEventId or "WeatherGrace" returns the corresponding entries
Single Source of Truth and Manual Edit Guardrails
Given Invoice I was modified by the Weather Grace sync When a user attempts to manually edit adjusted lines or totals in the UI Then the system blocks the change and displays "Locked by Weather Grace sync; use Adjustment Flow" And any attempted backend divergence from Processor P is rejected by validation And the nightly consistency check reports zero discrepancies between PawPilot and Processor P for synced invoices
Reconciliation Reporting of Waivers and Deposits
Given a selected date range D1..D2 with filters for location L and provider PR When generating the Weather Grace Reconciliation report Then the report shows totals for Waived Amounts, Recovered Revenue, Deposits Partially Refunded, and Deposits Retained as Credit And includes line-level rows with invoiceId, clientId, amount, ruleId, weatherEventId, and processor transactionId And totals reconcile within ±$0.01 to the sum of line-level rows and to the ledger totals for the same period and filters And the report exports to CSV and completes within 10 seconds for up to 10,000 rows And all timestamps are presented in the account's timezone
Idempotent Re-Processing and Retry Handling
Given a Weather Grace softener application S with id S_ID has been processed When the sync is re-run due to retry, admin action, or webhook replay Then no additional discounts, credits, or refunds are created for S_ID And processor failures are retried with exponential backoff for up to 24 hours And the operation status transitions to Succeeded or Failed with error details visible to admins And partial application triggers compensating rollback or marks the case as Needs Attention with no net monetary discrepancy
Smart Waitlist Coordination
"As a scheduler, I want waitlist logic to respect weather-related holds so that we keep the calendar accurate while maximizing backfill opportunities."
Description

Coordinate Weather Grace hold extensions with the Smart Waitlist by dynamically adjusting offer windows, pausing auto-fill for impacted slots, and recalculating availability when grace is accepted or expires. Communicate status changes to waitlisted clients with reason-aware messages, and ensure atomic updates to avoid double-booking. Provide staff with a timeline view showing holds, extensions, and waitlist actions driven by weather-related changes.

Acceptance Criteria
Dynamic Offer Window Adjustment on Weather Grace Hold
Given a slot with pending waitlist offers and a Weather Grace hold is activated When the hold extension duration is set to E minutes Then the expiration time of all pending offers for that slot is increased by E minutes without exceeding the configured maxOfferWindow And offers for other slots remain unchanged And the updated expiration timestamps are persisted within 1 second of the extension And an OfferWindowAdjusted event is emitted containing slotId, extensionMinutes=E, and affectedOfferCount
Auto-Fill Pause for Weather-Impacted Slots
Given Smart Waitlist auto-fill is enabled and a slot is marked WeatherGrace=Active When the system evaluates auto-fill for that slot during the hold window Then no new offers are dispatched for that slot And the auto-fill job for that slot is marked Paused with reason=WeatherGrace And upon hold expiration or cancellation, auto-fill resumes within 60 seconds and the job state transitions to Active And message logs confirm zero outbound offers for the paused interval
Availability Recalculation on Grace Acceptance and Expiry
Given a booked client receives a Weather Grace acknowledgment link for their slot and the slot has associated waitlist offers When the client acknowledges within the specified grace window Then the slot state changes to Reserved(WeatherGrace) for the configured extension duration And all pending waitlist offers for that slot are cancelled with reason=WeatherGrace and recipients are notified within 60 seconds And the slot is excluded from availability queries during the extension When the grace window expires without acknowledgment Then the slot is released to the waitlist And auto-fill evaluation runs within 60 seconds And previous holds are cleared from the timeline with a GraceExpired entry
Reason-Aware Messaging to Waitlisted Clients
Given a waitlisted client has an active offer for a slot that transitions due to Weather Grace When the slot status changes to HoldApplied, HoldExtended, or HoldReleased Then the client receives a single SMS per change within 60 seconds including the change type, updated response-by time, and reason 'Weather Grace' And the SMS suppresses duplicate sends for the same change id And the message template selected matches the configured severity band and client reliability segment for the slot And message delivery status is tracked to Delivered or Failed within 5 minutes
Atomic Booking Updates Under Concurrency
Given concurrent events target the same slot (e.g., staff manual booking, waitlist accept, grace acknowledge) When these events are processed Then exactly one operation may commit a reservation for the slot And all losing operations receive a consistent conflict error and are rolled back with no partial side effects (no SMS, no payment holds) And the final slot record reflects a single version increment and a unique reservationId And the audit log contains a total order of events with the winning transaction marked Committed and others Aborted
Staff Timeline Shows Weather-Driven Holds and Waitlist Actions
Given a staff member views the timeline for a day or slot with Weather Grace activity When a hold is applied, extended, expires, or affects waitlist offers Then the timeline lists entries with timestamp, actor (System/Staff/Client), action, reason=WeatherGrace, and before→after state And updates appear within 2 seconds without page refresh And entries are sorted by event time and can be filtered by reason=WeatherGrace and slotId And exporting the current view yields a CSV containing at least the columns: time, actor, action, slotId, clientId, reason
Audit Trail and Impact Reporting
"As a product owner, I want visibility into how Weather Grace affects cancellations, revenue, and client satisfaction so that we can fine-tune policies with data."
Description

Maintain an immutable audit trail for each Weather Grace decision, including inputs (weather, reliability), rule versions, staff actions, client acknowledgments, and financial adjustments. Offer dashboards and exports to analyze usage frequency, waived amounts, no-show deltas, acceptance rates, and client segments affected. Enable filtering by date range, location, weather event, and rule, with data retention aligned to policy and privacy requirements and tooling to honor deletion requests.

Acceptance Criteria
Immutable Audit Record Creation
Given a confirmed Weather Grace application for a booking at a specified location caused by a defined weather event and a known client reliability score under a particular rule version When the decision is applied by a staff user or auto-applied by the system Then an append-only audit record is created with fields: decision_id, created_at (UTC ISO-8601), booking_id, client_id, staff_id or "system", location_id, weather_event_type, weather_severity_index, weather_source, weather_payload_sha256, client_reliability_score, reliability_inputs_ref, rule_id, rule_version, action_type, pre_fee_amount, waived_fee_amount, deposit_adjustment_amount, rationale, channel, terms_version And the audit record cannot be edited via UI or API; any update attempt returns HTTP 403 and persists no changes And any correction is recorded as a new compensating audit record that references the original decision_id in parent_decision_id
Client Acknowledgment Linking and Capture
Given a client receives a Weather Grace SMS containing an acknowledgment link for a specific decision When the client taps the link and accepts the terms Then the system records an acknowledgment with fields: decision_id, acknowledged_at (UTC), message_id, msisdn (E.164), ack_ip_hash, terms_version, ack_method="SMS", ack_status="accepted" And the audit trail entry links one-to-one to the acknowledgment And if the client declines, ack_status="declined" is recorded and linked And repeat taps are idempotent and do not create duplicate acknowledgments
Impact Dashboard Metrics Accuracy
Given a seeded dataset of Weather Grace decisions within a known date range and locations with predetermined expected aggregates When a manager views the dashboard filtered to that date range with all locations and all weather events Then the dashboard displays metrics that match the fixture's expected values, including: total_decisions, acceptance_rate, total_waived_amount, average_waived_amount, and no_show_delta And metric definitions are available on hover, with no_show_delta defined as (no-show rate 30 days post minus 30 days pre among affected clients) And the dashboard loads within 3 seconds at the 95th percentile for the dataset size used in test
Multi-Dimensional Filtering Functionality
Given audit trail data spans multiple dates, locations, weather events, and rule versions When the user applies filters: date_range, location(s), weather_event_type, rule_id, and rule_version Then dashboard counts, tables, and charts reflect only matching audit records And all filters are combinable and persisted in the URL query string for shareable views And time boundaries are inclusive of start at 00:00 and end at 23:59:59 in the account timezone, while exported timestamps are in UTC
Data Export Completeness and Schema
Given a user with Manager role views filtered dashboard results When the user exports to CSV and JSON Then files are generated containing exactly the records that match the applied filters and fields defined by schema v1.0 And CSV includes a header row and uses UTF-8 with RFC 4180 quoting; JSON includes top-level schema metadata And monetary amounts include both integer cents and formatted currency strings; timestamps are ISO-8601 UTC; IDs are strings And export row counts equal the on-screen total, and download links expire within 15 minutes
Data Retention and Deletion Compliance
Given the account data retention policy is set to 24 months When current time exceeds created_at + 24 months for an audit record Then the record is removed from user-accessible views and exports within 24 hours while aggregate metrics remain intact And when a verified subject deletion request for a client is received Then within 7 days all client-identifying fields in audit and acknowledgment records are irreversibly deleted or pseudonymized, with client_id replaced by a tombstone token for reconciliation And each deletion action is written to a separate compliance audit log
Access Control and Tamper Evidence
Given role-based access controls are configured When a user without Reports:View permission attempts to access dashboards or exports Then access is denied with HTTP 403 and the attempt is logged And when an administrator invokes the audit verification endpoint for a date range Then the system returns a valid tamper-evidence proof (e.g., hash chain/merkle root) for records in range, and any altered record breaks verification and is flagged

Wave Orchestrator

Design and release booking invites in timed waves by client tier, promo list, or service type. Set per‑wave quotas and countdowns so regulars, VIPs, and first‑timers all get fair access. Deposits and holds are auto‑managed per wave, filling the day fast without overbooking or manual juggling.

Requirements

Wave Configuration & Segmentation Engine
"As an independent groomer, I want to configure booking waves by client tier, list, and service type so that I can release slots fairly and efficiently without manual sorting."
Description

Enable creation of booking waves targeted by client tier (VIP, regulars, first-timers), promo list membership, service type, and location. Configure wave start times, eligibility rules, per-service inclusion, and reusable templates. Pulls live segments from PawPilot CRM (tags, history, balances, opt-outs) up to wave launch to keep eligibility current. Includes guardrails to exclude ineligible clients (e.g., outstanding balance, banned, opt-out) and supports combining multiple services in the same day. Delivers fair access while reducing manual sorting and setup time.

Acceptance Criteria
Create Wave with Multi-Attribute Targeting
Given clients exist across tiers (VIP, Regular, First-Timer), promo lists, services, and locations When I configure a wave with Tier = VIP, Promo List = "Holiday2025", Service = "Full Groom", Location = "Downtown" Then the computed eligible set equals the intersection of those filters And the system displays the eligible count for verification And at wave launch, 100% of the final eligible set members match all selected filters
Live Segment Refresh Until Launch
Given a wave start time of 2025-10-01 10:00 in the account timezone And eligibility depends on CRM attributes (tags, history, balances, opt-outs) When any of those attributes change before 10:00 Then the preview and computed eligible set update within 1 minute And at 10:00 the final eligible set is derived from the latest CRM state When those attributes change after 10:00 Then the final eligible set for that wave remains unchanged
Guardrail Exclusions Applied
Given some clients match targeting filters but have an outstanding balance > $0, status = banned, or SMS opt-out = true When the eligible set is computed Then 0 of those clients appear in the eligible set And the system reports counts per exclusion reason (balance, banned, opt-out)
Reusable Wave Templates
Given I save a wave configuration as a template named "VIP Grooming" When I create a new wave from that template Then tier, promo list, services, location, start time offset, and guardrail settings are prepopulated And I can override any prepopulated field prior to saving And saving the wave does not alter the original template definition
Per-Service Inclusion and Same-Day Combination
Given date = 2025-10-01 and services selected = ["Full Groom","Bath"] When the eligible set is computed Then only clients eligible for at least one of the selected services are included And no clients for unselected services are included And the result indicates the service type(s) for which each client is eligible
Location Scoping for Eligibility
Given locations "Downtown" and "Uptown" exist and clients are linked to locations When I select Location = "Downtown" Then only clients linked to "Downtown" are included in the eligible set When no location is selected Then clients from all locations are eligible When a client is linked to both locations and one location is selected Then that client is included as eligible
Wave Start Time and Timezone Accuracy
Given account timezone = America/Los_Angeles and wave start time = 2025-10-01 09:00 When the system schedules eligibility finalization Then the final eligible set is locked within ±1 minute of 09:00 PT And no eligibility is locked before the configured start time When the start time is edited before launch to 10:00 Then the schedule updates and eligibility continues to refresh until 10:00
Per-Wave Quotas, Holds, and Deposit Rules
"As a sitter, I want per-wave quotas and automatic holds with deposits so that I can fill slots fast without overbooking or chasing payments."
Description

Set per-wave quotas by service and duration, enforce temporary slot holds when a client opens the booking link, and automatically release holds on timeout. Configure deposit requirements per wave (required/optional, amount, pre-auth vs charge, expiration/refund rules) with exceptions for tiers (e.g., VIP deposit waivers). Ensures quotas are respected across simultaneous attempts, prevents oversubscription, and secures revenue commitment before confirmation. Fully integrated with PawPilot payments and audit logging.

Acceptance Criteria
Enforce Per‑Wave Service/Duration Quotas Under Concurrency
Given a wave configured with quotas per service and duration (e.g., Groom 60m: 4, Walk 30m: 6) And available schedule matches these quotas And 20 clients simultaneously request Groom 60m via the wave link When requests are processed Then the sum of confirmed bookings + active holds for Groom 60m never exceeds 4 And the 5th and later clients receive a waitlist or next‑wave response without a booking or hold And quotas for other services and durations remain unaffected And releasing any hold immediately makes exactly one slot available for the next eligible requester
Temporary Holds on Link Open and Auto‑Release on Timeout
Given a wave with a configured hold timeout T And at least one slot matches the client's requested service and duration When the client opens the booking link and selects a time Then the system creates a temporary hold on that slot for that client And the slot becomes unavailable to other clients during the hold And a countdown timer visible to the client reflects the remaining hold time T When T elapses without deposit or confirmation Then the hold is automatically released And the slot is returned to the pool and becomes immediately bookable by others
Single Client Multi‑Tab and Multi‑Device Hold Control
Given a client opens the booking link in multiple tabs or devices for the same wave When a new hold is initiated by the same client Then only one active hold is maintained per client per wave And the newest hold replaces the prior hold, releasing the prior slot And no duplicate holds are created And hold ownership is validated by phone number and session token to prevent transfer
Deposit Enforcement with Tier‑Based Exceptions
Given a wave deposit policy configured as Required: true, Amount: A, Mode: Pre‑Auth or Charge, and VIP tier deposit waiver enabled And the client's tier is VIP When the client proceeds to confirm Then the system skips deposit and confirms the booking And the quota is decremented accordingly Given the client's tier is Standard When the client proceeds to confirm Then the system attempts the configured deposit (pre‑auth or charge) for amount A via PawPilot Payments And on success, the booking is confirmed And on failure, the hold is immediately released and the client is informed with a recoverable error to retry
Deposit Expiration and Refund Rules
Given a wave configured with deposit Mode: Pre‑Auth, Expiration: E hours, and an expiry policy set to Re‑auth or Cancel When a pre‑auth reaches E hours without capture Then the system executes the configured expiry policy (re‑auth or cancel) And the client is notified of the outcome Given deposit Mode: Charge with refund rules Full refund >= R hours before appointment and No refund < R hours When a client cancels at least R hours before the appointment Then the system automatically issues a refund of the deposit amount to the original payment method And when cancellation occurs inside R hours, no refund is issued
Audit Logging and Payment Integration Coverage
Given any of the following events occur within a wave: hold created, hold replaced, hold released (timeout or user), deposit attempted, deposit succeeded, deposit failed, pre‑auth re‑auth, deposit refunded, booking confirmed, booking canceled due to payment failure When the event occurs Then a corresponding immutable audit log entry is written containing at minimum: timestamp (UTC), wave ID, client ID, event type, service and duration, slot time, payment amount and mode (if applicable), actor (system or client) And audit entries are retrievable by wave ID and exportable in CSV and JSON And payment transactions are processed via PawPilot Payments with idempotency to prevent double charges under retries
Global Slot Capacity Maintained Across Overlapping Waves
Given two waves targeting the same provider and day with overlapping slot pools And each wave has its own quotas When clients from both waves attempt to book the same slot concurrently Then the underlying provider slot is atomically allocated to at most one booking across all waves And the losing attempt receives a clear, actionable message and is offered the next available time or waitlist And per‑wave quotas are decremented only upon successful allocation for that wave
Timed SMS Invite Sequencing with Countdown
"As a dog walker, I want automated SMS invites with clear countdowns by wave so that clients know exactly when to book and I don’t need to hand-send messages."
Description

Schedule and deliver SMS booking invites in timed waves with personalized, secure links and a mobile landing page that displays a live countdown to open and time remaining before expiry. Supports pre-announcements, reminders, message templates, A/B variants, throttling, retries, quiet hours, and timezone-aware scheduling. Fallback to email if SMS is undeliverable. Provides clear client experience about when access opens and closes, increasing conversion and reducing back-and-forth.

Acceptance Criteria
Live Countdown and State Transitions on Mobile Landing Page
Given a wave with openAt and expiresAt timestamps and a client opening the invite link When the page is loaded before openAt Then it displays a live countdown to open with second-level accuracy, localized to the client's timezone, and no manual refresh is required. Given the moment openAt is reached When the page is already open Then the CTA to "Book Now" becomes enabled within 1 second and the countdown switches to "Time remaining" until expiresAt. Given expiresAt has passed When the link is visited Then the page shows an "Invite expired" state with clear next steps and disables booking actions. Rule: Countdown time source is server-synchronized at load and re-syncs at least every 60 seconds; cumulative drift <= 1 second per 10 minutes.
Secure, Personalized Invite Links
Given an invite is generated for a client in a wave When the link is created Then it contains a signed, time-bounded token binding clientId, waveId, and expiresAt. Given the link is accessed without a valid signature or after expiry When the landing page is requested Then it returns a 403/invalid state and no booking actions are available. Given the link is used to complete a booking When it is visited again Then it redirects to a receipt/status page and cannot initiate a new booking. Rule: Tokens are single-use for booking initiation, use 128-bit (or stronger) entropy, and are not guessable; invalid access attempts are logged.
Pre-Announcement and Reminder Sends
Given a wave with openAt configured with preAnnouncementOffset and reminderOffset When scheduling messages Then a pre-announcement SMS is queued at openAt - preAnnouncementOffset and a reminder at openAt - reminderOffset, both evaluated in the recipient's timezone. Rule: Messages are deduplicated per recipient per wave; STOP/DNC/opt-out states suppress sends. Rule: Template variables (e.g., {client.firstName}, {openAt}, {expiresAt}, {link}) render correctly; preview matches sent content.
Throttling, Retries, and Quiet Hours Enforcement
Rule: SMS sends are throttled to the configured rate limit (e.g., <= X messages/second/account); overflow is queued FIFO with tier priority. Given a transient carrier error When sending an SMS Then the system retries up to 3 times with exponential backoff (e.g., 1m, 5m, 15m) and stops on success or hard-fail codes. Given quiet hours are configured (e.g., 9pm–8am recipient local time) When a send or retry would occur during quiet hours Then it is deferred to the next allowed window while preserving relative timing to openAt and not missing the post-open reminder. Rule: All send attempts log carrier response codes, timestamps, and final delivery status.
Timezone-Aware Scheduling and Display
Given clients across multiple timezones and a wave defined in the business's home timezone When messages are scheduled Then send times and quiet-hour checks use the recipient's timezone, and landing page timestamps display in the client's local timezone with the business timezone indicated (e.g., "Opens 10:00 AM your time"). Rule: If a recipient's timezone is unknown, default to the business timezone and flag in logs; counts of "unknown timezone" are reported per wave.
A/B Template Variant Assignment and Metrics
Given two SMS templates A and B with a defined split (e.g., 50/50) When a wave is executed Then recipients are randomly and deterministically assigned a variant and receive that template for all communications in the wave. Rule: For each variant, track sends, deliveries, clicks (invite link opens), and conversions (completed bookings) with counts and rates visible per wave. Rule: Variant assignment and performance metrics are exportable as CSV.
Fallback to Email on SMS Undeliverable
Given an invite SMS results in a hard failure or all retry attempts are exhausted When a valid, consented email exists for the recipient Then an email invite with the same personalized link and timing information is sent within 5 minutes and logged as "SMS fallback -> Email". Rule: No email is sent without consent or a valid address; the event is logged as "No fallback channel". Rule: Email contains equivalent content and CTA, including open/expiry times and countdown behavior where supported.
Real-time Availability & Overbooking Safeguards
"As a groomer, I want real-time availability with safeguards so that simultaneous bookings at wave launch don’t cause double-bookings."
Description

Provide an availability service that updates in real time as holds and confirmations occur, with atomic reservation and distributed locking to prevent double-booking at wave launch. Respect service durations, buffers, staff/resources, and overlapping service constraints. Display only valid, up-to-the-moment slots on the landing page. Ensure only deposit-confirmed bookings decrement quotas. Handles timezone and daylight saving transitions and gracefully resolves conflicts with clear client messaging.

Acceptance Criteria
Atomic Holds at Wave Launch
Given a wave opens at T0 with 50+ simultaneous requests for the same slot, When the Hold Slot endpoint processes the requests, Then exactly one hold is created and all others receive a slot-unavailable response within 1 second. Given a hold is created with a configured TTL, When the TTL elapses without deposit confirmation, Then the hold auto-expires and the slot returns to availability within 2 seconds. Given a client retries a hold within the TTL, When the request includes the same idempotency key, Then the same holdId is returned and no additional holds are created. Given concurrent requests target the same staff/resource-time window, When locking is applied, Then no two overlapping holds are recorded for that window.
Real-Time Landing Page Availability
Given the landing page is open for an active wave, When a slot changes state due to hold creation, expiration, or deposit confirmation, Then the visible slots update to the current state within 2 seconds. Given a user taps a slot that became unavailable since last render, When the booking attempt is submitted, Then the server returns 409 Conflict with a clear message and a payload of alternative slots. Given the realtime channel is unavailable, When polling occurs at the configured interval, Then slot staleness does not exceed the polling interval and only currently bookable slots are actionable.
Quota Decrements Only on Deposit Confirmation
Given a hold is created, When no deposit is collected, Then the wave quota remains unchanged. Given a deposit succeeds for a booking, When payment callbacks/webhooks are retried, Then the quota decrements exactly once (idempotent) and cannot go below zero. Given a deposit fails or times out, When the hold expires, Then the quota does not decrement and the slot returns to inventory. Given the wave quota reaches zero, When new hold requests arrive, Then they are rejected; holds created prior to exhaustion may complete if deposit is confirmed within the valid TTL.
Service Durations, Buffers, and Overlap Constraints
Given a service has a defined duration and buffer, When a hold is placed, Then the system prevents any overlap with existing holds/appointments that violates duration+buffer for the same staff/resource. Given a client changes the selected service type, When duration or buffer changes, Then availability recomputes immediately and only valid slots remain selectable. Given back-to-back bookings, When buffers are required, Then the schedule enforces the buffers without overlap and without permitting negative gaps.
Atomic Multi-Resource Locking
Given a service requires multiple resources (e.g., staff + equipment), When a hold is created, Then all required resources are locked atomically; if any resource is unavailable the hold fails and no locks persist. Given two concurrent holds request overlapping time on any required resource, When processed, Then at most one hold succeeds and the others receive a consistent conflict response. Given a hold is canceled or expires, When lock release occurs, Then all associated resource locks are released within 2 seconds and availability reflects the change.
Timezone and DST Correctness
Given the business timezone is configured, When a client opens the landing page from any timezone, Then all slot times display in the business timezone with an explicit timezone label and map to the correct UTC instants. Given the spring-forward DST transition where certain local times do not exist, When generating availability, Then nonexistent local times are not offered and cannot be held. Given the fall-back DST transition where times repeat, When a slot is selected, Then the booking stores the unambiguous UTC instant and displays the correct local offset in all client and staff views. Given SMS confirmations and reminders are sent, When times are included, Then they match the booked UTC instant and the correct local timezone formatting.
Client-Facing Conflict Messaging
Given a client selects a slot that is taken before confirmation, When the server responds with a conflict, Then the client sees a clear message that the slot is no longer available, plus the top 3 next available alternatives and an option to join the Smart Waitlist. Given a deposit attempt is blocked due to quota exhaustion, When payment is initiated, Then no funds are captured and the client sees a clear message that the wave is full with guidance to try another time or join the waitlist. Given a hold expires, When the client returns after the TTL, Then the UI displays a hold-expired message with a single-tap action to view current availability.
Waitlist Cascade & Auto-Fill Integration
"As a business owner, I want unfilled slots to cascade to my waitlist automatically so that my day fills without me manually juggling texts."
Description

Automatically trigger the next wave or Smart Waitlist outreach when quotas are unmet or cancellations occur, following configurable rules (e.g., promo list next, then first-timers). Reuses deposit and hold settings from the originating wave or overrides per cascade step. Sends targeted follow-ups until capacity is filled, then stops. Reduces manual intervention and accelerates fill while maintaining fairness across segments.

Acceptance Criteria
Auto-trigger Next Wave on Unmet Quota at Wave End
Given a wave with quota Q and a configured cascade order And at scheduled end time T, the number of confirmed bookings C < Q When the wave closes at T Then the system triggers the next cascade step in the configured order within 60 seconds And sends booking invites only to the segment defined for that step And sets the step quota to (Q - C) or the step-specific quota if configured, whichever is lower And applies deposit and hold settings from the step overrides if present; otherwise from the originating wave And records an audit entry with timestamp, step, target segment, quota, and trigger reason "unmet_quota"
Immediate Cascade on Cancellation
Given an active day with all waves configured and the schedule at capacity And a confirmed booking cancels or an active hold expires When capacity increases by N slots Then the system triggers the next eligible cascade step (e.g., Smart Waitlist) within 60 seconds And sends invites to fill up to N slots for that step's segment And stops outreach immediately once N acceptances complete checkout or holds are placed for all N slots And does not trigger subsequent steps while any outstanding holds are active for those N slots And records an audit entry with trigger reason "cancellation"
Deposit and Hold Reuse/Override Enforcement
Given an originating wave with deposit amount D and hold window H minutes And a cascade step with optional overrides D2 and H2 When invites are sent from that step Then the payment link requires a deposit equal to D2 if set, otherwise D And the slot hold duration is H2 minutes if set, otherwise H minutes And the hold is placed at invite acceptance and released automatically when the hold window elapses or the client declines And deposit capture, refund, or apply-to-balance behavior matches the business rule configured for the wave And all monetary values in client and admin views match the applied settings
Fairness, Deduplication, and Overbooking Prevention
Given clients may belong to multiple segments (e.g., VIP, promo, first-timer, Smart Waitlist) When cascade steps execute Then no client receives more than one active invite/hold per timeslot And clients who declined or let an invite expire during the current release are not re-invited for the same timeslot within the configurable cooldown window And invite lists are deduplicated across overlapping segments before sending And booking is locked per slot at checkout; the first client to complete payment wins the slot And other concurrent checkouts receive a sold-out message and their holds are released immediately And SMS delivery respects opt-out (STOP) and provider rate limits
Targeted Follow-ups Until Filled Then Stop
Given a cascade step with a follow-up plan (max_attempts N, intervals I1..IN) When the initial invite is sent Then the system schedules up to N-1 follow-up messages at the defined intervals only to non-responders And cancels any scheduled follow-ups as soon as capacity for the targeted slots is filled And does not trigger the next cascade step while any capacity remains filled by active holds or completed checkouts up to the step's quota And marks invites as expired and sends an "offer expired" SMS when the hold window elapses without action And logs the stop condition "capacity_filled" or "attempts_exhausted"
Admin Configuration and Audit Visibility
Given an admin configures Waitlist Cascade settings for a release When they set the cascade order, per-step segment, quota cap, deposit/hold overrides, and follow-up plan Then a preview displays the cascade steps and applied settings before saving And saving persists the configuration and versions it with an effective-from timestamp And changes affect only triggers that occur after the effective-from timestamp; in-flight cascades continue with their original settings And an activity log lists each cascade event with timestamp, step, segment, invites sent, accepts, declines, fills, stop reason, and operator/system user And logs are exportable as CSV for a selected date range
Admin Monitoring & Wave Performance Analytics
"As a business owner, I want visibility into wave performance so that I can optimize messaging, quotas, and timing to fill faster."
Description

Provide a dashboard to monitor live wave metrics (messages sent, delivery, opens, holds, deposits, confirmations, expirations) and historical reporting (fill rate by wave and segment, time-to-fill, conversion by template, deposit revenue, no-show impact). Offer alerts for stalled or underperforming waves and controls to pause, resume, or cancel a wave. Export data for analysis and compare A/B variants to optimize timing, quotas, and messaging over time.

Acceptance Criteria
Live Wave Metrics Dashboard Updates and Accuracy
Given an active wave is sending invites and events are flowing from the messaging and booking systems When an Admin opens the Wave Performance dashboard and selects a specific wave (or All Active) Then the dashboard displays counts for messages_sent, delivered, opened, holds, deposits, confirmations, and expirations for the current wave(s) And the metrics refresh automatically at least every 5 seconds with a visible last-updated timestamp And median event-to-UI latency is ≤2 seconds and p95 latency is ≤5 seconds under 1,000 events/minute load And the counts reconcile with the underlying event log within ±1% after 5 minutes of event arrival And filtering by wave, service type, and client tier updates all widgets consistently within 1 second And if the data stream is interrupted, a “Live data paused” banner appears within 3 seconds with the time since last update
Historical Reporting: Fill Rate, Time-to-Fill, Conversion, Revenue, and No‑Show Impact
Given an Admin selects a date range, time zone, and filters (wave(s), client tier, promo list, service type) When the Historical Performance report is generated Then the report shows per-wave and per-segment metrics: fill_rate, time_to_fill (p50/p90), open_rate, hold_rate, deposit_conversion_rate, confirmation_conversion_rate, expirations_rate, deposit_revenue, and no_show_rate And deposit_revenue totals reconcile to the payments ledger within ±0.5% for the same filters and period And no_show_rate and delta_vs_baseline_30d are calculated using the business’s configured no-show definition and 30-day pre-period baseline And all timestamps respect the business time zone selection and display it in the header And the report loads first page within 3 seconds for ≤10k rows and supports server-side pagination for larger datasets And metric definitions and tooltips match the analytics glossary document versioned in the app
Automated Alerts for Stalled or Underperforming Waves
Given alert rules are configured per wave (e.g., no_events_for=5m, open_rate_below=20% after=10m, hold_to_confirm_below=25% after=20m) When a wave breaches any configured threshold Then an alert is generated within 60 seconds and delivered via in-app banner and the Admin’s chosen channel(s) (SMS/email) And alerts are deduplicated so the same condition does not notify more than once every 15 minutes per wave And the alert includes wave ID, breached metric, current value, threshold, time observed, and quick actions (Pause, Boost quota, Edit message) And acknowledging or snoozing an alert is logged with user, timestamp, and action; snooze duration is respected And all alerts appear in an Alerts log with filter by status (Open/Ack/Snoozed/Resolved)
Wave Controls: Pause, Resume, and Cancel with Safeguards
Given a user with role Owner or Manager is viewing an active wave When the user clicks Pause Then no new messages are sent, and hold/deposit timers are frozen until Resume, with a banner indicating Paused state When the user clicks Resume Then sending restarts from the next unsent batch and frozen timers resume from remaining time (not reset) When the user clicks Cancel Then unsent messages are halted, pending holds are released, and any uncaptured deposit authorizations are voided; already captured deposits follow business refund rules And confirmations attempted after Cancel are blocked with an explanatory error to clients And all control actions (Pause/Resume/Cancel) are recorded in an immutable audit log with user, timestamp, and reason And users with Staff role cannot perform these actions and see controls disabled with a tooltip
Data Export for Wave and Performance Metrics
Given an Admin selects a date range, filters (wave, tier, promo, service), and export format (CSV or JSON) When Export is requested Then an export is generated asynchronously and a secure download link is delivered in-app and via email within 2 minutes for ≤100k rows And the export includes a header row/metadata with field names, types, and time zone; data values match on-screen aggregates within ±0.5% And exports over 100k rows are chunked with pagination tokens and delivered as a zipped bundle And download links require authentication and expire after 7 days; exports are deleted from storage after expiration And an export history log is available with requester, filters, row count, status, and expiry
A/B Variant Comparison and Optimization Insights
Given a campaign with two or more variants (timing/quota/message) under the same wave group exists When an Admin opens the Variant Comparison view and selects segments (tier, promo list, service type) Then the view displays per-variant metrics (open_rate, hold_rate, deposit_conversion, confirmation_rate, time_to_fill, deposit_revenue) with sample sizes And absolute and relative differences between variants are shown, with statistical significance indicated using a two-proportion z-test at α=0.05 for conversion metrics when n≥100 per variant And the leading variant is highlighted per metric with confidence indicator; insufficient-sample cases are explicitly labeled And the Admin can mark a winning variant and optionally auto-apply it to future waves; this action is logged And the comparison data can be exported with the same filters applied

QR Standby

Convert walk‑ups into confirmed clients on the spot. A posted QR or short code drops them into your SMS thread, collects basics (pet, size, notes), and shows live place‑in‑line. When a no‑show hits, the next standby gets an instant text with a one‑tap confirm and deposit—no clipboards, no chaos.

Requirements

Dynamic QR & Short Code Generation
"As a groomer, I want to create QR and short codes tied to my location and services so that walk-ups can instantly join my standby list without staff intervention."
Description

Provide business owners a self-serve way to generate location- and service-scoped QR codes and short codes that deep-link walk-ups into an SMS standby flow. Each code carries metadata (location, service type, operating hours, capacity rules) and supports configurable expiry, reuse, and UTM tracking. The system renders print-ready signage and shareable assets (PNG/PDF) with safe short links. Device-aware links open the native SMS app with a prefilled keyword; if SMS isn’t available, a mobile web intake fallback is used. Codes are tamper-resistant, auditable, and support multi-location routing and vanity aliases without collisions.

Acceptance Criteria
Self-serve scoped code generation with deep-link metadata
Given I am a logged-in owner with Manage Standby Codes permission When I open QR Standby > Generator and select Location=Downtown, Service=Full Groom, set Operating Hours=09:00–18:00, and choose Capacity Rule=Max 8/day And I set Reuse Policy=Multi-use and UTM Source=WindowSign and click Generate Then the system returns a QR image, a unique short code, and a safe short link within 3 seconds And the deep-link payload includes signed fields: locationId, serviceId, operatingHours, capacityRuleId, reusePolicy, utm And the preview displays the chosen location and service label And the deep link targets the SMS standby flow for the selected service
Device-aware SMS deep link with mobile web fallback
Given a generated code with an associated SMS keyword When the QR is scanned on iOS Safari Then the native Messages composer opens to the business SMS number with the body prefilled to the keyword When the QR is scanned on Android Chrome Then the default SMS app opens to the business SMS number with the body prefilled to the keyword When the QR is scanned on a device without an SMS handler or on desktop Then a mobile web intake page opens that collects pet name, size, and notes and shows live place-in-line on submission And the web intake creates a standby entry equivalent to the SMS flow and sends a confirmation text to the provided phone
Configurable expiry, reuse limits, and UTM tracking
Given I set Expiry=2025-08-31 18:00 local time and Reuse Policy=Single-use When the code is scanned and the first client completes the standby join Then any subsequent scans before expiry show "Already used" and no new standby entry is created When the current time is after Expiry Then any scan lands on an "Expired" page and the event is logged with reason=expired Given I set Reuse Policy=Multi-use with Limit=50 When the 51st unique client attempts to join before expiry Then the client sees "Capacity reached for this code" and no new standby entry is created And all scans include the configured UTM parameters in analytics for impressions and joins
Print-ready signage and shareable assets generation
Given a code has been generated When I click "Download Assets" Then I can download a PNG at 300 DPI and a vector PDF, each containing the QR, the short code, and the safe short link And each asset uses an error-correction level of at least Q and a quiet zone of 4 modules around the QR And each asset passes a built-in scan check at 3 feet under normal indoor lighting And the safe short link is HTTPS on an allowlisted domain and contains no PII outside the signed token And the share link URL for assets is created and expires according to the selected TTL
Tamper resistance and audit logging
Given a generated code whose payload is HMAC-signed with a rotating key When a scan request has a missing, invalid, or expired signature Then the request is rejected with a user-friendly "Invalid or tampered link" screen and a 4xx response And the attempt is recorded in the audit log with timestamp, source IP hash, user agent, and reason=signature_invalid When the owner revokes a code from the dashboard Then subsequent scans are blocked within 60 seconds and the audit log records status=revoked And the audit log for the code shows create, update, download, revoke, scan, and join events and can be exported as CSV
Multi-location routing and vanity alias collision prevention
Given a business with multiple locations and standby waitlists enabled When I set Location=Uptown and assign Vanity Alias="groom-now" Then the system enforces alias uniqueness (case-insensitive) within the tenant and short-link namespace and rejects reserved words And the generated short code and short link map to the Uptown standby flow only When the code is scanned outside the configured operating hours for Uptown Then the client sees "Outside operating hours" and cannot confirm, but may join standby for the next opening according to capacity rules When the capacity rule denies new entries for the service Then the client is placed on standby (not confirmed) and sees live position after opting in And no collision occurs with existing aliases or short codes; conflicts return a clear error before saving
SMS Standby Intake & Consent Capture
"As a walk-up client, I want to join the standby list via SMS and provide my pet’s details so that I can be considered for an opening without filling paperwork on-site."
Description

Implement a guided SMS conversation that collects essential client and pet details (name, contact, pet(s), size/coat, temperament notes, service requested, timing flexibility) with validation, branching, and persistence. The flow deduplicates returning clients, supports multiple pets, languages (EN/ES), and gracefully handles timeouts/recovery. Explicit SMS marketing/operational consent is captured with compliant language (TCPA/GDPR/CCPA), opt-out instructions, and stored with timestamp, channel, and source code. Intake creates/updates client and pet profiles in PawPilot, tags them as QR Standby, and associates them to the correct location and queue.

Acceptance Criteria
QR/Short Code Entry and Location/Queue Mapping
Given a unique QR code or short code is configured for a location's standby queue When a new or returning client scans the QR and opens the SMS link or texts the short code keyword to the designated number Then an SMS thread is initiated within 3 seconds and a welcome message is sent And the intake session is attributed with channel = "SMS", source_code = the QR/short code identifier, and the correct location_id and standby_queue_id And all subsequent messages in this session are attributed to that location/queue And when mandatory intake fields and operational consent are completed, the client is enqueued in the mapped standby queue, tagged "QR Standby", and sent a confirmation including their current place_in_line
Client Deduplication and Profile Update
Given the incoming phone number matches an existing client record When intake begins Then the system greets the client by stored first name and requests confirmation (Y/N) And if Y, the intake pre-fills existing client and pet data and does not create a new client record And if N, a new client record is created with the same phone and the original is flagged potential_duplicate And if no existing record is found, the system collects client first name (1–50 chars), last name (1–50 chars), and optional email (valid format) and saves them And when new values (e.g., last name, email) are provided and confirmed, the existing client record is updated And pets are deduplicated by case-insensitive name within the client's profile; matching pets are updated; non-matching create new pets And all creations/updates are audit-logged with actor = "client_sms", timestamp (UTC), and session_id
Multi-Pet Intake with Field Validation
Given a client indicates they have one or more pets When prompted for pet details Then for each pet the system collects: name (1–50 chars), size/coat (from configured options), temperament notes (0–280 chars), and service requested (from configured options) And the client can add more pets by replying "ADD" or "YES" up to 10 pets per session And inputs outside allowed sets or lengths trigger a clear validation message and re-prompt without losing prior responses And Unicode characters are accepted for names and notes And each pet's data is persisted upon entry and linked to the client's profile
Service Request and Timing Flexibility Capture
Given a client is in the intake flow When asked for desired service and timing flexibility Then the system offers the location-configured service list and records one selection per pet or applies a global selection to all pets when the client chooses And the system captures timing flexibility using explicit options (e.g., "Only today", "Today or tomorrow", "Any time this week") And free-text responses outside options are mapped to the closest valid option when unambiguous or trigger a re-prompt when ambiguous And the captured service choices and timing flexibility are stored on the intake session and client standby_preferences
Language Selection and Localization (EN/ES)
Given a new intake session starts When the client replies "ES"/"Español" or Spanish is detected from the first reply Then all prompts, validations, and consent language are presented in Spanish; otherwise in English And the client can switch languages at any time by sending "EN" or "ES" And the selected language is stored on the session and client profile And templates include localized service names where configured
Operational and Marketing SMS Consent Capture and Storage
Given the intake flow reaches the consent step When the system presents required disclosures (e.g., "Msg & data rates may apply. Reply STOP to opt out, HELP for help.") and operational texting consent language Then the client must explicitly reply YES/NO for operational messages to proceed with standby And marketing consent is requested separately with its own YES/NO And if operational consent = NO, the session ends, no standby enrollment occurs, and the client is marked do_not_text = true And if marketing consent = NO, operational messaging proceeds but marketing_opt_in = false And for each consent type the system stores: value, timestamp (UTC), language, channel = "SMS", source_code, location_id, and disclosure_text_version And a confirmation message summarizes consent statuses and opt-out instructions
Timeouts, Recovery, and Data Persistence
Given a client is mid-intake When there is no reply for 10 minutes (default, configurable per location) Then the system sends one reminder and retains session state And if there is still no reply after 24 hours (default, configurable), the session expires and the client is removed from standby intake state And when the client replies before expiry, the flow resumes at the last unanswered prompt with all prior responses intact And partial answers are persisted within 1 second of receipt with session_id and step index And sending "CANCEL" abandons the session and deletes the ephemeral intake session while retaining saved profile/pet updates with an abandoned flag
Live Queue Position & ETA Display
"As a client, I want to see my live place in line and ETA so that I can decide whether waiting fits my schedule."
Description

After intake, send a secure mobile link that shows real-time place-in-line, estimated wait time, and queue rules (e.g., service match, size constraints). The page live-updates (SSE/WebSocket) as capacity changes, with accessible, low-bandwidth fallbacks and periodic SMS summaries for users without web access. ETAs are computed from provider schedules, task durations, historical no-show rates, and prep times, with caps and pause states visible to users. The view supports multiple pets per client, explains how confirmation will work, and provides a single-tap leave/opt-out option that updates the queue immediately.

Acceptance Criteria
Secure Post-Intake Link Delivery
Given a walk-up client completes QR Standby intake When the intake is submitted Then an SMS with a secure mobile queue link is sent to the same phone number within 5 seconds Given the SMS link When opened Then the page loads the client’s queue data without additional login and the URL contains no PII Given the link token When 24 hours have elapsed since issuance or the client opts out Then the token is invalidated and opening the link shows an expired state with a CTA to request a new link via SMS Given a link token bound to a specific client When the token is invalid or reused Then access is denied and no queue data is exposed
Real-Time Queue Updates with Low-Bandwidth and Offline Fallbacks
Given the queue state changes (capacity added, confirmation, no-show, pause/unpause) When the user has the queue page open with network connectivity Then place-in-line and ETA update within 3 seconds via SSE/WebSocket Given the SSE/WebSocket connection drops or degrades When connectivity is limited Then the client falls back to polling every 30 seconds and displays a reconnecting indicator Given a low-bandwidth connection (2G profile) When loading the queue page Then initial payload is <= 100 KB and each update payload is <= 2 KB Given the client has not opened the link in the past 30 minutes When their position changes by 3+ places or they enter the top 3 Then they receive an SMS summary with current position and ETA and may reply PAUSE to stop or RESUME to restart summaries
Accurate ETA Calculation and Visibility of Caps/Pause States
Given provider schedules, selected services, pet size, historical task durations, no-show probabilities, and prep-time buffers When computing ETA Then the page displays an ETA range in whole minutes and never earlier than the current time Given a capacity pause or shop closed state When active Then the page shows Paused with reason and suppresses countdown while retaining position Given an ETA cap configured by the provider When computed ETA exceeds the cap Then the display shows Over X mins; expect delay and a summary SMS is sent to the client Given events that affect throughput (cancellation filled, no-show occurred, staff shift change) When they occur Then ETA is recomputed and updated within 5 seconds
Transparent Queue Rules and Movement Explanations
Given queue rules include service match and size constraints When a client views the page Then a concise Rules section is visible explaining eligibility and tie-breakers Given the client is skipped or held due to a rule mismatch When this occurs Then the page shows a banner Held for size/service match with the specific rule and the time of the last check Given the client’s position changes When it increases or decreases Then an inline changelog shows the reason (e.g., No-show at 10:12, Large-dog slot opened) with timestamps
Support for Multiple Pets per Client
Given a client submitted multiple pets in intake When viewing the queue page Then each pet appears with name, service, size, position, and ETA Given pets must be serviced consecutively When calculating position and ETA Then the combined position reflects earliest contiguous availability and ETA reflects the longer duration plus prep buffer Given the client removes a pet from standby When processed Then remaining pets’ positions and ETAs recalculate and the queue updates within 3 seconds
Immediate Leave/Opt-Out Action
Given the client wants to leave standby When they tap Leave Standby on the page Then they are removed from the queue within 2 seconds and the page confirms departure with a timestamp Given the client leaves via SMS by replying LEAVE When processed Then the queue updates and the page link reflects Not in queue on refresh Given the client leaves standby When their removal affects others Then downstream positions and ETAs update immediately and waiting list notifications proceed as configured
Accessible Mobile View Compliance
Given the queue page on a mobile device When tested against WCAG 2.2 AA Then it passes color contrast (>= 4.5:1), visible focus indicators, and keyboard operability for all interactive elements Given a screen reader user When position or ETA changes Then updates are announced via ARIA live regions without moving focus Given a small screen width of 320 px When rendering the page Then all content is readable without horizontal scrolling and tap targets are >= 44 x 44 px
Smart Auto-Fill Triggering & Routing
"As an operator, I want openings to auto-route to the most suitable standby clients so that I fill gaps quickly without manual screening."
Description

Integrate with PawPilot’s scheduling to detect cancellations, no-shows, and idle capacity, then automatically select the best-fit standby candidate using configurable rules (service compatibility, pet size/temperament, provider skills, prep time, historical reliability, distance/time flexibility, prior declines). Invitations are throttled and staged (single or batched) with a countdown confirmation window; timeouts cascade to the next candidate. The engine avoids double-booking, respects business hours, and logs decisions for audit. Operators can tune weights and guardrails (e.g., max simultaneous invites, hold duration, lead time minimum).

Acceptance Criteria
Cancellation-triggered Auto-Fill with Countdown and Deposit
Given a confirmed appointment is canceled during business hours and startTime − now >= configuredLeadTime and at least one standby candidate matches required service, pet size/temperament, and provider skill When the cancellation is recorded Then the engine computes weighted scores (service compatibility, pet size/temperament, provider skills, prep time, historical reliability, distance/time flexibility, prior declines) and selects the top-ranked candidate And sends exactly one SMS invite including one-tap confirm + deposit link And creates a hold on the slot for holdDuration minutes and updates candidate to Invited with expiry timestamp And prevents any other booking from acquiring the slot during the hold And on confirm with successful deposit before expiry, converts the hold to a confirmed appointment and notifies parties And on explicit decline or expiry, releases the hold and cascades to the next-ranked candidate And logs decision details (trigger, factors and weights, scores, candidateId, inviteId, timestamps, outcome)
No-Show or Idle Capacity Trigger with Timed Cascade
Given a booked slot is marked no-show after the configured grace period or the system detects idle capacity that meets routing rules and eligible standby candidates exist When the trigger fires Then the engine initiates invites per routing mode (single or batch) starting from the highest-ranked candidate(s) And each invite displays a countdown equal to holdDuration and requires confirm + deposit And timeouts or declines automatically cascade to the next candidate(s) until the slot is filled or the pool is exhausted And booking is created only upon confirm + successful deposit And all invite states, transitions, and outcomes are logged with timestamps
Throttled Batching and Max Simultaneous Invites Enforcement
Given configuration sets maxSimultaneousInvites = M, inviteBatchSize = B, holdDuration = H, inviteCooldown = C, and rateLimit = R invites/minute When dispatching invites for one open slot with N eligible candidates Then the first wave includes min(B, M, N) candidates (or 1 if single-invite mode) And at no time are more than M invites in Invited state concurrently And additional invites are sent only upon decline/expiry and respect per-candidate cooldown C And a candidate who declined/timed out is not re-invited within cooldown C And total invite sends do not exceed R per rolling minute And timestamps and counts prove compliance with M, B, H, C, and R
Business Hours, Availability Windows, and Lead Time Guardrails
Given business hours and leadTimeMin = L are configured When a slot opens via cancellation, no-show, or idle-capacity detection at time t Then if t is outside business hours, no invites are sent until the next opening hour, when the trigger is re-evaluated And if startTime − now < L, no invites are sent unless operator override is enabled, with the suppression reason logged And only candidates whose availability overlaps the slot and who accept short-notice (if required) are considered eligible And all suppressed triggers and eligibility filters are logged
Prep Time and Arrival Feasibility in Candidate Eligibility
Given each candidate has declared prepTime P and estimated travel time T_est and the slot start time is S with checkInBuffer B When evaluating eligibility Then a candidate is eligible only if now + P + T_est <= S − B And T_est is computed from the configured distance/time source using candidate’s current/last known location And candidates failing feasibility are skipped with reason code recorded And prepTime and distance/time flexibility contribute to the weighted score per configuration
Provider/Resource Locking and Double-Booking Prevention
Given providers and resources may have overlapping holds or appointments with buffers When an invite is sent or accepted Then a provisional lock is placed on the provider/resources for the hold window to prevent overlaps (including pre/post buffers) And on confirm, the system revalidates conflicts atomically before creating the booking And if a conflict is detected at confirm time, the confirm is rejected, an apology SMS is sent, the lock is adjusted, and the cascade continues to the next candidate And locks are released immediately upon decline or expiry And locking operations are atomic and idempotent under concurrent triggers
Decision Transparency, Auditability, and Safe Config Updates
Given operators can adjust weights and guardrails and audits are required When saving configuration changes Then inputs are validated (types, ranges, cross-field constraints) and rejected with actionable errors if invalid And accepted changes are versioned (who, when, what) and become active within 60 seconds When a routing decision occurs Then the log entry includes trigger type, candidate pool size, eligibility filters, per-factor weights, per-candidate scores and rank, tie-breaker used, invite timings, outcomes, and cascade path And authorized users can export logs by date range and trigger type with PII masked for non-privileged roles
One-Tap Confirmation & Deposit Capture
"As a client, I want to confirm my spot and pay the deposit in one tap so that I can lock in the opening immediately."
Description

Deliver an SMS link that preloads appointment details and enables one-tap confirmation with deposit payment using stored tokens or Apple Pay/Google Pay. Deposit rules (fixed/percent, refundable/non-refundable, hold vs capture) are configurable per service/location. The flow validates availability at tap time, places a temporary hold, confirms on successful authorization, issues receipts, and updates the schedule and queue atomically. Failures trigger graceful retries and fallback payment options; declines or timeouts release the slot to the next candidate. PCI-compliant handling and refunds/cancellation policy automation are included.

Acceptance Criteria
One-Tap Confirmation with Stored Token
Given a standby client receives an SMS link tied to their standby spot and a stored payment token And the appointment slot is currently available When the client taps the link and confirms Then the system revalidates availability in real time And places a payment authorization hold for the configured deposit amount And upon successful authorization confirms the appointment And sends an SMS receipt with confirmation number and deposit details And the standby queue and calendar reflect the booked status
Atomic Schedule and Queue Update Under Concurrency
Given two standby clients attempt to confirm the same slot within a short window When both tap confirm links Then exactly one confirmation succeeds And the other receives a message that the slot is no longer available And no double-bookings occur And all state changes (hold, confirmation, queue update) are committed atomically or rolled back together
Deposit Rules Applied by Service and Location
Given deposit configuration exists per service and location supporting fixed or percentage amounts, refundable or non-refundable, and hold vs capture When a client confirms for a specific service at a specific location Then the deposit is calculated using the active rule for that service and location And the system performs a hold or capture according to the rule And the receipt and confirmation SMS reflect the deposit type, amount, and policy summary And configuration changes apply to new confirmations without altering existing confirmed appointments
Apple Pay / Google Pay One-Tap Confirmation
Given the client’s device supports Apple Pay or Google Pay And no stored token is available or the client selects wallet payment When the client taps confirm and authorizes via their wallet Then the payment is authorized for the deposit amount And the appointment is confirmed on authorization success And an SMS receipt is issued including wallet type and a masked payment identifier
Failure Handling, Retries, and Slot Release to Next Standby
Given an authorization attempt fails due to soft decline, timeout, or network error When the client taps the retry link within the allowed retry window Then the system retries the authorization up to the configured limit without creating duplicate holds And offers alternate payment methods (stored token, Apple Pay/Google Pay, new card link) And each failed attempt records a reason code and user-facing message And if a definitive decline occurs, the retry window expires, or the client cancels Then any temporary hold is voided, the slot is released to the next eligible standby, the next standby receives an SMS invitation, and the original client is notified
Refunds and Cancellation Policy Automation
Given a confirmed appointment with a deposit and an active cancellation policy window When the client cancels outside the penalty window Then the deposit is automatically refunded or the hold is voided according to the rule And an SMS refund receipt is sent When the client cancels inside the penalty window or no-shows Then the deposit is automatically captured or retained per policy And accounting records and client communications reflect the policy outcome
PCI Compliance and Sensitive Data Handling
Given payment is processed for a deposit When the system stores payment references Then only tokenized payment identifiers are stored; no PAN, CVV, or unmasked expiry is persisted or logged And all payment data is transmitted directly to the PCI-compliant processor via secure channels And application logs and analytics contain no sensitive authentication data And quarterly compliance evidence (e.g., SAQ/ASV scans) is produced and linked to the release checklist
Staff Standby Console & Signage Tools
"As a groomer, I want a simple console and ready-to-print signage so that I can manage standby traffic and keep the front desk calm."
Description

Provide a real-time console within PawPilot showing standby entrants, pet/service details, eligibility, and current position, with controls to reorder, cap, pause/resume intake, send manual invites, and annotate notes. Include quick actions for mark no-show, walk-in served, and deposit overrides with role-based permissions and full audit trails. Embedded tools generate branded, print-ready posters and countertop cards with QR/short codes and simple instructions. Console is mobile-first for on-the-go management and supports multi-location context switching.

Acceptance Criteria
Real-Time Standby Queue Visibility
Given I am a signed-in staff user viewing the Standby Console for Location L When a new entrant completes the QR/short-code standby intake for Location L Then the entrant appears in the console within 3 seconds with: client first name or masked phone, pet name, pet size, requested service, notes, eligibility status, timestamp, and current position number And all other open consoles for Location L reflect the same order within 3 seconds And if the entrant’s eligibility status changes in the backend, the console updates the badge within 3 seconds And position numbers remain contiguous and correct after any change
Intake Cap and Pause/Resume Behavior
Given an intake cap N is set for Location L When the queue reaches N entries Then the public QR/short-code flow informs visitors that standby is full and blocks new entries, and the console displays Cap Reached Given intake is Paused for Location L When a visitor scans the QR or texts the short code Then they receive a paused message and no standby entry is created When staff Resume Intake Then new entries are accepted within 2 seconds and the paused/full banners clear for all consoles
Reorder Queue and Send Manual Invite
Given I have permission to manage the queue for Location L When I drag entrant E from position p to position q Then the new order is saved and reflected to all consoles within 2 seconds, with positions renumbered contiguously Given entrant E is Eligible When I click Send Invite Then E receives an SMS within 5 seconds containing a one-tap confirm and deposit link, E’s status becomes Invited with a countdown timer of configured duration T, and the invite is logged And if E does not confirm before T elapses, E’s status auto-expires and the next eligible entrant is flagged for invitation
Quick Actions — No-Show, Walk-In Served, Deposit Override with RBAC
Given a scheduled slot S has a no-show When I click Mark No-Show in the console Then the next eligible standby entrant is auto-texted within 5 seconds with one-tap confirm and deposit link, and slot S shows outreach status Given a standby entrant E is being served as a walk-in When I click Walk-In Served Then E is removed from the queue, marked Served with timestamp, and positions reflow contiguously Given I am a Staff role user When I attempt Deposit Override for entrant E Then the action is blocked with a permission error and no changes occur Given I am an Owner or Manager role user When I perform Deposit Override for entrant E and provide a reason note of at least 10 characters Then E’s confirmation bypasses the deposit step, and the override is logged with actor, reason, and timestamp
Full Audit Trail for Standby Actions
Given any of these actions occur: reorder, cap set/change, pause/resume, manual invite, mark no-show, walk-in served, deposit override, signage generation, location switch Then an audit record is created with: action type, actor user id and role, UTC timestamp, location id, affected entrant/resource id, before/after values where applicable, reason/note when required, and outcome (success/denied) And audit records are immutable and viewable in an Audit tab with filters by date range, action type, actor, and location And exporting the filtered audit view to CSV produces a file matching the on-screen results And audit records are retained for at least 365 days
Mobile-First Console and Multi-Location Switching
Given I open the Standby Console on a mobile device (viewport 320–430 px) over 4G Then primary controls (pause/resume, cap, reorder, quick actions) are reachable without horizontal scrolling, and there is no content overflow And initial data for queues up to 100 entries loads in 2 seconds or less Given my user has access to multiple locations When I switch the active location via the selector Then the queue and stats refresh to the selected location within 2 seconds, all actions scope to that location, and the selected location persists for my next session on the same device And users without access to a location cannot view or switch to it
Branded Signage Generation with QR/Short Code
Given I open Signage Tools for Location L When I generate print assets Then print-ready PDFs are produced for Letter (8.5×11), A4, and 4×6 card at 300 DPI with location branding (logo, colors, business name), a unique QR code, and a short code mapped to Location L And each asset includes succinct 1–2 step instructions When the QR is scanned from a test device Then it opens an SMS or deep link pre-filled for Location L and, upon submission, the entrant appears in the console for Location L And regenerated codes remain stable unless I rotate codes, in which case old codes stop accepting new entries and the console displays a rotation notice
Analytics, Compliance & Anti-Abuse Controls
"As a business owner, I want visibility into performance and safeguards for consent and abuse so that I can scale QR Standby confidently and compliantly."
Description

Offer dashboards and exports that track walk-up conversions, time-to-fill, fill rate uplift, deposit capture rate, standby abandonment, and no-show reduction, segmented by location, service, daypart, and campaign (QR/short code). Maintain event-level audit logs across intake, routing, invites, confirmations, and payments. Enforce compliance with SMS consent, STOP/HELP handling, quiet hours, and message rate limits. Prevent abuse via duplicate entry detection, device fingerprint heuristics, join frequency caps, and optional CAPTCHA on web fallback. Provide data retention and deletion controls aligned with privacy obligations.

Acceptance Criteria
Dashboard Metrics & Segmentation Accuracy
Given a user selects a date range and filters for location, service, daypart, and campaign, When they open Analytics > QR Standby, Then the dashboard shows KPIs: Walk-up conversion rate, Median time-to-fill, Fill rate uplift vs baseline, Deposit capture rate, Standby abandonment rate, No-show reduction, and underlying counts. Given the KPI definitions, When calculations run, Then they are computed as: Walk-up conversion = confirmed standbys / standby entries; Median time-to-fill = median(time(confirm) − time(join)); Fill rate uplift = (filled_with_standby − baseline_filled) / baseline_filled; Deposit capture = deposits collected / confirmations requiring deposit; Standby abandonment = abandoned / standby entries; No-show reduction = (baseline no-show rate − current no-show rate) / baseline no-show rate, with baseline selectable or default trailing 28 days pre-enablement. Given any filter change, When applied, Then numerators and denominators update consistently and results match the raw event export within ±0.5% or 1 record, whichever is greater. Given new events occur, When viewed on the dashboard, Then data freshness is displayed and KPIs update within 5 minutes.
Analytics Export Consistency & Performance
Given filters and a date range up to 90 days producing ≤500k rows, When the user exports CSV, Then the file generates within 60 seconds and downloads successfully. Given the export schema, When the file is produced, Then it contains one row per event with columns: event_id, event_type, occurred_at (ISO 8601 with timezone), tenant_id, location_id, service_id, daypart, campaign_id, standby_id, invite_id, booking_id, payment_id, amount_cents, phone_sha256, device_fingerprint, message_id, consent_state, status. Given the same filters, When aggregating the export, Then totals match the dashboard within ±0.5% or 1 record, whichever is greater. Given role-based access control, When a user without the Analytics Export permission attempts export, Then access is denied and no file is produced.
Event-Level Audit Logging Completeness & Integrity
Given any intake, routing, invite, confirmation, payment, consent change (START/STOP/HELP), quiet-hour deferral, rate-limit, duplicate-detected, CAPTCHA-challenge, deletion, or retention purge event occurs, When processed, Then an audit record is written with monotonic event_id, occurred_at (UTC), actor (system/user), request metadata (ip, user_agent), resource identifiers, before/after state (where applicable), and a SHA-256 hash linking to the prior record for the same tenant. Given audit storage, When records are queried by time window or identifier, Then matching records are returned within 2 seconds for up to 10k results and the hash chain verifies without gaps. Given audit immutability guarantees, When an edit or delete is attempted, Then it is rejected and a tamper-attempt event is logged.
SMS Consent, STOP/HELP, and Opt-Out Enforcement
Given a new contact enters via QR or short code, When the first outbound message is sent, Then it contains required consent language and the system records consent with timestamp, source, and campaign. Given an inbound STOP (or STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT), When received, Then the system immediately suppresses future messaging to that number, sends a single opt-out confirmation within 5 seconds, records the event, and blocks messages across all campaigns and locations until re-consent. Given an inbound HELP, When received, Then the system replies within 5 seconds with business name, contact info, and STOP instructions, and logs the interaction. Given START/UNSTOP is received after a STOP, When processed, Then the system requests and records fresh consent before any non-mandatory messages resume.
Quiet Hours Enforcement and Message Rate Limiting
Given quiet hours are configured per location (default 21:00–08:00 recipient local time), When a non-required message (invite, reminder, marketing) would be sent during quiet hours, Then it is deferred until the window ends, queued in order, and marked as deferred in logs; STOP/HELP responses are never deferred. Given a per-location throughput limit of 60 SMS/minute and per-recipient caps of 4 messages/hour and 1 standby invite/15 minutes, When limits would be exceeded, Then messages are queued with exponential backoff, order preserved, and a throttle event is logged; no recipient receives more than the caps. Given deferred or throttled messages, When the window opens or capacity is available, Then messages send automatically without manual intervention.
Anti-Abuse: Duplicates, Device Heuristics, and Join Frequency Caps
Given a standby join attempt with a phone number or device fingerprint that already has an active standby for the same location+service within 24 hours, When processed, Then the attempt is rejected with a friendly SMS and no new place-in-line is created; a duplicate-detected event is logged. Given device/network heuristics (IP, ASN, user_agent, cookies, velocity) produce a risk score ≥ threshold, When the web fallback form is used, Then a CAPTCHA challenge is shown and must be passed before submission; after 3 failed challenges, the user is rate-limited for 15 minutes. Given join frequency caps of max 3 joins/day per phone and per device and max 5 submissions/minute per IP, When limits are exceeded, Then the system blocks the attempt, sends an explanatory SMS, and logs the abuse type; allowlisted staff bypass these caps.
Data Retention Configuration and Deletion Controls
Given tenant-level retention settings, When applied, Then message bodies are purged after 90 days, event logs after 18 months, analytics aggregates after 24 months, and PII mappings after 24 months by default; settings are configurable within documented bounds and visible in Admin. Given an administrator submits a deletion request for a contact, When processed, Then PII fields (name, notes, phone in clear) are irreversibly deleted within 72 hours, references are replaced with salted hashes, suppression status is retained, and an audit record is written. Given tenant offboarding is initiated, When the 30-day grace period elapses, Then all tenant data is purged per retention settings and a deletion certificate is generated for download.

Pace Meter

See real‑time throughput and adjust on the fly. PawPilot tracks actual service times and check‑in velocity, then auto‑tunes wave size, slot lengths, and standby intake to match your pace. You stay on schedule, minimize idle time, and avoid bottlenecks as conditions change.

Requirements

Real-Time Throughput Tracking
"As an independent groomer/dog walker, I want my actual check-ins and service times tracked automatically so that the system can keep me on pace without me doing extra admin work."
Description

Captures and aggregates live operational timestamps across SMS interactions and staff actions to measure actual service durations and check-in velocity. Ingests events from client SMS check-in links/keywords, staff start/finish actions, calendar state changes, and payment completion to build a unified timeline. Supports manual time entry and corrections, background syncing when offline, and normalization by service type, staff member, and location. Maintains a rolling time series with per-slot metrics (arrival rate, service time, queue length) to power auto-tuning and analytics, with idempotent event handling and data quality checks. Integrates with PawPilot messaging, scheduling, and billing modules.

Acceptance Criteria
Ingest and Correlate Core Events into Unified Timeline
Given an existing appointment with client, service_type, staff_id, and location_id When the system receives any of the following events for that appointment: - SMS check-in via link/keyword - Staff "start service" action - Staff "finish service" action - Calendar state change (created, rescheduled, canceled) - Payment completed Then each event is persisted with fields: event_type, appointment_id, client_id, staff_id (nullable), service_type, location_id, occurred_at (UTC), received_at (UTC), source, correlation_id And the appointment’s unified timeline is updated within 5 seconds of event receipt (p95) And GET /appointments/{id}/timeline returns events ordered by occurred_at ascending with stable ordering for identical timestamps And each timeline event is visible within 5 seconds (p95) of ingestion
Idempotent and Ordered Event Processing
Given two or more events with identical correlation_id, event_type, and appointment_id When processed Then only a single timeline record exists for that logical event And per-slot metrics derived from that event are incremented at most once Given events arrive out of chronological order When the timeline is requested Then events are sorted by occurred_at; when occurred_at is missing, received_at is used And processing is repeatable: reprocessing the same payload produces no change in stored state And deduplication adds ≤ 200 ms (p95) to ingestion latency
Offline Staff Actions with Background Sync
Given a staff device is offline When the staff records start/finish actions Then the actions are stored locally with an idempotency key and occurred_at timestamp And when connectivity is restored Then events are synced in the background within 2 minutes (p95) And conflicts with existing events are resolved by latest updated_at, preserving an audit trail entry And if sync fails, retries use exponential backoff up to 6 hours; unsynced records are flagged to the user
Manual Time Entries and Corrections with Audit Trail
Given a user with role permission "Edit Time Entries" When the user adds, edits, or deletes a time entry for an appointment Then the change requires a reason code and optional note And the system recalculates affected per-slot metrics within 1 minute And an immutable audit record captures before/after values, actor, timestamp, and reason And validation prevents impossible entries (service duration < 1 minute or > 8 hours, finish before start); violations show actionable errors And all downstream metrics and the unified timeline reflect the correction
Rolling Per-Slot Metrics Computation
Given incoming events across appointments When the metrics job runs every minute Then for each 5-minute slot the system computes: - arrivals_per_slot = count of SMS check-ins with occurred_at in slot - median_service_time_minutes and p90_service_time_minutes from completed start/finish pairs completing in the last 4 hours - queue_length_at_slot_end = count of checked-in not-yet-started appointments at slot end And metrics are calculated per dimension: overall, by service_type, staff_id, and location_id And metrics are available via GET /metrics/throughput?slot_start=... with response time ≤ 300 ms (p95) for a 1-day range And late-arriving events trigger backfill and correction within 2 minutes
Normalization by Service Type, Staff, and Location
Given events with missing or inconsistent service_type, staff_id, or location_id When events are ingested Then normalization maps to canonical dimension ids; unknowns are assigned an "unknown" dimension and excluded from staff-specific metrics And all events and metrics include normalized dimension keys and human-readable labels And API filtering by any combination of service_type, staff_id, and location_id returns correct subsets And cross-location data is isolated unless the user has multi-location permissions
Data Quality Checks and Alerts
Given continuous ingestion When data quality checks run every minute Then the system flags and quarantines events with negative durations, overlapping start/finish for the same appointment, missing required fields, or clock skew > 10 minutes between occurred_at and received_at And an alert is emitted to Ops when error rate exceeds 1% over 5 minutes with a summary of top failure reasons And quarantined events are excluded from metrics until resolved; resolution applies retroactively And overall ingestion success rate is ≥ 99.9% per day; failures are observable via dashboards
Auto-Tuned Slot Lengths & Wave Sizing
"As a busy sitter managing a tight schedule, I want the system to automatically adjust upcoming slot lengths and wave sizes so that I stay on time and avoid idle gaps or bottlenecks."
Description

An adaptive control engine that recalibrates upcoming slot durations, wave sizes, and staff load balancing in response to measured pace, within configurable guardrails. Applies smoothing to throughput signals to avoid oscillation, enforces min/max bounds per service type and time of day, and runs a short-horizon simulation before committing changes. Adjustments apply to future flexible slots while preserving confirmed appointments unless rescheduling rules permit. Generates client messaging to confirm time adjustments and preserves deposits. Exposes override toggles and produces an audit log of each auto-tune decision. Integrates tightly with the scheduling engine for conflict-safe updates.

Acceptance Criteria
Pace-Driven Recalibration Within Guardrails
- When rolling measured pace deviates by ≥10% from plan for ≥10 consecutive minutes, the system recalibrates upcoming flexible slots within the next 4 hours (or next 2 waves, whichever is smaller). - Slot-length change per adjustment is capped at ±5 minutes and remains within service-type/time-of-day min/max guardrails. - Wave-size change per adjustment is capped at ±1 and remains within configured min/max wave bounds. - Rate limit: no more than 1 auto-tune publish every 10 minutes per location. - Adjustments must not reduce total remaining bookable capacity for the day by >2% unless an admin “allow capacity tradeoff” flag is enabled.
Smoothed Throughput Signal Prevents Oscillation
- Throughput smoothing uses an exponential moving average with a 15-minute half-life. - A hysteresis band of ±5% around the planned pace suppresses adjustments unless the deviation persists beyond the band for ≥5 minutes. - Consecutive opposing adjustments (increase followed by decrease, or vice versa) must not occur within a 20-minute window. - Backtest on the prior 7 open days shows ≤1 oscillation per hour; otherwise the engine enters dampened mode (no changes >±2 minutes) until stability persists for 30 minutes.
Short-Horizon Simulation Gate
- Before publishing any change, simulate the next 2 hours using current demand, roster, and proposed slot/wave settings. - Approve a proposal only if simulated on-time completion rate is ≥ baseline and either (a) average client wait time improves by ≥5% or (b) projected idle time decreases by ≥5%. - Simulation confirms zero hard conflicts (double-bookings, overlapping breaks) and keeps per-staff utilization within 70–95%. - If the gate fails, the proposal is discarded and the reason is logged; no schedule changes are published.
Confirmed Appointment Protection and Rescheduling Rules
- Only future flexible slots are modified; confirmed appointments are not moved unless rescheduling rules permit. - Rescheduling rules require client flex consent, minimum notice ≥2 hours, shift magnitude ≤15 minutes per change, and start time within the allowed window for the service type. - Deposits and confirmation status are preserved on any time-only change; no deposit is voided or re-charged. - If a permitted confirmed move is applied, the client is notified and must confirm; on decline or no response within 15 minutes, the original time is restored automatically.
Client Messaging for Time Adjustments
- For any client-facing time shift ≥5 minutes, send an SMS within 60 seconds of publishing changes including: new start window, change magnitude, confirmation CTA (Y/N), and a help link. - If no response within 10 minutes for same-day changes, send one reminder; if still no response, retain the original time for confirmed appointments and keep flexible slots on the waitlist/standby path. - Messaging failures trigger up to 3 retries over 5 minutes and fallback to the configured alternate channel if available; outcome is recorded. - Deposits remain applied without additional charge for time-only adjustments.
Staff Load Balancing During Auto-Tune
- Recalibration allocates workload so no staff member exceeds configured capacity and all assignments match required skills and locations. - Projected per-staff utilization stays between 75% and 90% over the next 2 hours when feasible; otherwise a variance reason is logged. - Breaks and time-off are honored; zero overlapping assignments are created. - Standby intake limits per staff are adjusted to meet utilization targets without exceeding the max standby per hour.
Audit Log, Overrides, and Conflict-Safe Updates
- Each auto-tune decision writes an immutable audit entry with timestamp, inputs (pace, queue depth, EMA), guardrails applied, proposed vs. applied changes, simulation metrics, conflicts detected, overrides used, and messaging outcomes. - Audit entries are visible in the UI within 30 seconds and exportable (CSV/JSON) with filters by date, location, and service type. - Admin can toggle auto-tune On/Off per location/service and pin specific slots/waves; pinned entities are not modified by the engine. - All updates execute via versioned transactions; on conflict, the change aborts, retries once after state refresh, and never produces double-bookings.
Standby Intake Automation
"As a dog walker with variable day-to-day demand, I want standby clients automatically invited when I’m ahead of schedule so that I keep my day full without manual outreach."
Description

Dynamically invites clients from the Smart Waitlist to fill early finishes or underutilized capacity based on real-time pace and predicted openings. Ranks the waitlist with rules (service fit, distance, responsiveness, deposit status) and sends SMS offers with auto-expiring holds and one-tap confirm/deposit links. Coordinates with auto-tuned waves to prevent overbooking, enforces fairness and throttling, and instantly updates the schedule upon acceptance. Handles declines, timeouts, and cancellations gracefully with next-in-line outreach.

Acceptance Criteria
Under-Capacity Trigger Sends Standby Offer
Given business hours and an active Smart Waitlist When Pace Meter predicts ≥1 openable slot in the next X minutes (configurable) based on real-time throughput Then the system selects the highest-ranked eligible candidate and sends an SMS offer within 60 seconds of detection And the trigger fires once per new opening and does not duplicate invites for the same opening And the opening window matches required service duration plus buffers And offers respect configured quiet hours; if outside, they queue and send at window start only if the opening remains valid
Waitlist Ranking, Eligibility, and Fairness
Given a pool of waitlisted clients When building a candidate list for an opening Then the system filters by service fit, availability window, travel radius, responsiveness score threshold, and deposit requirement And it sorts by weighted score (service fit > proximity > responsiveness > deposit on file), breaking ties by waitlist timestamp And enforces fairness: max N active offers per client per day, M-hour cooldown after decline/timeout, and no same-client top rank on 2 consecutive days And writes an audit log of inputs, weights, scores, and final order for each invite cycle
SMS Offer Hold, Content, and One-Tap Confirmation
Given a candidate is invited When the SMS offer is sent Then the message includes business name, service, proposed time window, location, price/deposit, expiry countdown, and a one-tap confirm/deposit link And a hold is placed for T minutes (configurable 5–20) and is visible as "Held" on the calendar And the confirm link pre-fills client details and processes deposit if required; on deposit failure, the user is prompted to retry up to R times before expiry And the SMS includes STOP to opt out; opt-outs are honored immediately and logged
Overbooking Prevention with Auto-Tuned Waves
Given auto-tuned waves and dynamic slot lengths When an offer hold is active Then capacity for the corresponding wave is reduced to prevent double-booking And if auto-tuning adjusts wave size/slot length during a hold, holds are reconciled without displacing any confirmed appointment And new offers are suppressed if acceptance would exceed staff/resource limits or daily capacity caps
Acceptance Commits Schedule and Deposit
Given a client taps the confirm link When confirmation succeeds Then an appointment is created with correct service duration, buffers, assigned staff/resource, and location And the calendar updates within 2 seconds and triggers confirmation SMS to client and provider And deposit authorization/capture succeeds and is recorded; on capture failure, the confirmation is rolled back and next-in-line is invited And reminders and billing workflows are scheduled per policy
Declines/Timeouts/Cancellations with Next-in-Line Outreach
Given an active offer When the client declines Then the hold is released immediately and the next eligible candidate is invited within 60 seconds When the offer times out Then the hold auto-expires, client receives an expiry SMS, and the next-in-line is invited When a client cancels within C minutes of acceptance Then the slot is optionally re-offered to next-in-line per policy, and deposit handling (refund/forfeit) follows configured rules with audit trail
Concurrency, Race Handling, and Throttling
Given multiple clients interact with overlapping offers When two or more confirmations arrive for the same opening Then the first successful confirmation wins; others receive a "slot taken" SMS and remain on the waitlist And the system enforces max K concurrent outbound offers per opening and global rate limit Q SMS/min And all offer state transitions are idempotent and retried safely; no orphaned holds persist beyond 1 minute after expiry
Pace Dashboard & Alerts
"As a solo groomer on the go, I want a simple view and timely alerts about my pace so that I can take quick actions without digging through the schedule."
Description

A mobile-first, real-time dashboard showing current pace versus target, upcoming load, and recommended actions. Visual indicators (on time/ahead/behind) with color cues, plus key metrics like average service time by type, arrivals per 15 minutes, and next bottleneck ETA. Provides one-tap controls to expand/shrink upcoming waves, pause intake, or trigger standby invites. Sends configurable SMS or in-app alerts to staff when deviation thresholds are crossed, with quiet hours and role-based notification settings.

Acceptance Criteria
Mobile Real-Time Pace Dashboard Update
Given a signed-in staff user on a mobile device viewing the Pace Dashboard When new check-ins, service starts, or service completions are recorded in the location Then the "Current Pace vs Target" metric and status (On Time/Ahead/Behind) update on the dashboard within 5 seconds of the event And the status is computed as: Ahead if current throughput >= target + 5%; Behind if current throughput <= target - 5%; On Time otherwise And a "Last updated" timestamp is visible and is within 10 seconds of the current time And data freshness persists under a 3G network profile (400 ms RTT, 1 Mbps down) without user refresh
Upcoming Load & Bottleneck ETA Accuracy
Given the schedule has at least 2 hours of upcoming appointments and the system has recorded average service times by type When the user opens the Upcoming Load section Then arrivals per 15-minute interval are displayed for the next 120 minutes and counts match the appointment schedule exactly And average service time by type is displayed with the sample size and last-updated time And the "Next bottleneck ETA" is shown and refreshes at least every 60 seconds And in a controlled test load, the predicted bottleneck time is within ±3 minutes of the observed bottleneck over a 60-minute window
Visual Status Cues and Accessibility
Given the dashboard is displaying a pace status When the status is Ahead, On Time, or Behind Then the color cue uses green (Ahead), neutral/blue (On Time), and red (Behind) with a text label and icon so the meaning is not conveyed by color alone And all status elements meet WCAG 2.1 AA contrast (>= 4.5:1 for text, >= 3:1 for non-text) And a screen reader announces the status change within 2 seconds with the label "Pace status: <state>" And the status cue is perceivable in a simulated deuteranopia and protanopia test
One-Tap Operational Controls
Given recommended actions are presented for the current session When the user taps Expand Wave or Shrink Wave Then the upcoming wave size/slot lengths adjust immediately for the next 60 minutes within configured min/max bounds and a confirmation snackbar shows the effective change with an Undo option for 30 seconds When the user taps Pause Intake and selects 15/30/60 minutes Then new client check-ins are blocked for the selected duration, a visible countdown banner appears, and SMS auto-replies inform clients of the pause When the user taps Trigger Standby Invites for a service type Then SMS invites with deposit link are sent to eligible waitlist clients within 10 seconds, the dashboard shows sends/accepts/fills in real time, and invites stop automatically when the target fill is reached
Deviation Threshold Alerts Delivery (SMS & In-App)
Given deviation thresholds are configured per location and role, and staff have verified contact methods When pace deviates beyond the configured threshold for 2 consecutive 1-minute intervals Then in-app and/or SMS alerts are delivered to the assigned recipients within 30 seconds And the alert includes current pace vs target, deviation percentage, projected delay or idle time, and the top recommended action And repeated alerts of the same type are deduplicated with a 10-minute cooldown until the state returns to normal And alert delivery and receipt are logged with timestamps and recipient IDs
Quiet Hours and Role-Based Notification Routing
Given quiet hours are set for users and role-based notification preferences are configured When a deviation threshold is crossed during a recipient's quiet hours Then SMS alerts are suppressed and in-app alerts are queued for delivery after quiet hours end, unless an explicit "Allow critical during quiet hours" override is enabled And only users with roles mapped to the alert type receive notifications; users with multiple roles receive a single notification at the highest severity And the notification settings page reflects the next delivery time for queued alerts
Manual Overrides & Safeguards
"As a business owner, I want the ability to cap, pause, or roll back auto-tuning so that my client commitments are protected and changes never spiral out of control."
Description

Operational guardrails and controls to ensure safe, predictable adjustments. Includes freeze switches (global/per-staff), min/max bounds for slot length and wave size, maximum change rates per day, holiday/peak presets, and end-of-day locks. Detects and resolves conflicts with existing appointments and deposits, supports instant rollback to a prior schedule with client re-notification, and records a complete audit trail of auto-tuning decisions and user overrides. Backed by feature flags for phased rollout.

Acceptance Criteria
Freeze Switches (Global and Per‑Staff) Enforcement
Given auto-tuning is enabled and the Global Freeze is ON When the auto-tune job runs Then no schedule parameter changes are applied and an audit event "global_freeze_block" is recorded with timestamp, actor, and scope=all-staff Given Global Freeze is ON When a user attempts a manual override of slot length or wave size Then the change is rejected with error code PP-FRZ-001 and an audit event "manual_blocked_by_global_freeze" is recorded Given Staff A has Per-Staff Freeze ON and Staff B is unfrozen When auto-tuning runs Then Staff A's parameters remain unchanged while Staff B's may change within safeguards, and two audit events are recorded for the differing outcomes
Bounds & Change Rate Limits Enforcement
Given min/max slot length bounds are configured (e.g., 15–60 minutes) and a proposed auto-tune change would set slot length below minimum When applying the change Then the value is clamped to the minimum, the change reason "bound_clamp" is recorded, and the user sees the clamped value in the UI Given the daily change-rate limit for slot length is 10% and the cumulative change today is 8% When a new change of +5% is proposed Then the applied change is limited to +2% (reaching 10%), the remainder is deferred, and an audit event "rate_limited" includes before/after/delta values Given a manual override proposes a value outside configured bounds When saved Then the save is blocked with error code PP-BND-001 and a validation message specifies the allowed range
Holiday/Peak Presets Application
Given the date is labeled Peak and the Peak Day preset is selected When applying the preset Then wave size, slot length, and standby intake match the preset definition and remain within bounds and change-rate limits, with a single audit event "preset_apply" referencing preset id and version Given an active preset would conflict with existing appointments When applied Then the system presents a conflict summary and requires user confirmation to proceed or cancel; if canceled, no changes are committed and "preset_apply_aborted_conflict" is logged Given presets are available When a preset is applied Then the effective schedule is recalculated within 5 seconds and reflected in the calendar
End‑of‑Day Lock for Current‑Day Adjustments
Given the end-of-day lock time for a location is 2 hours before close and the current time is past the lock When auto-tuning proposes changes affecting today Then changes are not applied, and an audit event "eod_lock_block" is logged with scope=today Given the current time is past the lock When a user attempts a manual override affecting today's remaining schedule Then the action is blocked with error code PP-EOD-001; overrides for future days remain allowed Given the current time is before the lock When changes are applied Then they may proceed subject to other safeguards
Conflict Detection and Resolution with Existing Appointments and Deposits
Given there are existing appointments and some have paid deposits When a schedule change would move or shorten those slots Then the system identifies all conflicts before commit and displays count and details including deposit status and amount Given conflicts exist without deposits When the user confirms Auto-resolve Then the system reflows appointments to the nearest valid slots within bounds, sends SMS notifications to affected clients, and logs "conflict_autoresolved" with mapping old->new times; resolution completes within 60 seconds for up to 200 appointments Given conflicts exist with deposits When the user confirms Auto-resolve Then the system preserves deposits, moves the appointment only if the new time is within user-defined tolerance; otherwise the change is blocked and "deposit_protected" is logged; no client is moved without notification
Instant Rollback with Client Re‑notification
Given a schedule snapshot exists from the last committed state When the user clicks Rollback for today Then the system restores the prior parameters and appointment placements within 30 seconds and records "rollback" with a correlation id to the rolled-back change set Given clients were re-timed by the change set being rolled back When rollback completes Then affected clients receive an SMS re-notification with the prior confirmed time and a link to confirm or request change; SMS delivery events are recorded per client Given rollback fails partially When an inconsistency is detected Then the system auto-retries up to 3 times and, if still failing, marks the rollback as failed_partial and provides a downloadable diff for support
Feature Flags for Phased Rollout
Given the Manual Overrides & Safeguards feature flag is OFF for cohort C When auto-tuning runs or a user attempts to access the new controls Then legacy behavior and UI are shown, and "feature_flag_gated" is logged; no new safeguards are active Given the feature flag is ON for cohort C and OFF for others When the same action is performed Then only cohort C sees and can use the new controls; telemetry captures usage segmented by cohort id Given the feature flag rules are updated When evaluated at runtime Then changes take effect without deploy within 1 minute and are auditable with rule version
Historical Analytics & Tuning Insights
"As an operator, I want historical insights into throughput and tuning outcomes so that I can set better targets and prove the ROI of auto-tuning."
Description

Aggregated reports that quantify impact and refine targets over time. Provides weekly/monthly views of throughput, schedule adherence, idle time, and bottlenecks by service type, staff, location, and weekday. Compares auto-tuned versus static schedules, standby fill rates, no-show reduction, and payment timing improvements. Offers CSV export and lightweight BI connectors. Applies retention policies and anonymization where appropriate to protect client data.

Acceptance Criteria
Weekly/Monthly KPI Aggregations by Dimension
Given I select a date range within the last 24 months and one or more dimensions (service type, staff, location, weekday), When I switch between Weekly and Monthly views, Then the dashboard displays aggregates for throughput, schedule adherence, idle time, and bottlenecks. Given data exists for the selected period, When the view loads, Then metrics are computed in the account’s timezone and reflect data freshness of 15 minutes or less. Given Weekly view is active, When aggregating, Then throughput = count of completed appointments per ISO week; schedule adherence = % of appointments starting within ±5 minutes of scheduled time; idle time = sum of staff idle minutes between appointments; bottlenecks = top 3 queues with average wait time > 10 minutes. Given Monthly view is active, When aggregating, Then the same KPI definitions apply but grouped by calendar month. Given a filter is applied or removed, When the view refreshes, Then all KPIs and charts reflect the filter change within 5 seconds for up to 50,000 appointments. Given a bucket has no data, When rendering, Then counts display 0 and rates display N/A.
Auto‑Tuned vs Static Schedule Comparison
Given I select a date range and dimensions, When I toggle Comparison = Auto‑Tuned vs Static, Then side‑by‑side KPI values for throughput, schedule adherence, idle time, and bottlenecks are displayed with delta % = (auto − static) / static. Given Static baseline sample size is fewer than 30 completed appointments, When comparison is requested, Then a message "Insufficient baseline" is shown and deltas are not computed. Given comparison view is active, When exporting or querying via BI connector, Then each KPI includes columns with suffixes _auto, _static, and _delta and uses the same definitions as the dashboard. Given I switch dimensions or date range, When the view updates, Then the comparison recalculates within 5 seconds for up to 50,000 appointments.
Standby Fill Rate Analytics
Given canceled slots exist in the selected period, When Smart Waitlist fills any of those slots, Then standby fill rate = (waitlist‑filled slots ÷ canceled slots) × 100% is displayed by week and month. Given at least one slot is filled via waitlist, When viewing details, Then median time‑to‑fill and 90th percentile time‑to‑fill are displayed for the selected dimensions (service type, weekday). Given no canceled slots exist in the period, When rendering, Then standby fill rate shows N/A and an empty state message is displayed. Given filters are applied, When the view refreshes, Then standby fill rate and time‑to‑fill metrics reflect the filters consistently across charts and exports.
No‑Show Reduction Reporting
Given I choose a baseline period and a current period, When I open the No‑Show report, Then no‑show rate = (no‑shows ÷ scheduled appointments) × 100% is displayed for each period and improvement % = (baseline_rate − current_rate) ÷ baseline_rate. Given deposit status is available, When I segment by Deposit Collected (Yes/No), Then no‑show rate is displayed for each segment in the current period and baseline. Given a segment has fewer than 100 scheduled appointments, When rendering, Then a "Low sample size" badge appears and improvement % is hidden for that segment. Given filters or dimensions are changed, When the report refreshes, Then rates and improvements update within 5 seconds and maintain consistent definitions across views and exports.
Payment Timing Improvement Reporting
Given completed appointments with payments exist in the selected period, When viewing Payment Timing, Then p50 (median) and p90 time‑to‑payment (completion to capture) and same‑day payment rate (%) are displayed by week/month and by service type. Given a baseline period is selected, When comparison is enabled, Then improvement % for p50 time‑to‑payment = (baseline_p50 − current_p50) ÷ baseline_p50 is displayed and color‑coded (improvement positive when time decreases). Given no payments exist in the period, When rendering, Then metrics display N/A with an empty state. Given timezone is set at the account level, When calculating time‑to‑payment, Then timestamps are normalized to the account timezone.
Data Export & BI Connectors
Given a dashboard view with any filters applied, When I click Export CSV, Then a UTF‑8 CSV with a header row, schema version, and account timezone applied is generated within 60 seconds for up to 250,000 rows. Given a BI connector token is configured, When I query the read‑only analytics endpoint, Then I receive paginated JSON (page size up to 10,000) including updated_at, schema_version, and fields matching the dashboard definitions. Given an export or connector request fails, When the error occurs, Then a retriable error code and message are returned and the account owner receives an email notification within 5 minutes. Given a user includes comparison or segmentation in the view, When exporting or querying, Then the dataset includes corresponding columns for segments and comparison suffixes (_auto, _static, _delta) where applicable.
Data Retention & Anonymization Enforcement
Given workspace retention is set to 180 days for raw appointment and message data, When a record exceeds 180 days, Then PII fields (client name, phone, email, message body) are irreversibly anonymized and identifiers replaced with salted hashes while aggregate analytics remain available. Given a verified client erasure request is received, When processing completes, Then all PII for that client is anonymized across analytics stores, exports, and connectors within 7 days; aggregates remain unaffected. Given an export or connector query includes data older than the retention window, When data is returned, Then only anonymized fields are included and no raw PII is exposed. Given retention settings are changed, When the nightly retention job runs, Then changes take effect for data newly exceeding the configured window and an audit log entry is created for each batch.

On Deck Alerts

Keep the line moving with timely texts. Clients receive a heads‑up when they’re 2–3 away, plus directions to the station and a one‑tap “I’m here” or “Running late.” The queue updates automatically, reducing PA calls and no‑shows while keeping arrivals perfectly spaced.

Requirements

Real-time Queue Positioning
"As a groomer, I want the system to keep an accurate, live queue so that the right clients are alerted at exactly 2–3 away without manual tracking."
Description

Continuously compute each client’s live position in the service queue based on appointment time, service duration, staff availability, current check-ins, late signals, and active work-in-progress. Expose a lightweight event stream that triggers notifications when a client becomes 3-away and 2-away. Integrate with existing appointment and waitlist data models to ensure consistency across SMS threads, staff console, and reporting. Provide safeguards for race conditions (simultaneous updates) and idempotent recalculations so the queue remains accurate under high change volume. Expected outcome is timely, reliable alerting and a single source of truth for who is up next.

Acceptance Criteria
Threshold Alerts: Emit 3-away and 2-away on Crossing
Given an appointment is in the active queue and has not yet received a 3-away alert And its position changes from >= 4 to exactly 3 When the queue is recalculated Then a threshold_reached event with threshold=3 is emitted within 1 second including appointment_id, position, timestamp, idempotency_key And exactly one 3-away SMS notification is enqueued within 1 second of event emission Given an appointment is in the active queue and has not yet received a 2-away alert And its position changes from >= 3 to exactly 2 When the queue is recalculated Then a threshold_reached event with threshold=2 is emitted within 1 second including appointment_id, position, timestamp, idempotency_key And exactly one 2-away SMS notification is enqueued within 1 second of event emission And no duplicate 2-away or 3-away notifications are sent for the same appointment unless it exits and re-enters the queue
Realtime Recompute SLA on Queue-Changing Events
Given a queue-changing event occurs (check-in, late signal, appointment create/update/cancel, staff start/stop, job start/complete, waitlist fill) When the event is received by the queue service Then affected appointments' positions and ETAs are recomputed and persisted within 2 seconds And a position_changed event is emitted for each appointment whose position or ETA changes And positions reflect appointment time, service duration, staff availability, check-ins, late signals, and active work-in-progress
Idempotent Recalculation and Duplicate Alert Suppression
Given an event with idempotency_key K is processed successfully When the same event with key K is received again within 10 minutes Then no additional state changes occur And no additional threshold_reached or position_changed events are emitted And no additional notifications are sent Given a recalculation is retried due to a transient error When processing resumes Then the resulting queue state matches the state as if the calculation ran exactly once
Concurrent Updates Resolve to Deterministic Single State
Given two or more events affecting overlapping appointments arrive within 100 ms When the events are processed concurrently Then the system applies them in a deterministic order using event_time then appointment_id as a tiebreaker And the final queue has no duplicate positions or gaps And at most one threshold notification per threshold is sent per appointment
Cross-Surface Consistency: SMS, Console, Reporting
Given a position update is persisted at time T When the staff console loads the queue, the client's SMS thread renders next-position, and the reporting API is queried Then all three surfaces display the same position and last_updated >= T within 3 seconds And the audit log records actor/event_id, old_position, new_position, and timestamp
Client Signals: "I'm here" and "Running late" Reflow Queue
Given a client taps "I'm here" via SMS When the signal is received Then their arrival is timestamped and status set to Ready And the queue reorders without bypassing earlier Ready clients And any future 3-away/2-away alerts for this appointment are canceled Given a client taps "Running late" with delay D minutes When the signal is received Then their ETA shifts by at least D minutes or they are moved behind the next Ready client per business rules And any scheduled threshold alerts are rescheduled to the new positions
Waitlist Backfill Maintains Order and Triggers Alerts
Given an active appointment is canceled And a waitlisted client is auto-booked into the freed slot When the backfill is confirmed Then the queue includes the inserted appointment in correct order based on appointment time, duration, and staff assignment And threshold alerts emit only if thresholds are newly crossed And reporting marks the appointment as backfilled=true
Heads-Up SMS Alerts (2–3 Away)
"As a client, I want a text when I’m 2–3 away so that I can head over at the right time and avoid waiting around."
Description

Send templated, personalized SMS alerts automatically when a client is 3-away and again at 2-away. Messages include friendly heads‑up copy, dynamic ETA window, appointment/service name, and a short link with directions and station details. Respect client time zone, opt-in status, quiet hours, and locale (i18n). Support per-business templates and throttling to avoid over-messaging. Track link clicks for engagement analytics to measure effectiveness and refine timing windows.

Acceptance Criteria
Automatic Heads-Up at 3‑Away and 2‑Away Queue Positions
Given a client is SMS‑opted‑in and has an active same‑day appointment in the queue outside quiet hours in the client’s local time When the queue updates and the client transitions to 3‑away Then send a 3‑away SMS within 30 seconds containing client_first_name, service_name, ETA window, and a directions short link And When the queue later updates and the client transitions to 2‑away without having received a 2‑away in the past 60 minutes Then send a 2‑away SMS within 30 seconds with updated ETA and the same personalization And Ensure no more than two heads‑up SMS are sent per appointment
Quiet Hours, Opt‑In, and Time Zone Respect
Given the client’s time zone and locale are resolved (fallback to business time zone if missing) When current time in the client’s time zone is within business‑defined quiet hours OR the client is not SMS‑opted‑in Then do not send a heads‑up SMS and record a suppression event with reason (quiet_hours or no_opt_in) And When quiet hours end and the client remains 2–3 away Then send the most recent applicable heads‑up (prefer 2‑away) within 2 minutes; otherwise do nothing And All timestamps in the SMS reflect the client’s local time formatting
Per‑Business Templates and Personalization Tokens
Given a business has custom templates for 3‑away and 2‑away with tokens {client_first_name}, {service_name}, {eta_window}, {station_name}, {directions_link} When generating a heads‑up SMS Then render the business template for the trigger, substituting tokens with live values And If a token value is missing, apply fallbacks: service_name → "your appointment", station_name → business default And Limit message to max 2 SMS segments; if longer, truncate at sentence boundary and always preserve the short link And Store the rendered message body and template version for audit
Dynamic ETA Window Calculation and Display
Given historical average service duration and the current queue state are available When computing the ETA window for 3‑away and 2‑away Then produce a start–end window of configurable width (default 15 minutes, allowed 10–20) in the client’s local time And Recalculate for the 2‑away alert using the latest queue state And Format times per client locale (12h/24h, day if crossing midnight) And Include "ETA {start}–{end}" in the SMS body
Short Link, Directions, and Station Details
Given a directions page exists for the appointment location and station When composing the SMS Then include a branded short link that redirects (301/302) to the directions page with appointment_id and source=headsup And The landing page returns HTTP 200 and p95 load time < 2s, showing business name, station name/number, map link, and parking notes And Record a unique click event per alert with client_id, appointment_id, trigger_type (3‑away|2‑away), and timestamp
Throttling, Debounce, and Idempotency
Given rapid queue updates or duplicate trigger events When a client becomes 3‑away and then 2‑away within 5 minutes Then suppress the 3‑away and send only the 2‑away And If the same trigger repeats for the same appointment within 10 minutes, send at most one SMS using idempotency key (appointment_id + trigger_type) And Enforce a minimum 8‑minute interval between any two heads‑up SMS to the same client across all appointments And Emit metrics for suppressed_due_to_throttle and deduped
Internationalization and Localization Coverage
Given client locale and language are set or inferred When rendering the heads‑up SMS Then select the localized template for the trigger; if unavailable, fallback to business default; else fallback to en‑US And Format date/time and numerals per locale conventions and ensure link placement renders correctly in RTL languages And Verify translations exist for top 5 business locales and do not exceed 1.8× the base template length
One-Tap Arrival & Delay Responses
"As a client, I want one-tap “I’m here” or “Running late” options so that I can update the team quickly without calling."
Description

Embed secure, tokenized one-tap actions in the SMS: “I’m here” and “Running late.” On tap, record the client’s status without login, optionally capturing ETA or a preset delay (5/10/15 minutes). Update appointment status, notify staff, and display the arrival in the queue. Provide optional geofence check to reduce false “I’m here” taps. Handle error states gracefully with a fallback SMS reply keyword. Store action audit logs for reconciliation and support.

Acceptance Criteria
One-Tap 'I'm Here' within Geofence
Given a client receives an On Deck SMS containing an "I'm here" link with a valid, unexpired token and geofence is enabled When the client taps the link within the appointment's configured arrival window and is inside the configured geofence radius of the service location Then the system validates the token without requiring login and records the appointment status as Arrived with a UTC timestamp within 2 seconds And associates the event to the correct appointment and client And stores the geofence check result (inside) with coordinates where available And updates the live queue to show the client as On Site within 5 seconds And notifies staff via the dashboard (and configured channel) within 5 seconds including client name, appointment time, and geofence status And displays a confirmation page to the client stating "You're checked in"
One-Tap 'Running Late' with Preset Delay
Given a client receives an On Deck SMS containing a "Running late" link with a valid, unexpired token When the client taps the link and selects a preset delay of 5, 10, or 15 minutes (or enters an ETA if enabled) Then the system validates the token without requiring login and records the appointment status as Delayed with the selected delay or ETA within 2 seconds And updates the live queue to reflect the new expected arrival time within 5 seconds And notifies staff via the dashboard (and configured channel) within 5 seconds including client name, original appointment time, and delay/ETA And sends a confirmation SMS to the client acknowledging the delay and the updated expected time
Tokenized Link Security, Single-Use, and Expiration
Given one-tap links are generated for a specific appointment and client phone number When a link is used for the first time within its validity period Then the token is verified as signed, time-bound, and unmodified, and a state change is permitted And the token is marked as consumed to prevent replay When a token is expired, revoked, or already consumed Then no appointment state changes occur, an error page is shown stating the reason, and fallback SMS keyword instructions are displayed And all requests are served over HTTPS and only token-validated requests can mutate appointment state
Fallback SMS Keyword Processing
Given a client cannot use the one-tap link or prefers SMS When the client replies with HERE Then the system records the appointment status as Arrived and updates the queue and staff notifications as with the one-tap flow When the client replies with LATE 5, LATE 10, or LATE 15 (case-insensitive) Then the system records the appointment status as Delayed with the specified minutes and updates the queue and staff notifications accordingly When the client replies with ETA HH:MM in the business local time Then the system records the appointment status as Delayed with the parsed ETA and updates the queue and staff notifications accordingly When the message cannot be parsed Then the system sends a help template with valid keyword examples and makes no state change
Idempotent and Duplicate Tap Handling
Given a client taps the same one-tap link multiple times within a 10-minute window When the first tap successfully updates the appointment state Then subsequent identical taps do not create duplicate events, notifications, or additional state changes and return an "Already recorded" confirmation When a different valid action is taken later (e.g., 'Running late' after 'I'm here', or vice versa) Then exactly one new state transition is recorded using last-write-wins by event timestamp, the queue is updated accordingly, and staff are notified once
Action Audit Logging and Export
Given any status change occurs via one-tap or SMS keyword When the event is processed Then an immutable audit log entry is stored with correlation ID, appointment ID, client phone hash, action type (Arrived/Delayed), channel (tap/keyword), timestamp (UTC), token ID (if applicable), geofence result and coordinates (if available), delay/ETA (if any), and outcome (success/failure with reason) And logs are retained for at least 12 months and are searchable by appointment ID, client phone, date range, and action type And authorized staff can export matching logs to CSV including all stored fields
Auto Queue Rebalancing & Waitlist Backfill
"As an operator, I want the queue to self-balance and backfill no-shows so that the line keeps moving and revenue isn’t lost."
Description

Automatically adjust queue order when clients mark arrival, indicate delays, or miss a grace period. Apply business rules for hold times, grace windows, and late penalties. If a gap is detected, trigger Smart Waitlist backfill with a deposit link to keep arrivals evenly spaced and reduce idle time. Sync updates to the staff console and client SMS threads in real time, ensuring all parties have the latest order and ETAs.

Acceptance Criteria
Arrival Marked -> Immediate Queue Rebalance
Given a scheduled client in position > 1 and an active arrival grace window When the client replies "I'm here" via SMS or taps the arrival link Then the system timestamps the arrival and reorders the queue into the earliest eligible slot per business rules within 3 seconds And ETAs for all impacted clients are recalculated and persisted And the arriving client receives an SMS confirming updated position and ETA within 5 seconds And the staff console displays the new order with reason code "Arrival Check-In" on the client
Running Late -> Hold Window and Penalty Application
Given configured hold_time_minutes H and late_penalty_rule P for the service When a client taps "Running late" and provides delay D minutes Then if D <= H, the system holds the slot for up to H minutes, updates ETAs for downstream clients, and displays a countdown timer on the staff console And if D > H, the system releases the slot and applies penalty P (e.g., demote to end or mark as standby) immediately And the client receives an SMS stating the outcome (held until <time> or slot released with penalty) within 5 seconds And the queue order reflects the hold or penalty state without manual intervention
Grace Window Expiry -> Auto Skip and Notification
Given grace_window_minutes G is configured per service When G elapses for a client without arrival confirmation Then the client is marked "Missed (Grace Expired)", removed from the active queue, and any configured late penalty is applied And the next eligible client is promoted and ETAs are recalculated And the missed client receives an SMS with rejoin options (e.g., join waitlist link) within 5 seconds And the staff console records the skip with reason code and timestamp
Gap Detection -> Backfill Trigger Conditions
Given a slot is released or a station is idle creating a gap >= minimum_fillable_duration with buffer B When the gap is detected Then Smart Waitlist backfill is triggered exactly once per gap with a cooldown of C minutes to prevent duplicate triggers And no backfill is triggered if outside business hours or staff capacity is set to zero And the trigger event is recorded with gap_start_time and target_start_time
Candidate Selection & Deposit Confirmation Window
Given a detected gap with target_start_time T and duration L and a waitlist of candidates When selecting candidates Then only candidates whose service fits in L ± tolerance and whose ETA <= T + arrival_buffer are eligible And offers are sent to up to K top-ranked candidates concurrently with a response/deposit window of W minutes And the first candidate to complete the deposit within W minutes is confirmed; the queue updates and all other pending offers auto-expire with SMS notice And if no deposit is received within W, the system cascades to the next K candidates until exhaustion or a maximum elapsed time E is reached
Real-Time Sync to Staff Console and Client SMS Threads
Given any reorder, hold, skip, or backfill confirmation event occurs When the state change is persisted Then the staff console reflects updated queue order, ETAs, and reason codes within 2 seconds And affected clients receive updated SMS messages (position/ETA or status) within 5 seconds And an immutable audit log entry is stored with actor, timestamp, prior_state, new_state, and correlation_id
Concurrency, Tie-Breakers, and Idempotency
Given two or more events on the same slot occur within a 5-second window (e.g., arrival, running late, deposit payment) When processing events Then a deterministic priority is applied: arrival confirmation > deposit confirmation > running-late update > cancellation And at most one client is assigned to any slot; losing events are rejected gracefully with SMS "Slot already filled" where applicable And duplicate messages/webhooks with the same idempotency key are ignored for 24 hours And all clients and staff views converge to the same state within 5 seconds
Directions & Station Guidance
"As a first-time client, I want clear directions and station details so that I can arrive at the right place without confusion."
Description

Generate a short link that opens maps with the correct location and embeds station-specific guidance (parking, entrance, suite number, indoor directions, or mobile van pin). Allow per-location and per-station instructions with rich text and images where supported. Provide a plain-text fallback when links are blocked. Cache directions for speed and reliability, and respect accessibility best practices for screen readers.

Acceptance Criteria
Short Link Opens Correct Map Destination
Given an On Deck alert is sent for a station with a configured address and/or GPS coordinates When the recipient taps the short link on iOS, Android, or desktop Then the default maps app (or a web map if no app is available) opens to the configured coordinates within 30 meters of the station location And the destination label includes the business name and station identifier And for mobile van stations, the destination uses the most recent coordinates updated within the last 5 minutes, else falls back to the configured service area address And the short link resolves in under 2.0 seconds at the 75th percentile And the link remains valid for at least 48 hours from send and returns a friendly expired page thereafter
Rich Guidance Rendered Per Station
Given a station has guidance content with rich text, lists, and up to 5 images When the recipient opens the guidance page from the short link Then the guidance renders with preserved formatting (headings, lists, emphasis) And images are responsive, lazy-loaded, and each under 500 KB And Open in Maps and Copy Address buttons are visible without scrolling on a 360x640 viewport And the page's Largest Contentful Paint is under 1.5 seconds on a 4G connection at the 75th percentile And embedded phone numbers and suite/entrance details are selectable and copyable
Plain-Text Fallback When Links Blocked
Given an On Deck alert SMS is composed When the carrier or device blocks link opening or the landing page cannot load within 10 seconds Then the SMS body contains the street address and a <=120-character entrance summary in plain text directly following the short link And an automatic follow-up SMS with plain-text directions (address + entrance summary + contact) is sent within 15 seconds of detection And the plain-text message does not exceed 2 GSM-7 segments (<=306 characters) And no images or rich formatting are required to understand how to arrive
Directions Cached for Speed and Offline Resilience
Given guidance content and assets are published When a recipient opens the guidance link Then the guidance HTML and media are served via CDN with a cache hit ratio >= 95% And Time to First Byte is under 100 ms at the 75th percentile And a Service Worker caches the latest visited guidance so that, when offline, the page shows the address and last-known instructions And cached content uses versioned URLs so updates are visible within 5 minutes of publish (stale-while-revalidate) And total page weight on first load is <= 1.5 MB and <= 600 KB on subsequent loads
Accessible Guidance for Screen Readers
Given a user with a screen reader opens the guidance page When navigating via keyboard or assistive technology Then focus order is logical: title → address → Open in Maps → Copy Address → instructions → images And all interactive elements have accessible names and visible focus indicators And non-decorative images include meaningful alt text; decorative images are aria-hidden And color contrast meets WCAG 2.2 AA (>=4.5:1 for text) And the address is provided as selectable text; no information is conveyed by color alone And automated WCAG scans report zero critical violations and no more than two minor warnings
Per-Location and Per-Station Instruction Overrides
Given a location has default guidance and a station within that location has station-specific overrides When an On Deck alert is generated for that station Then station-level guidance is used, inheriting any unset fields from the location defaults And if no station-level guidance exists, location-level guidance is used And if neither exists, a safe global default with address only is displayed And changes to station guidance appear in newly generated links within 5 minutes and do not alter already sent links unless republished
Staff Queue Console & Overrides
"As front desk staff, I want a live queue with manual controls so that I can handle edge cases and keep operations smooth."
Description

Provide a real-time console showing who is 3-away, 2-away, arrived, late, and in-service. Allow manual overrides: reorder queue, pause/resume alerts, set grace periods, and mark walk-ins. Surface delivery/engagement indicators (sent, delivered, clicked, responded) and suggest calling when response thresholds aren’t met. Include bulk actions for multi-pet or family groups. All actions audit-logged for accountability.

Acceptance Criteria
Delivery Monitoring, Compliance & Fallbacks
"As a business owner, I want reliable, compliant delivery with smart fallbacks so that important alerts always reach clients without risking violations."
Description

Track SMS delivery statuses and retries; flag unreachable numbers and provide configurable fallbacks (secondary number, email, or staff call prompt). Enforce opt-in/opt-out rules, quiet hours, sender ID regulations, and message frequency caps to remain compliant. Centralize consent records and template approvals. Provide alerts and reports for failed deliveries and compliance exceptions to minimize PA calls and prevent no-shows due to missed texts.

Acceptance Criteria
Real-Time SMS Delivery Tracking & Retry
Given an On Deck Alert is queued for an opted-in mobile number When the carrier returns delivery webhooks (queued, sent, delivered, temporary_failure, permanent_failure) Then the system records each status with timestamp and carrier code within 5 seconds of receipt And the appointment timeline and Delivery Monitor dashboard display the latest status in under 10 seconds And on temporary_failure the system retries up to 3 times with exponential backoff (1m, 3m, 5m) And retries cease immediately on delivered or permanent_failure And carrier error codes are mapped to normalized reasons and persisted in an audit log tied to the appointment and contact
Unreachable Number Fallback Orchestration
Given a message attempt results in permanent_failure or the number is pre-flagged unreachable When fallback routing is enabled and configured (e.g., secondary SMS -> email -> staff call task) Then the system initiates the next available channel within 60 seconds And includes the same On Deck content and deep links for "I'm here" and "Running late" And deduplicates so that only the first successful channel completion notifies the client; subsequent late deliveries are suppressed And the staff call task includes the phone number, appointment details, and a call script And the UI shows the full fallback chain with per-channel status and timestamps
Opt-In/Opt-Out Enforcement for On Deck Alerts
Given a contact without recorded SMS consent or with an active opt-out When an On Deck Alert is triggered Then no SMS is sent and the attempt is blocked with a compliance reason logged And STOP/UNSUBSCRIBE keywords are processed within 5 seconds and persist an opt-out record tied to the phone number And START re-opt-in is recorded with timestamp and source and allows future sends And if fallbacks are configured, non-SMS channels are used instead and the routing is logged And sending requires that the specific On Deck template/version is marked approved prior to use
Quiet Hours & Message Frequency Caps
Given organization quiet hours and a per-contact SMS cap are configured When an On Deck Alert would be sent during quiet hours Then the system does not send SMS and either schedules for the next allowed window or triggers the configured fallback within 2 minutes, per org setting And when sending would exceed the cap within its rolling window, the SMS is blocked, a compliance exception is logged, and configured fallback is used And administrators can adjust quiet hours and caps per location with effective-from timestamps; all changes are audit-logged And a visible badge indicates "quiet hours" or "frequency cap" on the blocked attempt in the Delivery Monitor
Sender ID/10DLC Compliance Gate
Given the business has registered 10DLC campaign(s) and approved templates When an On Deck Alert is about to be sent Then the system selects a compliant sender ID for the recipient's country and route And blocks the send if the template, campaign, or locale is unapproved, surfacing a descriptive error and triggering fallback if configured And messages sent carry the campaign ID and template version in metadata for audit And non-compliant sender mappings cannot be saved or used for sending
Failure Alerts & Compliance Reporting
Given monitoring thresholds are configured When delivery failures or compliance exceptions exceed the threshold (e.g., >5% in 15 minutes or >10 events) Then a real-time alert is sent to designated staff channels (in-app, email, or SMS) within 2 minutes And a daily report is generated summarizing failures by reason, fallbacks used, unreachable contacts, and impacted appointments And reports are exportable as CSV and accessible via API with date filters And alert acknowledgements and report downloads are captured in the audit log
Centralized Consent & Template Approvals Repository
Given an admin views the Compliance Center When searching by client, phone number, template, or date range Then the system returns consent records and template approval statuses with fields: contact ID, phone, status (opt-in/opt-out), source, timestamp, template ID, version, approver, and validity And records are immutable, retained for at least 24 months, and exportable And role-based access control restricts view/export to authorized roles And every change (who, what, when) is captured in an audit trail

Host Portal

Give partner venues a simple, masked view of today’s pop‑up: booked count, live ETA curve, and a shareable booking link. Hosts can promote remaining slots to their audience without seeing addresses or payments, boosting foot traffic and reducing coordination overhead.

Requirements

Magic-Link Host Access
"As a host manager, I want to access the portal via a secure one-click link so that I can view today’s pop-up status without creating or remembering a password."
Description

Provide secure, venue-scoped access to the Host Portal via expiring email/SMS magic links, eliminating passwords and minimizing onboarding friction. Hosts are invited by PawPilot partners or internal admins, and each link binds access to a specific venue and event date as needed. Include role-based permissions for “Host” with least-privilege defaults, single-click sign-in, session management, and the ability to revoke access instantly. Implement audit trails for invitations, sign-ins, and data views to support compliance. Integrate with PawPilot’s user directory and venue records to ensure hosts only see data for their assigned locations.

Acceptance Criteria
Invite Creation and Magic-Link Delivery (Venue-Scoped)
Given an authorized admin or partner initiates a host invite for a specific venue and optional event date When they submit the invite with a delivery channel of email or SMS Then the system generates a single-use, cryptographically signed magic link scoped to that venue and event date And sets the expiry to the default 24 hours (configurable) And delivers the link via the selected channel within 60 seconds And records the invite in the audit log with inviter, host contact, venue, event date, expiry, and delivery channel And never stores or logs the full token value (only a non-reversible hash)
Single-Click Sign-In via Magic Link
Given a host opens a valid, unexpired, unused magic link on a supported device When the link is accessed Then the host is authenticated without a password And a session is created with the Host role applied And access is restricted to the link’s venue and event date scope And the Host Portal loads the scoped dashboard within 2 seconds of server processing time (p95) And the magic link is invalidated immediately after successful sign-in And any subsequent use of the link returns HTTP 410 Gone with a support message
Least-Privilege Host Permissions and Masked Data
Given an authenticated Host session When the host views the portal Then the host can see booked count, live ETA curve, and a shareable booking link And the host cannot see client names, addresses, phone numbers, emails, or payment data And attempts to access data outside the scoped venue/date are blocked with HTTP 403 and logged And API responses and UI omit PII fields for the Host role And the shareable booking link contains no secrets or internal IDs and routes only to the public booking flow
Session Management and Timeouts
Given an authenticated Host session Then the idle timeout is 30 minutes (configurable) And the absolute session lifetime is 12 hours (configurable) from sign-in And on timeout or sign-out the user is redirected to a re-authentication screen indicating a new magic link is required And the user can terminate the session via Sign Out And the session cookie is HttpOnly, Secure, SameSite=Lax, and bound to the user agent and IP range And only one concurrent session per invite is allowed; a new sign-in invalidates the prior session
Instant Access Revocation
Given a host has an active invite and/or active session When an admin revokes access for the invite or for the venue/date Then all associated magic links become invalid within 5 seconds And any active sessions are terminated within 10 seconds and show a Revoked Access notice And subsequent requests receive HTTP 401 with reason "access_revoked" And an audit entry is recorded with actor, target host, venue, event date, timestamp, and reason
Audit Trail for Invitations, Sign-Ins, and Data Views
Given audit logging is enabled When an invite is created, delivered, re-sent, revoked, or expires Then an immutable audit record is stored with timestamp, actor, subject, venue, event date, and outcome When a magic link sign-in is attempted (success or failure) Then an audit record captures IP, user agent, result, and failure reason (expired, invalid, reused) When a Host views the dashboard or retrieves data via API Then an audit record captures event type, scope, and counts accessed without PII And authorized admins can query audit records by host, venue, and date with p95 response under 2 seconds and export CSV
Directory and Venue Record Enforcement
Given a host exists or is created in the PawPilot user directory When an invite is issued Then the invite binds the host directory record to the specified venue and event date And at authentication time the system verifies the directory record is active and still assigned to that venue/date And if the assignment has changed or been removed, sign-in is denied with HTTP 403 and logged And hosts with multiple events receive distinct links; no link grants access to multiple venues or dates
Masked Daily Summary
"As a host manager, I want a masked summary of today’s schedule so that I can understand bookings and remaining availability without seeing customer details."
Description

Deliver a simplified, real-time dashboard showing today’s pop-up metrics at the host’s venue without exposing client PII or payment information. Display booked count, remaining slots, operating window, service types offered, and team arrival/setup times. Pull live counts from the PawPilot scheduler and waitlist engine, refreshing automatically. Ensure all data is aggregated and anonymized, with zero visibility into customer names, addresses, phone numbers, notes, or payments. Provide mobile-first layout optimized for quick checks during operations.

Acceptance Criteria
PII and Payment Masking
Given an authenticated host opens the Masked Daily Summary for their venue on the day of a pop-up When the dashboard and its APIs load Then no client PII (names, phone numbers, emails, addresses, notes) or payment fields (amounts, methods, tokens, last4) are rendered in the UI And no PII or payment fields are present in API responses, logs, or network payloads And any attempt to access customer-detail routes returns 403 or a masked view with no PII
Real-Time Booked and Remaining Counts
Given the host has the dashboard open during operating hours When a booking is created, canceled, or auto-filled from the waitlist for today at this venue Then "Booked" and "Remaining" counts update within 15 seconds without manual refresh And the counts match the scheduler/waitlist source of truth for the venue/day And the last-updated timestamp reflects the most recent data fetch time
Operating Window and Timezone Display
Given a pop-up is scheduled today at the host’s venue When the dashboard loads Then the operating window is displayed as local venue time with timezone abbreviation (e.g., 9:00 AM–5:00 PM PT) And the display is DST-aware and matches the scheduler’s start/end times And if no pop-up exists today, a clear zero-state message is shown instead of times
Service Types Offered Display
Given services are configured for today’s pop-up When the dashboard loads or services are updated Then the list of offered service types is shown using customer-friendly names only And no per-customer service details are shown And changes to services are reflected in the dashboard within 60 seconds
Team Arrival and Setup Times
Given team arrival and setup times are configured for today’s pop-up When the dashboard loads Then the planned arrival window and setup-complete time are displayed And updates to these times propagate to the dashboard within 60 seconds And no staff names or personal identifiers are shown
Mobile-First Layout and Performance
Given the host opens the dashboard on a mobile device (320–414 px width) When the page loads and is scrolled Then all required metrics (booked count, remaining slots, operating window, service types, arrival/setup times) are visible without horizontal scrolling And primary tap targets are at least 44x44 px And LCP is under 2.5 s on a simulated 4G connection and the page remains responsive (TTI under 3.5 s)
Venue Scoping and Access Control
Given a host is authenticated or uses a valid signed link scoped to a single venue and date When they access the Masked Daily Summary Then only today’s metrics for the assigned venue are visible And attempts to change the venue or date via query params or navigation are rejected with 403 or ignored And unsigned/expired links are denied with 401/403
Live ETA Curve
"As a host manager, I want a live ETA curve so that I can staff and prep based on expected foot traffic and timing shifts."
Description

Render a live ETA curve that visualizes expected customer arrivals across the operating window, adjusting for lateness, cancellations, and waitlist fills. Ingest appointment times, confirmation statuses, and real-time operational signals to produce a smoothed arrival forecast with early/late indicators. Update on a frequent cadence to reflect the latest schedule dynamics. Present the curve with clear callouts (e.g., peak in 30 minutes) to help hosts plan staff and floor readiness. Ensure the visualization remains anonymized and never reveals individual appointments.

Acceptance Criteria
Real-time Updates on Schedule Changes
- Given the Host Portal is open on today’s pop-up, when a cancellation event is received for an appointment within the operating window, then within 60 seconds the forecast updates and the integral (sum of expected arrivals) from now to end decreases by 1. - Given the Host Portal is open on today’s pop-up, when a waitlist fill event assigns a client into a specific slot, then within 60 seconds the forecast updates and the integral from now to end increases by 1 and the added demand appears in the nearest 5‑minute bin to the slot time. - Given the Host Portal is open on today’s pop-up, when an appointment is rescheduled by ≥5 minutes, then within 60 seconds the corresponding forecast mass shifts to the new time window. - Given the page is open for more than 1 minute, when no new signals are received, then the curve still refreshes at least once every 60 seconds and the “Last updated” timestamp reflects the latest refresh time.
Early/Late Arrival Indicator Overlay
- Given the forecasted median arrival offset over the next 30 minutes is ≥ +5 minutes, when the Host Portal is open, then display an indicator “Late arrivals likely (+X min)” where X is the rounded median offset, and render a shaded band offset by +X minutes. - Given the forecasted median arrival offset over the next 30 minutes is ≤ −5 minutes, when the Host Portal is open, then display an indicator “Early arrivals likely (−X min)” where X is the rounded median offset, and render a shaded band offset by −X minutes. - When the absolute median offset remains < 5 minutes for 10 consecutive minutes, then the early/late indicator is hidden. - When the median offset changes by ≥ 5 minutes, then the indicator’s displayed value updates within 60 seconds.
Peak Callout Rendering for Upcoming Demand
- Given the next 60 minutes contain a local maximum 5‑minute bin with expected arrivals ≥ 3 and at least 20% higher than each adjacent bin, when the Host Portal is open, then render a callout “Peak in [MM] min” anchored at that time and include the expected arrivals value. - Given multiple qualifying upcoming peaks, then the callout selects the highest expected arrivals; if tied, the earliest peak time is used. - When the current time is within 5 minutes of the peak time, then the callout text changes to “Peaking now”. - When the current time is > 2 minutes past the peak time, then the peak callout is removed.
Anonymized Aggregation and Privacy Protections
- Rule: The visualization displays only aggregated counts per 5‑minute bin and does not expose names, phone numbers, addresses, emails, payment details, appointment IDs, or any per‑client metadata in the UI or DOM. - Rule: If any 5‑minute bin contains fewer than 3 expected arrivals, it is merged with adjacent bins until the combined bin count ≥ 3 before any tooltip or label is rendered. - Rule: Tooltips and labels show only a time range and aggregated expected count; no interactions (click/hover) reveal or link to individual appointments. - Rule: Data export or download actions for the curve are disabled in the Host Portal.
Venue Time Zone and Operating Window Integrity
- Given the venue time zone is set (e.g., America/Los_Angeles), when the curve renders, then x‑axis tick labels, callouts, and the “now” marker display local times, and the “now” marker aligns with the venue’s current local time. - Given the operating window is 10:00–16:00 local, when the curve renders, then the x‑axis begins at 10:00 and ends at 16:00 and no curve data is plotted outside this range. - When the operating window is updated, then the curve resizes to the new boundaries and reflows within 60 seconds.
Performance, Mobile Responsiveness, and Accessibility
- Rule: On a cold load over a 4G network, the Host Portal displays the ETA curve above the fold within 2.0 seconds median time on a mid‑tier mobile device. - Rule: Subsequent forecast updates re‑render the curve within 1.0 second of data receipt. - Rule: On mobile widths 360–768 px, axis labels, callouts, and the now marker remain legible without overlap, and horizontal scrolling is not required. - Rule: The curve and callouts meet WCAG 2.1 AA contrast ratios (≥ 4.5:1 for text, ≥ 3:1 for non‑text graphics), with non‑color alternatives for any color‑only cues. - Rule: The chart container is keyboard‑focusable and exposes an accessible name and description to screen readers, including the current peak callout if present. - Rule: If the curve cannot load within 5 seconds, a user‑friendly error message and a retry control are shown.
Host-Specific Booking Link & Attribution
"As a host manager, I want a shareable booking link tied to my venue so that I can promote remaining slots and see the impact of my promotions."
Description

Generate a short, venue-branded booking link that pre-filters to the host’s pop-up and highlights remaining slots. Include unique host attribution parameters to track clicks, conversions, and fills resulting from host promotions. Surface referral metrics in the Host Portal (e.g., bookings driven, fill rate impact) without revealing customer identities. Ensure links are safe to share publicly, never expose addresses, and handle inventory changes in real time. Provide optional channel-specific variants (social, email, in-store QR) under one attribution umbrella.

Acceptance Criteria
Create Venue-Branded Short Link Pre-Filtered to Host Pop-Up
Given a pop-up event is associated with Host H and is bookable When a staff user generates a host booking link for H Then a short URL is created using a branded domain/subpath and host-friendly slug with total length <= 40 characters Given the host booking link exists When a customer opens the link Then the landing page is pre-filtered to Host H’s pop-up and displays date/time window and remaining slot count without exposing any addresses or payment details Given the link is shared publicly When inspected via URL and page/API payloads Then no customer PII or exact addresses are present and the host identifier is an opaque, non-sequential token Given the event status changes (active or canceled) When the link is opened Then it resolves appropriately (active -> booking landing; canceled -> informative message + waitlist CTA) without returning a raw 404
Attribution Tracking for Clicks-to-Bookings and Fill Impact
Given a host link or any of its channel variants is clicked When the landing page loads Then a click event is recorded with host_id and channel, de-duplicated per device within a 30-minute window Given a booking is completed within 7 days of a tracked click from a host link When the booking is confirmed Then the booking is attributed to the host and channel and increments “bookings driven” Given a booking attributed to a host is later canceled and the slot is refilled via a non-host source When referral metrics recompute Then fill rate impact reflects the net fills attributable to the host and updates within 5 minutes Given multiple eligible clicks precede one booking When attribution is applied Then last non-direct host click receives credit and duplicate attributions for the same booking ID are prevented
Real-Time Inventory Reflection on Link Landing
Given remaining slot count changes due to bookings or cancellations When a customer is viewing the landing Then remaining slots and time options update within 5 seconds without manual refresh Given a customer selects a time that becomes unavailable before confirmation When they submit the booking Then an immediate unavailable message is shown and the next available options are offered without losing entered contact information Given the event sells out When the link is opened Then the landing clearly indicates sold out and provides a Join Waitlist CTA while the link remains functional and share-safe Given two customers attempt to book the last slot concurrently When both submit Then only the first confirmation succeeds and the second is declined with an offer to choose another time or join the waitlist
Host Portal Referral Metrics Without Customer Identity
Given a host user is authenticated in the Host Portal When viewing Today’s Pop-Up Then the dashboard displays host-driven clicks, bookings, conversion rate, and fill rate impact with a last updated timestamp and without exposing customer names, phone numbers, emails, or addresses Given channel variants exist When the host expands metrics Then a per-channel breakdown (social, email, QR, other) is shown and aggregate totals equal the sum of channels Given date filters (Today, This Week, Custom Range) When applied Then metrics recompute within 2 seconds and CSV export contains only aggregated metrics with no PII fields Given access control constraints When a host attempts to access another host’s referral metrics via URL manipulation Then access is denied with a friendly error and no data is leaked
Channel-Specific Variants Under Unified Attribution
Given a primary host link exists When a user generates variants for social, email, and in-store QR Then unique URLs and downloadable QR codes (PNG and SVG) are produced with a shared host attribution umbrella (common host_id; distinct channel tags) Given variants are distributed When clicks and bookings occur Then per-channel metrics accumulate correctly and aggregate host metrics equal the sum of all variants within ±0.5% Given a variant is deleted When future clicks occur on the deleted URL Then traffic is redirected to the primary host link with channel marked legacy and attribution continues to roll up under the host Given performance constraints When generating any variant Then the operation completes within 2 seconds at the 95th percentile
Public Safety and Privacy Protections for Shared Links
Given any host booking link is publicly accessible When viewed or inspected Then street addresses, unit numbers, client addresses, and payment artifacts never appear in the URL, page content, or API responses prior to booking confirmation Given hostile traffic attempts parameter guessing When more than 10 invalid tokens are requested from a single IP within 60 seconds Then rate limiting with 429 responses is applied without revealing which tokens are valid Given search engine crawlers fetch the landing When they access the page Then robots meta noindex and X-Robots-Tag headers are present to prevent indexing Given a link token is malformed, expired, or for a past date When visited Then a non-identifying fallback page is shown with options to view current pop-ups or join the waitlist and no internal IDs or stack traces are exposed
Privacy-by-Design Masking & Audit
"As a compliance-conscious operator, I want strict masking controls so that hosts never see customer addresses or payments, reducing risk and maintaining trust."
Description

Enforce privacy controls that prevent hosts from accessing any client PII or payment data. Present only aggregated counts and operational signals; mask any free-text fields and block access to addresses, phone numbers, names, and transaction details. Implement server-side authorization checks, field-level redaction, and secure logging of access events. Add automated tests to verify masking rules and a periodic audit report to demonstrate compliance. Provide configurable policies to expand or restrict what aggregates are visible per venue, with sensible secure defaults.

Acceptance Criteria
Host Dashboard Shows Aggregates Only (No PII)
Given an authenticated host user accesses the Host Portal for a venue When the dashboard loads Then only aggregate metrics are displayed: today’s booked count, remaining capacity, and ETA curve And no client names, phone numbers, emails, street addresses, free-text notes, or payment amounts are present in the DOM or API responses And inspecting network responses shows that fields {name, phone, email, address, notes, transactionId, cardLast4, amount, cardBrand} are absent or null for host role And the shareable booking link contains no PII in path or query parameters and routes to a public booking page scoped to the venue/date
Server-Side Authorization Blocks PII Endpoints for Host Role
Given a host user’s valid session or token When calling customer, booking-detail, or payment-detail APIs directly (REST/GraphQL) Then the response status is 403 Forbidden with error code HOST_FORBIDDEN And the response body contains no PII fields And the same endpoints return 200 for authorized admin/groomer roles per RBAC configuration And all authorization decisions are enforced server-side (no client-side gating)
Field-Level Redaction of Free‑Text and Structured PII
Given a booking includes free-text content containing PII-like patterns (e.g., phone numbers, emails, addresses, codes) When retrieved via any host-facing view or API Then the free-text content is replaced with a redaction token "[REDACTED]" before serialization And structured PII fields (e.g., addressLine1, customerEmail) are omitted or null for host role, not partially masked And network payloads and page source contain only redacted or null values for these fields
Per‑Venue Policy Controls With Secure Defaults
Given a new venue with no custom policy When a host logs in Then only default aggregates (booked count, remaining capacity, ETA curve) are visible And additional aggregates are disabled by default Given a super-admin enables additional aggregates in the venue’s privacy policy When the host refreshes the dashboard Then only the enabled aggregates render and disallowed aggregates are not returned by the API And hosts cannot modify policy; attempts return 403 and are logged And policy changes are timestamped and attributable to an admin identity
Tamper‑Evident Access Event Logging
Given any host access to the portal or API When a page is viewed or an endpoint is invoked Then an access event is recorded with fields {eventId, timestamp (UTC), userId, venueId, resource, method, statusCode, ip, userAgent, requestId} And the event contains no PII field values And logs are stored in a tamper‑evident store with retention >= 12 months And integrity checks run daily and record success/failure without exposing PII
Automated Test Coverage for Masking and Authorization
Given the CI pipeline runs When unit, integration, and end‑to‑end tests execute Then tests assert that host role receives only aggregates and all PII fields are absent or redacted And tests assert that PII/detail endpoints return 403 for host role and 200 for authorized roles And code coverage for masking/authorization modules is >= 90% lines and branches And any failing masking/authorization test blocks deployment to production
Scheduled Privacy Audit Report Generation
Given a monthly audit schedule When the audit job runs Then a report is generated that summarizes host access events by venue, current venue policy settings, test pass/fail status for masking/authorization, and detected anomalies And the report contains no PII values, includes a signed checksum, and a report version and timestamp And the report is stored in a restricted location and delivered to designated compliance recipients
Host Operational Alerts
"As a host manager, I want timely alerts about remaining capacity and delays so that I can respond quickly and coordinate with my team."
Description

Offer opt-in SMS and email alerts for key milestones and exceptions relevant to hosts, such as low inventory (e.g., 20% slots remaining), surge expected (upcoming peak), delays (running 10 minutes behind), weather impacts, or early closure. Allow hosts to set quiet hours and channel preferences in the portal. Alerts include the host attribution link to simplify rapid promotions when capacity is available. All alert content remains aggregate and anonymized.

Acceptance Criteria
Alert Preferences & Quiet Hours Configuration
- Given a host is authenticated in the Host Portal, When they open Alert Settings, Then they can opt in or out per alert type (Low Inventory, Surge Expected, Delay, Weather Impact, Early Closure) and per channel (SMS, Email). - Given a host updates preferences, When they click Save, Then the settings persist and reflect identically after page refresh and on a different device. - Given a host sets quiet hours with start time, end time, and time zone, When saved, Then the inputs are validated (start != end, duration <= 16h, time zone stored) and persisted.
Quiet Hours Enforcement & Relevancy Queue
- Given an alert is triggered during a host's quiet hours, When delivery is evaluated, Then no SMS or email is sent during the quiet hours. - Given an alert is triggered during quiet hours, When quiet hours end, Then the alert is delivered only if the pop-up is still active and, for promotional alerts, remaining slots > 0; otherwise it is discarded. - Given multiple identical alerts are triggered during quiet hours, When quiet hours end, Then at most one deduplicated alert per type is sent within a 30-minute window.
Low Inventory Alert Trigger & Content
- Given a pop-up with total capacity > 0 is active, When remaining slots / total capacity <= 0.20 and remaining slots > 0, Then a Low Inventory alert is sent via the host's opted-in channels. - Then the alert content includes booked count, remaining slot count and percentage, the next available time window, and the host attribution booking link; it contains no client names, addresses, phone numbers, or payment details. - Then the system throttles Low Inventory alerts to at most one every 60 minutes per venue and does not resend unless the remaining slots cross from >20% to <=20% again after increasing above 20%.
Surge Expected Alert Trigger & Content
- Given the ETA forecast is available, When predicted arrivals in the next 60 minutes are >= 1.5x the previous 60 minutes OR projected utilization reaches >= 85% within the next 90 minutes, Then a Surge Expected alert is sent via opted-in channels. - Then the alert content states the expected peak window and magnitude, includes no PII, and includes the host attribution booking link only if remaining slots > 0.
Delay & Early Closure Alerts Trigger & Content
- Given operations are in progress, When average schedule lag (actual start - scheduled start) across the next 5 bookings is >= 10 minutes for at least 10 consecutive minutes, Then a Delay alert is sent via opted-in channels stating the current estimated delay rounded to the nearest 5 minutes and the updated start window; no PII is included. - Given the event's end time is updated to an earlier time than previously published, When the change is saved, Then an Early Closure alert is sent via opted-in channels stating the new closing time and remaining capacity (aggregate only); no PII or payment info is included. - Then subsequent Delay alerts are throttled to one per 60 minutes unless the delay increases by >= 10 additional minutes; Early Closure alert is sent at most once per event.
Weather Impact Alert Trigger & Content
- Given the venue location and schedule, When the weather service reports a >= 60% chance of precipitation or sustained winds >= 20 mph (or gusts >= 30 mph) within the next 3 hours overlapping the pop-up hours, Then a Weather Impact alert is sent via opted-in channels. - Then the alert content identifies the time window and impact type (e.g., rain, high winds), suggests generic preparation language, contains no PII, and includes the host attribution booking link only if remaining slots > 0.
Aggregate & Anonymized Content Compliance
- Given any alert template is rendered, When the message body is generated, Then it must not contain client identifiers (names, phone numbers, emails), street addresses, appointment-specific timestamps, or payment amounts/links; only aggregate counts, percentages, and public event metadata are permitted. - Given an alert is sent, When auditing logs are reviewed, Then the message is recorded with type, timestamp, channel, venue, and aggregate fields only; no PII is stored in the alert payload. - Given promotional capacity exists (remaining slots > 0), When an alert of type Low Inventory or Surge Expected is sent, Then the host attribution booking link with the correct venue attribution code is included; otherwise the link is omitted.
Promo Toolkit & QR Assets
"As a marketing lead at the venue, I want ready-made promo assets and QR codes so that I can promote the pop-up quickly across channels."
Description

Provide ready-to-use promotional assets that reflect live availability: downloadable QR codes pointing to the host-specific booking link, auto-generated social captions and images, and an embeddable widget snippet for the host’s website. Ensure assets auto-update or remain valid as inventory changes, and include attribution tracking. Offer brand-safe templates with the venue name and event date while preserving PawPilot’s guidelines. Make assets accessible on mobile for quick posting by on-site staff.

Acceptance Criteria
Host-Specific QR Code Generation and Scannability
Given a host user is viewing the Host Portal Promo Toolkit for a specific event When they click Download QR Then the system generates both PNG (1024x1024, 300 DPI) and SVG files with error correction level M or higher And the QR encodes a host- and event-scoped short URL including UTM_source=qr_host and event_id And scanning the QR from 1 meter using standard smartphone cameras resolves to the booking page in at least 19 of 20 trials And the booking page loads with live availability and no 404/410 responses before event end time plus 24 hours And time to first byte for the resolved URL is <= 1.5 seconds on a 4G network with ~100 ms RTT
Auto-Generated Social Captions and Images per Network
Given the host selects Generate Social Post in the Promo Toolkit When they choose a network (X, Instagram, Facebook) Then the generated caption includes venue name, event date/time (host timezone), a clear call to action, and a short link containing UTM_source set to the selected network And caption length is <= 280 characters for X, <= 2200 for Instagram, and <= 63206 for Facebook And image assets are generated in 1080x1080 and 1080x1920 with PawPilot logo, venue name, and event date; no client addresses or payment details present And each image includes alt text/description metadata of 120-160 characters And one-tap Copy caption and Download images actions are available on desktop and mobile
Embeddable Live Availability Widget
Given the host copies the embeddable widget snippet When the snippet is inserted into a standard webpage and loaded on a 4G connection Then the widget renders booked count, live ETA curve, and a Book Now button linking to the host booking page And it reflects inventory changes within 30 seconds without full page reload And no client addresses or payment details are displayed And performance meets LCP <= 2.5 s, total JS <= 120 KB gzipped, and zero uncaught console errors And accessibility meets WCAG 2.1 AA (focus order, ARIA labels, keyboard operability, color contrast >= 4.5:1) And layout is responsive from 320 px to 1440 px viewport widths And when no inventory is available, the widget shows a Sold Out or Join Waitlist state with working links
Attribution Tracking Across QR, Social, and Widget
Given a visitor arrives via a QR, social post, or embedded widget When they initiate and complete a booking including any deposit step Then the booking record stores source in {qr_host, social_x, social_instagram, social_facebook, widget}, medium=host_portal, and event_id And UTM parameters persist through the deposit link and confirmation flow And attributed bookings and revenue appear in Host Portal analytics within 15 minutes with >= 99% event processing success And direct visits do not overwrite an existing non-direct attribution within a 7-day attribution window And attribution parameters are excluded from client-facing receipts and notifications
Mobile Access and One-Tap Sharing for On-Site Staff
Given an on-site staff member opens the Promo Toolkit on a mobile device When they access QR codes, social assets, and share actions Then the page loads in <= 3 seconds on a 4G connection and all interactive targets are >= 44 px in both dimensions And Copy caption, Download image, and Share actions invoke the native share sheet on iOS Safari and Android Chrome And downloaded images save to Photos/Downloads and are viewable offline And if a network error occurs, an inline error with Retry appears and succeeds upon connectivity restoration And the interface supports dark and light mode without legibility issues
Brand-Safe Templates with Venue Name and Event Date
Given assets are generated for a host event When rendering text and imagery Then templates include venue name and event date formatted as Fri Aug 15, 3-6 PM in the event timezone And venue names longer than 30 characters are truncated with an ellipsis without overlapping other elements And PawPilot brand colors, logo safe area, and minimum logo size are applied (logo min height >= 24 px; margin >= 8 px) And no client addresses, phone numbers, or payment details appear in any asset And all template text passes spellcheck and color contrast is >= 4.5:1

Overflow Relay

When capacity maxes out or the schedule slips, PawPilot auto‑offers the next pop‑up date or nearby slot by text, carrying deposits forward per your rules. Clients rebook in one tap, preserving revenue and goodwill instead of issuing refunds or losing demand.

Requirements

Overflow Trigger Detection
"As an independent groomer, I want PawPilot to automatically detect when I’m overbooked or running late so that clients are proactively offered alternatives without me stopping work."
Description

Detects when capacity is reached or schedule slippage crosses defined thresholds, initiating Overflow Relay. Supports per-service, per-staff, and global capacity limits; lateness windows; lead-time rules; and travel buffer awareness. Monitors live calendar updates and overrun predictions to proactively trigger alternatives. Emits structured events for downstream actions and deduplicates triggers to prevent message spam. Fully configurable in admin settings with an optional manual confirmation mode.

Acceptance Criteria
Per-Service Capacity Limit Triggers Overflow Relay
Given tenant "Happy Paws" has service "Full Groom" with a configured daily capacity limit of 8 for 2025-08-20 in admin settings And there are already 8 confirmed "Full Groom" bookings on 2025-08-20 When a new "Full Groom" booking or reschedule attempts to add a 9th booking on 2025-08-20 Then Overflow Trigger Detection marks the service/day as overflowed and blocks automatic confirmation for that request And exactly one OverflowRelay.TriggerRequested event is emitted within 5 seconds with fields: trigger_type="capacity", scope="service", service_id, date="2025-08-20", configured_limit=8, observed_count=9, mode="auto", dedupe_key, version
Per-Staff Capacity Limit Triggers Overflow Relay
Given staff "Alex" has a configured daily capacity of 6 appointments for 2025-08-20 in admin settings And Alex already has 6 confirmed appointments on that date When a new appointment assigned to Alex is created or moved into 2025-08-20 Then Overflow Trigger Detection raises an overflow trigger with scope="staff" and staff_id for Alex and blocks automatic confirmation for that request And exactly one OverflowRelay.TriggerRequested event is emitted within 5 seconds with fields: trigger_type="capacity", scope="staff", staff_id, date="2025-08-20", configured_limit=6, observed_count=7, mode="auto", dedupe_key, version
Global Capacity Limit Triggers Overflow Relay
Given tenant "Happy Paws" has a configured global daily capacity of 40 appointments for 2025-08-20 in admin settings And there are already 40 confirmed appointments across all services and staff for that date When a 41st appointment is created or moved into 2025-08-20 Then Overflow Trigger Detection raises an overflow trigger with scope="global" and blocks automatic confirmation for that request And exactly one OverflowRelay.TriggerRequested event is emitted within 5 seconds with fields: trigger_type="capacity", scope="global", date="2025-08-20", configured_limit=40, observed_count=41, mode="auto", dedupe_key, version
Lateness Window and Overrun Prediction Triggers Overflow
Given a lateness threshold of 10 minutes is configured and overrun prediction is enabled in admin settings And an ongoing appointment for staff "Alex" is 12 minutes late or predicted to end 15 minutes after its scheduled end When the predicted start time of Alex's next appointment would exceed the 10-minute threshold Then Overflow Trigger Detection raises an overflow trigger for the next appointment's slot with trigger_type="lateness_overrun" and scope="staff" And exactly one OverflowRelay.TriggerRequested event is emitted within 30 seconds with fields: scope="staff", staff_id, affected_appointment_id, predicted_delay_minutes, threshold_minutes=10, confidence>=0.7, mode="auto", dedupe_key, version
Lead-Time Rule Triggers Overflow for Last-Minute Requests
Given service "Drop-In Walk" has a configured lead-time requirement of 24 hours in admin settings When a client attempts to book or reschedule into a slot starting in 3 hours Then Overflow Trigger Detection raises an overflow trigger with trigger_type="lead_time" and scope="service" and blocks automatic confirmation for that request And exactly one OverflowRelay.TriggerRequested event is emitted within 5 seconds with fields: service_id, scope="service", trigger_type="lead_time", lead_time_required_minutes=1440, lead_time_actual_minutes=180, mode="auto", dedupe_key, version
Travel Buffer Awareness Triggers Overflow
Given mobile operations require a travel buffer of 20 minutes between locations for staff "Jamie" configured in admin settings And the proposed booking or schedule slippage would leave only 10 minutes between two appointments that require an estimated 25 minutes of travel When the detection engine computes available buffer around the impacted slot for Jamie Then Overflow Trigger Detection raises an overflow trigger with trigger_type="travel_buffer" and scope="staff" and blocks automatic confirmation for that request And exactly one OverflowRelay.TriggerRequested event is emitted within 10 seconds with fields: staff_id, scope="staff", trigger_type="travel_buffer", required_buffer_minutes=20, available_buffer_minutes=10, estimated_travel_minutes=25, from_appointment_id, to_appointment_id, mode="auto", dedupe_key, version
Manual Confirmation Mode and Trigger Deduplication
Given manual confirmation mode is enabled in admin settings for Overflow Relay triggers And a capacity breach is detected for service "Full Groom" on 2025-08-20 When the breach is first detected Then a pending overflow trigger record is created with state="awaiting_confirmation" and no client message or downstream event is emitted And no OverflowRelay.TriggerRequested event is emitted until a user with role "Owner" or "Admin" approves When the user approves the pending trigger in the UI Then exactly one OverflowRelay.TriggerRequested event is emitted within 5 seconds with fields: mode="manual", trigger_type="capacity", scope, related_ids, dedupe_key, version And subsequent detections of the same underlying condition within a 10-minute deduplication window using the same dedupe_key do not emit additional events or client messages And if the condition clears and reoccurs after the 10-minute window, a new dedupe_key is generated and exactly one new event is emitted And all state changes are audit-logged with user_id, action, timestamp, and trigger_id
Automated SMS Slot Offers
"As a client, I want to receive a timely text with the best alternate times so that I can quickly rebook without calling or waiting."
Description

Automatically composes and sends personalized SMS offers for the next pop-up date or nearby available slots when overflow is detected. Prioritizes alternatives by proximity, service compatibility, staff availability, and client preferences/history. Includes one-tap deep links to rebook, time-limited soft holds, and clear deposit terms. Respects quiet hours, opt-out statuses, and per-client frequency caps while supporting link shorteners and fallbacks for carrier constraints.

Acceptance Criteria
Overflow-triggered prioritized SMS offers
Given capacity is at max or a schedule slip exceeds the configured threshold for service S at location L And client C is eligible (not opted-out, within frequency cap, has relevant request/history) When an overflow event is detected Then a personalized SMS is composed including client first name, business name, clear deposit terms, and up to 3 alternative slots plus the next pop-up date if applicable And alternatives are ordered by a composite of proximity, service compatibility, staff availability, and C’s preferences/history And the message length (after link shortening) is <= 480 characters or <= 3 SMS segments And an offer_sent event with slot IDs, TTL, and scoring metadata is logged
One-tap deep link rebooking and deposit carry-forward
Given C receives an offer SMS with a rebook deep link and taps it within the hold window When the landing page opens Then the selected slot, service S, staff (if applicable), location L, and deposit terms are pre-filled And any existing deposit is carried forward per configured rules (carry, partial, forfeit) with remaining balance clearly shown And upon confirmation, the appointment is created, the soft hold is converted to a booking, a confirmation SMS is sent, and offer_accepted + payment events are logged And if the link is opened after hold expiry, the page shows an expired state with refreshed options and no booking is created
Time-limited soft hold and release
Given an offer includes a soft hold TTL of T minutes When a slot is offered to client C Then that slot is held exclusively for C for T minutes and is unavailable to others And if C declines or does not respond before T expires Then the hold is released, an offer_expired event is logged, the next eligible candidate is notified, and C receives an expiration SMS And no double-booking occurs during or after the hold transition
Quiet hours, timezone, and frequency caps
Given business quiet hours and client C’s timezone are configured When an offer is triggered during C’s quiet hours Then the SMS is queued and sent at the next allowable send time And if C is opted-out or suppressed by the per-client frequency cap (N offers per M days) Then no SMS is sent and a suppression event with reason is logged And the queued send respects daylight saving changes and updates if quiet hours/config change before send
STOP/HELP compliance and opt-out updates
Given C replies STOP to an offer SMS When the reply is processed Then C is immediately marked as opted-out for SMS And a confirmation SMS is sent if policy/carrier allows, otherwise only the opt-out state is recorded And HELP replies return a compliant help message including business contact and opt-out instructions And all opt-out/HELP events are timestamped and auditable, and future offers are suppressed
Carrier and link shortener fallback
Given the offer includes a deep link When a branded short URL is available Then the SMS uses the branded short URL and preserves tracking parameters And if pre-send checks or delivery receipts indicate carrier filtering/blocking of the domain Then the message is retried with an approved alternate domain or long URL And the final sent message remains within 3 segments (<= 480 chars) and tracking is preserved And deliverability outcomes (sent, failed, filtered) are logged per message
Deterministic slot selection and tie-breakers
Given multiple candidate slots exist for service S When computing the offer list Then slots failing service compatibility or staff availability are excluded And remaining slots are sorted deterministically by proximity and client preference score And ties are broken by earliest start time then lowest travel time then slot ID And no slot outside the configured date window or max travel radius is included
Deposit Carry-Forward & Settlement
"As a groomer, I want deposits to move automatically to the new appointment so that I preserve revenue without manual refunds or back-and-forth."
Description

Implements configurable rules to carry deposits forward on rebook, including full or partial transfers, expiry windows, service equivalence, and maximum carry count. Automatically settles payment differences with surcharges or partial refunds through the payment processor and issues itemized receipts. Handles multi-pet bookings, split services, taxes, and rounding. Ensures idempotent updates, reversal workflows, and complete financial auditability.

Acceptance Criteria
Full Carry-Forward Within Expiry for Equivalent Service
Given Booking B1 for Service A priced $120 with a captured deposit of $50 And a carry-forward rule is configured: transfer=100%, expiry=30 days, equivalence includes Service A <-> Service B, maxCarry=2 And the client rebooks via Overflow Relay to Service B priced $120 within 30 days When the rebook is confirmed Then $50 is applied to Booking B2 as a “Deposit carry-forward” credit and no new charge or refund is created And B2’s receipt itemizes: Service B=$120, Deposit carry-forward=-$50, Tax computed per configured policy, Total due reflects pricing engine output with 2-decimal rounding And the audit log records transferId, originalChargeId, ruleId, amount=50, sourceBooking=B1, targetBooking=B2, performedBy=system
Partial Carry-Forward With Surcharge Settlement
Given Booking B1 with a captured deposit of $100 And a carry-forward rule is configured: transfer=50%, expiry=30 days And the client rebooks via Overflow Relay within the expiry window When the rebook is confirmed Then $50 is carried forward to Booking B2 and a surcharge of $50 is automatically charged through the payment processor in a single successful transaction attempt And the receipt itemizes: Service lines, Deposit carry-forward=-$50, Surcharge=+$50, Taxes computed once per configured policy, Grand Total matches ledger entries with 2-decimal half-up rounding And the audit log includes surchargeChargeId, transferAmount=50, surchargeAmount=50, ruleId, timestamps, and reconciliation status=balanced
Expired Deposit Requires New Deposit and Forfeiture Ledger Entry
Given Booking B1 with a captured deposit of $50 and a carry-forward rule: expiry=30 days And the client attempts to rebook via Overflow Relay after 31 days When the rebook is confirmed Then no deposit amount is carried forward and the client is prompted to pay a new deposit per the service settings And upon client confirmation, a new deposit charge is created and linked to Booking B2; the original $50 remains associated with B1 and is marked forfeited with reason=EXPIRED per policy And the receipt for B2 shows: New Deposit=+$[configured], Deposit carry-forward=-$0, Taxes per policy, Total due computed; audit log records forfeitureEntryId with policy code and notes
Maximum Carry Count Enforcement
Given Booking B1 deposit $75 with a carry-forward rule: transfer=100%, maxCarry=2 And the deposit has already been carried forward twice (carryCount=2) When the client attempts a third rebook via Overflow Relay Then the system disallows further carry-forward and requires a new deposit payment to proceed And the UI and API return errorCode=MAX_CARRY_REACHED and present the payable amount for the new deposit And the audit log records attemptedTransfer declined with ruleId, priorCarryCount=2, declinedAt timestamp, and no financial transactions created
Multi-Pet Split Services Allocation and Rounding
Given Booking B1 includes two line items: Pet A Full Groom=$80 (pretax), Pet B Bath=$40 (pretax), total pretax=$120, tax per policy, captured deposit=$60 And rule: transfer=100%, expiry not reached When the client rebooks both services via Overflow Relay Then the $60 carry-forward credit is allocated proportionally by pretax value: Pet A=$40, Pet B=$20, with 2-decimal half-up rounding and remainder applied to the last line if needed And taxes are recomputed per configured policy on B2; the receipt itemizes credits per line, and Grand Total equals pricingEngineTotal - 60 exactly And the audit log stores allocation map [{lineIdA:40.00},{lineIdB:20.00}], roundingRemainder=0.00, and links to originalChargeId
Automatic Partial Refund on Rebook to Cheaper Service
Given Booking B1 with a captured deposit of $80 and rule: transfer=100%, expiry not reached And the client rebooks via Overflow Relay to a service bundle requiring only a $50 deposit equivalent When the rebook is confirmed Then the system applies $50 to Booking B2 and automatically issues a partial refund of $30 to the original payment method via the payment processor as a single refund transaction And the receipt itemizes: Deposit carry-forward applied=$50, Refund issued=-$30, service lines, taxes per policy; the ledger shows one refund with amount=30 and status=Succeeded And the audit log includes refundId, originalChargeId, transferAmount=50, refundAmount=30, and reconciliation status=balanced
Idempotent Settlement and Reversal Workflow with Full Auditability
Given a client clicks the Overflow Relay rebook link multiple times and the payment processor webhooks may be delivered more than once When the system processes the rebook and settlement Then exactly one financial settlement (charge and/or refund) is created, enforced via idempotency keys and uniqueness constraints; duplicate requests/webhooks are acknowledged with no side effects And if staff triggers a reversal within the configured window, the system restores the original booking/deposit state, issues compensating transactions (reverse surcharge or refund reversal if supported, or offsetting entries), and re-issues corrected receipts And the audit log captures immutable before/after snapshots, correlationIds across API, UI, and processor events, and a trail that passes an end-to-end audit (all sums reconcile to $0 net difference after reversal)
Instant Rebook & Slot Locking
"As a client, I want to confirm a new time in one tap so that I know my appointment is secured immediately."
Description

Confirms a client’s selection in one tap, validates availability in real time, and atomically locks the slot while updating the original appointment. Sends confirmations to both client and provider, adjusts route buffers, and syncs external calendars. Applies concurrency controls to prevent double-booking and coordinates with waitlist claims. Provides graceful fallback states, retries, and alternative suggestions if a slot is taken mid-flow.

Acceptance Criteria
Atomic Slot Lock on One‑Tap Rebook
Given two clients initiate rebooking into the same slot S within 200 ms When both confirm via the one‑tap link Then exactly one booking for S is created and the other receives a "slot taken" SMS within 2 seconds and no double‑booking occurs Given a client taps the rebook link for slot S When the transaction begins Then slot S is locked for up to 10 seconds, the lock prevents other claims, and the lock is released immediately upon commit or rollback with an audit entry recorded Given the slot lock is acquired When the booking commit succeeds Then the original appointment is updated atomically to the new time and the lock is released without deadlocks or orphaned locks
Real‑Time Availability Validation
Given provider capacity, service duration, travel buffers, external calendar blocks, and blackout rules When validating slot S for rebooking Then the system returns available/unavailable within 800 ms at p95 and includes a reason code on unavailability Given slot S conflicts with any buffer, calendar block, capacity, or service rule When the client taps to rebook Then the booking is prevented and an alternative suggestion set is prepared within 2 seconds
Original Appointment Update and Route Buffer Adjustment
Given a successful rebook from time T1 to T2 When the transaction commits Then the original appointment status is set to Rebooked, previous time T1 is logged in history, and the new time is T2 with a reference to the triggering message/session Given provider route/travel buffer rules When T2 is saved Then pre/post buffers are recalculated, persisted, and visible in the schedule UI/API within 1 second of commit and day constraints are not violated
SMS Confirmations to Client and Provider
Given a successful rebook When confirmations are sent Then the client receives an SMS within 5 seconds containing date, time, location, service name, deposit carry‑forward status, cancellation policy link, and a confirmation code Given a successful rebook When notifications are sent Then the provider receives an SMS within 5 seconds containing client name, pet name(s), service, date/time, address, and a summary of route buffer changes Given an SMS delivery failure callback When delivery fails Then the system retries up to 3 times with exponential backoff and logs outcome with final status visible in message history
External Calendar Sync on Rebook
Given a provider has connected an external calendar (e.g., Google) When the rebook commit completes Then the external event is created/updated within 60 seconds with correct start/end times, location, notes, and a link to the appointment Given the external calendar API responds with 429/5xx When syncing the event Then the system retries with exponential backoff for up to 10 minutes and notifies the provider on final failure while preserving internal booking Given sync succeeds When the event is updated Then the external event ID is stored and subsequent updates target the same event
Waitlist Coordination and Claim Precedence
Given a waitlist hold exists on slot S for another client and is not expired When a different client attempts one‑tap rebook into S Then the rebook is blocked, no booking is created, and the client receives alternatives within 2 seconds Given a waitlist hold exists on slot S for the same client When the client taps to rebook Then the hold is converted to a firm booking atomically and the waitlist entry is closed without duplication Given simultaneous waitlist claim and client rebook into slot S When processed concurrently Then exactly one winner is recorded based on precedence rules and the loser receives a "slot taken" SMS; no double‑booking occurs
Graceful Fallback, Retries, and Alternative Suggestions
Given a transient system or network error during rebook When committing the booking Then the system retries up to 3 times over 30 seconds before failing gracefully and no partial bookings are left behind Given a slot becomes unavailable mid‑flow When the client confirms Then the client receives a "slot taken" SMS and at least 3 alternative slots that meet service duration and buffer constraints within 2 seconds Given the original appointment has a deposit When the client selects an alternative slot Then the deposit is carried forward automatically per business rules and the confirmation reflects the applied deposit without double‑charging
Smart Waitlist Coordination
"As a busy dog walker, I want Overflow Relay to work with my waitlist so that open times are filled efficiently without confusing clients."
Description

Coordinates Overflow Relay with the Smart Waitlist to maximize fill rate without conflicts. When no qualified slots exist, offers a seamless waitlist join with preference capture. Reconciles soft holds versus waitlist claims, avoids duplicate outreach, and auto-releases expired holds. Leverages client priority and history to rank offers and reduce churn.

Acceptance Criteria
Overflow Offer Fallback to Waitlist Join
- Given a client receives an Overflow Relay offer but no qualified slot exists per the client’s stored preferences and business rules, When the client taps the rebook link or replies to rebook, Then the system offers a waitlist join option with prefilled service, pet(s), and location. - And the SMS with the waitlist join option is delivered within 15 seconds of the client action (as recorded by SMS provider webhook). - And on confirmation, the client is added to the waitlist with captured preferences (time windows, days, provider, max distance, price tolerance). - And any existing deposit on the original booking is marked as “held—transferable” and linked to the waitlist entry. - And an audit record is created with correlation ID covering the offer, client action, preference capture, and deposit state change.
Duplicate Outreach Suppression Across Overflow and Waitlist
- Given a client has an active waitlist entry for a service/time window, When a new Overflow Relay offer is triggered for a slot overlapping that window, Then the client is suppressed unless the new offer is earlier by ≥30 minutes or matches a higher-priority day/time window from the client’s preferences. - And no client receives more than one offer for the exact slot (service + start time + location) within a 24-hour suppression window. - And deduplication uses an idempotency key of clientId-serviceId-startTime-locationId; duplicate sends are dropped and recorded. - And all suppressed outreach events are logged with reason code and are visible in the outreach log.
Soft Hold vs Waitlist Claim Resolution
- Given a slot is on soft hold for Client A with a configurable TTL (default 15 minutes), When Client B on the waitlist attempts to claim the same slot, Then only one confirmation can succeed via an atomic availability check. - And if Client A confirms within TTL, the slot is assigned to A; Client B receives an immediate regret SMS and remains queued. - And if TTL expires without A confirming, the hold is auto-released and the slot is assigned to the highest-ranked claimant; A receives an expiry notice. - And deposits are transferred to the final assignee; superseded holds are voided without double-charge and reflected in the payment ledger.
Auto-Release of Expired Holds and Next-Client Notification
- Given a hold expires or is canceled, When the release event is processed, Then the slot is freed in the calendar within 5 seconds and the next ranked waitlist candidate is notified via SMS within 15 seconds. - And clients who declined the same slot within the last 12 hours are skipped automatically. - And notification attempts are retried up to 3 times with exponential backoff on transient SMS failures; final failures surface in the dashboard. - And all state transitions (released, notified, accepted, declined, timed out) include timestamps and are available in audit logs.
Waitlist Preference Capture and Validation via SMS
- Given a client chooses to join the waitlist, When prompted via SMS or link, Then the system captures preferences: days of week, time windows, service type, groomer preference, location tolerance, and deposit carry-forward consent. - And inputs are validated (time format HH:MM, distance 0–50 mi, price delta 0–20%); invalid inputs are rejected with corrective prompts and are not persisted. - And the client can confirm in ≤3 interactions for common defaults; defaults prefill from past bookings when available. - And captured preferences are stored, immediately available to the matching engine, and visible on the client profile.
Client Priority Ranking and Offer Sequencing
- Given multiple candidates are eligible for an opening, When offers are generated, Then candidates are ranked via a deterministic score using loyalty tier, no-show rate, on-time payment history, recency of service, and churn-risk flag, with configurable weights. - And ties are broken FIFO by waitlist join timestamp. - And the ranking explanation displays the top 3 contributing factors for each candidate in the UI. - And an A/B toggle allows selecting between ranking models; the chosen model version is logged per offer.
Deposit Carry-Forward Across Overflow and Waitlist
- Given a booking with a paid deposit transitions to Overflow Relay or waitlist, When the client rebooks or claims a slot, Then the deposit is automatically applied to the new appointment. - And any shortfall is collected via secure SMS link before confirmation; any surplus is credited or retained per business policy and shown on the receipt. - And the deposit state machine enforces one active hold per booking and prevents double capture. - And if the client does not rebook within the configured window (e.g., 7 days), the deposit is refunded or retained per policy and the client is notified via SMS.
Admin Controls & Messaging Templates
"As a business owner, I want to set the rules and messages for overflow handling so that rebooking reflects my policies and brand voice."
Description

Provides an admin interface to configure overflow thresholds, quiet hours, search radius, eligible services/staff, maximum reschedules, deposit policies, and escalation steps. Includes editable SMS templates with personalization tokens, localization support, and optional A/B variants. Offers preview, test sends, and per-business overrides to align with brand tone and policies.

Acceptance Criteria
Configure Overflow Thresholds & Escalation Steps
- Admin can configure overflow triggers by: capacity utilization (%) and/or schedule slip (minutes). - Capacity threshold accepts integer 50–150; schedule slip accepts 0–240 minutes; invalid inputs blocked with inline error. - Multiple triggers can be active; admin selects AND/OR logic; selection persists. - Escalation steps can be added/reordered/disabled; each step has delay (minutes), audience (waitlist/active-day clients), template, and search radius override. - Preview shows the computed execution timeline for steps based on current time and delays. - Changes are saved atomically and visible to other admins within 5 seconds. - All changes are recorded in an audit log with admin, timestamp, old/new values. - Feature toggle per business (On/Off) controls whether Overflow Relay uses these settings; default Off for new businesses.
Quiet Hours Enforcement
- Admin can set quiet hours per business in local timezone (start/end HH:MM); supports up to two ranges per day; inputs validated for overlaps. - During quiet hours, outbound SMS from Overflow Relay are queued, not sent. - Queued messages are automatically released at quiet-hours end, preserving escalation step order and delays. - Admin can mark a step as "urgent"; urgent messages are still suppressed unless admin explicitly overrides per send. - Test sends from the editor bypass quiet hours for admin-verified numbers only. - No client receives more than one queued release within a 15-minute window; excess are staggered automatically. - System honors opt-out (STOP) regardless of quiet hours settings.
Search Radius & Slot Matching Controls
- Admin can set default search radius with unit selection (mi/km); allowed range 1–80 km or 1–50 mi; invalid inputs blocked. - Business geolocation is derived from business address; if missing, admin must set latitude/longitude before saving radius. - When Overflow triggers, matching respects radius, travel-time buffer (if configured), and returns only within radius. - P95 response time for generating offers is ≤ 2 seconds for up to 200 candidate slots. - If no slots found within radius, system expands per configured escalation step or returns "no offers" without sending SMS. - All matched/excluded slots are logged with reason codes (out of radius, ineligible service/staff, quiet hours).
Eligible Services and Staff Configuration
- Admin can include/exclude services and staff via multi-select lists with search; defaults: all new services/staff excluded until explicitly added. - Conflicts (e.g., service included but assigned staff excluded) resolve to exclusion; conflict explanation shown in UI. - Changes take effect within 60 seconds for new Overflow evaluations. - If configuration yields zero eligible combinations, UI blocks save with actionable error. - Export/import of the eligibility configuration (CSV/JSON) is available and preserves IDs and labels. - Audit log records adds/removes for services and staff with admin and timestamp.
Maximum Reschedules & Deposit Policies
- Admin sets Max Reschedules per booking (0–5) and Lookback Window (0–365 days); invalid inputs blocked. - Deposit policy options: Carry Forward (full), Carry Forward (partial X%), Forfeit; selection required; X% range 1–100. - Deposit expiry (days) configurable 0–365 when carry-forward is selected; 0 means no expiry. - On client rebook via Overflow, deposit is automatically applied per policy; ledger updates are same-day with reference to original booking. - System displays reschedules remaining and deposit balance token values for use in templates. - When max reschedules exhausted, Overflow offers are suppressed and a configured fallback template is used. - Refund actions are disabled when policy is Carry Forward; enabled when Forfeit is off; UI reflects state consistently.
Messaging Templates, Tokens, Localization, and A/B Variants
- Template editor supports the following tokens and validates usage: {client_first_name}, {pet_name}, {business_name}, {offer_date}, {offer_time}, {staff_name}, {service_name}, {deposit_balance}, {reschedules_remaining}, {rebook_link}, {support_number}. - Saving fails if unsupported or unmatched tokens are present; error lists offending tokens. - Character counter shows GSM-7/Unicode length and segment count; warns when exceeding 1 segment; links auto-shortened to approved domain. - Each template supports locales; at least en-US is required; missing locale falls back to default without token loss. - A/B testing: up to 2 active variants per template; traffic split adjustable 0–100%; variant assignment is deterministic per recipient for 30 days. - Opt-out footer is enforced and not removable; save blocked if removed. - Link tracking UTM parameters can be toggled on/off per template; default on.
Preview, Test Send, and Per-Business Overrides
- Preview renders with sample data for selected locale, timezone, and variant; token substitution is accurate and highlights missing data. - Test send delivers to up to 5 admin-verified numbers per action; deliveries complete within 30 seconds P95; messages marked as TEST in logs. - Per-business overrides can be created for any global template; scope resolution: business > region > global; preview shows active scope. - Versioning: templates have Draft and Published states; published versions are immutable; new drafts can be created and published; rollback restores prior published version. - Access control: only users with "Template Admin" role can publish, create overrides, or change A/B splits; others can preview. - Analytics exclude test sends and include variant-level send counts for production sends.
Analytics & Audit Trail
"As an owner, I want clear reports and an audit trail so that I can prove outcomes, optimize policies, and resolve client questions quickly."
Description

Captures KPIs such as offers sent, acceptance rate, time-to-rebook, revenue preserved versus refunds, deposit carry-forward utilization, and no-show reduction. Provides dashboards and CSV export for analysis. Maintains immutable audit logs for messaging and financial events with traceable IDs to support dispute resolution and customer support.

Acceptance Criteria
KPI Metrics Capture & Idempotent Aggregation
Given Overflow Relay emits offer_sent, offer_accepted, rebook_created, refund_issued, deposit_carried_forward, and no_show_recorded events with unique event_id and org_id in ISO 8601 with org timezone When the daily aggregation job runs at 02:00 local org time Then KPIs are computed per org_id, location_id (if present), and service/staff for the period: offers_sent, acceptance_rate = accepted/offers, time_to_rebook p50/p90 in minutes from offer_sent to rebook_created, revenue_preserved = sum(rebook_total_with_deposit_applied) − sum(refunds_issued_due_to_overflow), deposit_carry_forward_utilization = count(deposit_carried_forward)/count(rebook_created via Overflow Relay), no_show_reduction = 1 − (no_shows_overflow_period/scheduled_overflow_period) compared to baseline And aggregation is idempotent; duplicate events (same event_id) are de-duplicated and do not change results And metrics are available within 15 minutes of job completion and stamped with aggregation window and version
Dashboard KPIs: Filtering, Grouping, and Drill-Down
Given a user selects a date range, location(s), service(s), and staff filter When the dashboard loads Then all KPI tiles and charts reflect the filters and the org timezone consistently And group-by selection (day/week/month) changes chart aggregation accordingly; totals equal the sum of groups within ±0.1% And clicking a KPI segment reveals a drill-down table of underlying events with trace_id, event_id, timestamp, and amounts; row counts reconcile to chart counts And 95th percentile dashboard load time is ≤ 2 seconds for up to 100k aggregated rows
CSV Export: Metrics and Raw Events
Given the user requests CSV export with the same filters as the dashboard When the export is generated Then two CSVs are produced: KPIs (aggregated) and Events (raw), RFC 4180 compliant, UTF-8, comma-delimited, with header row, and ISO 8601 timestamps with offset And column names and counts match the data dictionary; data types are consistent; currency fields are two-decimal fixed with currency code And exports of ≤ 1,000,000 rows complete within 120 seconds and are available via signed URL valid for 24 hours And exported totals reconcile to on-screen totals within ±0.1% and to the financial ledger within max($1, 0.5%)
Immutable Audit Log for Messaging and Financial Events
Given a messaging or financial event is recorded When it is written to the audit log Then the system stores it append-only with event_id, trace_id, actor, payload hash, prev_hash, and signature, and prohibits updates/deletes via API or DB And any attempted mutation is rejected, logged as a security event, and leaves the original record intact And the verification endpoint can return a valid hash chain for any trace_id span and detect tampering And audit logs are retained ≥ 24 months and are queryable by org_id, trace_id, event type, and date
End-to-End Traceable IDs for Dispute Resolution
Given an Overflow Relay offer is sent When subsequent messages, client replies, rebook creation, deposit carry-forward, charges, and refunds occur Then a single trace_id persists across all events and is searchable in UI and API And trace_id uniqueness is guaranteed across all orgs; format is ULID or UUIDv4; collisions are rejected And payment transactions store external processor IDs and map to the same trace_id; a dispute view shows the full chain in chronological order
Revenue Preserved & Deposit Carry-Forward Reconciliation
Given a date range and org When the reconciliation report runs Then revenue_preserved = sum(gross of rebooked appointments with applied deposits) − sum(refunds attributable to canceled original bookings) and is reported alongside counts And every deposit_carry_forward event produces balanced ledger entries linking original booking_id and new booking_id under the same trace_id And the sum of applied deposits equals the sum of deposit reductions on liabilities for the period within max($1, 0.5%) And exceptions (e.g., partial refunds, multi-use deposits) are flagged and itemized with links to audit records
No-Show Reduction Methodology and Display
Given baseline selection (default trailing 8 weeks pre-Overflow adoption, filter-matched) When no-show reduction is displayed Then the metric shows absolute and relative change with numerator/denominator and sample size And baseline and current calculations exclude business-initiated cancellations and reschedules and include only comparable service types and time windows And methodology and filters are visible via an info tooltip and are consistent between dashboard and CSV

LingoSense

Real‑time SMS language detection that locks a per‑client default with a confidence score and one‑tap override. Automatically applies the right templates, policy snippets, and pay links so every message lands in the client’s language without guesswork or retyping.

Requirements

Real-time SMS Language Detection
"As a groomer texting a client, I want the system to recognize the client’s language from their SMS so that replies and automations use the right language without me guessing."
Description

Detects the language and script of each inbound client SMS in real time and attaches a normalized ISO language code and confidence score to the message and conversation context. Handles short, informal texts common to SMS (emojis, abbreviations, fragments) and gracefully falls back to the last known client language when confidence is below threshold. Emits events so downstream modules (templates, policy snippets, pay links) can react without polling, and persists detection metadata for audit and analytics. Supports an extensible set of languages relevant to our market and operates within latency targets that keep the inbox responsive.

Acceptance Criteria
Attach ISO Language and Script with Confidence on Inbound SMS
Given an inbound SMS is accepted by the webhook When LingoSense processes the message Then the message record contains language_code (ISO 639-1 if available else ISO 639-3), script_code (ISO 15924), bcp47_tag, confidence (0.000–1.000), and detection_timestamp (UTC) And the conversation context is updated with last_detected_language = bcp47_tag and last_detected_confidence = confidence And the GET /messages/{id} endpoint and message.created event payload expose these fields exactly And codes are normalized (language lowercase, script title case; e.g., es, Latn; bcp47 like es-Latn)
Low-Confidence Fallback to Client Default
Given a client's last_known_language is set And the detection confidence threshold is 0.80 by default (configurable per tenant between 0.50 and 0.95) When a new message is detected with confidence < threshold Then fallback_applied = true is recorded and emitted And the conversation default language remains last_known_language (no change) And downstream selectors (templates, snippets, pay links) receive last_known_language via event/API And no update to the client default occurs unless a manual override is triggered
Event Emission for Downstream Modules
Given a message is detected When detection completes Then a language.detected domain event is published within 50 ms of message persistence And the event includes: message_id, client_id, language_code, script_code, bcp47_tag, confidence, fallback_applied, detector_version, occurred_at, dedupe_key, correlation_id And events are at-least-once with semantic deduplication enforced via dedupe_key And no polling by consumers is required to receive the language
Accuracy on Short, Informal SMS
Given a stratified test set of ≥500 labeled messages per supported language reflecting SMS style (emojis, abbreviations, fragments, code-switching), held out from training When evaluated offline Then macro-F1 ≥ 0.90 and accuracy ≥ 0.92 across supported languages And for messages ≤5 tokens or containing ≥50% emojis/abbreviations, accuracy ≥ 0.85 And for mixed-language messages, the primary language predicted matches the majority language ≥ 90% of the time And confidence for correct predictions has median ≥ 0.85
Latency and Inbox Responsiveness
Given normal operating conditions at 100 messages/second sustained load When processing inbound messages with LingoSense enabled Then additional per-message detection latency p95 ≤ 120 ms and p99 ≤ 200 ms And end-to-end inbox render time from webhook receipt to message visible p95 ≤ 300 ms And zero message drops or timeouts attributable to detection are observed in a 1-hour soak test
Extensible Language Support and Configuration
Given the initial supported set includes en, es, fr, pt, de, it, zh-Hans, zh-Hant, vi, tl When a new language (e.g., pl) is added via configuration without redeploy Then the detector begins emitting the new language’s codes within 10 minutes And templates/snippets/pay-link routing recognize the new bcp47_tag without code changes And removal or disablement of a language hides it from selection and events within 10 minutes
Persistence and Audit of Detection Metadata
Given detection completes for a message When data is persisted Then detection metadata (language_code, script_code, bcp47_tag, confidence, detector_version, detection_timestamp, fallback_applied, threshold) is stored with the message and retained for ≥24 months And audit logs record manual overrides with actor_id, previous_default, new_default, reason, and timestamp And analytics export includes these fields and is queryable by date range, client_id, language_code, and confidence buckets
Client Language Profile & Auto-Lock
"As a business owner, I want each client to have a default language that locks after we’re confident so that messaging stays consistent and doesn’t flip-flop."
Description

Maintains a per-client default language profile that can be auto-set after accumulating sufficient high-confidence detections or manually set by staff. Includes a lock flag to prevent automatic changes once stabilized, with stored metadata for source (auto/manual), actor, timestamp, and rationale. Stores locale variants and script when applicable, and propagates the profile across the inbox, booking flows, reminders, Smart Waitlist outreach, and payment flows so the correct localized assets are selected by default. Provides merge and conflict resolution when contacts or numbers are deduplicated and keeps a full change history for auditability.

Acceptance Criteria
Auto-set Default After High-Confidence SMS Detections
Given a client receives inbound messages that LingoSense detects as the same language L with confidence >= 0.90 And at least 5 such detections occur within a 30-day rolling window And there are no conflicting detections for a different language with confidence >= 0.80 in that window When the 5th qualifying detection is recorded Then the client's default language profile is set to L (including detected locale/script if available) And the change is stored with source=auto, actor=system, timestamp (UTC ISO-8601), and rationale including detection count, window, and confidence stats And the profile becomes available for downstream flows immediately And an audit entry is created for this auto-set event
One-Tap Manual Set and Override of Client Language
Given a staff user opens a client profile or composer And selects a language (and locale/script) via the one-tap Set/Override control When the user confirms the selection and provides a rationale (required free text or chosen reason) Then the client's default language profile updates immediately to the selected value And the change is stored with source=manual, actor={user id}, timestamp (UTC ISO-8601), and the entered rationale And the user can optionally set lock=true at the time of save And subsequent template/policy/pay-link selection uses the updated profile by default
Lock Prevents Automatic Changes
Given a client language profile where lock=true When subsequent detections disagree with the locked language at confidence >= 0.80 Then the profile is not changed automatically And an audit log entry is recorded with outcome=blocked_by_lock including detection language, confidence, and message ids And no auto-change is suggested or applied until lock=false When a staff user attempts a manual change while lock=true Then the system requires explicit unlock confirmation (set lock=false) before saving the new language And both the unlock and the language change are recorded as separate history entries
Locale Variant and Script Handling
Given a detected or manually set language includes a locale and/or script (e.g., es-MX, es-ES, zh-Hant, zh-Hans) When selecting localized assets (templates, policy snippets, pay links) Then the system chooses an exact locale+script match if available And if an exact match is unavailable, it falls back to the base language while preserving script when possible And any fallback event is logged (asset_type, requested_locale_script, used_locale_script) And the selected assets render in the correct script (e.g., Traditional vs Simplified)
Propagation Across Inbox, Booking, Reminders, Smart Waitlist, and Payments
Given a client has a default language profile (with locale/script) When staff send messages from the Inbox using templates, initiate booking flows, schedule or send reminders, trigger Smart Waitlist outreach, or generate payment requests Then the system preselects assets that match the client's language profile in each flow And the rendered outbound content and pay links match the profile's locale/script And if a matching asset is missing, the system applies the defined fallback and records a telemetry event And a delivery record stores the asset locale/script actually used for each message/link
Merge and Conflict Resolution on Contact Deduplication
Given two contacts are being merged and their language profiles differ When the merge is executed Then conflict resolution is applied in this order: (1) any profile with lock=true wins; (2) else the most recent manual change wins; (3) else the profile with higher detection consistency wins (>=3 high-confidence detections and at least 20% higher count over the other within 30 days); (4) else require staff to choose explicitly And the resulting profile is saved with source=merge, actor=system or {user id} if manual choice, timestamp, and rationale indicating the rule applied And both source profiles' histories are preserved and linked in the merged contact's history
Comprehensive Change History and Audit Export
Given any profile mutation or attempted mutation occurs (auto-set, manual set, lock toggle, merge resolution, blocked change) When the event is recorded Then the history entry includes: before_value, after_value, source, actor, timestamp (UTC ISO-8601), rationale, detection_evidence_summary (language(s), confidence stats, message ids/count) And history entries are immutable and append-only And history can be filtered by date range, source, and actor And an export produces CSV and JSON files with exactly these fields for the selected range And history remains intact after merges and soft-deletes
Confidence Scoring & Threshold Management
"As an admin, I want to set confidence thresholds for auto-apply and lock so that we balance automation with accuracy across my team."
Description

Normalizes language detection confidence to a 0–100 scale and applies configurable thresholds for auto-applying localized assets and for auto-locking the client’s language profile. Supports organization-level defaults with workspace overrides and per-client exceptions. Defines clear behaviors for ambiguous detections (e.g., show review banner, do not auto-switch, do not alter locked profiles). Captures calibration metrics to continuously tune thresholds and improve precision/recall over time.

Acceptance Criteria
Normalized Confidence 0–100 on Detection Output
Given any inbound or outbound message triggers language detection When detection completes Then the detection result includes normalizedConfidence on a 0–100 inclusive scale And normalizedConfidence is present and numeric in API responses, event logs, and operator UI And identical inputs with the same detector version produce the same normalizedConfidence And detector certainty of minimum maps to 0 and maximum maps to 100
Auto-Apply Localized Assets Based on Threshold
Given the organization has T_apply set to 75 and the client is not locked to a different language When a detection returns language=es with normalizedConfidence=82 (>= T_apply) Then SMS templates, policy snippets, and pay links default to Spanish for that interaction And the system records the action as auto-applied And no review banner is shown Given the organization has T_apply set to 75 When a detection returns language=es with normalizedConfidence=74 (< T_apply) Then no localized assets are auto-applied And a Review language banner is shown to the operator And the prior/default language remains active for assets
Auto-Lock Client Language Based on Threshold
Given the organization has T_lock set to 90 and the client has no locked language When a detection returns language=fr with normalizedConfidence=92 (>= T_lock) Then the client’s language profile is locked to French And subsequent conversations default to French regardless of future detections until explicitly unlocked by a user Given a client language profile is locked to English When a detection returns language=es with normalizedConfidence=98 Then the lock is not altered automatically And messaging assets remain in English by default
Hierarchy: Org Defaults, Workspace Overrides, Per-Client Exceptions
Given org-level defaults T_apply=70 and T_lock=90 and Workspace A overrides T_apply=75 and T_lock=92 and Client C has a per-client exception T_apply=80 When computing effective thresholds for Client C in Workspace A Then T_apply=80 (per-client) and T_lock=92 (workspace) are used Given org-level defaults and no workspace override and no per-client exception When computing effective thresholds for any client in that workspace Then the org-level defaults are used Given a workspace override is created, updated, or removed When the change is saved Then the new effective thresholds are used on the next detection and the change persists for that scope only
Ambiguous Detections Behavior (Review Banner, No Auto-Switch)
Given the organization has T_apply set When a detection returns normalizedConfidence below T_apply Then no language auto-switch occurs for assets And a Review language banner is shown to the operator And the client’s locked language (if any) is not altered Given a client language profile is locked to English When a detection returns any different language at any confidence Then no auto-switch occurs and the lock remains unchanged And an informational notice may be shown indicating the profile is locked
Calibration Metrics Capture for Threshold Tuning
Given any language detection completes When the system processes the result Then a calibration event is recorded containing clientId, workspaceId, predictedLanguage, normalizedConfidence, T_apply, T_lock, wasAutoApplied, wasAutoLocked, clientLockedBefore, clientLockedAfter, userOverrideUsed, and finalLanguage And the event is available to the analytics store within 2 seconds of detection And events are retained for at least 90 days for precision/recall analysis
Threshold Configuration & Validation (0–100, Required, Audit)
Given an admin sets T_apply or T_lock via UI or API When a value outside 0–100 inclusive is submitted or a non-numeric value is provided Then the system rejects the change with a validation error and does not persist it Given valid numeric values within 0–100 are submitted When the change is saved Then the thresholds update successfully and take effect on the next detection only (no retroactive changes) And an audit record is written including actor, timestamp, scope (org/workspace/client), and old/new values
One-Tap Language Override
"As a groomer in the inbox, I want a one-tap way to switch the message language and update the client profile so that I can correct mistakes quickly without retyping."
Description

Provides a one-tap control in the SMS thread header and composer to override the active language for the current conversation and optionally update the client’s default profile. Displays suggested languages based on recent detections and business locale, with a searchable full list. Immediately rebinds templates, policy snippets, and payment links to the selected language and re-renders any draft without losing typed content. Supports undo and optional reason capture, and records overrides to the audit log while respecting lock rules.

Acceptance Criteria
Header and Composer One-Tap Control Visibility and Accessibility
Given an active SMS thread, When the thread loads, Then a language control is visible in both the header and the composer, each displaying the same active language code. Given keyboard navigation, When tabbing through controls, Then the language control in header and composer are focusable and have screen-reader labels indicating the current language and action (e.g., "Change language"). Given either control is used, When a new language is selected, Then the other control reflects the same selection within 1 second without a page reload. Given role permissions, When a user lacks override permission, Then the language control is disabled or hidden per policy and cannot change the active language.
Instant Language Switch and Draft Preservation
Given a draft message containing free text and inserted template/snippet tokens in language L1, When the user switches the conversation language to L2, Then the draft remains intact, free text is preserved verbatim, and template/snippet tokens re-render in L2. Given the cursor is within the draft, When the language changes, Then the cursor position is maintained within 1 character of its prior index. Given the UI language indicator, When the override occurs, Then the header/composer language indicator updates within 1 second and subsequent sends use L2. Given bound assets (templates, policy snippets, payment links), When L2 is selected, Then pickers filter to L2 variants; if an asset lacks L2, it is flagged "Missing translation" and falls back to the business default language without altering free text.
Suggested Languages and Searchable List
Given recent inbound detections with confidence scores and a business locale, When opening the language selector, Then a Suggestions section shows up to 3 detected languages with confidence ≥ 0.50 plus the business locale if not already included, sorted by confidence (desc) then business locale. Given the suggestions are displayed, Then each item shows localized name, ISO code, and confidence as a percentage rounded to the nearest whole number. Given the user taps "All languages" or types ≥2 characters, When searching by name or ISO code, Then the filtered list returns results within 250 ms and selecting a result immediately sets the active language. Given no recent detections, When opening the selector, Then suggestions include the business locale and the client’s last-used language if available.
Update Client Default Language Option
Given the language selection sheet, When a new language L2 is chosen, Then a checkbox labeled "Set as client default" is visible and unchecked by default. When the checkbox is checked and the change is confirmed, Then the client’s default language is updated to L2, persists to the profile, and is reflected after refresh/reopen of the thread. When the checkbox is unchecked and the change is confirmed, Then only the conversation’s active language changes and the client’s default remains unchanged. When the client default is updated, Then an audit entry indicates isDefaultUpdated=true with before/after values.
Undo of Language Override with State Reversion
Given a successful language override, Then an inline confirmation with an "Undo" action is displayed for 5 seconds. When the user taps Undo within 5 seconds, Then the active language reverts to the prior language, the draft re-renders back to prior template/snippet language, and pickers rebind accordingly. When Undo is taken, Then an audit log entry records a revert event linking to the original override. When 5 seconds elapse or a message is sent, Then the Undo action expires and no automatic reversion is possible.
Audit Logging and Lock Rules Enforcement
Given a client with a locked default language (confidence ≥ 0.95 or admin lock), When a non-privileged user opens the selector, Then override options are disabled with a tooltip "Language locked" and no change can be saved. Given a privileged user overrides a locked default, When selecting a new language, Then a reason field is required (min 5, max 240 characters) before confirmation. For any override (including undo and default updates), When the action completes, Then an audit record is written within 5 seconds containing userId, clientId, conversationId, fromLang, toLang, isDefaultUpdated, isLocked, reason (nullable), and ISO-8601 timestamp. Given audit retrieval, When querying the audit log via API or admin UI, Then the above fields are present and accurately reflect the action taken.
Localization Auto-Apply for Templates, Policies, and Pay Links
"As an operator, I want templates, policy snippets, and pay links to auto-apply in the client’s language so that every outbound SMS is consistent and compliant."
Description

Automatically selects and applies the correct language variant of message templates, policy snippets, and payment/deposit links for each outbound SMS based on the active language (detected, profile, or override). Supports per-template language packs with variable substitution, localized date/time and currency formatting, and regional phone formats. Validates existence of localized assets prior to send; if missing, surfaces a non-blocking warning with safe fallback to the business default and a quick action to create the missing variant. Ensures deposit/payment links route to the localized checkout and policy links open the correct language document. Works seamlessly in Smart Waitlist blasts, reminders, and ad-hoc replies.

Acceptance Criteria
Ad‑hoc reply applies active language with correct template variant
Given a client with profile language fr-CA, no explicit override, and a detected language es-MX When the agent opens the SMS composer and selects a multi-language template Then the active language is set to fr-CA (Profile) And the fr-CA variant is applied to the message body And the UI shows "Active language: fr-CA (Profile)" Given the agent sets an explicit override to es-MX When the preview refreshes Then the es-MX variant replaces the current content And the UI shows "Active language: es-MX (Override)" And the send uses the es-MX content Given no override and no profile language but a detected language de-DE exists When composing a message Then the active language is set to de-DE (Detected) And the de-DE variant is applied And if no detected language exists, the business default language is used
Smart Waitlist blast applies per‑recipient language variants at send time
Given a Smart Waitlist blast to 50 recipients with active languages [en-US:20, es-MX:20, fr-CA:10] When the agent sends the blast using a template with language packs Then each recipient receives the variant matching their active language And no recipient with an available variant receives the business default And the blast summary shows counts per language variant sent And the per-recipient send log records the language used for each message
Automated reminders insert localized policy snippets and substitute variables
Given an upcoming appointment reminder for a client with active language es-MX When the reminder is generated Then the es-MX policy snippet is inserted And the policy link resolves to the es-MX document And variables {client_first_name}, {pet_name}, {appointment_date}, {appointment_time}, {location} are substituted without placeholders left unresolved And the message queues and sends without manual intervention
Locale-aware formatting for date, time, currency, and regional phone formats
Given clients with active languages/regions en-GB, en-US, and fr-CA When the same reminder template with {appointment_date}, {appointment_time}, {amount_due}, and {business_phone} is rendered Then en-GB uses day-first dates and 24-hour or locale-appropriate time, en-US uses month-first dates and 12-hour time with AM/PM, and fr-CA uses locale-appropriate formats And currency symbols, thousand separators, and decimals match the client locale (e.g., fr-CA uses space/thin space and comma as decimal where applicable) And regional phone numbers are formatted per locale while preserving dialability (E.164 in link targets)
Payment and deposit links route to localized checkout with correct currency
Given a deposit request to a client with active language es-MX and currency MXN supported When the message is rendered Then the payment/deposit link opens the checkout in Spanish And the amount and currency displayed match MXN with locale formatting And the link UTM/params include a language/locale parameter reflecting es-MX And if the payment provider lacks es-MX, the link opens in the closest supported language while preserving the correct currency and amount
Missing localized asset yields non‑blocking warning, safe fallback, and quick‑create action
Given a client with active language it-IT And the selected template has no it-IT variant for the body or a referenced policy snippet When the agent prepares to send Then a non-blocking warning is shown indicating the missing it-IT asset(s) And the system safely falls back to the business default language for only the missing parts And a one-click "Create it-IT variant" action is available that opens the editor prefilled with the default text And sending is not blocked and proceeds with the fallback content And the fallback decision is recorded in the message audit log
One‑tap language override re-renders content and updates client default
Given the agent taps the language pill to override to fr-CA for the current message When the override is applied Then all applied assets (template body, policy snippet, pay link) switch to fr-CA And all locale-sensitive formats re-render for fr-CA And the UI indicates the source as Override And when the agent chooses "Set as client default", the client profile language is updated to fr-CA and is used for future messages unless overridden again
Multilingual SMS Encoding & Segmentation Awareness
"As a dispatcher, I want to see segment counts for multilingual messages so that I can avoid unexpected SMS charges and message truncation."
Description

Determines GSM-7 vs UCS-2 encoding based on content and selected language, calculates real-time segment counts, and alerts when localized messages will split into multiple segments or exceed carrier limits. Applies safe transliteration only when allowed by business settings and never for critical data. Preserves tracking when shortening links and ensures compatibility with non-Latin scripts. Provides pre-send validation and a live counter in the composer so staff can optimize content and avoid unexpected charges or truncation.

Acceptance Criteria
Real-time encoding detection and live segment counter in composer
Given a staff user is typing an SMS that contains only GSM-7 basic/extended characters When the user types or deletes characters Then the UI displays Encoding: GSM-7 and shows a live counter of characters remaining and segments using 160 chars for a single segment and 153 per segment when concatenated, counting GSM-7 extended characters as two septets, and the counter updates within 200 ms of the keystroke (95th percentile) Given the user inserts any non-GSM-7 character (e.g., 🐶 or ع) When the character is added Then the encoding switches to UCS-2 within 200 ms, the counter recalculates using 70 chars for a single segment and 67 per segment when concatenated, and a visible multi-segment warning appears if segments > 1 Given the message content changes back to GSM-7-only characters When non-GSM-7 characters are removed Then the encoding reverts to GSM-7 and the counter reflects the correct GSM-7 segment counts
Carrier limit validation and actionable block on send
Given the organization max allowed segments per SMS is 10 When the composed message exceeds 10 segments for its current encoding Then the Send action is disabled, a pre-send validation banner states "Exceeds 10-segment carrier limit" and shows the current segment count, and the UI presents options to edit, shorten links, or apply safe transliteration if eligible Given the composed message is > 1 and ≤ 10 segments When the user hovers or taps the counter Then the UI indicates "This will send as N segments" and the Send action remains enabled Given a send is attempted via API with a payload exceeding the max segments When the request is validated server-side Then the API responds with HTTP 422 and a machine-readable error code SEGMENT_LIMIT_EXCEEDED including calculated segment_count and encoding
Safe transliteration respects settings and protects critical data
Given Business Setting Allow Safe Transliteration = On and the message requires UCS-2 solely due to characters with safe GSM-7 equivalents (e.g., á→a, ç→c) When the system detects this condition Then it surfaces a Suggest transliteration control showing a preview diff and projected segment reduction before acceptance When the user accepts transliteration Then only non-critical text is transliterated (critical fields are any content tagged as {pay_link}, {amount}, {code}, {appointment_time}, and all URLs), encoding recalculates, and the counter updates immediately Given transliteration would modify any critical field When evaluating eligibility Then the transliteration control is disabled with a tooltip naming the blocked field and no changes are applied Given Allow Safe Transliteration = Off When composing messages Then no transliteration is auto-applied and no suggestion is shown
Short link preserves tracking and supports non-Latin URLs
Given the message contains a URL with Unicode domain/path and UTM parameters When the user applies link shortening Then the URL is replaced with a short link on the PawPilot domain (≤ 22 visible characters), the segment counter recomputes based on the shortened URL length, and the short link redirects to the original URL preserving all query parameters and appending msg_id and client_id for tracking When the short link is opened Then the redirect logs include msg_id, client_id, and language, and the destination successfully loads for Unicode domains (via Punycode) and Unicode paths (via percent-encoding) without breaking on major mobile devices Given the message is encoded as UCS-2 When the short link is inserted Then the short link itself remains ASCII-only and does not introduce additional UCS-2 characters
Accurate GSM-7 extension table and concatenation counting
Given a GSM-7 message includes extended characters (^ { } \ [ ] ~ | €) When calculating length Then each extended character counts as two septets and segment counts follow GSM-7 rules: 160 chars for a single segment and 153 chars per segment when concatenated Given a message of 161 GSM-7 characters with no extended characters When the counter computes segments Then it displays 2 segments with 145 characters remaining in segment 2 (153 - 8 used) Given a UCS-2 message of 70 characters When the counter computes segments Then it displays 1 segment; and for 71 characters it displays 2 segments of 67 characters each
Language override re-templates message and recalculates encoding
Given a client default language is English When staff overrides the language to Arabic and inserts the Arabic template Then the composer displays Encoding: UCS-2, recalculates segments using 70/67 limits, and shows a multi-segment warning if segments > 1 within 200 ms of template insertion When staff switches back to an English template containing only GSM-7 characters Then encoding updates to GSM-7, the segment counter recalculates using 160/153 limits, and any placeholders {pay_link}, {amount}, {code} and URLs remain unmodified (no transliteration applied to them)

ToneMirror

Preserves your voice across languages—casual, professional, or warm—by choosing the right pronouns, honorifics, and local idioms (e.g., tú/usted, vous/tu). Keeps rapport intact while avoiding awkward phrasing, so confirmations, reminders, and nudges feel authentically you.

Requirements

Tone Profiles in Composer
"As an independent groomer, I want to pick a consistent tone for my messages so that clients feel my usual voice across languages in confirmations and reminders."
Description

Introduce selectable tone profiles (Casual, Professional, Warm) that map to linguistic parameters such as formality, directness, idioms, and emoji policy. Integrate profile selection into PawPilot’s SMS composer, templates, and automated flows (confirmations, reminders, nudges). Persist the selected tone with each message job and apply it during generation while respecting SMS constraints (segmenting, character limits, GSM vs. Unicode) and preserving dynamic placeholders (e.g., {client_first_name}, {pet_name}, {appointment_time}) and payment/deposit links. Provide sensible account-level defaults with per-campaign and per-message overrides to ensure consistent brand voice across languages and channels. Ensure graceful degradation to base text if the tone engine is unavailable, with observability hooks for debugging.

Acceptance Criteria
Composer Tone Selection and Preview
- Given a user opens the SMS Composer with account default tone set to Professional, When the user selects the Casual tone profile, Then the previewed message updates within 500 ms and reflects Casual tone without altering any dynamic placeholders or links. - Given a tone profile is selected in the Composer, When the message is sent, Then the delivered SMS content matches the preview exactly (excluding carrier normalization) and the message job record persists tone_profile=Casual. - Given language=Spanish is selected for the recipient, When Casual tone is applied, Then pronouns and idioms adopt informal usage (e.g., tú) and the emoji policy for Casual is enforced.
Template and Flow Tone Overrides
- Given account-level default tone=Professional and a template has no tone set, When a campaign using that template sets tone=Warm, Then generated messages use Warm tone. - Given precedence rules per-message override > per-campaign > template > account default, When multiple levels specify tones, Then the highest-precedence tone is applied and recorded on the message job. - Given an automated flow (confirmation/reminder/nudge) has tone=Warm, When the account default is later changed to Professional, Then existing scheduled jobs retain Warm and newly scheduled jobs follow the updated precedence.
Placeholder and Link Integrity Under Tone Transformation
- Given a template contains {client_first_name}, {pet_name}, {appointment_time}, and a deposit URL, When tone transformation is applied in any language, Then all placeholders remain exactly intact and in place, and the URL string is unchanged. - Given tone transformation runs, When output would remove or rename a required placeholder, Then message generation is blocked with a validation error identifying the missing placeholder. - Given a message includes a shortened payment link, When tone adds or rephrases text, Then the link remains single (not duplicated) and is not split across segments.
SMS Constraints Compliance Across Tones
- Given GSM-7 is required and emoji_policy=Disallow, When tone output includes Unicode, Then the system replaces or removes non-GSM characters per policy and recalculates segment count prior to send. - Given the composed message exceeds 160 GSM-7 or 70 UCS-2 characters after tone application, When the user reviews the message, Then the UI displays the accurate final segment count and estimated cost before sending. - Given a message spans multiple segments, When splitting occurs, Then splits happen at word boundaries and never within placeholders or URLs.
Cross-Language Tone Fidelity (Pronouns and Honorifics)
- Given client language=Spanish and tone=Professional, When generating confirmations, Then the text uses usted with formal phrasing; When tone=Casual or Warm, Then the text uses tú with appropriate informal idioms. - Given client language=French and tone=Professional, When generating messages, Then the text uses vous; When tone=Casual or Warm, Then the text uses tu with correct conjugations. - Given locale=es-MX or es-ES, When tone parameters specify local idioms, Then region-appropriate idioms are used without altering placeholders or links.
Graceful Degradation and Observability
- Given the tone engine is unavailable or exceeds a 2 s timeout, When generating any message, Then the system sends the base template text (no tone adjustments) within 3 s total and shows a non-blocking banner to the user. - Given degradation occurs, When the job is stored, Then it records tone_profile_applied=none, degradation_reason, engine_status, and correlation_id, and emits an event to the observability pipeline. - Given degradation occurs in an automated flow, When the send completes, Then no retry is attempted for the same message and the job is marked delivered with degradation metadata.
Tone Persistence and Auditability
- Given any message job created via composer, template, or flow, When viewing job details, Then tone_profile, language, locale, emoji_policy, engine_version, and precedence_source are displayed. - Given a jobs export (CSV or API) is requested, When the export completes, Then each row includes tone_profile and tone_applied_at timestamp fields. - Given a user with audit permissions views the audit log, When tone settings change at account, template, campaign, or message level, Then the log shows who changed what, the previous value, the new value, and timestamps.
Per-Client Pronoun & Honorific Preferences
"As a dog walker, I want PawPilot to use the right pronouns and honorifics for each client so that I maintain the right balance of respect and familiarity."
Description

Enable automatic selection of appropriate pronouns and honorifics (e.g., tú/usted, vous/tu, Señor/Señora) based on inferred locale and relationship formality, with explicit per-contact overrides. Inference signals include client inbound language, phone country code, past interactions, and onboarding selections. Store preferences in the CRM contact record and apply them to all outbound messages and templates. Provide admin controls to set global defaults by region, bulk update existing contacts, and lock formality for sensitive clients. Surface non-intrusive prompts to confirm changes when the system detects a shift in client preference. Ensure compatibility with tone profiles and translation engine, and synchronize changes across automations and the Smart Waitlist.

Acceptance Criteria
Auto-Inference of Formality from Signals
Given a contact without an explicit formality override And inbound message language is detected as Spanish (confidence >= 0.9) And phone country code is MX And no onboarding selection is present And past two client replies are classified as informal (confidence >= 0.8) When the system infers pronoun and honorific Then pronoun is set to "tú" and honorific is omitted by default And the inferred values are saved to the contact record within 1 second And active message previews refresh to display the inferred values And if an onboarding selection exists, it overrides the inference and is saved as the formality source
Per-Contact Override Persists and Applies
Given a contact currently using inferred formality When an admin sets Pronoun = "usted" and Honorific = "Señor" in the CRM and saves Then the override persists in the contact record with source = "override" And an audit log entry records actor, timestamp, and old→new values And all outbound messages and template previews immediately reflect "usted" and "Señor" And GET /contacts/{id} returns updated fields within 1 second And auto-inference is disabled for this contact until the override is removed
Region Defaults and Bulk Update Accuracy
Given Global Defaults for Region FR are set to Pronoun = "vous" and Honorific = none And there are 10,000 contacts with country code FR and no explicit overrides When an admin runs Bulk Update with "Apply region defaults" Then ≥ 99.9% of eligible contacts are updated within 5 minutes And contacts with explicit overrides or locked formality are skipped with reason codes And a job report shows counts for updated, skipped (override), skipped (locked), errors, and provides a downloadable CSV And updated contacts’ next outbound messages use "vous" consistently
Formality Lock Prevents Auto-Changes
Given a contact marked Formality = Locked with Pronoun = "usted" and Honorific = "Señora" When the inference engine detects two consecutive informal signals ("tú") with confidence ≥ 0.9 Then the stored pronoun/honorific do not change And a non-blocking activity log notes a prevented change due to lock And bulk and region-default jobs skip the contact with reason = "locked" And the UI displays a lock indicator and disables auto-inference controls for the contact
Preference-Shift Prompting and Confirmation
Given a contact set to "usted" and not locked When the client sends two consecutive messages classified as informal with confidence ≥ 0.9 within 14 days Then a prompt appears in the Inbox within 5 minutes suggesting switch to "tú" And the prompt includes one-click Confirm and Dismiss and a sample preview using "tú" And Confirm updates the contact record (source = "confirmed prompt"), writes an audit log entry, and sends no outbound message And Dismiss snoozes prompts for 30 days unless a new onboarding selection is submitted And no prompt is shown if an admin changed formality in the last 24 hours
End-to-End Application Across Templates and Automations
Given a contact with pronoun/honorific set and tone profile = "warm" When the system sends Booking Confirmation, Reminder, No-show Nudge, Deposit Request, or Smart Waitlist Offer in ES or FR Then messages use the selected pronoun and honorific consistently and preserve the "warm" tone in translation And template variables {pronoun}, {honorific}, {formality}, {greeting} resolve without placeholders And regression tests show 0 occurrences of incorrect formality across the top 20 templates per language And English templates bypass pronoun substitution without altering tone
Synchronization with Smart Waitlist and Integrations
Given a contact’s formality is changed via override or confirmed prompt When the Smart Waitlist sends the next outbound message Then messages sent ≥ 60 seconds after the change use the new settings And 99% of outbound messages across services reflect the new settings within 2 minutes of change And emitted webhooks and data exports include pronoun, honorific, and formality source fields And caches in downstream services are invalidated within 60 seconds of change
Tone-Preserving Translation Engine
"As a sitter, I want my messages translated to my client’s language without losing my casual or professional style so that communication feels natural and on-brand."
Description

Deliver a backend service that converts base templates into target languages while preserving selected tone, pronouns, and local idioms. Leverage a style-controlled MT pipeline with guardrails to protect placeholders and links, deterministic caching by template+tone+locale, and configurable style tokens per language. Meet performance targets of <500 ms p95 for single messages and provide batch mode for campaigns. Support a fallback path to a curated phrase library using formal-safe variants if confidence drops below threshold or the model is unavailable. Expose a clear API to the messaging pipeline with structured inputs (text, placeholders, tone, locale, pronoun settings) and outputs (final text, confidence, metadata) for observability and auditing.

Acceptance Criteria
Casual tú es-MX Single Message
Given a base template containing placeholders {first_name}, {appointment_time}, and {deposit_link} with tone=casual, locale=es-MX, pronoun=tú When the service translates the template Then final_text preserves all placeholder tokens and the deposit link exactly as provided And final_text uses second-person informal (tú) conjugations for all second-person verbs And final_text contains no usted/usted-related honorifics or possessives (e.g., usted, su) addressing the recipient And metadata includes tone=casual, locale=es-MX, pronoun=tú, and style_tokens applied And confidence >= configured_threshold And fallback_used=false
Placeholder and Link Guardrails
Given a template with multiple placeholders and embedded URLs When the service translates to any supported locale and tone Then every placeholder token is preserved byte-for-byte and not translated, reordered, or surrounded by added punctuation And every URL remains unchanged, valid, and clickable with query parameters intact And guardrail_violations=0 in metadata for the final response And if a generation attempt alters any token or URL, the engine must regenerate and/or switch to fallback so that the final response preserves all tokens and URLs exactly
Deterministic Caching by Template+Tone+Locale+Pronoun
Given identical inputs (template text, tone, locale, pronoun, style_tokens, model_version) When the service is called twice Then the second response returns cache_hit=true and final_text is byte-identical to the first response And the cache_key remains constant across calls with identical inputs And changing any of template text, tone, locale, pronoun, style_tokens, or model_version produces cache_hit=false and a different cache_key And whitespace normalization and placeholder ordering are consistent so deterministic equality holds
Single-Message Latency p95 <= 500 ms
Given a test set of 10,000 single-message requests with input length <=320 characters and warmed service When requests are executed at steady state without batch mode Then the p95 end-to-end latency measured at the service boundary is <=500 ms And no more than 1% of requests exceed 800 ms And responses include cache_hit flags so cached vs non-cached can be observed
Batch Campaign Mode with Ordered Results
Given a batch request containing N>=50 items with varying locales and tones When the service processes the batch Then results are returned with a 1:1 mapping to inputs, preserving input order via stable correlation IDs And each item includes final_text, confidence, and metadata fields identical in shape to single-message responses And per-item failures trigger per-item fallback without failing the entire batch And a job-level summary is returned with counts for success, fallback_used, and errors
Fallback to Formal-Safe Phrase Library on Low Confidence or Model Unavailability
Given a configured confidence threshold and a curated phrase library with formal-safe variants per locale When the MT model returns confidence < threshold or is unavailable/timeout Then the service returns final_text from the phrase library using formal-safe honorifics/pronouns for the target locale (e.g., usted, vous) And placeholders and links are preserved exactly And metadata.fallback_used=true with fallback_reason populated and source='phrase_library' And confidence is populated according to library policy or marked as not applicable per schema And an audit event is recorded for the fallback path
API Contract and Observability/Auditing
Given the public API schema with inputs {text, tone, locale, pronoun_settings, style_tokens, placeholders} When a request is valid Then the response contains {final_text, confidence ∈ [0,1], metadata:{locale, tone, pronoun, style_tokens, model_id, model_version, cache_key, cache_hit, guardrail_violations, fallback_used, fallback_reason?, request_id, timestamp}} And when a required field is missing or invalid, the service returns HTTP 400 with a structured error code and field-level messages And every successful or failed request emits a structured audit record without PII values, including request_id, outcome, confidence, fallback_used, and timing metrics And all responses are JSON and validate against the published OpenAPI schema
Preview with Back-Translation QA
"As a groomer, I want to preview how my message reads in the client’s language and see a quick back-translation so that I can confirm the tone and wording before it’s sent."
Description

Add a preview panel that shows the final target-language SMS alongside a back-translation into the user’s language to increase confidence before sending. Highlight adjusted elements such as pronouns, honorifics, and idiomatic substitutions, and allow quick toggling among tone profiles. Support per-contact previews to reflect stored preferences and regional norms. Provide inline edits that can be saved as a variant of the template for future reuse. Record diffs and decisions for auditability and future model improvements without exposing sensitive client data.

Acceptance Criteria
Side-by-Side Preview with Back-Translation
Given a composed SMS and a target language are available, when the preview panel is opened, then the left pane displays the target-language SMS and the right pane displays a back-translation in the user's UI language. And both panes render within 800 ms at the 95th percentile for messages under 500 characters. And placeholders (e.g., {client_name}, {appointment_time}) render as tokens and are not translated. And URLs, phone numbers, and dates are preserved exactly in both panes. When no target language is selected, then the preview defaults to the contact's preferred language; if unavailable, defaults to the user's UI language and shows a non-blocking notice. When the back-translation service fails, then a non-blocking error banner is shown and the last successful back-translation is retained.
Highlighting Tone Adjustments and Idioms
Given ToneMirror has applied pronoun, honorific, or idiom substitutions, when the preview renders, then each modified segment is visually highlighted in both panes. And hovering or focusing a highlight shows a tooltip with category (pronoun/honorific/idiom) and a brief rationale in the user's UI language. And a legend displays counts per category and a toggle to show/hide highlights; the toggle state persists for the current session. And all highlights are keyboard accessible and expose ARIA labels meeting WCAG 2.1 AA. When no modifications are applied, then a "No tone adjustments detected" message appears and no highlights are shown.
Tone Profile Toggle Updates Preview
Given tone profiles (Casual, Professional, Warm) are available, when the user selects a different tone profile, then both panes re-render to reflect the selected tone. And the re-render completes within 1,000 ms at the 95th percentile. And changes resulting from the toggle are highlighted as diffs. And the selected tone profile is saved with the template or variant for subsequent use. When the selected tone profile is unsupported for the chosen language, then the option is disabled and an explanatory tooltip is shown.
Per-Contact Preview Honors Preferences
Given a contact with stored preferences (e.g., locale es-MX, pronoun usted), when the user selects that contact in the preview, then ToneMirror applies regional norms and the stored pronoun preference in the target SMS. And an inline indicator shows the applied settings (e.g., "usted — es-MX"). And switching to a different contact updates the preview accordingly within 1,000 ms at the 95th percentile. When a contact has no preferences, then regional defaults based on the contact's country are applied and a "Using defaults" indicator is shown. And per-contact changes do not overwrite the base template unless explicitly saved as a variant.
Inline Edit Saved as Template Variant
Given the preview is open, when the user edits text inline in the target-language pane, then the back-translation updates to reflect the edit within 1,000 ms at the 95th percentile. And when the user edits text inline in the source pane, then the target-language text re-generates and highlights updated segments. And clicking "Save as variant" prompts for a name and optional default tone profile; upon save, the variant appears in the template picker and is selectable without page reload. And saved variants store only the diff from the parent template and metadata (creator, timestamp, tone profile, language), and can be deleted or set as default. And pressing Undo or Redo reverts or reapplies the last edit without losing highlight annotations.
Audit Log Records Diffs Without PII
Given a preview is finalized by sending or saving a variant, when the action is completed, then an audit record is created with the template ID, variant ID (if any), language, tone profile, regional norm set, and a diff between original and final texts. And the audit record stores pseudonymized placeholders for PII (e.g., {client_name}, {phone}) and never persists raw client names, phone numbers, email addresses, or addresses. And PII detection and masking achieve at least 99% precision on a curated test set of 200 messages. And audit records are retained for 365 days and are accessible to admins via export, excluding raw message bodies. And all audit export endpoints are protected by role-based access control and are logged. And audit data includes a model decision summary sufficient for offline evaluation without exposing client content.
Safety Guardrails and Fallbacks
"As a business owner, I want safeguards that prevent awkward or non-compliant phrasing so that my messages stay professional, accurate, and carrier-safe."
Description

Implement multi-layer safety checks across locales, including profanity and sensitive-term filtering, carrier compliance validation (e.g., opt-out keywords), and hard protection of structured content such as dates, amounts, times, and URLs. Enforce a minimum confidence threshold from the translation engine; if unmet, automatically fall back to a formal-safe variant or the source template in the client’s preferred language when available. Maintain immutable handling of payment and deposit links and ensure message length remains within carrier limits. Capture audit logs (inputs, outputs, decisions, confidences) with redaction for PII and provide alerting for anomalous outputs or elevated fallback rates.

Acceptance Criteria
Profanity and Sensitive-Term Filtering Across Locales
Given any generated or transformed message text in a supported locale, When the safety pipeline runs, Then the text is evaluated against a multilingual profanity and sensitive-term list with severity scoring, And the final text contains zero banned terms. Given a message containing terms on the allowlist, When evaluated, Then allowlisted terms are not masked or altered. Given test inputs containing profane or sensitive terms in multiple locales (e.g., English, Spanish, French), When processed, Then each flagged term is removed or neutralized and an audit event "content_sanitized" is logged with policy_version, locale, action, and hashed terms_flagged. Given the filter modifies content, When completed, Then structured tokens (dates, times, amounts, URLs) remain byte-for-byte identical to input.
Carrier Compliance and Opt-Out Keyword Preservation
Given an outbound message template that includes an opt-out footer, When translated or tone-shaped, Then the footer retains the exact uppercase keywords STOP and HELP unmodified and unlocalized, and the message passes carrier compliance validation for the active profile. Given the active compliance profile requires an opt-out footer, When a message is missing it, Then the send is blocked and a compliant default footer is auto-suggested; upon user acceptance the final message still preserves STOP and HELP exactly. Given a message violates carrier profile rules (e.g., missing required business identifier or contains prohibited phrases), When validated, Then the send is blocked with error code COMPLIANCE_BLOCKED and no dispatch occurs. Given an opt-out keyword is removed or translated by any step, When validating, Then the pipeline fails validation, logs "compliance_violation", and prevents send.
Hard Protection of Structured Content (Dates, Amounts, Times, URLs)
Given tokens typed as DATE, TIME, MONEY, URL, PHONE, and PAYMENT_LINK in the source, When ToneMirror processes the message, Then each token appears in the output exactly as in the input (byte-for-byte), including formatting, casing, and punctuation. Given a payment or deposit link, When processed, Then it is immutable: no added tracking parameters, no substitutions, and no whitespace insertion; output equals input exactly. Given any structured token before/after hashing, When compared, Then SHA-256(source_token) equals SHA-256(output_token) for every token. Given an attempted mutation of any structured token is detected, When validating, Then the send is blocked, a fallback rewrite is attempted that surrounds preserved tokens, and an audit event "protected_token_mutation_blocked" is logged.
Confidence Threshold and Fallback Orchestration
Given a translation confidence score t for the primary ToneMirror output, When t >= 0.85, Then the primary output is sent. Given t < 0.85 for the primary output and a formal-safe variant is available with confidence f >= 0.85, When evaluated, Then the formal-safe variant is sent. Given both t < 0.85 and f < 0.85 (or formal-safe variant unavailable) and a source template exists in the client's preferred language, When evaluated, Then that source template is sent unmodified. Given none of the above are available at or above threshold, When evaluated, Then the original source template (default locale) is sent unmodified. Given a fallback path is taken, When logging, Then audit includes decision_path, all confidence scores, selected_variant, and reason=fallback_below_threshold.
Message Length and Encoding Compliance
Given the final message text, When encoding is detected as GSM-7, Then the message length is <= max_segments(profile) * 160 characters and no structured token is split across segments. Given the final message text, When encoding is detected as UCS-2, Then the message length is <= max_segments(profile) * 70 characters and no structured token is split across segments. Given the composed message would exceed the configured segment limit, When validated, Then the pipeline first attempts a safe condensation that preserves semantics and all structured tokens; if still over limit, the send is blocked with error code LENGTH_LIMIT and no dispatch occurs. Given URLs are present, When the message is segmented, Then URLs remain intact (no inserted breaks or spaces) and maintain clickability.
Audit Logging, Redaction, and Alerting
Given any message is processed, When the pipeline completes, Then an audit record is stored with fields: message_id, user_id (hashed), client_id (hashed), input_locale, output_locale, model_version, policy_version, confidence_scores, decision_path, protected_token_hashes, compliance_flags, segment_count, and timestamp. Given audit logging occurs, When storing content, Then PII is redacted: E.164 phone numbers masked as +*******NN, emails masked as f***@domain, street addresses removed, and payment/deposit URLs replaced with SHA-256 hash plus domain. Given an anomalous output is detected (post-safety profanity, structured token mutation, confidence NaN), When evaluated, Then an alert is sent within 60 seconds to the configured channel(s) with message_id, anomaly_type, locale, and redacted snippet. Given the 5-minute rolling fallback rate for a tenant exceeds 5%, When computed, Then an alert is emitted with rate, window, and top causes; the condition clears automatically when the rate drops below 3% for two consecutive windows.
Tone Analytics and Auto-Adjustment
"As a sitter, I want to see which tone works best for my clients so that I can increase confirmations and reduce no-shows."
Description

Provide reporting that correlates tone, locale, and pronoun choices with key outcomes (reply rate, booking confirmations, deposit completions, no-shows). Offer A/B testing across tone profiles and regional idiom sets, with statistically sound defaults. Use aggregated outcomes to recommend tone defaults per client or segment and surface insights in the dashboard. Allow export to CSV and integrate with existing analytics. Respect privacy settings and opt-outs, and ensure all learning signals are anonymized and compliant.

Acceptance Criteria
Outcome Correlation Reporting by Tone and Locale
Given historical messages are tagged with tone profile, locale, and pronoun choice And outcomes (reply, booking confirmation, deposit completion, no-show) are recorded with timestamps When a user selects a date range and applies segment filters (e.g., service type, region, client cohort) Then the dashboard displays outcome rates and deltas versus baseline for each tone/locale/pronoun combination And confidence intervals and significance indicators are shown for each metric And users can sort and filter by uplift, significance, and sample size And data refresh latency is under 15 minutes for new messages And 95th percentile page load time for the analytics view is under 3 seconds for datasets under 1M rows
A/B Testing Setup and Statistical Validity
Given a user creates an experiment with two or more tone profiles and/or regional idiom sets And selects randomization unit (client) and target audience with eligibility criteria Then the system computes required sample size using 95% confidence and 80% power based on baseline rates And enforces minimum sample per arm (>=200 messages) and minimum runtime (>=7 days) before calling a winner When the experiment is started Then assignments are randomized, sticky per client, and logged with experiment and variant IDs And live metrics (rates, CIs, p-values) update at least daily And a winner is only declared when significance threshold p<=0.05 and guardrails (no metric degrades >3% relative) are met And the experiment can be paused or stopped with results preserved and exportable
Auto-Recommendation of Tone Defaults per Client or Segment
Given aggregated outcomes meet minimum sample thresholds (k>=100 messages per candidate, >=30 outcomes) and pass privacy checks When evaluating a specific client or defined segment Then the system recommends a default tone/idiom with expected uplift and 95% confidence interval versus current baseline And no auto-change occurs if expected uplift <1% absolute or significance <95% And recommendations are recalculated at most once every 24 hours and include timestamp, sample sizes, and rationale And users can accept, reject, or override recommendations, with changes audited (who, when, before/after) And a rollback option reverts to prior default within two clicks
Dashboard Insights and Explanations
Given statistically notable differences exist across tones/locales When the user opens the ToneMirror analytics view Then insight cards summarize top findings (metric, segment, variant, uplift, confidence) with a clear recommendation CTA And each insight links to a methodology modal explaining metrics, significance, and data sources And insights can be dismissed, pinned, or shared via link respecting role-based access And all surfaced insights exclude opted-out clients and suppressed small samples
CSV Export and External Analytics Integration
Given a user selects filters and clicks Export CSV Then the export includes rows with tone profile, locale, pronoun choice, segment keys, counts, rates, confidence intervals, p-values, date range, and experiment/variant IDs And timestamps are in UTC ISO 8601 and numeric fields use a dot decimal separator And column names are stable, documented, and versioned with a schema version field And exports larger than 100k rows stream progressively or are delivered via secure link within 15 minutes And an authenticated REST API endpoint returns the same dataset with pagination and rate limits And exported data contains no PII; client identifiers are salted hashes scoped per tenant
Privacy, Opt-Outs, and Anonymization Compliance
Given a client has opted out of analytics learning or a tenant has disabled data sharing Then their messages and outcomes are excluded from training, aggregation, and exports And all stored learning signals are anonymized with irreversible hashing and per-tenant salts And segment-level reporting enforces k-anonymity (k>=20) before display or export And data retention follows tenant policy (default 12 months) with automated purge of aged data and corresponding re-aggregation And audit logs capture access to analytics data (who, when, what) for 24 months
Small Sample Handling and Significance Indicators
Given a tone/locale/pronoun combination has low sample size Then the UI displays an "insufficient data" state until minimum N thresholds are met (N>=100 messages and >=30 outcomes) And confidence intervals are shown for all rates; where N is low, intervals widen and no winner is called And segments with N<20 are hidden by default and require explicit user reveal with a caution tooltip And tooltips document thresholds, formulas, and last refresh time for transparency

PetLexicon

A pet‑specific glossary that remembers breed names, coat notes, commands, meds, and behavior flags in both languages. Ensures consistent, accurate translation in care notes and instructions, preventing misunderstandings that could impact safety or service quality.

Requirements

Canonical Term Library (Bidirectional Mapping)
"As an independent groomer, I want PawPilot to remember pet-specific terms and translate them consistently so that my care notes and client instructions are always accurate and clear in both languages."
Description

A centralized, versioned terminology store that maintains canonical entries for breeds, coat notes, commands, medications, and behavior flags with bidirectional English↔Spanish mappings. Supports synonyms, common misspellings, and regional variants, with per-term attributes (category, safety-critical, capitalization rules, formatting). Provides fast lookup and normalization APIs used by messaging, notes, scheduling, billing, and reporting flows to ensure consistent wording across the product. Includes seed dataset import, conflict resolution, and automated tests to guarantee deterministic outputs.

Acceptance Criteria
Bidirectional lookup with synonyms and misspellings
Given a term input in English or Spanish including a configured synonym, regional variant, or common misspelling When the Lookup API /terms/normalize is called with targetLanguage set to the opposite language Then the response returns the same canonicalTermId and the normalizedLabel in the targetLanguage. Given the canonicalTermId returned from an English→Spanish lookup When used to request the English label via GET /terms/{id}?language=en Then the label matches the library’s canonical English label, ensuring round‑trip equivalence. Given an ambiguous input that maps to multiple canonical entries When normalize is called Then the API returns 409 with code=AMBIGUOUS_TERM and a ranked list of candidate canonicalTermIds with scores, and no normalization is applied. Given an unknown input not matching any configured variant When normalize is called Then the API returns 404 with code=TERM_NOT_FOUND and suggestions=[] within <=30 ms p95 and <=70 ms p99.
Versioned terminology and consumer version targeting
Given library version V exists and a term is updated creating version V+1 When a consumer calls normalize without specifying a version Then the response uses version V+1. Given the same input and header Accept-Terminology-Version: V When normalize is called Then the response uses version V labels and canonicalTermId and matches historical output for V. Given a term is rolled back to a prior revision When normalize is called with version pinned to the rollback target Then outputs are identical to those produced before the rollback. Given a requested version that does not exist When normalize is called Then the API returns 400 with code=VERSION_NOT_AVAILABLE and includes availableVersions in the payload.
Per-term attributes enforcement
Given a term with capitalizationRule=TitleCase When normalize is called with any casing of the input Then the normalizedLabel obeys TitleCase. Given a term with formattingTemplate defined When normalize is called Then the returned formattedLabel applies the formattingTemplate exactly. Given a term with safetyCritical=true When normalize is called Then the response includes safetyCritical=true in metadata and messaging and notes services receive and persist this flag unchanged. Given category ∈ {breed, coat_note, command, medication, behavior_flag} When normalize is called Then the response includes category and downstream services route the value to the corresponding domain field.
Normalization consistency across product flows
Given identical raw inputs appear in messaging, care notes, scheduling, billing, and reporting flows When each flow calls normalize Then the persisted normalizedLabel and canonicalTermId are identical across all flows. Given any flow bypasses normalization When daily consistency checks run Then a failure is raised with the flow identifier and offending record count >0 causes the job to fail. Given multi-term free text containing multiple recognized terms When bulkNormalize is invoked Then each recognized term is normalized using the same rules and start/end character offsets are returned for downstream highlighting.
Seed dataset import and deterministic conflict resolution
Given a seed dataset containing duplicates, synonyms, and regional variants in both languages When the import job runs Then a single canonical entry is created per concept with variantAliases populated and all rows linked deterministically to that entry. Given collisions in canonical labels across categories When importing Then the system resolves conflicts according to priority rules (safetyCritical first, then category order, then newest source) and emits an audit record; the same input set always yields the same canonicalTermIds. Given invalid rows (missing required fields, invalid language codes) When importing Then the job rejects only those rows, logs validation errors with line numbers, and proceeds with valid rows; overall job exits non‑zero if rejection rate >5%.
API performance and availability SLOs
Given production traffic at 500 requests/second sustained with p95 payload size 80 bytes When normalize is called Then latency is <=30 ms p95 and <=70 ms p99 over a 5‑minute window, with uptime >=99.9% monthly. Given cold start or cache miss conditions When normalize is called Then latency remains <=120 ms p99 for the first call and subsequent calls meet standard SLOs. Given traffic exceeds 1000 rps burst for 60 seconds When rate limits engage Then clients receive 429 with Retry-After header and no data corruption occurs.
Automated test suite ensures deterministic outputs
Given a fixed library snapshot and seed test inputs covering each category, synonym, misspelling, and regional variant When the automated test suite runs in CI Then 100% of tests pass and outputs are byte-for-byte identical across 3 consecutive runs. Given the same inputs executed in staging and production against the same library version When the conformance job runs nightly Then the diff is empty; any diff >0 triggers an alert. Given a new library version is published When contract tests run against the previous version’s baseline Then backward compatibility holds for canonicalTermId unless an explicit breaking change flag is set; otherwise the publish is blocked.
SMS Auto-Extraction & Suggestions
"As a dog walker, I want the system to auto-detect new commands or medications mentioned in texts and suggest saving them so that I don’t have to manually maintain the glossary."
Description

An opt-in NLP pipeline that scans inbound/outbound SMS and care notes to detect potential new terms (e.g., commands, meds, coat descriptors), normalizes them, and suggests additions or updates to the Lexicon. Highlights uncertain detections for review, learns from accepted/declined suggestions, and respects privacy settings and access controls. Provides inline prompts in the SMS composer and post-conversation summary to streamline capture without disrupting workflows.

Acceptance Criteria
Opt-In and Privacy Controls for NLP Scanning
Given Auto-Extraction is disabled at Settings > PetLexicon When inbound/outbound SMS and care notes are received Then no content is scanned and no suggestions are generated Given Auto-Extraction is enabled and channels "SMS" and "Care Notes" are selected When new messages arrive Then only the selected channels are scanned and others are ignored Given a phone number or conversation is added to the exclusion list When messages from that source arrive Then they are not scanned and an "Excluded" entry appears in the scan log Given scanning occurs When PII patterns (phone, email, street address) are detected Then those tokens are redacted before persistence and only salted hashes are stored for audit Given a data export is generated by an admin When the export contains Auto-Extraction records Then no raw message content is included, only term candidates, redacted references, timestamps, and confidence
Term Detection and Normalization Across Languages
Given messages contain "Apoquel 16mg", "apoquel", or "APOQUEL" When scanned Then a single medication candidate is suggested with canonical name "Apoquel" and dose "16 mg" Given messages include "No jalar" and "No pull" referring to leash behavior When scanned Then both map to the same canonical command with language tags ["es","en"] Given typos or variants like "dobble coat", "double-coat", "Double coat" When scanned Then they are deduplicated to the canonical descriptor "double coat" Given a candidate matches an existing lexicon entry with different casing or spacing When scanned Then an "Update existing" suggestion is produced instead of a duplicate "Add new" Given a detection confidence score is computed When score >= 0.90 Then the suggestion is labeled "Confident"; when 0.60–0.89 it is labeled "Uncertain" and highlighted for review; when < 0.60 it is not shown
Inline Composer Prompts Are Non-Blocking
Given a user is composing an SMS When a new potential term is detected Then an inline, non-blocking chip appears within 300 ms with actions Accept, Edit, and Dismiss Given the user presses Send When a chip is visible Then message sending latency increases by no more than 50 ms compared to baseline with no chip Given the user taps Accept on the chip When network is available Then the term is added to the Lexicon in <= 2 taps within 2 seconds and a confirmation toast appears Given the user taps Dismiss on the chip When in the same conversation Then the same suggestion is snoozed for 7 days and will not reappear during that period Given the user taps Edit When the edit drawer opens Then they can modify category (command/med/descriptor), canonical name, and language before accepting
Post-Conversation Summary Suggestion Review
Given no messages have been exchanged in a conversation for 10 minutes When Auto-Extraction is enabled Then a post-conversation summary shows all new suggestions with type, canonical value, and confidence Given the summary panel is open When the user selects multiple suggestions and clicks Accept Then all selected items are added in one action and an audit entry is recorded per item Given the summary panel is open When the user clicks Edit on a suggestion Then they can change canonical name, category, and language and save or cancel without losing other selections Given the summary panel is open When the user clicks Decline on a suggestion Then it is removed from the list and feedback is recorded for learning Given the summary panel is dismissed When there are remaining suggestions Then a badge count appears on the conversation and inbox list until reviewed
Learning From Accept/Decline Feedback
Given a suggestion is accepted When similar text appears again in future messages Then the candidate confidence increases by at least 0.05 and the accepted term appears in autocomplete within 24 hours Given a suggestion is declined with reason "not a pet term" When similar text appears in the next 30 days Then suggestions with the same pattern are suppressed below the Uncertain threshold and not shown Given at least 50 accept/decline events have occurred in the workspace When the learning job runs Then model parameters update locally and no raw message text leaves the workspace Given a term has been accepted When scanning the same conversation Then the identical suggestion is not resurfaced again within that conversation
Role-Based Access and Auditing
Given a user role is Owner or Lexicon Editor When viewing suggestions Then they can Accept, Edit, and Decline suggestions Given a user role is Staff without lexicon permissions When viewing suggestions Then they can only Dismiss locally and cannot change the shared Lexicon Given any lexicon change occurs When the action completes Then an immutable audit record is stored with user ID, timestamp, action (add/update/decline), source (SMS/Care Note), and message fingerprint Given a user lacks permission When opening the post-conversation summary Then suggestion details are hidden and a "Requires Lexicon permissions" notice is shown with an option to notify an editor
Context-Aware Translation Pipeline
"As a sitter, I want translations that respect my saved terms and flag low-confidence items so that I can trust instructions and avoid mistakes."
Description

A translation service that injects PetLexicon terminology at runtime to preserve saved terms, enforce safety-critical do-not-translate rules, and handle grammar/number/gender agreement where applicable. Produces confidence scores, flags low-confidence segments, and provides fallbacks (original term retention or alternative phrasing) to avoid unsafe substitutions. Optimized for SMS length constraints with smart abbreviation rules and includes unit tests and evaluation datasets covering common pet-care scenarios.

Acceptance Criteria
Runtime Lexicon Injection Preserves Saved Terms
Given PetLexicon contains entries for terms present in the message in either language And the translation direction is EN<->ES When the pipeline translates the message Then each matched term is rendered exactly as the PetLexicon target_form for the target language (case-insensitive match on input; exact-case output as stored) And non-matched terms are handled by the MT engine without overriding any matched term And the API response includes a per-message list of injected term IDs used
Safety-Critical Do-Not-Translate Enforcement
Given PetLexicon marks certain entries with do_not_translate=true (e.g., medication brand names, dosage labels) When the pipeline translates a message containing those entries Then those terms appear unchanged in the output And surrounding grammar remains valid without auto-inflecting the protected term And the API response includes a DNT_applied=true flag per protected term And no alternative translation suggestions are emitted for DNT terms
Grammar/Number/Gender Agreement Handling (EN<->ES)
Given a message where nouns from PetLexicon include metadata for grammatical gender and number And the output language requires agreement (e.g., Spanish) When the pipeline renders determiners, adjectives, and pluralization for coat notes, commands, and behavior flags Then articles, adjectives, and noun forms agree in gender and number with the lexicon metadata And numeric quantities use correct pluralization and unit forms (e.g., 1 ml vs 2 ml; 1 paseo vs 2 paseos) And the output passes automated morphological checks for 100% of cases in the provided test fixtures
Confidence Scoring, Low-Confidence Flagging, and Safe Fallbacks
Given the pipeline segments the message and computes per-segment confidence scores in [0.0, 1.0] And a configurable threshold of 0.85 is set When any segment score is below threshold Then the output flags that segment with the token "[verify]" immediately before the segment And for any lexicon term within low-confidence segments, the pipeline retains the original source term or uses the lexicon-provided safe alternative phrase instead of a low-confidence translation And the API response includes per-segment scores, flags, and the chosen fallback type And segments at or above threshold are not flagged
SMS-Length Optimization with Smart Abbreviations
Given a translated SMS exceeds 160 GSM-7 characters and optimize=true When abbreviation rules are applied Then PetLexicon terms and do-not-translate entries are never abbreviated And non-critical phrases are replaced using a whitelisted mapping to reduce length And numeric units and dosages remain canonical (no abbreviation of units) And the final SMS length is <= 160 GSM-7 characters And the API response contains the list of abbreviations applied
Unit Tests and Evaluation Dataset Coverage for Pet-Care Scenarios
Given a CI pipeline runs on each commit When unit tests covering lexicon injection, DNT enforcement, agreement handling, confidence fallback behavior, and SMS optimization execute Then overall unit test pass rate is 100% And evaluation datasets spanning at least 200 sentences across common pet-care scenarios achieve >= 95% exact-match on lexicon terms, >= 99% DNT enforcement, and >= 95% agreement correctness And any failure blocks deployment and surfaces a report artifact with failing cases
Lexicon Management & Approval Workflow
"As a business owner, I want to approve changes to the glossary and manage who can edit terms so that quality and safety are maintained."
Description

In-app tools to create, edit, merge, and retire terms with role-based permissions. Supports categories, synonyms, per-client/per-pet overrides, safety-critical flags, and rich notes (e.g., dosage instructions). Includes a review queue for auto-extraction suggestions, side-by-side diffs, version history, and audit logs. Provides bulk import/export (CSV/JSON) and validation to prevent duplicates or conflicting mappings.

Acceptance Criteria
Create/Edit Term with Categories, Synonyms, Flags, and Rich Notes
Given I am an Admin or Editor When I create a term providing canonical name, category, source locale value, target locale value, and optional synonyms and notes Then the system saves the term with a unique ID, marks it Active, and makes it available for translation lookups Given a safety-critical flag is set When saving the term Then a reason note is required and the term displays a Safety badge in lists and detail views Given a canonical name or synonym that normalizes equal (case, diacritics, punctuation, whitespace-insensitive) to an existing term in the same category and locale When attempting to save Then the system blocks creation with a duplicate error and offers to open Merge Given I edit any field of an existing term When I save Then a new version is created with a diff of changed fields and the current version becomes effective immediately Given a synonym is already mapped to a different canonical in the same locale When I attempt to assign the synonym Then the system requires selecting a single canonical or removing the synonym before saving
Merge Duplicate Terms and Retire Obsolete Entries with Conflict Validation
Given I select two or more terms to merge When I choose a keep term and confirm Then all synonyms, overrides, and references re-point to the keep term and merged terms are set to Retired with redirects Given source terms have conflicting safety-critical flags When merging Then the resolved keep term's safety-critical flag is True if any source term was True Given source terms have differing field values (e.g., category, translations, notes) When merging Then the resolver requires explicit field-by-field choice or union for list fields before allowing Confirm Given I attempt to retire a term that has active dependencies without a replacement When I click Retire Then the action is blocked and the system lists blocking dependencies with links Given a term has been merged or retired When a user searches by its old canonical or ID Then the result points to the keep term with a Retired alias indicator
Role-Based Permissions for Lexicon Changes
Given role definitions Admin (all actions), Editor (create, edit, override; cannot merge or retire), Reviewer (approve or reject suggestions), Viewer (read-only) When users perform actions Then capabilities are enforced per role Given a user without required permission attempts a protected action When the action is invoked via UI or API Then the system returns 403 PERMISSION_DENIED and no changes are persisted Given permissions are enforced When rendering UI controls Then actions the user cannot perform are disabled or hidden based on role
Review Queue for Auto-Extracted Suggestions with Side-by-Side Diffs
Given the system auto-extracts a potential term from incoming SMS or care notes When a suggestion is created Then it appears in the Review Queue with source snippet, detected category, locale, and confidence score Given a suggestion conflicts with an existing term When opened Then a side-by-side diff highlights differences and shows potential matches to link or merge Given I am a Reviewer or Admin When I Approve a suggestion Then a term is created or linked to the selected existing term and the suggestion is removed from the queue and logged Given I Reject a suggestion When I provide a reason (required) Then the suggestion is closed and will not reappear for the same source text Given duplicate suggestions exist for the same canonical When I approve one Then remaining duplicates are auto-closed as Duplicates with references
Version History and Audit Logs for Term Changes
Given any create, edit, merge, retire, or override action occurs When the action completes Then a version entry and an audit log record are created with actor, role, timestamp, action type, source (UI, API, Import), entity ID, and before/after summaries Given a term with multiple versions When I view Version History Then I see chronological entries with diffs of changed fields and the actor for each change Given I have Admin or Editor role When I click Revert on a prior version and provide a reason Then a new version is created matching the prior state without deleting history Given compliance needs When I export audit logs with filters (date range, actor, action, entity) Then a CSV is generated containing the filtered records
Bulk Import/Export with Validation
Given I upload a CSV or JSON conforming to the documented schema When I choose Import Mode = Create Only Then only new terms are created and rows matching existing canonicals in the same locale and category are skipped with reasons Given Import Mode = Upsert When canonicals match existing terms Then allowed fields (translations, synonyms, notes, flags, categories) are updated and a new version is created per updated term Given the file contains invalid rows (missing required fields, unknown categories, duplicate canonicals per locale/category, conflicting synonym mappings, invalid scope references) When I run import Then invalid rows are rejected and listed in a downloadable error report with row numbers and messages; valid rows are imported Given a large import greater than 10,000 rows When processed Then the system performs the import in batches with progress, and the final summary shows processed, created, updated, skipped, and error counts Given I export terms When I select filters (category, status, scope, updated date) Then the system generates CSV or JSON with all fields including IDs, locales, synonyms, flags, notes, and scope info
Per-Client and Per-Pet Overrides and Precedence Rules
Given a pet context When both pet-level and client-level overrides exist for a term Then resolution precedence is Pet Override > Client Override > Global Given no overrides exist in the active context When resolving a term Then the global term value is used Given an override attempts to unset a safety-critical flag that is set globally When saving Then the system prevents downgrading and safety-critical resolves True if any scope sets it True Given I view a term in a client or pet context When the term is resolved Then the UI indicates the active scope (Pet, Client, or Global) and the overridden fields Given an override is created or edited When saved by Admin or Editor Then version history and audit logs record the scoped change
Safety Flags & Real-time Alerts
"As a groomer, I want safety flags to surface automatically during booking and messaging so that I don’t miss critical risks."
Description

Mechanisms to surface behavior and medication safety flags at decision points across PawPilot (booking, waitlist fills, route planning, SMS composition). Triggers inline warnings, requires acknowledgment on high-risk items (e.g., bite history, seizure meds), and auto-injects essential cautions into outgoing messages and job tickets. Includes configurable escalation rules and event logs for traceability.

Acceptance Criteria
High-Risk Flag Acknowledgment During Booking
Given a pet with one or more high-risk safety flags is selected in the booking flow When the user opens the Booking Details step Then an inline warning banner listing each high-risk flag by name and severity is displayed within 500 ms And the Save/Confirm action is disabled until each warning is acknowledged via a required checkbox And each acknowledgment is recorded with user id, timestamp, booking id, and flag ids on save And if multiple pets are on the booking, warnings are grouped per pet And if no high-risk flags exist, no warning is shown and the Save/Confirm action remains enabled
Waitlist Fill Blocked by Safety Incompatibilities
Given Smart Waitlist is attempting to fill a cancellation And the candidate pet has safety constraints (e.g., requires two handlers, assigned groomer) When slot matching is evaluated Then only slots that satisfy staff capability/tags and shop policy constraints are considered And incompatible slots are skipped with a machine-readable reason code stored on the attempt record And the first eligible candidate is messaged with a deposit link and an auto-injected safety caution summary And the message includes PetLexicon translations per client language preference And if no eligible candidates exist, the automation exits and creates an alert for manual review
Route Planning Highlights Aggression and Med Windows
Given a route is being optimized for visits including pets with aggression or medication timing flags When optimization runs Then stops with medication windows are scheduled within the defined window tolerance (+/- 15 minutes) or flagged as unschedulable And stops with incompatible behavior flags are marked with warnings if scheduled consecutively by the same solo worker And each flagged stop displays a safety icon and summary in the route overview and stop detail And publishing the route requires acknowledgment of each high-risk stop
Auto-Injected Safety Cautions in Outgoing Messages and Job Tickets
Given the system composes an outbound SMS (confirmation, reminder, ETA) or generates a job ticket for a booking with safety flags When the preview is rendered Then essential safety cautions derived from PetLexicon are auto-inserted at the top of the message/ticket And content appears in the client's preferred language(s) with consistent terminology for breed, commands, meds, and behavior flags And the injection cannot be removed or edited unless the user has the "Override Safety Copy" permission and enters a required reason (min 10 characters) And SMS character count and segment count update accurately to reflect injected text And safety cautions are injected idempotently (not duplicated) on subsequent edits And low-risk notes are excluded by default but can be toggled on
Configurable Escalation on Critical Flags
Given an action is taken on a booking with a critical safety flag (e.g., bite level ≥ 3, seizure meds) that requires escalation And escalation rules define channels, recipients, and timeouts When the triggering action occurs (booking confirmation, auto-fill, or route publish) Then notifications are sent to Tier 1 recipients via the configured channels within 30 seconds And if no acknowledgment is received within the configured timeout (default 10 minutes), the event auto-escalates to Tier 2 And acknowledgments and declines are recorded with user id, timestamp, and decision notes And blocked actions remain blocked until an acknowledgment is received or an authorized override is recorded
Event Log Captures Decisions and Acknowledgments
Given any safety warning display, acknowledgment, override, injection, or escalation event occurs When the event completes Then an append-only log entry is written with event type, timestamp (UTC), actor, pet id(s), booking id, flag ids, channel, correlation id, and outcome And entries are readable in a paginated UI with filters (date range, pet, user, event type, outcome) And entries are exportable to CSV with the same fields And logs are retained for at least 24 months and are immutable to non-admin users
Client & Pet Profile Binding
"As a sitter, I want each pet’s terms tied to their profile so that the right notes auto-fill into reminders and instructions."
Description

Associates lexicon entries with specific pets and clients, supporting multi-pet households and service-type-specific defaults (e.g., grooming vs. walking). Ensures the correct terms auto-fill into reminders, invoices, and care reports. Includes duplicate detection, profile merges, portable term sets when clients migrate, and admin tools for cleanup and reconciliation.

Acceptance Criteria
Multi‑Pet Household Term Binding
Given a client with two pets (Milo: P1, Luna: P2) and PetLexicon enabled And a behavior flag entry "No cats" is bound to Milo only When a staff member opens Luna’s profile, creates an appointment for Luna, or previews messaging templates for Luna Then "No cats" is not displayed or auto-selected for Luna in any context And when the staff member opens Milo’s profile, creates an appointment for Milo, or previews messaging templates for Milo Then "No cats" is displayed and auto-selected for Milo And when exporting Milo’s term set, Luna’s terms are not included And searching the lexicon with a Pet filter returns only entries bound to that Pet ID
Service‑Type Default Term Sets
Given pet Milo has service-type defaults configured: Grooming → ["Double coat", "Mat-prone"], Walking → ["Harness only", "Reactive to bikes"] When a grooming appointment is created for Milo Then the grooming default terms pre-populate in appointment notes, reminder templates, and invoice notes for that appointment And when a walking appointment is created for Milo Then the walking default terms pre-populate in the same locations And when the service type on an in-progress appointment is changed from Grooming to Walking (or vice versa) Then the default term set updates accordingly while preserving any manually added non-conflicting terms And when service-type defaults are cleared in settings Then no default terms auto-select for new appointments of that type
Auto‑Fill Into Reminders, Invoices, and Care Reports
Given system templates contain placeholders for PetLexicon categories And the client’s preferred language is English with Spanish as secondary When generating a reminder, invoice, or care report for Milo Then placeholders resolve to Milo’s bound entries in the preferred language, and secondary language where the template is bilingual And entries marked "internal-only" do not render in any client-facing output And for multi-pet households, only the selected pet’s terms are inserted per message instance And the same resolved content appears consistently across SMS, PDF, and email outputs And message rendering including lexicon resolution completes within 300ms at p95 for 1K messages batch generation
Duplicate Lexicon Detection and Merge Suggestion
Given an existing Equipment entry "Harness only" (en) / "Solo arnés" (es) for Milo When a user attempts to add "Harness-only" or "Solo arnes" in the same category for the same pet/client Then the system blocks exact-normalized duplicates (case/diacritics/punctuation-insensitive) with a clear error And for near-duplicates with similarity ≥ 0.92 within the same category and pet/client, the system shows a non-blocking "Potential duplicate" warning with a Merge option And when Merge is chosen, the entries consolidate into one, preserving bilingual pairs, usage counts, and most-recent updated_at And duplicate checks run on manual add, CSV import, and API upsert flows
Profile Merge Consolidates Lexicon
Given two duplicate pet profiles for the same client are selected for merge (source → target) When the merge is executed Then all lexicon entries from source and target are combined under the target pet And exact/near duplicates are auto-collapsed using the duplicate rules; conflicts are resolved by keeping the most recently updated label pair And all historical references (appointments, messages, invoices) now point to the target pet with the consolidated terms And an audit record captures pre/post counts, IDs merged, conflicts resolved, and the operator ID And no entries remain orphaned or associated to the deleted source pet
Portable Term Sets on Client Migration
Given a client is migrated to a new account or transferred to another provider using PawPilot When exporting the client’s term sets Then the export includes pet-level bindings, service-type defaults, categories, bilingual labels, and timestamps And when importing into the destination account Then all terms bind to the matching pets (by preserved Pet ID or mapped external ID) and retain service-type defaults And unmapped categories or pets are listed with actionable errors without blocking import of valid records And after import, auto-fill behavior works identically on the next reminder, invoice, and care report for those pets
Admin Cleanup and Reconciliation Tools
Given an admin with appropriate permissions opens the PetLexicon Cleanup dashboard When they run a scan Then the dashboard lists unbound terms, potential duplicates, invalid bilingual pairs, and orphaned entries with counts And the admin can approve merges, delete entries, or bulk reassign entries to pets/clients with preview and confirmation And all actions are logged with timestamp, operator, before/after snapshots, and are undoable for 7 days And access is restricted to roles with the LexiconAdmin permission And the admin can export a CSV report of findings and applied actions
Template & Messaging Integration
"As a walker, I want my message templates to auto-insert translated terms so that I can communicate quickly and consistently."
Description

Deep integration between PetLexicon and SMS/email templates, reminders, invoices, and care reports. Adds token replacement for terms, bilingual previews, character-count optimization for SMS, and graceful fallbacks when a translation is missing. Exposes API hooks for third-party integrations (e.g., booking widgets) and tracks usage analytics to identify coverage gaps and improvement opportunities.

Acceptance Criteria
Token Replacement: SMS Appointment Reminder
Given an SMS reminder template containing PetLexicon tokens (e.g., {{pet.breed}}, {{pet.coatNotes}}, {{pet.behaviorFlags}}) and a scheduled appointment linked to a pet and client profile When the reminder is rendered for a client whose preferred language is set to Spanish Then all PetLexicon tokens are replaced with their Spanish values and respect the template’s capitalization and surrounding punctuation And unresolved tokens use a provided default value or are removed cleanly without leaving braces or extra spaces And the rendered body contains no HTML or unsafe characters and preserves intended newlines And the system records a render audit entry with templateId, messageId, language, and token keys used
Bilingual Preview: Invoice Email Template
Given an invoice email template that uses PetLexicon tokens and the organization has two configured languages (e.g., English as primary, Spanish as secondary) When the user selects Bilingual Preview in the template editor Then the system displays previews for both languages with all tokens resolved correctly And currency, date, and number formats reflect the client’s locale in each preview And missing translations display fallback text while clearly indicating which tokens fell back, without blocking preview And the user can copy or download each preview in both HTML and plain-text formats
SMS Character Count Optimization
Given an SMS template with PetLexicon tokens that may expand differently by language When a preview is generated for a specific client and pet Then the system detects GSM-7 vs UCS-2 encoding and displays accurate character and segment counts And an auto-optimize option abbreviates configured PetLexicon terms (e.g., coat types, commands) using organization-approved short forms And if auto-optimize is applied, the message fits within 1 segment (<=160 GSM-7 or <=70 UCS-2) without removing required tokens And if the message exceeds the configured max segments, the send action is blocked with a clear error explaining segment overage
Graceful Fallback on Missing Translation
Given a template that requires Spanish output and includes tokens like {{pet.commands}} and {{pet.meds}} When one or more underlying PetLexicon entries lack Spanish translations Then the renderer substitutes the default-language value using the organization’s configured fallback pattern (e.g., plain default text without braces) And the message renders successfully without broken syntax or placeholder artifacts And an analytics event is emitted per missing item including templateId, tokenKey, language, and entityId And the preview/send record displays the count of missing translations for QA visibility
Care Report Integration: Tokens in PDF and Email
Given a care report template containing {{pet.meds}}, {{pet.behaviorFlags}}, and {{pet.coatNotes}} with bilingual values in PetLexicon When a caregiver submits a care report for a client whose preferred language is English Then both the PDF and email outputs show the English values consistently across sections and attachments And safety/behavior flags are rendered with the same emphasis style in both outputs And HTML is sanitized (no script/style tags) and line breaks are preserved as intended And token rendering produces identical text content in PDF and email (ignoring layout differences)
Third-Party API Hook: Token Resolve Endpoint
Given a registered partner with valid API credentials and a template string containing PetLexicon tokens When the partner calls POST /v1/petlexicon/render with entity identifiers and requested languages Then the API responds 200 with resolved text for each requested language plus metadata: language, charset, segmentCount, and missingTranslations[] And unauthorized requests return 401; rate-limited requests return 429 with a Retry-After header And p95 latency is <=500 ms at 100 requests/second in staging load tests And the endpoint is idempotent for identical inputs and returns a stable schema with versioning in the response
Usage Analytics: Coverage and Gap Identification
Given message previews and sends occur across SMS and email templates When the analytics job processes events Then the dashboard displays per-token usage counts, language coverage percentages, and the top missing translations by frequency And metrics update within 15 minutes (p95) of the originating event And exported CSVs match on-screen totals and exclude PII (only token keys, templateIds, and hashed entityIds) And each missing translation event links back to the source template and token for remediation

Template Twins

Create and manage paired SMS templates once—PawPilot generates the second language and keeps both versions synced to your tone and terminology. Smart placeholders adapt grammar and number so times, prices, and policy lines read naturally in each language.

Requirements

Paired Template Composer
"As an independent groomer, I want to create a message once and get a synced second-language version so that I can reach all clients without duplicating work."
Description

Provide a UI and backend service to create an SMS template in a primary language and auto-generate a synced second-language version (initially English ↔ Spanish). Ensure bidirectional syncing so edits on either side update the paired template without breaking placeholders. Include live dual-preview, segment/character counters per language, template categories (e.g., confirmation, reschedule, waitlist fill, deposit request), and safe-guards against deleting or mangling tokens. Integrate with PawPilot’s template library and permissions so teams can share and reuse paired templates across locations and roles. Expected outcome: faster authoring with consistent, high-quality bilingual messages maintained from a single source of truth.

Acceptance Criteria
Create paired template from English to Spanish
Given a user with Template Editor permission is creating a new template When they enter primary language = English and content containing tokens {client_name}, {appointment_time}, {price}, {policy_link} and select Spanish as the paired language Then a Spanish version is generated within 3 seconds of Save or Generate And both versions contain all tokens unchanged in name and curly-brace syntax And the pair is saved with a shared Pair ID and the selected category And the template pair appears in the Template list with English and Spanish badges within 1 second of save And an audit entry records user, languages, Pair ID, and timestamp
Bidirectional sync updates paired template without breaking tokens
Given an existing English–Spanish paired template When the English text (excluding token names) is edited and saved Then the Spanish version is re-generated to reflect the change within 3 seconds And all tokens remain present and valid in both versions And the secondary shows a 'Synced' indicator with timestamp When the Spanish text (excluding token names) is edited and saved Then the English version is re-generated to reflect the change within 3 seconds And all tokens remain present and valid in both versions And a history entry records the source language of the change And if auto-generation fails, a non-blocking error banner is shown, original content is preserved, tokens remain intact, and a Retry action is available
Dual preview with per-language segment and character counters
Given the composer is open on a paired template Then two live previews are displayed side by side, one per language And each preview shows character count and SMS segment count computed using GSM-7/Unicode rules appropriate to its content And counts update within 200 ms of each keystroke And a warning is displayed when segments exceed the organization's configured limit And toggling sample data updates both previews and counters consistently
Smart placeholders adapt grammar, number, and formatting
Given placeholders {pets_count}, {appointment_time}, {price} with sample data pets_count=1, appointment_time=2025-09-12T15:05:00-05:00, price=25 When previewed in English Then the message uses singular nouns and US time/currency formats (e.g., 3:05 PM, $25.00) When previewed in Spanish Then the message uses correct pluralization for pets_count, localized time format per org setting (12/24h), and currency placement/decimal separators appropriate to Spanish locale And there are no orphaned determiners or malformed spaces around tokens in either language And automated tests cover at least 10 pluralization and formatting cases with ≥95% pass rate
Safeguards against deleting or mangling tokens
Given a template containing tokens When a user attempts to delete a required token Then a blocking validation error is shown and Save is disabled When a user types an invalid token pattern (e.g., {Client Name}, {{price}}, {unknown_token}) Then an inline error appears and the server responds 422 with a descriptive message if Save is attempted And the editor prevents breaking a token by splitting braces across lines or inserting unsupported characters inside And pasting content with tokens preserves exact token text And a 'Restore Tokens' action reinstates missing required tokens in one click
Categories and template library integration
Given category options [confirmation, reschedule, waitlist fill, deposit request] exist When creating or editing a pair, the user can assign exactly one category from the list Then the pair is filterable by that category in the list view When importing a paired template from the library Then both languages and category are imported, maintaining Pair ID mapping or creating a new Pair ID when crossing org boundaries When importing a single-language template Then the system prompts to auto-generate the paired language before save And access respects library permissions so users without Library Edit cannot publish to the library
Permissions and sharing across locations and roles
Given an organization with multiple locations and roles When a template pair is shared with Locations A and B Then users with Template View at those locations can view but not edit, and users with Template Edit can edit and save And users outside the shared scope cannot see the pair in list, search, or API And edits by one shared location are visible to other shared locations within 5 seconds And permission changes take effect within 30 seconds and are logged in the audit trail
Smart Placeholder Localization Engine
"As a sitter, I want placeholders to adjust grammar and formatting per language so that my texts feel natural and professional."
Description

Implement a localization-aware placeholder system that renders natural grammar in each language for dates/times, prices, services, pet names, counts, and policy snippets. Support pluralization, gender/number agreement where applicable, ordinals, localized date/time formats, currency symbols and spacing, and conditional clauses (e.g., show deposit line only when required). Provide schema validation, preview data, and fallbacks when data is missing. Ensure GSM vs Unicode handling and accurate segment estimation post-render. Integrate with PawPilot data sources (appointments, services, pricing, waitlist) to populate values at send-time. Expected outcome: templates read naturally in both languages with zero manual grammar fixes.

Acceptance Criteria
Pluralization and Number Agreement for Counts and Service Names
Given a template pair with {pet_count}, {service_count}, and {service_name} placeholders in both languages When rendering with pet_count ∈ {0,1,2,5,21,101} and service_count ∈ {0,1,2} Then the selected plural form in each language matches CLDR rules for the target locale And the rendered nouns/adjectives agree in number (and gender where provided via metadata) with the count And “1” uses singular forms and any other count uses the correct plural form And when plural metadata for a dynamic service is unavailable, the engine falls back to “{count} {singular_name}” without ungrammatical pluralization And no dangling braces, duplicated words, or double spaces appear after rendering
Localized Date, Time, and Ordinal Rendering
Given placeholders {appt_date}, {appt_time}, and {ordinal_n} with optional parameters (locale, timezone, gender) When rendering in the primary and secondary languages for a business timezone and test dates across month/day boundaries Then dates and times are formatted per locale conventions (ordering, separators, month names, 12/24‑hour) And ordinals render correctly per locale (e.g., en: 1st/2nd/3rd; es: 1.º/2.º or 1.ª when gender=feminine) And times reflect the business/appointment timezone with no offset error And leading zeros, spacing, and punctuation conform to locale rules And removing or adding an ordinal clause does not introduce extra spaces or punctuation
Currency Symbols, Spacing, and Price Grammar
Given a {price amount currency} placeholder used in sentences in both languages When rendering amounts {0, 1, 1.2, 1234.5} for currencies {USD, EUR, GBP} Then currency symbol placement, thousands/decimal separators, and spacing match locale rules (e.g., en‑US: $1,234.50; es‑ES: 1.234,50 €) And zero values render with correct minor units per currency policy (e.g., $0.00) And when GSM‑only mode is enabled, any Unicode‑only spacing or symbols are replaced by GSM‑safe equivalents without changing numeric value And if the symbol is unavailable, the engine falls back to the ISO code with correct spacing (e.g., 1.234,50 EUR) And surrounding grammar (articles/prepositions) adapts to singular/plural price phrases per language rules
Conditional Policy and Deposit Line Rendering
Given a template containing an optional deposit policy line with placeholders {deposit_required}, {deposit_amount}, and {policy_snippet} When deposit_required=true with valid deposit_amount Then the deposit line is included with correctly formatted amount and localized phrasing in both languages And when deposit_required=false or deposit_amount is null Then the entire deposit line (including preceding separators) is omitted without leaving extra spaces or dangling punctuation And percentage and fixed deposit types render with correct symbols, spacing, and grammar And the preview shows both included and omitted cases for QA toggling And no raw placeholder tokens appear in the final message in any case
Schema Validation, Preview Data, and Fallback Behavior
Given a template that declares its placeholder schema (types, required/optional, fallbacks) When saving the template Then unknown placeholders and type mismatches are rejected with actionable errors And required placeholders without fallbacks cannot be saved And in Preview mode, deterministic sample data is injected to exercise plural/date/currency edge cases And at send‑time, missing optional data uses the defined fallback or omission rule, yielding a grammatical sentence with no visible tokens And a dry‑run render returns output plus a validation report of applied fallbacks and omissions
GSM/Unicode Encoding and Post‑Render Segment Estimation
Given rendered messages across a corpus using ASCII, GSM basic, GSM extended, and Unicode characters (accents, emojis, non‑breaking spaces) When computing encoding and segment counts after all placeholders are resolved Then the selected encoding is GSM‑7 when all characters are in GSM (counting extended table escapes) or UCS‑2 otherwise And the reported character count and segment count match the SMSC/Twilio reference for all test cases (0 off‑by‑one errors) And enabling “force GSM” replaces curly quotes/NDash/NBSP with GSM‑safe equivalents without altering meaning materially And the metadata includes per‑segment counts and encoding rationale for audit
Real‑Time Data Source Integration at Send‑Time
Given a queued send with placeholders for appointment, service, pricing, and waitlist data When rendering at send‑time with concurrent updates to source records Then the engine resolves values from the latest committed records at enqueue time (idempotent snapshot) And if a referenced record is deleted or unavailable, defined fallbacks are applied and the message still renders without errors And cross‑field dependencies (e.g., service duration affects time window phrasing) are evaluated consistently in both languages And integration errors are logged with correlation IDs and do not block other messages in the batch
Glossary and Tone Control
"As a dog walker, I want to set my preferred terms and tone so that every template matches my brand in any language."
Description

Add account-level controls to define preferred terminology, banned terms, and tone/style (e.g., formal vs friendly) that guide generation and synchronization of the paired template. Support a bilingual glossary mapping (e.g., “no-show” ↔ “ausencia”), domain phrases, and nickname handling (e.g., “pup”). Allow locking specific terms so they are preserved across regenerations and flag deviations during edits. Maintain a translation memory to improve consistency over time. Expected outcome: messages in both languages consistently reflect each business’s brand voice and vocabulary.

Acceptance Criteria
Account Glossary Management
Given an account owner is on Settings > Glossary, When they add a source term with a target-language mapping and save, Then the term pair is persisted and visible in the glossary list with language codes and entry type (single word, phrase, nickname). Given a duplicate source term for the same language pair exists, When the user attempts to save, Then the system blocks save and displays a duplicate error message. Given a glossary entry is a multi-word domain phrase, When generating paired templates, Then the exact phrase is inserted as provided in both languages. Given nickname handling is configured for a term, When generating paired templates, Then the nickname appears in the output only where allowed by tone rules and the mapped equivalent is used in the other language. Given a glossary entry is edited or deleted, When generating after the change, Then new generations reflect the update while previously saved templates remain unchanged.
Tone and Style Configuration and Enforcement
Given account tone is set to Friendly in English and Informal (tú) in Spanish, When generating a new paired template, Then the English text contains 0 occurrences of configured formal markers (e.g., "Dear ", "shall") and may use contractions, and the Spanish text contains 0 occurrences of "usted"/"su" and uses tú/tu forms for second person. Given account tone is switched to Formal in English and Formal (usted) in Spanish, When regenerating the same template, Then the English text contains 0 contractions and avoids casual slang, and the Spanish text uses usted/su forms exclusively. Given tone settings are changed, When syncing an existing paired template, Then both language versions update to match the new tone without altering any locked terms.
Banned Terms Enforcement and Suggestions
Given an account has banned terms defined, When generating paired templates, Then the output contains 0 occurrences of any banned term and uses the glossary-preferred alternatives where defined. Given a user edits a template and types a banned term, When the input is validated (on keystroke or blur), Then the editor highlights the term and presents a one-click replacement suggestion. Given a template contains a banned term at save time, When the user clicks Save, Then a non-blocking warning is shown with the list of banned terms detected and suggested replacements; the user can proceed or apply fixes.
Locked Term Preservation Across Regenerations
Given a user locks specific terms in a template, When the template is regenerated or synced, Then all locked terms remain unchanged in both language versions. Given a locked term conflicts with glossary or tone rules, When regenerating or syncing, Then the system preserves the locked term and displays a non-blocking conflict notice listing the affected term(s). Given a locked term is unlocked, When regenerating, Then the term becomes eligible for replacement according to glossary and tone rules.
Translation Memory Reuse and Learning
Given a source segment has a previously approved target translation in the account's translation memory, When generating or syncing a template containing an exact match of that source segment, Then the stored translation is auto-applied verbatim. Given only a fuzzy match (similarity >= 85%) exists in translation memory, When generating, Then the best match is surfaced as a suggestion and is not auto-applied without user confirmation. Given a user approves or edits a translation, When saving the template, Then the translation memory is updated with the latest approved pair and metadata (date, user, template ID).
Deviation Validation and Fixes on Save/Sync
Given glossary, banned terms, locked terms, and tone settings exist, When the user saves or syncs a paired template, Then validation returns Pass only if: 0 banned terms present; glossary terms are applied where applicable; tone-specific pronouns/register are correct per language; and all locked terms are preserved. Given validation detects deviations, When results are shown, Then each deviation lists the term/phrase, location, and suggested fix, and the user can auto-apply all fixes in one action. Given the user applies suggested fixes, When saving again, Then validation returns Pass.
Sync and Override Workflow
"As a team owner, I want to review and approve changes across both languages with the ability to override specific lines so that quality stays high without losing automation."
Description

Provide a two-way sync engine with segment-level overrides. When one language is edited, auto-update the other while highlighting diffs and preserving manual overrides on locked segments. Include draft/publish states, version history, rollback, and change audit (who, what, when). Offer side-by-side comparison and inline comments for reviewers. Expected outcome: teams confidently manage edits without losing human refinements while keeping both languages aligned.

Acceptance Criteria
Two-Way Auto-Sync for Unlocked Segments
Given a paired template exists with EN and ES and both segments are unlocked When a user edits and saves the EN segment in Draft Then the ES segment auto-updates to the synchronized content within 2 seconds And a sync event is added to version history with actor=system and affected segment IDs Given a paired template exists with EN and ES and both segments are unlocked When a user edits and saves the ES segment in Draft Then the EN segment auto-updates to the synchronized content within 2 seconds And a sync event is added to version history with actor=system and affected segment IDs
Locked Override Preservation
Given a target-language segment is locked due to manual override When the corresponding source-language segment is edited and saved Then the target-language segment remains unchanged And the segment displays a Locked status indicator with tooltip "Manual override preserved" And the diff view shows no changes applied due to lock for that segment And version history records the attempted sync as Skipped with reason=locked
Diff Highlighting Post-Sync
Given a paired template with changes since the last publish When the user enables Diff mode Then additions are highlighted and deletions are struck-through within each changed segment And unchanged segments display no highlights And the user can toggle Diff mode on or off per template And the highlights correspond exactly to character-level differences between versions
Draft/Publish Workflow Controls
Given a template pair is in Draft state When a user with Publish permission publishes the pair Then both languages' drafts become the new Published version with an incremented version number And the publish event records publisher, timestamp, and version ID in audit And Published content is read-only until a new Draft is created And users without Publish permission cannot publish (action disabled)
Version History and Rollback Integrity
Given at least two Published versions exist for a template pair When a user with Rollback permission rolls back to version N Then both languages revert to the exact content of version N And segment lock states from version N are restored And a new version entry is created marking the rollback (rolledBackFrom=N) And inline comments not present in version N are not shown after rollback
Comprehensive Change Audit
Given any segment edit, lock/unlock action, publish, rollback, or auto-sync occurs When a user opens the Change Audit view for the template pair Then each event lists actor (user or system), action type, segment ID(s), language, timestamp, and before/after summaries And audit entries are immutable once recorded And audit entries can be filtered by action type and date range
Side-by-Side Compare with Inline Comments
Given the reviewer opens Side-by-Side Compare for a template pair When the reviewer adds an inline comment on a specific segment and language Then the comment anchors to that segment and language And replies are threaded and can be resolved or reopened And resolved comments are hidden by default but can be revealed via a filter And comments persist across syncs and drafts and are referenced in version history
Language Preference and Fallback Delivery
"As a groomer, I want messages to automatically send in each client’s preferred language with safe fallbacks so that communication is always understood."
Description

Store and respect per-contact language preferences and auto-select the correct variant at send time. If unknown, infer from last reply language or default to the account’s primary language; optionally send a combined bilingual message when within segment limits. Log which variant was delivered and why. Ensure opt-out and required compliance text is shown in the recipient’s language. Expected outcome: clients reliably receive understandable messages with minimal operator effort.

Acceptance Criteria
Auto-select stored language variant
Given a contact with languagePreference='es' and a paired template with variants ['en','es'] When the user sends the template to that contact Then the system selects and delivers the 'es' variant And the compliance footer and opt-out instructions render in 'es' And the delivery log records variant='es' and reason='contact-preference'
Infer from last reply language when preference unknown
Given a contact with no languagePreference and a most-recent inbound message detected as 'fr' And the paired template has variants ['en','fr'] When the user sends the template Then the system selects and delivers the 'fr' variant And the delivery log records variant='fr' and reason='inferred-from-last-reply'
Default to account primary language when no detection available
Given a contact with no languagePreference and no prior inbound messages And the account primary language is 'en' When the user sends any paired template Then the system selects and delivers the 'en' variant And the delivery log records variant='en' and reason='account-primary-default'
Send bilingual message when unknown and within segment limit
Given a contact with no languagePreference and no prior inbound messages And the account setting "Send bilingual when language unknown" is enabled And the predicted segment count for the combined bilingual message is <= account.maxSegments When the user sends a paired template with variants ['en','es'] Then the system delivers a combined bilingual message containing both 'en' and 'es' content in the configured order And opt-out and compliance text are included in both languages And the delivery log records variant='bilingual' and reason='unknown-language-within-segment-limit'
Exceeding segment limit falls back to primary language
Given a contact with no languagePreference and no prior inbound messages And the account setting "Send bilingual when language unknown" is enabled And the predicted segment count for the combined bilingual message is > account.maxSegments And the account primary language is 'en' When the user sends a paired template with variants ['en','es'] Then the system delivers only the 'en' variant And the delivery log records variant='en' and reason='bilingual-exceeds-segment-limit'
Localize opt-out and compliance text
Given any outbound message with a selected single-language variant 'xx' When the message is rendered for delivery Then the opt-out instructions and required compliance text appear in 'xx' And if the delivery is bilingual, the opt-out and required compliance text appear in both languages
Delivery log records selected variant and reason
Given an outbound message is delivered by the system When viewing the message delivery log entry Then it includes fields: variant (e.g., en|es|fr|bilingual), selectionReason (e.g., contact-preference|inferred-from-last-reply|account-primary-default|bilingual-exceeds-segment-limit|unknown-language-within-segment-limit), selectedAt timestamp, contactId, and templateId
Link, Compliance, and Segment Optimization
"As a business owner, I want links and compliance handled automatically in both languages so that my texts deliver reliably and stay within cost and regulations."
Description

Support dynamic insertion of short links (deposit, booking, waitlist, policy) with localized link text and automatic UTM tagging. Run preflight checks for 10DLC/DLT compliance, GSM vs Unicode, and per-language segment/character counts. Provide warnings and auto-optimization suggestions (e.g., shorten policy line, compress date format) to stay within segment targets without losing meaning. Track per-variant deliverability and error codes. Expected outcome: reliable delivery and cost-efficient messages that meet carrier and regulatory requirements in both languages.

Acceptance Criteria
Dynamic Short Link Insertion with Localized Link Text and UTM Tagging
Given a template containing {deposit_link}, {booking_link}, {waitlist_link}, and/or {policy_link} placeholders When a message is rendered for a specific recipient and language Then each placeholder is replaced with a functioning short URL unique to the recipient, template, and link type And the clickable link text is localized to the selected language while the destination URL remains the same And the destination URL includes UTM parameters: utm_source=pawpilot, utm_medium=sms, utm_campaign=<template_slug>, utm_content=<language_code>, utm_term=<link_type> And no PII (names, phone numbers, emails) appears in any UTM value And the short URL length is <= the configured maximum (default 28 characters) And if link shortening fails or the destination is unreachable at render time, preflight returns Fail with an actionable error and the message cannot be sent
Preflight Compliance Check for 10DLC/DLT, GSM/Unicode, and Segment Counts
Given a fully rendered message variant (after placeholder substitution) and target phone country/carrier When the preflight check runs Then for US destinations using 10DLC it verifies brand/campaign registration status and required disclosures per message category; missing or disallowed elements return Fail with specific reasons And for India (DLT) it validates presence of registered template/header IDs and exact content match rules; mismatches return Fail with specific reasons And it determines encoding (GSM-7 vs Unicode) based on the final rendered text and calculates segments using 160/153 (GSM-7) or 70/67 (Unicode) rules And it displays per-language character count, segment count, and estimated cost based on configured pricing And it produces a single Pass/Warn/Fail outcome summarizing compliance and segmentation findings
Auto-Optimization Suggestions to Meet Segment Targets
Given an organization-configured segment target per language (default 1 segment) When a variant exceeds the target Then the system generates concrete suggestions that preserve meaning and placeholders, including: shorten policy text to approved short copy, compress localized date/time format (e.g., Wed 15 Aug 15:30), remove emojis/Unicode where safe, replace smart quotes with straight quotes, and abbreviate common terms And each suggestion displays projected character and segment counts and estimated cost impact before apply And applying a suggestion re-renders the message and immediately recalculates encoding and segment counts And suggestions are shown only if they reduce at least one full segment or >=10% of characters
Per-Variant Deliverability Tracking and Error Codes
Given messages are sent from both language variants of a template When delivery receipts and provider callbacks are received Then the system records per message: provider message ID, send timestamp, segments billed, delivery status (queued, sent, delivered, undelivered), and carrier error code/reason And aggregates per template and per language variant: delivery rate, undelivered rate, average segments per send, and top carrier error codes over selectable date ranges And exposes these metrics in the UI and as an exportable CSV download And all timestamps are presented in the organization timezone with UTC offset
Placeholder Rendering Integrity and Encoding Detection
Given templates use smart placeholders for names, numbers, prices, dates, and pluralization When rendering both language variants Then placeholders are substituted using locale-appropriate grammar, plural rules, number/currency formats, and date/time formats And encoding detection is performed on the final rendered string, not the template source, to ensure correct GSM-7 vs Unicode classification And segment calculations include the exact localized link text and shortened URL as they will be sent And any Unicode-introducing characters from localized content are correctly accounted for in the segment count
Localized Link Text Mapping and Sync Across Template Twins
Given a paired template in two languages When a link type (deposit, booking, waitlist, policy) is added, removed, or renamed in the primary variant Then the secondary variant automatically updates the corresponding placeholder mapping and localized link text to stay in sync And tone/terminology profiles remain applied to both variants after the change And preflight is automatically re-run for both variants and results are updated And an alert is shown if either variant becomes non-compliant or exceeds the segment target post-change
Send Gate with Warnings and Failures
Given a preflight result for a message When the result is Pass Then the user can send without additional prompts And when the result is Warn (e.g., exceeds segment target but is compliant) Then the user must explicitly acknowledge the warning to proceed and optimization suggestions are displayed And when the result is Fail (compliance or technical) Then the send action is disabled and specific reasons with links to the editable sections are shown And the final sent content snapshot and the preflight report are stored with the message record for audit

CodeSwitch Guard

Understands bilingual households and mid‑thread language switches. Replies follow the client’s current language automatically, and for mixed‑language groups it can include a concise bilingual summary of the essentials (time, address, deposit) to prevent costly mix‑ups.

Requirements

Real-time Language Detection
"As a groomer using PawPilot, I want the system to detect a client’s language in real time so that every reply matches how the client is currently texting without me manually switching languages."
Description

Implement a low-latency (≤200 ms) per-message language detection service that identifies English, Spanish, and mid-thread code switches with a confidence score. The detector runs on every inbound SMS, updates the conversation’s current-language state, and emits events used by templating, routing, and analytics. It must recognize code-switching within a single message, support configurable confidence thresholds, and fall back to the contact’s saved preference or account default when confidence is low. Integrate with PawPilot’s messaging pipeline, Smart Waitlist triggers, booking/reschedule flows, reminders, and payment/deposit requests so replies automatically use the latest detected language. Provide tokenization that preserves pet names and addresses, avoid mistranslating PII, and handle emojis/diacritics. Expose an API for override and a test harness with labeled corpora for QA. Log detections for audit, respecting privacy and retention policies.

Acceptance Criteria
Sub-200ms Detection on Inbound SMS
Given a batch of 1000 inbound SMS messages ranging from 1 to 320 characters including emojis and diacritics When they are processed by the language detector under nominal production configuration with up to 50 concurrent requests Then the per-message end-to-end detection latency is ≤200 ms at p95 and ≤300 ms at p99, with no single detection exceeding 500 ms And each detection result includes fields: languagesDetected, confidencePerLanguage, codeSwitchDetected, and decision.currentLanguage
In-Message Code-Switch Recognition with Confidence
Given the confidence threshold is configured to 0.70 And an inbound message "Hola, can you confirm 3pm?" When the message is processed Then languagesDetected includes ["es","en"], codeSwitchDetected is true And confidencePerLanguage["es"] ≥ 0.70 and confidencePerLanguage["en"] ≥ 0.70 And decision.currentLanguage is "en" Given the confidence threshold is configured to 0.70 And an inbound message "Can you venir mañana?" When the message is processed Then languagesDetected includes ["en","es"], codeSwitchDetected is true And confidencePerLanguage["en"] ≥ 0.70 and confidencePerLanguage["es"] ≥ 0.70 And decision.currentLanguage is "es"
Low-Confidence Fallback to Saved Preference
Given a contact with savedPreferenceLanguage = "es" and account defaultLanguage = "en" And the confidence threshold is configured to 0.80 And an inbound message "ok" yields confidencePerLanguage["en"] = 0.55 and confidencePerLanguage["es"] = 0.52 When the message is processed Then fallbackUsed is true with fallbackReason = "low_confidence" and confidenceThreshold = 0.80 And decision.currentLanguage is "es" And the emitted event includes savedPreferenceLanguage and defaultLanguage values used for the decision
Event Emission, State Update, and Flow Integration
Given an inbound message detected as Spanish with decision.currentLanguage = "es" and fallbackUsed = false When the detection completes Then conversation.currentLanguage is updated to "es" within 50 ms of detection completion And a language_detected event is emitted with messageId, conversationId, languagesDetected, confidencePerLanguage, codeSwitchDetected, decision.currentLanguage, and fallbackUsed And a templated auto-reply for a booking confirmation triggered by this message renders the Spanish template variant And a Smart Waitlist offer triggered by a cancellation in the same conversation renders in Spanish And a payment/deposit request initiated in the same conversation within 60 seconds renders in Spanish
PII-Preserving Tokenization with Emojis/Diacritics
Given an inbound message "Fidó @ 123 Main St., 3️⃣pm" When tokenization runs Then tokens include a pet_name span for "Fidó" and an address span for "123 Main St." with original casing and diacritics preserved And emojis and diacritics do not split or corrupt token boundaries And the language detection output does not modify or translate PII spans And the detection result exposes token spans and types for downstream templating to mark PII as non-translatable
Override API and QA Harness
Given a conversation with currentLanguage = "en" When a client calls PUT /conversations/{id}/language-override with body { language: "es", reason: "user_request", ttlMinutes: 60 } Then the override is persisted, decision.currentLanguage becomes "es" for new outbound messages, and overrideActive = true And while the override is active, inbound detections continue but do not change conversation.currentLanguage And DELETE /conversations/{id}/language-override clears the override and emits an override_cleared event Given a labeled corpus containing English, Spanish, and code-switch samples in JSONL When the QA test harness is executed Then it ingests the corpus, runs the detector, and outputs per-language and code-switch metrics (precision, recall, F1) plus a confusion matrix as JSON artifacts
Audit Logging with Privacy and Retention
Given detection runs for an inbound message When audit logging is enabled Then a log record is written containing timestamp, messageId, conversationId, languagesDetected, confidencePerLanguage, codeSwitchDetected, decision.currentLanguage, fallbackUsed, and detectorVersion, without storing full message bodies when redaction is enabled And PII fields (pet_name, address) are masked or omitted per policy And logs are retained and automatically purged according to the configured retention period And log access is restricted to authorized roles and is queryable by messageId and conversationId
Auto-Reply Language Matching
"As a client, I want PawPilot’s texts to arrive in the language I’m currently using so that I can quickly confirm, reschedule, or pay without confusion."
Description

Route all outgoing messages through a localization layer that selects the correct template and locale based on the conversation’s current-language state and recipient preferences. Localize dynamic fields (dates, times, currency, address format) and ensure deposit/payment links render the correct locale on landing pages. Enforce SMS constraints with GSM-7/UCS-2 awareness, segment counting, and smart shortening to keep critical content (time, address, deposit) intact. When detection confidence is below threshold, switch to bilingual mode or the contact’s saved preference automatically. Integrate with confirmations, reminders, waitlist offers, and no-show recovery flows to reduce errors and rework.

Acceptance Criteria
Locale Selection Based on Current Language and Preferences
Given a conversation where the most recent inbound messages are detected as Spanish with confidence >= 0.80 and the recipient’s saved preference is Auto When the system generates any outgoing SMS for that thread Then it selects the Spanish template and locale appropriate to the recipient (e.g., es-MX if specified), otherwise the default Spanish locale And the message body language matches the detected language And the chosen language and locale are recorded in message metadata
Dynamic Field Localization and Payment Landing Locale
Given an outgoing message contains dynamic fields: date, time, currency amount, address, and a deposit/payment link When the locale is selected for the message Then date and time are formatted according to that locale’s conventions And currency symbol, decimal, and thousands separators follow the locale And address lines and ordering follow the locale’s convention And the deposit/payment link opens to a landing page whose UI language equals the SMS locale And the landing page’s currency and date/time formats match the SMS locale
GSM-7/UCS-2 Awareness and Smart Shortening
Given an outgoing message contains only GSM-7 characters When the message is composed Then GSM-7 encoding is used and the segment count is calculated per GSM-7 limits And if the segment count would exceed the configured maxSegments, non-critical text is shortened or removed first while preserving time, address, and deposit link intact And the final sent message does not exceed configured maxSegments Given an outgoing message contains any non-GSM-7 character When the message is composed Then UCS-2 encoding is used and the segment count is calculated per UCS-2 limits And smart shortening is re-applied to stay within configured maxSegments while preserving time, address, and deposit link
Confidence Threshold and Fallback Behavior
Given language detection confidence for the latest inbound message is below the configured threshold And the contact has a saved language and locale preference When composing the reply Then the reply uses the saved language and locale And bilingual mode is not used for that message Given language detection confidence is below the configured threshold And the contact has no saved language preference When composing the reply Then the reply uses bilingual mode including both supported languages for essentials
Mixed-Language Group Essentials Summary
Given a group conversation includes recipients with mixed language preferences or recent messages in different languages When sending a confirmation or reminder message Then the main body follows the current-language of the initiating client And a concise bilingual essentials summary (time, address, deposit link) is included in both languages most relevant to the group And the essentials summary per language is <= 120 characters excluding the link and uses standard abbreviations And the essentials summary contains no duplicated non-essential text And if adding the summary would exceed configured maxSegments, non-essential body text is compressed before removing the summary; if still over, send only the bilingual essentials summary
Flow Integration Across Outbound Messages
Given each flow type: confirmations, reminders, waitlist offers, and no-show recovery When generating messages in supported languages and regional variants Then each message is routed through the localization layer, selects the correct template and locale, and localizes all dynamic fields And for each flow and language, the deposit/payment landing page renders in the same locale as the SMS And if a required template translation is missing, the system falls back to bilingual mode for that message and logs a missing-translation event
Bilingual Essentials Summary
"As a parent in a bilingual household, I want a short bilingual summary of the key details so that no one misses the time, address, or deposit requirements."
Description

Add an optional, concise bilingual block that includes the critical details—appointment time, location/address, and deposit/payment instructions—whenever a thread includes mixed-language participants or detection confidence is low. The block uses pre-approved microcopy for each language, a clear separator, and must fit within a strict segment budget (e.g., ≤2 segments) while prioritizing essentials. Auto-apply to group conversations, first-contact scenarios, and sensitive events (booking confirmations, changes, reminders, payment requests). Localize currency/time formats and ensure links resolve in the reader’s language. Provide configuration to enable/disable per account and per trigger, with A/B test hooks to measure reduction in errors and missed payments.

Acceptance Criteria
Auto-apply bilingual block on mixed-language and low-confidence threads
Given a conversation is a group thread OR is a first-contact DM OR the outbound message type is one of [booking confirmation, booking change, reminder, payment request] And the thread has (a) at least two distinct participant preferredLanguages OR (b) the last 5 inbound/outbound messages include two or more detected languages OR (c) current language detection confidence < 0.75 When the system composes the reply Then it appends a bilingual essentials block And the block includes in both languages: appointment time, location/address, and deposit/payment instruction (with link) And if none of the above conditions hold, no bilingual block is appended
Essentials block stays within ≤2 SMS segments with prioritization
Given the composed bilingual essentials block When calculating length using SMS rules for the actual character set (GSM-7: 153 chars/segment; UCS-2: 67 chars/segment) Then total segments for the message payload (including the block) ≤ 2 And if > 2, the system trims per priority: remove non-essential filler/courtesy text, abbreviate address to street + city, shorten time format (retain timezone), before removing any essentials And after trimming, time, short address, and deposit link remain present in both languages And if still > 2, the system sends only these essentials in both languages and passes a “segment_budget_exceeded” flag to analytics
Use pre-approved microcopy with clear bilingual separation
Given bilingual essentials are required When rendering the block Then each language uses the pre-approved microcopy template by templateId and language code And a clear separator line '---' appears between language sections And the first section matches the recipient’s current language (or thread majority language if group) And no ad-hoc free text appears; only templated strings and injected fields (time, address, amount, link) And the render event logs templateId, languages order, and checksum for compliance audit
Localized time/currency formats and language-resolving links
Given a bilingual block is rendered for languages L1 and L2 When formatting appointment time and deposit amount Then time uses CLDR patterns for each language (e.g., en-US h:mm a; es-MX HH:mm) and includes timezone abbreviation And currency symbol, decimal/thousands separators match the account currency localized for each language And each recipient receives a personalized deposit URL containing a lang parameter matching their language And visiting the URL renders the landing page in that language regardless of device/browser settings
Per-account and per-trigger configuration controls
Given an account admin updates settings When toggling the Bilingual Essentials Summary globally or per trigger [first-contact, booking confirmation, booking change, reminder, payment request] Then the change is persisted via API and UI And takes effect for new messages within 5 minutes And the effective policy (enabled/disabled, per-trigger) is recorded in message metadata And when disabled for a trigger, the block is not appended even if mixed-language or low-confidence conditions are met
A/B test hooks and outcome measurement
Given the feature flag 'bilingual_essentials' is configured with variants [control, treatment] When eligible messages are composed Then conversations are randomly assigned per-account or per-conversation (stable bucketing) with a configurable split And analytics events are emitted: summary_shown, payment_link_clicked, deposit_completed, correction_sent, with fields {conversation_id, account_id, languages[], variant, trigger_type} And reports can compute deltas in missed payments and correction messages between variants over a date range And experiment assignment and exposure are logged for auditability
Household Language Profiles
"As an office manager, I want PawPilot to remember each household’s language preferences so that group texts are clear without me manually tracking who prefers which language."
Description

Maintain per-contact language preferences and build a household-level profile that informs group-message strategy. For threads with multiple recipients, compute a language plan: single-language if aligned, or bilingual mode if preferences differ or code-switching is detected. Allow saving a primary and secondary language per contact, with opt-in/opt-out controls. Persist recent detected language to improve next-interaction defaults. Ensure compatibility with CRM records, Smart Waitlist targeting, and consent handling (STOP, AYUDA/HELP) in the appropriate language. Provide merge rules when contacts are linked or households change.

Acceptance Criteria
Contact-Level Language Preferences Persisted
Given a contact with no language preferences, When the user sets Primary Language=Spanish and Secondary Language=English and saves, Then the record persists both values and they are retrievable via UI and API. Given any attempt to set Secondary equal to Primary, When saving, Then validation blocks the save and shows "Secondary must differ from Primary". Given a language value not in the supported list (ISO 639-1), When saving, Then the save is rejected with "Unsupported language" and the supported codes are displayed. Given only Primary is provided, When saving, Then Secondary is stored as null and the save succeeds. Given preferences are changed, When saved, Then an audit log entry includes previous values, new values, timestamp, and user id. Given a contact is opted out, When an authorized user records explicit consent via UI, Then the contact is marked opted-in, an audit log records the action, and an opt-in confirmation SMS is sent in the contact’s Primary language.
Multi-Recipient Language Plan Computation
Given a thread where all recipients’ Primary Language = English, When computing the language plan, Then Plan = single-language: English. Given any recipients have differing Primary Languages (e.g., English and Spanish), When computing the language plan, Then Plan = bilingual (English + Spanish). Given aligned primaries but at least one recipient has a recent detected language different from the group primary within the last 14 days, When computing the plan, Then Plan = bilingual (group primary + detected language). Given one or more recipients lack preferences, When computing the plan, Then the system uses the org default as their Primary; if any recipient’s effective primary differs, Then Plan = bilingual (org default + most common known language among recipients). Then the computed plan is stored with the thread id and used for subsequent outbound messages in that thread.
Mid-Thread Code-Switch Detection
Given an active thread with Plan = single-language: English, When a recipient replies in Spanish and the language detector confidence >= 0.85, Then the thread’s plan updates to bilingual (English + Spanish) before the next outbound message. Then the contact’s recentDetectedLanguage is updated to Spanish with timestamp. Given the same contact initiates a new conversation within 30 days, When defaulting outbound language, Then Spanish is used unless their Primary Language has changed since. Given a reply where detector confidence < 0.85, When evaluating a plan change, Then no update occurs and an event is logged for manual review. Given the plan changes to bilingual due to code-switching, When composing the next outbound in supported templates, Then the bilingual-summary flag is set to True.
Consent Keywords Handling per Language
Given a message from a contact containing "STOP" (any casing) or local equivalent, When received, Then all SMS to that contact are blocked, opt-out is recorded at the channel level, and a confirmation is sent in the detected/stored language (English or Spanish) using compliant text. Given a message containing "HELP" or "AYUDA", When received, Then a help response is sent in the detected/stored language and opt-out status remains unchanged. Given a bilingual thread, When a participant sends a consent keyword, Then the response language matches the sender’s detected/stored language regardless of the thread plan. Given an opted-out contact is in a group thread, When computing deliveries, Then no messages are sent to that contact and delivery status reflects "Opted Out".
CRM and Smart Waitlist Compatibility
Given a contact with Primary=ES, Secondary=EN, and recentDetectedLanguage=ES with timestamp, When syncing to CRM, Then fields map to primaryLanguage (ISO 639-1), secondaryLanguage (ISO 639-1 or null), and recentDetectedLanguage (code + ISO 8601 timestamp). Given Smart Waitlist filter "Language = Spanish", When evaluating eligibility, Then include contacts with Primary=Spanish OR recentDetectedLanguage=Spanish within the last 30 days AND exclude any opted-out contacts. Given an import row with only one language value, When processing, Then it sets Primary to that value and Secondary to null. Given an import row with an unsupported code, When processing, Then the row is rejected and an error with contact id and reason is logged.
Merge and Household Profile Rules
Given two contacts are linked into the same household, When computing the household language profile, Then it contains the set of member primary languages and any recent detected languages from the last 30 days. Then each contact retains their individual Primary/Secondary values; only the household profile informs group-thread plan. Given two duplicate contacts for the same person are merged and their Primary languages differ, When merging, Then prefer the most recent recentDetectedLanguage (<=30 days) as the new Primary; otherwise keep the existing Primary and set Secondary to the other. Then opt-out status on the merged contact is opted-out if either source contact was opted-out. Then an audit log records merge source/target ids, pre/post language values, consent state, and timestamp. Given a contact is removed from a household, When removal is saved, Then the household profile and any cached language plans recompute within 60 seconds.
Localized Template Manager
"As a practice owner, I want to manage translated templates with previews and guardrails so that messages stay compliant, on-brand, and within SMS limits."
Description

Provide an admin UI for creating, reviewing, and approving message templates in each supported language with side-by-side translation, placeholder validation, and character/segment counters. Support variables for time, address, pet name, and deposit amounts with locale-aware formatting. Include compliance snippets (opt-out/help) in the correct language, preview for SMS length and link rendering, and versioning with approval workflows. Expose APIs to fetch the right template variant at send time and guard against missing translations with safe fallbacks. Store audit trails for regulatory and brand governance needs.

Acceptance Criteria
Side-by-Side Translation Editor
Given an admin with Template Manager permission opens a template When they select a base language and a target language Then two synchronized panes display for base and target text And placeholders are visually highlighted in both panes And switching the target language preserves unsaved base content And clicking Save Draft persists both language variants under a single version And reopening the draft restores both panes exactly as saved
Placeholder Consistency Validation
Given the allowed placeholders are {time}, {address}, {pet_name}, and {deposit} And a template has variants in multiple languages When the admin attempts to Save Draft or Submit for Approval Then the system blocks the action if any variant contains unknown placeholders And the system blocks the action if the placeholder sets differ across variants And inline errors list missing/extra placeholders per language And the action succeeds only when all variants have identical, valid placeholder sets
SMS Length, Encoding, and Link Preview
Given the editor displays a live character and segment counter When the content contains only GSM-7 characters Then the counter reflects GSM-7 limits and segmenting When any non‑GSM character is present Then the counter switches to UCS‑2 limits and segmenting When the content contains a URL Then the preview renders the clickable link exactly as it will appear to recipients and counts characters accordingly And the preview appends the language-appropriate compliance snippet (opt‑out/help) and includes it in the segment count
Locale-Aware Variable Formatting
Given sample data for {time}, {address}, {pet_name}, and {deposit} When the preview locale is set to en-US Then {time} renders in 12‑hour format with locale-specific date, {deposit} in USD with correct separators, and {address} in U.S. ordering When the preview locale is set to es-MX Then {time} renders in 24‑hour format with locale-specific date, {deposit} in MXN with correct separators, and {address} in Mexican ordering And pet names render exactly as provided without locale transformation And changing locale updates all rendered variables consistently across all language variants
Versioning and Approval Workflow Gate
Given a template with at least one language variant When the admin saves changes Then a new Draft version is created without affecting the current Active version When the Draft is submitted for approval Then only users with Approver role can Approve or Reject And the API does not serve Draft versions When Approved Then the new version becomes Active for all included languages and the previous Active version is archived but retrievable When Rejected Then the Draft remains non‑Active with the rejection reason stored
API Variant Selection with Safe Fallbacks
Given a send-time API request provides template_key and a BCP‑47 language tag When an exact language match exists (e.g., es-MX) Then the API returns the approved es-MX variant When no exact match exists but a parent language exists (e.g., es) Then the API returns the parent language variant and flags a fallback in the response metadata When neither exact nor parent exists Then the API returns the default language variant if configured, otherwise a 409 error with code TEMPLATE_MISSING_TRANSLATION And the API never returns content with unresolved placeholders; such cases yield a 409 error with code PLACEHOLDER_UNRESOLVED
Regulatory Audit Trail Capture and Retrieval
Given any template create, edit, submit, approve, reject, publish, or archive action When the action is performed Then an immutable audit entry is recorded with actor ID, role, timestamp, IP, action type, template key, version, locales affected, and before/after diffs And audit entries are readable via UI with filter by template key, version, actor, action type, and date range And audit entries are retrievable via API with pagination and the same filters And audit entries cannot be modified or deleted via UI or API by end users
Accuracy Analytics & Overrides
"As a support lead, I want visibility into language detection performance and simple override controls so that we can correct errors quickly and improve outcomes."
Description

Deliver dashboards and exports that track detection accuracy, language mix by account, fallback rates, message length/segmentation, and conversion outcomes (confirms, reschedules, payments) by language mode (single vs. bilingual). Allow staff to override detected language per conversation or contact via dashboard or SMS commands, with time-bound overrides and audit logs. Provide sampling and labeling tools to review misclassifications, plus A/B testing for bilingual summary efficacy. Trigger human review when confidence remains low across multiple messages or when high-risk actions (e.g., deposit forfeiture) are involved.

Acceptance Criteria
Analytics dashboard: accuracy, language mix, fallback, length, segmentation
Given an admin selects an account and date range on the Analytics dashboard, When the view loads, Then the dashboard displays metrics: detection_accuracy_percent, fallback_rate_percent, language_mix_percent_by_language, avg_outbound_message_length_chars, avg_outbound_segments_per_sms. Given the dashboard is loaded, When the admin changes the date range or account filter, Then all metrics recalculate within 3 seconds for the 95th percentile and reflect the selected filters. Given the admin applies a language mode filter (single, bilingual), When applied, Then metrics update and the sum of filtered values equals the unfiltered totals within ±0.1% for the same date range. Given a metric card is displayed, When the admin opens its info tooltip, Then the tooltip shows the exact formula, data source timestamp, and the account time zone used for computation.
CSV export: conversion outcomes by language mode
Given an admin requests the "Conversion Outcomes by Language Mode" export for a selected account and date range, When the export completes, Then a CSV is delivered with headers: account_id, start_date, end_date, date_granularity, language_mode, conversations, confirmations, reschedules, payments, conversion_rate_confirm, conversion_rate_payment. Given the CSV is downloaded, When the admin aggregates rows to the dashboard date range, Then totals match the dashboard values for the same filters exactly. Given the export is requested, When generation starts, Then a link is produced within 60 seconds for datasets under 100k rows, and the file uses UTF-8 with comma delimiters and RFC 4180 quoting. Given the export finishes, When the admin opens the file, Then each row contains a valid language_mode of {single,bilingual} and non-negative integers for count columns.
Dashboard language override with duration and audit logging
Given a staff user with edit permissions opens a conversation in the dashboard, When they set a language override to "Spanish" with a duration of 24 hours and save, Then all system-generated replies in that conversation are sent in Spanish until the override expires. Given an override is active, When viewing the conversation, Then a visible banner shows the override language and the exact expiry timestamp in the account time zone. Given an override is created, updated, or cleared, When the action is saved, Then an audit log entry is recorded with fields: actor_user_id, conversation_id, previous_language_mode, override_language, start_time, end_time, source="dashboard". Given the override reaches its end_time, When the next message is sent, Then the system reverts to automatic detection and the banner is removed within 1 minute.
SMS command language override with confirmation
Given a staff phone number is authorized for the account, When the staff sends "#lang es 2h" in a conversation thread, Then a conversation-scoped override to Spanish is activated for 2 hours and the system replies with a confirmation SMS including the expiry time. Given an override is active, When the staff sends "#lang auto", Then the override is cleared, automatic detection resumes, and a confirmation SMS is sent. Given an invalid command is sent (e.g., malformed code or duration), When processed, Then the system replies once with a help message showing valid patterns ("#lang <code> <duration>", "#lang auto") and no override change occurs. Given an override is changed via SMS, When the change is applied, Then an audit log entry is recorded with source="sms", command_text, parsed_parameters, outcome, and timestamp.
Sampling and labeling workflow for misclassifications
Given a reviewer opens the Sampling tool, When they request a random sample of 200 messages from the last 30 days filtered by confidence < 0.60, Then a queue of 200 unique messages is created and the random seed and filter criteria are stored with the sample. Given a sampled message is displayed, When the reviewer submits a ground-truth language label, Then the system saves label, reviewer_id, timestamp, original_detected_language, and original_confidence, and advances to the next item. Given two or more reviewers label at least 100 overlapping messages, When results are viewed, Then the tool computes and displays inter-annotator agreement (Cohen's kappa) and disagreement counts by language. Given labeled data exists, When the reviewer requests an export, Then a CSV and JSON export are generated including message_id, ground_truth_language, detected_language, confidence, is_misclassified boolean, reviewer_id, and created_at.
A/B testing of bilingual summary efficacy
Given an admin creates an experiment targeting mixed-language group conversations with a 50/50 split, When the experiment is started, Then conversations are randomly assigned at the conversation level and assignments remain sticky for the experiment duration. Given the experiment is running, When outcomes occur, Then the system attributes to each variant: confirmation_rate, reschedule_rate, payment_completion_rate, and average response time, and displays conversion lift and p-values using a two-proportion z-test. Given minimum sample size is reached or the admin stops the experiment, When results are exported, Then a CSV is generated with per-variant totals, rates, confidence intervals, and start/end timestamps. Given a conversation is single-language mode, When assignment is evaluated, Then it is excluded from the experiment and receives standard messaging (guardrail).
Automated human-review triggers for low confidence and high-risk actions
Given detection confidence remains below 0.50 for 3 consecutive inbound messages in a conversation, When the third message is processed, Then a human-review task is created, the conversation is flagged, and a notification is sent to the review queue within 10 seconds (95th percentile). Given an action that could cause deposit forfeiture is about to be executed, When the system prepares the outbound message, Then the message is held for approval, a review task is created, and the responsible staff are notified via dashboard and email/SMS. Given a review task exists, When a reviewer approves or rejects it, Then the system logs approver_id, decision, reason, timestamp, and executes or cancels the held action accordingly, with a confirmation back to the conversation thread. Given a review task is pending, When SLA of 30 minutes is exceeded, Then the system escalates by notifying the on-call role and marking the task as escalated.

FormFlow Translate

End‑to‑end translation of waivers, vaccination requests, and intake forms, including field labels, errors, and policy acknowledgments. Signed consents attach in the client’s language plus an English mirror for your records, reducing liability, rework, and back‑and‑forth.

Requirements

End-to-End Multilingual Form Rendering
"As a client, I want every part of the form to appear in my language so that I can understand and complete it accurately on my phone without confusion."
Description

Render all form surfaces (waivers, vaccination requests, intake forms) in the client’s selected language, including field labels, placeholders, help text, validation hints, error messages, policy acknowledgments, confirmation screens, and SMS deep-link landing pages. Provide right-to-left layout support, locale-specific typography, and date/number formatting. Persist a per-client language preference and negotiate locale automatically from SMS intent, device/browser settings, or explicit selection with clear fallback to English. Centralize strings in an i18n catalog with keys and versioning, enabling cache-friendly delivery and runtime switching without redeploys. Ensure parity across web view and SMS snippets, guarantee that any new form field or policy text is covered by translations, and expose a health check for missing strings to prevent shipping incomplete locales.

Acceptance Criteria
Full Form Surface Translation Coverage
Given a client with preferred locale L and an available form (waiver, vaccination request, or intake), When they open the SMS deep-link landing page and proceed through the form, Then 100% of field labels, placeholders, help text, validation hints, inline/global error messages, policy acknowledgments, and confirmation screens render in locale L. Given automated i18n coverage tests for locales L in the supported set, When the form is rendered, Then audits report 0 missing translation keys and 0 hard‑coded English strings for those surfaces. Given locale L, When dates and numbers are displayed in any form surface, Then they are formatted using the locale’s conventions (calendar, date order, decimal/thousand separators) with no exceptions.
Right-to-Left Layout and Locale Typography Support
Given locale ar or he, When any form surface is rendered, Then document direction is rtl, UI components are mirrored, and text alignment follows locale conventions. Given locale ar, When numerals, dates, or currency are displayed, Then they are formatted per ar locale and the selected font family fully supports the script with no tofu/fallback glyphs. Given viewport widths 320–1024px and locales switching between ltr and rtl, When rendering top form screens, Then no UI element overflows its container and no text is truncated (visual diff threshold <= 0.1%).
Automatic Locale Negotiation with Clear English Fallback
Given a user session, When negotiating locale, Then priority is: explicit selection in URL/UI > stored client preference > inbound SMS detected language (confidence >= 0.90) > device/browser settings > default en. Given the negotiated locale is unsupported or cannot be determined, When rendering, Then English (en) is used and a telemetry event logs the reason code and negotiation path. Given the user explicitly selects locale L, When the session continues, Then L is applied immediately and saved to the client profile for future sessions.
Per-Client Language Preference Persistence Across Channels
Given a client profile with stored locale L, When they open any future SMS deep-link landing page or web view, Then the content renders in L without re-selection. Given the client completes a session where locale is negotiated or explicitly changed to L2, When the profile is updated, Then subsequent sessions across SMS and web render in L2. Given a client’s account lifecycle events (STOP/START, new device/browser), When they return, Then the stored locale is honored consistently.
Centralized i18n Catalog with Versioning and Cache-Friendly Delivery
Given translation strings are stored in a versioned catalog (by semantic version), When a new catalog version v+1 for locale L is published, Then clients fetch v+1 within 5 minutes via cache revalidation without any application redeploy. Given a catalog asset request, When served, Then responses include strong ETag and Cache-Control headers (versioned: public, max-age>=86400, immutable; unversioned alias: max-age<=60) enabling cache-friendly delivery. Given a running session, When switching from locale L to L2 at runtime, Then visible strings update from the catalog without a full page reload and within 200ms on a mid-tier mobile device.
Parity Between Web Views and SMS Snippets
Given base locale en and target locale L, When diffing i18n key sets for web views and SMS snippet templates, Then the sets match exactly (0 missing/extra keys) and snapshot tests pass for phrasing parity within allowed character limits. Given a new form field or policy text is added, When CI runs, Then the build fails if corresponding keys or translations for all supported locales (web and SMS) are missing.
Missing Strings Health Check Blocks Incomplete Locales
Given the health endpoint /i18n/health, When called with any supported locale L, Then it returns HTTP 200 with status Pass only if there are 0 missing or empty translations versus base en for the active catalog version. Given any missing/empty translations for locale L, When /i18n/health is called, Then it returns HTTP 503 with a machine-readable list of missing keys and counts, and CI/CD blocks promotion. Given pre-deploy smoke tests, When executed, Then /i18n/health is called for all supported locales and all must return Pass before release.
Dual-language Consent Attachment
"As a business owner, I want a signed consent saved in the client’s language plus an English copy so that my records are clear and defensible in case of disputes."
Description

Generate and store signed consent artifacts in the client’s language alongside an English mirror for business records. On submission, produce tamper-evident PDFs (or PDFs plus HTML snapshots) that include signature, timestamps, signer metadata, policy version, and language used. Attach both artifacts to the client profile, appointment, and message thread, with secure links for retrieval, export, and audit. Ensure consistent layout across languages, include page markers indicating original vs. English mirror, and embed a unique document ID and hash for legal integrity. Support re-consent flows when policy text changes, and automatically relink new artifacts to future bookings.

Acceptance Criteria
Tamper‑Evident Dual PDFs on Submission
Given a client completes and submits a consent form in language L (non-English or English) When the submission is accepted by the system Then exactly two PDF artifacts are generated: one in language L (Original) and one in English (Mirror) And each PDF includes the captured signature image, signer full name, contact (phone or email), IP address, user agent, locale, UTC timestamp (ISO 8601), policy version, and language code And each PDF embeds a unique document ID and a SHA-256 checksum in its metadata and visible footer And if the "Store HTML snapshot" setting is enabled, HTML snapshots for both Original and English Mirror are also persisted and associated with the same document ID
Attach Artifacts to Client, Appointment, Thread
Given the dual-language artifacts are generated for a submission tied to a client and appointment When the submission processing completes Then both PDFs (and HTML snapshots if enabled) are attached to the client profile, the specific appointment record, and the originating message thread And each attachment record stores document_id, policy_version, language, created_at (UTC), and artifact_type (PDF or HTML) And listing attachments from any of these three entities returns both language artifacts for the same document_id
Secure Retrieval, Export, and Audit Links
Given a staff user with permission consents.read requests access to an artifact When they generate a retrieval link Then the system issues a signed URL that requires authentication and expires in 24 hours And every access event is logged with user_id, document_id, action (view or download), timestamp (UTC), IP, and user agent And users without consents.read or clients other than the artifact owner receive HTTP 403 on access attempts And when an export is requested, a ZIP is produced containing both PDFs (and snapshots if enabled) plus a JSON manifest with document_id, policy_version, language, hashes, timestamps, and signer metadata
Consistent Layout and Page Markers Across Languages
Given the Original (language L) and English Mirror PDFs for the same consent When both are rendered Then headers, footers, logo, margins, and section order are identical across both PDFs And corresponding field blocks align within ±5 px in X and Y positions And fonts render without missing glyphs or tofu characters in either PDF And a visible page marker appears on every page: "Original — <L>" for the non-English PDF and "English Mirror" for the English PDF
Unique Document ID and Hash Integrity
Given a stored consent artifact with document_id and embedded SHA-256 hash When the system recalculates the SHA-256 over the stored PDF bytes Then the recalculated hash equals the embedded hash and the server-stored hash value And document_id is globally unique (UUIDv4) and present in both PDF metadata and visible footer And if the PDF bytes are modified, integrity verification returns mismatch for that artifact
Re‑Consent on Policy Version Change
Given policy text is updated and the policy_version increments from V to V+1 When a client with last signed consent at version V has an upcoming booking Then the client is prompted to re-consent for version V+1 via SMS/email link before or during their next interaction And upon successful re-consent, dual-language artifacts for V+1 are generated and stored with a new document_id And the client’s current_consent_version becomes V+1 while V artifacts remain retained and retrievable for audit
Auto‑Relink Latest Consents to Future Bookings
Given a client has multiple consent artifacts across policy versions When a new appointment is created or an existing future appointment is updated Then the appointment links to the latest valid consent artifact for the current policy_version And if the client lacks a consent for the current policy_version, the booking flow requires re-consent before confirmation And when a client re-consents after a booking is created, the appointment’s consent links update to the new artifact without duplicating attachments
Localized Validation and Data Normalization
"As a client, I want clear, localized error messages and accepted local formats so that I can fix issues quickly and submit my form successfully."
Description

Validate inputs using the client’s locale (dates, times, numbers, addresses, phone formats) and display errors in the same language, preserving field context. Normalize submitted values into canonical formats (e.g., ISO-8601 dates, E.164 phone, standardized vaccine names) for downstream scheduling, reminders, and reporting. Provide both client-side and server-side validation to prevent inconsistent states, with shared rules that ensure equivalence across languages. Include vaccination expiration checks, required consent acknowledgments, and file-type/size validation for uploaded records. Maintain translation keys for all validation messages and guarantee they are covered for every supported locale.

Acceptance Criteria
Locale-Specific Date, Time, and Number Validation (es-MX)
Given a client locale of es-MX and inputs date='31/12/2025', time='14:30', weight='12,5' When the form is submitted Then client-side and server-side validators accept all fields And the normalized outputs are date='2025-12-31', time='14:30:00', weight=12.5 Given locale es-MX and date='12/31/2025' When the form is submitted Then both validators reject And the date field displays an inline Spanish error referencing the localized field label and expected format 'DD/MM/AAAA' using translation key 'validation.date.format' Given locale es-MX and time='2:30 PM' When the form is submitted Then both validators reject And the time field shows a Spanish 24-hour format hint 'HH:mm' using key 'validation.time.format_24h' Given locale es-MX and weight='12.5' When the form is submitted Then both validators reject And the number field shows a Spanish error to use a comma decimal separator using key 'validation.number.decimal_separator'
Phone Number Validation and E.164 Normalization (en-GB)
Given locale en-GB and phone='07400 123456' When the form is submitted Then both validators accept And the normalized phone is '+447400123456' (E.164) Given locale en-GB and phone='+1 415 555 2671' When the form is submitted Then both validators reject And the phone field displays an English error 'Enter a UK phone number' using key 'validation.phone.locale' Given locale en-GB and phone='07400 123' When the form is submitted Then both validators reject And the phone field shows an English error 'Enter a valid phone number' using key 'validation.phone.format' Then the server rejects any payload where phone is not E.164-normalized with a 422 response containing the field path 'client.phone' and error key 'validation.phone.format'
Address Validation by Locale (en-US vs en-CA)
Given locale en-US and address state='CA', postal='94107' When the form is submitted Then both validators accept And the normalized address contains country='US', state='CA', postal='94107' Given locale en-US and postal='K1A 0B1' When the form is submitted Then both validators reject And the postal field shows an English error 'Enter a 5- or 9-digit ZIP code' using key 'validation.postal.us' Given locale en-CA and province='ON', postal='K1A 0B1' When the form is submitted Then both validators accept And the normalized address contains country='CA', province='ON', postal='K1A0B1' Given locale en-CA and province='Ontario' When the form is submitted Then both validators reject And the province field shows an English error 'Use the two-letter code' using key 'validation.region.code'
Client- and Server-Side Validation Parity and Rule Equivalence
Given shared validation ruleset version '2025.08' loaded in both client and server When running a parity test suite of 100 localized samples across locales ['en-US','es-MX','fr-FR','pt-BR','en-GB','en-CA'] Then for every input the client and server produce identical pass/fail outcomes and identical validation message keys When an invalid payload bypasses client validation and is posted to the API Then the server returns HTTP 422 with machine-readable errors containing translation keys and field paths And no partial data is persisted Then the localization audit for namespace 'validation.*' reports 0 missing translation keys across all configured supported locales
Vaccination Expiration and Vaccine Name Normalization
Given locale en-US and vaccine name='Rabies' administered='2025-07-01' expires='2026-07-01' When the form is submitted Then both validators accept And the normalized record stores vaccine_code='rabies', administered='2025-07-01', expires='2026-07-01' (ISO-8601) Given locale es-MX and vaccine name='antirrábica' administered='01/07/2025' expires='01/07/2026' When the form is submitted Then both validators accept And the normalized record stores vaccine_code='rabies', administered='2025-07-01', expires='2026-07-01' (ISO-8601) Given expires date earlier than today's date '2025-08-15' When the form is submitted Then both validators reject And the vaccine 'expires' field shows a localized error using key 'validation.vaccine.expired' without creating or updating the record
Required Consent Acknowledgments with Localized Errors
Given locale pt-BR and a required consent checkbox unchecked When the form is submitted Then both validators reject And the checkbox displays an inline Portuguese error using key 'validation.consent.required' preserving the localized field label Given all required consent checkboxes are checked When the form is submitted Then both validators accept And normalized booleans are persisted server-side (e.g., consent_terms=true, consent_medical=true)
File Upload Type and Size Validation with Localized Errors (pt-BR)
Given locale pt-BR and an uploaded file 'vax.pdf' of size 2 MB When the form is submitted Then both validators accept And the file metadata is normalized with mime='application/pdf' and size bytes recorded Given locale pt-BR and an uploaded file 'record.heic' of size 1 MB When the form is submitted Then both validators reject And the uploader shows a Portuguese error 'Formato de arquivo não suportado' using key 'validation.file.type' allowing only ['application/pdf','image/jpeg','image/png'] Given any uploaded file larger than 5 MB When the form is submitted Then both validators reject And the uploader shows a Portuguese error including the max size using key 'validation.file.size' And the server returns HTTP 422 with the same error key
SMS Language Detection and Routing
"As a client, I want the form link and texts to arrive in my language so that I can complete everything without needing to ask for help."
Description

Detect and store a client’s language preference from SMS interactions (keywords like “ES/Español,” prior sessions, device locale from link opens) and route all outbound texts and form links accordingly. Generate language-bound short links that open the form in the correct locale and preserve that locale through the entire flow. Provide simple SMS commands to switch languages, confirm the change, and reissue links. Fail safely to English if detection is ambiguous, and ensure consistent language across reminders, error replies, and confirmation messages. Expose admin overrides to set a preferred language per client.

Acceptance Criteria
Keyword-Based SMS Language Detection
Given an inbound SMS from a client contains a supported language keyword as a standalone token (e.g., "ES", "Español", "FR", "Français", "PT", "Português"), When the message is processed, Then client.preferredLanguage is set to the corresponding ISO code, detection.source="keyword", detection.confidence=1.0, and the auto-reply is sent in that language. Given an inbound SMS contains mixed text but includes a standalone supported keyword, When processed, Then the same update occurs and subsequent outbound texts in that conversation use the detected language. Given the message contains no supported keyword, When processed, Then no change is made by keyword detection.
Device Locale Inference on Link Open
Given a client opens a PawPilot short link that has no locale parameter, When the HTTP request includes an Accept-Language header (e.g., "es-MX,es;q=0.9,en;q=0.8"), Then the session locale is set to "es" and all subsequent form URLs in that session include lang=es. Given the client completes and submits any form in that session, When submission succeeds, Then client.preferredLanguage is updated to the session locale with detection.source="session", detection.confidence=0.8 unless an admin-set preference exists. Given an admin-set preference exists, When a link is opened, Then the session locale equals the admin-set preference regardless of Accept-Language.
Language-Bound Short Link Generation and Preservation
Given an outbound SMS includes a form link and a language L is known, When the link is generated, Then a short URL is created that resolves to the form in locale L and embeds lang=L in all subsequent requests. Given the recipient encounters validation errors or page reloads, When navigating within the form, Then all field labels, errors, and confirmations remain in locale L for the entire flow. Given the client changes language via the form UI, When a new short link is issued from the backend, Then it encodes the updated locale and subsequent navigation stays in that locale.
SMS Commands to Switch Language, Confirm, and Reissue Links
Given a client sends "LANG <code>" or a single-token language code ("ES","EN","FR","PT") case-insensitive, When processed, Then client.preferredLanguage is updated to the specified language with detection.source="command", detection.confidence=1.0. Then a confirmation SMS is sent in the new language stating the change and listing the same command to revert. And any pending/most recent form links referenced in the last 24 hours are reissued in the new language. Given an unsupported code is received, When processed, Then a help SMS is sent listing supported codes and no change is made. Given multiple commands are received within 60 seconds, When processed, Then only the latest command is executed and others are ignored (idempotent within 60s).
Source Priority and Persistence Rules
Rule: When multiple signals exist, preference selection follows priority: Admin > SMS command > Keyword > Completed session locale > Device locale > Default "en". Given two signals arrive within 5 minutes with different languages, When updating client.preferredLanguage, Then the higher-priority signal is persisted and the lower-priority signal is logged but does not overwrite. Given a higher-priority signal exists, When a lower-priority signal is received later, Then client.preferredLanguage remains unchanged. Given client.preferredLanguage is updated, When audited, Then an audit record exists capturing old value, new value, source, timestamp, and actor (system/user).
Safe Fallback to English and Consistency Guarantees
Given signals are conflicting with equal priority or overall confidence < 0.6, When choosing a language, Then default to "en" and append a one-line footer offering switch instructions (e.g., "Reply ES for Español"). Given an automation sequence (e.g., reminder series, error reply, confirmation), When messages are sent within a single sequence, Then all messages in the sequence use the same selected language with no mixed-language content. Given a language is switched mid-conversation, When sending the next message, Then the new language is used consistently for all subsequent messages.
Admin Override: Set, View, and Precedence
Given an admin opens a client profile, When they set Preferred Language to a supported option and save, Then client.preferredLanguage is updated immediately with source="admin", detection.confidence=1.0 and is used for the next outbound message. Given an admin-set preference exists, When automated keyword or device-locale detections occur, Then they do not overwrite the admin-set preference. Given the client sends an explicit SMS command to change language, When processed, Then it updates the preference even if an admin-set value exists, and the change is audited. Given future scheduled reminders exist, When they are sent after an admin change, Then they render in the admin-selected language (evaluated at send-time, not queue-time).
Translation Memory and Glossary Management
"As an operator, I want consistent translations with protected terminology so that legal and industry-specific terms remain accurate across all forms."
Description

Implement a translation memory and domain glossary (e.g., Rabies, Bordetella, spay/neuter, matting, temperament) to ensure consistent, high-quality translations across all forms and policies. Use a provider-agnostic translation layer with pluggable MT engines (e.g., DeepL, Google, AWS) and configurable fallbacks. Cache approved strings, score translation quality, and surface low-confidence items for review. Support business-specific glossary overrides and locked terms to protect legal wording. Version translation assets so updates roll out safely without breaking existing forms or signed artifacts.

Acceptance Criteria
TM Reuse Across Forms
Given an approved translation for a source string and language pair exists in Translation Memory (TM) And the source string contains placeholders like {pet_name} and {appointment_date} When the same normalized source string (case-insensitive, trimmed whitespace, punctuation-normalized) appears in a different form or policy Then the system must return the exact TM translation without calling any MT engine And all placeholders are preserved in position and count And the event is logged as a TM hit with entry ID and version And p95 TM lookup latency is ≤ 150ms for single-string lookup And for fuzzy matches with ≥ 0.90 similarity, the highest-scoring TM suggestion is used and logged as a fuzzy match; otherwise a new MT translation is requested
Glossary Overrides and Locked Legal Terms
Given a tenant-level glossary defines overrides for terms [Rabies, Bordetella, spay/neuter, matting, temperament] with target-language equivalents And a legal clause is marked as Locked Terms = true When any text containing these terms is translated by any MT engine Then glossary overrides are enforced so the specified terms appear exactly as configured (respecting case and spacing rules) And locked legal terms are not altered by MT or post-processing And an attempt to modify a locked term requires admin role, reason, and confirmation, and creates an audit log entry And glossary enforcement is verified in the final output with automated checks that fail the translation if a term deviates And approved glossary changes create a new glossary version and do not retroactively change previously signed artifacts
Provider-Agnostic MT with Configurable Fallbacks
Given a language pair has a configured MT policy with primary=DeepL, fallbacks=[Google, AWS], per-engine timeout=1200ms, and quality threshold=0.78 When a translation request is made for that language pair Then the system attempts the primary engine first and measures its QE score And if the engine times out, errors, or returns a QE score < 0.78, the system automatically retries with the next fallback in order And only one final translation is committed to output and TM And the chosen engine, response times, and QE score are logged And p95 end-to-end translation time across engines is ≤ 3000ms And engine selection is overridable per tenant and per language pair via configuration
Low-Confidence Translation Review Workflow
Given translations are scored for quality and glossary compliance When a field’s translation has QE score < threshold or a glossary/locked-term violation is detected Then the field is flagged and added to the Translation Review Queue with source text, candidate translation, score, engine, and detected issues And a reviewer is notified and can Accept or Edit the translation And on Accept/Edit, the approved translation is written to TM with status=approved and linked to the source hash and asset version And the form re-renders using the approved translation without requiring a new MT call And review actions are fully auditable with user, timestamp, changes, and previous value
Versioned Translation Assets and Non-Breaking Rollout
Given TM and glossary are versioned assets (vN) with the ability to stage vN+1 When vN+1 is activated for a tenant or language pair Then new translations use vN+1 immediately And existing in-flight forms and previously signed artifacts remain pinned to their original asset version and do not change And rollback from vN+1 to vN is possible without data loss And all activations and rollbacks are logged with actor, timestamp, and scope And a migration report lists added/changed/removed entries between versions
Performance and Cost Controls for Translation Pipeline
Given a batch of up to 100 unique strings (≤ 5,000 characters total) is submitted for translation When processing begins Then duplicate strings within the batch are deduplicated before MT calls And TM hits incur zero MT calls and are counted toward a TM hit rate metric And for a batch with ≥ 80% TM hits, external MT calls ≤ 20 And p95 batch completion time is ≤ 2000ms under nominal load And per-batch cost estimate and engine call counts are logged for observability
Placeholder and Formatting Preservation
Given source strings may contain placeholders ({pet_name}, {appointment_date}, %s), ICU plurals, line breaks, and HTML tags (e.g., <strong>) When the strings are translated by any engine Then placeholders are masked before MT and restored after MT without translation or reordering And the number and names of placeholders in output exactly match the source And ICU message syntax remains valid and passes format validation And HTML tags remain well-formed and not duplicated or dropped And an automated validator fails any output that violates these rules and routes it to the Review Queue
Admin Translation Console and Policy Versioning
"As a business owner, I want to review and publish translated forms and policies confidently so that I can ensure accuracy and trigger re-consent when terms change."
Description

Provide an admin console to preview forms in any language, edit default copy, manage business-specific custom questions, and author policy text per locale. Show translation coverage, missing keys, and side-by-side diffing of changes. Include draft/review/publish workflow with effective dates, automatic re-consent prompts for affected clients, and rollback capabilities. Log who approved changes and when, and require acknowledgments before publishing legal text. Integrate with the translation memory/glossary for suggestions and enforce approvals on low-confidence machine outputs.

Acceptance Criteria
Preview and Coverage for Multi-Locale Forms
Given I am an admin with preview permission When I select a form and a locale Then the preview renders field labels, placeholders, help text, validation errors, and policy text in the selected locale And an English mirror view is available via a toggle And a coverage indicator displays the percentage of translated keys and the count of missing keys for that form and locale And clicking Missing keys lists each key with its path and navigates to the editor for that key And the preview loads in under 2 seconds for forms with up to 150 fields
Locale Editing with Draft/Review/Publish and Effective Dates
Given I am editing locale-specific copy for a form When I create a draft change Then the draft is versioned and does not alter the currently published version And I can request review from designated reviewers When a reviewer approves the draft Then the status updates to Approved and is eligible for publish scheduling And I can schedule publish with an effective date/time (timezone-aware) And once effective, the version is marked Published and served to clients And all state transitions (Draft, In Review, Approved, Published) are logged with user, timestamp, and reason And attempts to publish without at least one approval are blocked with a descriptive error code
Custom Questions Management per Locale
Given I add or edit a business-specific custom question When I specify default text and locale overrides Then the question appears in the preview for each selected locale with the correct localized text And if a locale override is missing, the system falls back to the default language and flags the key as missing for that locale And validation rules and error messages are configurable per locale and display accordingly in preview And removing or deactivating a custom question creates a new draft version and flags affected forms prior to publish
TM/Glossary Suggestions and Low-Confidence Approval Gate
Given translation memory and glossary are enabled When I focus a string to translate Then suggestions are displayed ranked by similarity with confidence scores And glossary terms are highlighted and cannot be altered without an explicit override action And any machine translation with confidence below 85% is flagged Needs human approval And publish is blocked until all flagged strings are explicitly approved by a reviewer distinct from the author And the approver’s identity and timestamp are recorded per string in the audit log
Side-by-Side Diffing and Version Rollback
Given I select two versions of a form locale When I open the diff view Then additions, deletions, and modifications are highlighted per key with before/after text side by side And filters allow limiting the diff to policy/legal text, field labels, help, or error messages And choosing Rollback creates a new draft identical to the selected prior version without altering the published version And publishing a rollback follows the same approval and acknowledgment gates as any change And an audit entry records the rollback initiator, target version, and rationale
Automatic Re-Consent on Legal Policy Updates
Given a published change includes text marked as legal/policy When the new version reaches its effective date/time Then clients whose last consent version differs for that locale are queued for re-consent And re-consent prompts are sent via SMS within 15 minutes, including links to the localized consent and an English mirror And affected clients are prevented from booking or signing new waivers until they acknowledge the new policy And the admin dashboard displays counts of pending, sent, acknowledged, and expired re-consents with export capability And non-responders receive up to 3 retries over 72 hours with configurable cadence
Pre-Publish Legal Acknowledgment and Audit Logging
Given a change set includes legal/policy text When attempting to publish Then the publisher must check an acknowledgment box and enter their full name to confirm legal accuracy And publishing requires at least one approver distinct from the publisher And an immutable audit record is created with publisher, approver(s), content hash, effective date/time, and IP addresses And publish is blocked if any required acknowledgments, approvals, or flagged low-confidence strings remain outstanding

VaxVision

Clients snap a photo of vaccination cards right in the SMS thread; PawPilot auto-reads vaccine types and dates, matches the pet and clinic, flags blur or missing shots, and calculates expiry. You get instant pass/fail with precise fix-it instructions, cutting back-and-forth and preventing day‑of surprises.

Requirements

SMS Photo Intake & Threaded Capture
"As a pet owner texting with my groomer, I want to snap and send my pet’s vaccine card in the same SMS thread so that I don’t have to download an app or log into a portal."
Description

Enable clients to upload vaccination card photos directly in the existing PawPilot SMS thread via MMS, with support for multiple images per pet, common image formats, and automatic acknowledgment. Associate inbound images to the correct client account using phone number and conversation context, and prompt for the pet name if multiple pets exist. Persist original images and normalized thumbnails with metadata (timestamp, sender, message id, pet context), de-duplicate identical uploads, and enforce file size/type limits. Provide consent and privacy messaging where required and a fallback secure mobile web link when carrier MMS fails. Expose events/webhooks when a new document is received to kick off downstream processing.

Acceptance Criteria
MMS Photo Upload in Existing Thread with Auto Acknowledgment and Multi‑Image Support
Given a client with an existing PawPilot SMS thread and a verified phone number When the client sends an MMS containing 1–N images (N = configured per-message limit, default 5) in JPEG, PNG, or HEIC formats Then the system ingests all supported images, associates them to the thread, and replies via SMS within 3 seconds acknowledging the number of images received Given multiple MMS messages are sent within 5 minutes of each other When received Then the system preserves message order and groups images under a single intake session for downstream processing and auditing Given the client sends more than N images in a single MMS When received Then the system accepts the first N images, rejects the extras, and replies indicating how many were accepted and why the remainder were not
Client–Pet Association and Disambiguation Prompt in Thread
Given an inbound MMS from a phone number mapped to a single-pet client When images are received Then the intake session is auto-associated to that pet and the acknowledgment names the pet Given an inbound MMS from a phone number mapped to a multi-pet client and no active pet context in the last 24 hours When images are received Then the system prompts via SMS: "Which pet is this for? Reply with the pet’s name" and pauses pet association until a valid reply arrives Given the client replies with a pet name or known alias matching an existing pet (case-insensitive, tolerant of Levenshtein distance ≤1) When processed Then the intake session is associated to that pet and the thread is updated confirming the association Given a pet cannot be resolved after two attempts When unresolved Then the system lists available pet names to choose from and flags the intake for staff review while retaining the images
Image Persistence, Thumbnail Generation, Metadata Capture, and De‑duplication
Given each received image When stored Then the original binary is persisted losslessly and a normalized thumbnail (max dimension 1024px, JPEG quality 80) is generated Given each stored image When metadata is recorded Then the system saves: UTC timestamp (ISO 8601), sender phone (E.164), messaging provider message ID, client ID, pet ID (or null), intake session ID, MIME type, byte size, SHA‑256 content hash, and storage URI for original and thumbnail Given a new image is received whose SHA‑256 matches an image from the same client within the last 30 days When evaluated Then a new record is not created; the intake references the existing image as a duplicate, and the client is informed that a duplicate was detected with the original receipt date
File Type and Size Validation with User Feedback
Given an image with MIME type not in image/jpeg, image/png, image/heic When received via MMS Then the image is rejected and the SMS reply states the supported types and provides instructions to retry or use the secure link Given an image exceeds the per-image size limit (configured, default 10MB) or the total message payload exceeds the per-message limit (configured, default 20MB) When processed Then oversized images are rejected, in-limit images are accepted, and the SMS reply lists each image’s accept/reject status with reason Given an image file is corrupted or unreadable When processed Then the image is rejected and the SMS reply requests a clearer re-upload and offers the secure link fallback
Consent and Privacy Messaging Prior to Capture
Given a client is submitting vaccination images for the first time in the past 12 months When their first image in that period is received Then the system sends a one-time SMS notice including privacy policy link and data-use summary, logs the notice timestamp, and proceeds with intake Given the deployment is configured to require explicit consent prior to processing When the notice is sent Then the system pauses processing until the client replies YES, sends one reminder after 24 hours if no reply, and cancels intake if no consent after 48 hours Given the client has opted out (e.g., STOP) or withdraws consent When any new images arrive Then the system does not store the images, replies with a message indicating intake cannot proceed without consent, and provides a link to manage preferences
MMS Failure Detection and Fallback Secure Upload Link
Given the messaging provider reports an MMS media download error, carrier rejection, media stripped, or unsupported MMS for the recipient’s carrier When the failure is detected Then the system sends an SMS containing a unique, single-use, TLS-secured upload link that pre-fills client context and expires after 24 hours Given the client completes upload via the secure link When files are successfully received Then the same validations are applied, the intake session is updated in the SMS thread with an acknowledgment, and the failure reason is logged Given the secure link expires or is used more than once When accessed Then access is denied and a fresh link can be requested via SMS keyword or staff action
Webhook Events on Document Receipt and Association
Given one or more images are successfully stored When the intake session is created Then a vax_document.received webhook is emitted within 2 seconds containing event_id, occurred_at, client_id, pet_id (or null), message_id, intake_session_id, and media[] with file_id, mime_type, size_bytes, sha256, and storage_uri Given a duplicate image is detected When classified Then a vax_document.duplicate webhook is emitted referencing original_document_id and duplicate_document_id Given a pet association is resolved or changed after client disambiguation When updated Then a vax_document.associated webhook is emitted with the resolved pet_id and intake_session_id; all webhooks are HMAC-SHA256 signed, retried with exponential backoff for up to 24 hours, and include an idempotency key
Vaccine OCR & Entity Extraction Engine
"As a provider, I want the system to auto-read vaccine details from photos so that I don’t spend time manually typing dates and vaccine names."
Description

Process uploaded images through an OCR pipeline to extract vaccine entities: vaccine type (e.g., Rabies, DHPP/DAPP, Bordetella, Leptospirosis), administration date, expiration date, clinic name, veterinarian, and optional lot/serial. Support template-free parsing for printed and handwritten cards, multiple vaccines per image, and mixed layouts. Return structured output with per-field confidence scores, bounding boxes, and normalized dates. Handle common synonyms and abbreviations, detect ambiguous results for clarification, and provide an extensible lexicon/model configuration for regional naming.

Acceptance Criteria
Extract Vaccine Entities from Clear Printed Card
Given a 300–600 DPI color image of a standard printed vaccination card containing Rabies, DHPP/DAPP, Bordetella, and Leptospirosis records When the engine processes the image Then it outputs one structured vaccine record per vaccine present with fields: vaccine_type, administration_date, expiration_date, clinic_name, veterinarian, lot_serial (optional) And on a labeled test set of ≥100 printed cards, exact-match accuracy is ≥95% for vaccine_type, ≥95% for administration_date and expiration_date parsing, and ≥90% for clinic_name and veterinarian And for records where lot/serial is present on card, extraction accuracy is ≥85%
Extract from Handwritten and Mixed Layout Cards
Given a mobile photo (72–300 DPI) of a handwritten or mixed-layout vaccination card with up to 15° rotation, shadows, or skew When the engine processes the image without any template configuration Then it extracts the same set of fields per vaccine and returns structured records And on a labeled test set of ≥100 such images, vaccine_type exact-match accuracy is ≥90%, date parsing accuracy (administration, expiration) is ≥90%, and clinic_name/veterinarian accuracy is ≥85% And if any key field confidence < 0.80, the record is marked requires_clarification=true with reasons including the low_confidence_field names
Multiple Vaccines in a Single Image
Given an image containing N distinct vaccine entries (2 ≤ N ≤ 8) When the engine processes the image Then the output contains exactly N vaccine records And fields within each record are correctly grouped (no cross-assignment) with grouping purity ≥98% on a labeled set And duplicate records (IoU ≥ 0.6 overlap of source text regions) are not produced
Structured Output: Confidence, Bounding Boxes, Normalized Dates, and Expiry
Given any processed image When the engine returns results Then each extracted field includes a confidence in [0,1] with at least two decimal places And each extracted field includes a bounding_box with integer x,y,width,height in pixel coordinates relative to the original image, satisfying 0 ≤ x < image_width, 0 ≤ y < image_height, width > 0, height > 0 And on a labeled set, bounding boxes have IoU ≥ 0.7 against ground truth for ≥95% of fields And all dates are normalized to ISO 8601 format (YYYY-MM-DD) And when expiration_date is absent but administration_date and vaccine_type are present, expiration_date is computed from configurable rules and matches expected values for ≥95% of records
Synonyms, Abbreviations, and Regional Lexicon Mapping
Given inputs containing synonyms or abbreviations (e.g., DAPP, DHPP, DHLPP, Bord, Lepto, Rab.) When the engine processes the image with region set to "US" Then vaccine_type is mapped to one of the canonical values {RABIES, DHPP/DAPP, BORDETELLA, LEPTOSPIROSIS} accordingly And when a custom lexicon adds a new synonym (e.g., "C5" -> "DHPP/DAPP"), the engine maps occurrences of that synonym without code changes And on a test set covering ≥20 synonym variants across regions "US" and "CA", mapping accuracy is ≥98%
Ambiguity Detection and Clarification Payload
Given a case where multiple vaccine_type candidates exist within 5% confidence of each other or a key field has confidence < 0.80 or conflicting date candidates are detected When the engine produces output Then the record includes requires_clarification=true And reasons includes one or more of ["low_confidence_field","multiple_type_candidates","multiple_date_candidates","date_format_ambiguous"] And alternatives include up to the top 3 candidate values with their confidences for the ambiguous field And ambiguous date values include both interpretations with ISO 8601 candidates and the assumed region/date_format used when selecting a default
Image Quality and Missing Required Vaccines Flags
Given an image with blur or severe noise exceeding a configurable threshold When the engine processes the image Then it sets quality_flag to "image_blur" or "low_quality" and reflects reduced confidence in affected fields And blur/low_quality detection achieves ≥90% precision and ≥90% recall on a labeled quality set And when a required_vaccines configuration is provided (e.g., ["RABIES","DHPP/DAPP"]) then the output includes missing_required_vaccines listing any required types not found
Pet & Clinic Matching Logic
"As a provider managing multi-pet clients, I want the system to match uploads to the correct pet and clinic so that records stay accurate without back-and-forth."
Description

Match extracted vaccine records to the correct pet profile and clinic within PawPilot using client identity, pet names, historical records, and context from the SMS thread. If multiple pets or clinics are possible, trigger a conversational SMS prompt to confirm the pet and/or clinic. Create and enrich new clinic entries when recognized, then link each vaccine record to a pet, a clinic, and an upload session for traceability. Expose APIs/events for downstream sync.

Acceptance Criteria
Auto-Link: Single Pet, Single Clinic, High Confidence
Given an upload session with at least one extracted vaccine record and the client has exactly one active pet profile When the pet match confidence >= 0.92 and the clinic is found by exact normalized phone match or fuzzy name+location score >= 0.90 Then the system links the record to that pet_id and clinic_id without sending a confirmation SMS And the record stores match_method="auto", pet_confidence, and clinic_confidence And a system message is added to the SMS thread noting the linkage And p95 processing latency per record is <= 2 seconds
Disambiguation: Multiple Pets Requires SMS Confirmation
Given a client with 2 or more pets and the top pet candidates are within 0.05 confidence of each other or all candidates are below 0.92 confidence When a vaccine record is processed Then the system sends an SMS listing up to 4 candidate pets with lettered options plus a "None of these" choice And the system maps replies by letter, pet name, or explicit "none" and ignores unrelated text And upon a valid reply, the chosen pet is linked and match_method="sms_confirm_pet" is stored And if no reply within 2 hours, one reminder SMS is sent; after 24 hours, the record status becomes "pending_pet_confirmation" with no linkage applied
Clinic Resolution: Match, Create, or Confirm
Given extracted clinic fields (name, phone, city/state) from the upload session When there is an exact normalized phone match to an existing clinic Then the clinic is linked and clinic_confidence=1.0 is recorded Else when the best fuzzy name+location score >= 0.88 without a phone match Then link that clinic and store the score Else create a new clinic with the extracted fields, source="vaxvision", pending_verification=true, and link it And if multiple clinics score within 0.03 of each other and none has a phone match, send an SMS to confirm the clinic with an "Unknown/Other" option And if a later phone match is detected for a newly created clinic, merge into the existing clinic and preserve an audit of the prior clinic_id
Low-Confidence Handling and Staff Review
Given a vaccine record where pet_confidence < 0.50 with no client SMS reply or the clinic could not be resolved When processing completes Then the record is marked needs_review=true and appears in the staff review queue with reason codes (e.g., low_pet_confidence, ambiguous_clinic) And no linkage is applied until a staff user confirms and saves And staff actions capture actor_id, timestamp, and before/after values in an audit log
Traceability: Linkage and Audit Trail
Given any matched vaccine record When the record is stored Then it includes pet_id, clinic_id, upload_session_id, match_method, pet_confidence, clinic_confidence, and source_document_id And an immutable audit entry records matching inputs (hashed), decision rationale, and any SMS message_ids used for confirmation And retrieving the record via API returns these fields consistently
Events/API: Downstream Sync of Match Results
Given a vaccine record reaches a terminal matching state (auto_linked, sms_confirmed, or needs_review) When the state changes Then a webhook event "vax.match.updated" is delivered within 3 seconds p95 to all subscribed endpoints with an HMAC-SHA256 signature header And the payload includes record_id, pet_id (nullable), clinic_id (nullable), upload_session_id, match_method, pet_confidence, clinic_confidence, status, and timestamps And failed deliveries are retried with exponential backoff for up to 24 hours with a maximum of 10 attempts And the REST API exposes GET /v1/vax-records/{id} and GET /v1/vax-records?upload_session_id=... returning the same payload fields
Context-Aware Matching Using SMS and History
Given the last 50 SMS messages in the thread within the past 72 hours and the account's pet profiles When a single pet name or alias appears in the most recent client message and matches exactly one pet Then that pet receives a deterministic tie-breaker boost sufficient to win ties within a 0.05 confidence delta And if multiple pet names are mentioned, no boost is applied and SMS confirmation is required when below 0.92 confidence And if the most recent appointment or vaccine record within 6 months belongs to a candidate pet and the confidence gap < 0.03, prefer that pet
Image Quality Scoring & Retake Guidance
"As a client, I want instant tips if my photo is unreadable so that I can quickly retake and move on."
Description

Automatically evaluate photo quality (blur, glare, low contrast, cropping/cutoff, low resolution) and text legibility. If quality is below threshold or critical fields are missing, send an immediate SMS with friendly, specific retake instructions and examples, then accept additional images in-thread until quality passes. Resume processing automatically upon acceptable quality.

Acceptance Criteria
Blur/Legibility Fail -> Immediate Retake SMS
Given an image is received via the SMS thread When blur_score > 0.60 OR legibility_score < 0.80 Then mark quality_status="Fail", pause OCR/extraction, and send a retake SMS within 5 seconds Given a retake SMS is sent When composing the message Then include the top 2 detected issues and 2 actionable tips plus 1 example link Given the client replies with a new image in the same thread When quality_status becomes "Pass" Then automatically resume processing without agent action
Glare/Contrast Detection and Guidance
Given text_region_glare_area_ratio >= 0.15 OR text_contrast_ratio < 4.5:1 When scoring completes Then set quality_status="Fail" and send glare/lighting-specific retake instructions within 5 seconds including an example link Given multiple issues are present When generating guidance Then order issues by severity (highest contribution to failure first) Given 2 or more images arrive within 2 minutes after a fail When evaluating Then choose the image with the highest composite_quality_score to process and archive the rest
Crop/Cutoff and Orientation Auto-Correct
Given key-field bounding boxes are within 8 pixels of any image edge OR detected crop_ratio > 0.10 When the image is evaluated Then set quality_status="Fail" and send a retake SMS instructing the user to include all edges Given image rotation is between 10° and 180° When detected Then auto-rotate to upright and re-score before deciding to fail; request a retake only if still below threshold Given the vaccination card has front and back sides When back-side indicators are missing Then request the back side via SMS and hold processing until received or a 15-minute timeout elapses
Critical Field Presence and Missing Shots Alert
Given OCR confidence for any critical field (vaccine type, date, pet name, clinic) < 0.70 OR the field is missing When validation runs Then set quality_status="Fail" and send an SMS listing each missing/unreadable field with a tip to capture that area clearly Given a missing Rabies or DHPP record is detected When sending SMS Then include the specific shot name(s) and a reminder that expiry cannot be calculated until provided Given the client submits a focused close-up of the missing field When OCR confidence for that field >= 0.90 and overall quality passes Then mark quality_status="Pass" and proceed
SMS Retake Flow Controls and Limits
Given a quality failure occurred When sending guidance Then rate-limit to 1 guidance SMS per 60 seconds per thread and deduplicate identical advice Given up to 5 retake attempts occur within a 15-minute window When the limit is reached without passing Then send an escalation SMS to the client and flag the thread for manual review Given quality_status="Pass" is achieved at any attempt When confirming with the client Then send a success SMS and immediately resume downstream processing
Composite Quality Scoring and Pass Threshold
Given an image is scored When calculating composite_quality_score Then apply weights: blur 40%, glare 20%, contrast 15%, crop 15%, resolution 10%, and persist all sub-scores Given minimum_long_edge_px < 1200 OR effective_DPI < 200 When evaluating Then set quality_status="Fail" and send a retake SMS advising better lighting, holding steady, and using the rear camera Given composite_quality_score >= 0.80 AND no critical fields are missing When finalizing Then set quality_status="Pass" and do not send retake guidance
Expiry Calculation & Policy Rules Engine
"As a business owner, I want vaccine expiry calculated according to my policies and appointment dates so that I can enforce requirements consistently."
Description

Calculate vaccine validity and expiry per configurable business rules. Support 1-year and 3-year Rabies policies, 6/12-month Bordetella, DHPP/DAPP series rules, clinic-stamped expirations vs. inferred expirations (admin date + policy duration), grace periods, and service-specific requirements. Normalize dates to the business’s timezone and evaluate validity at both “today” and specific appointment dates. Produce next-due dates and machine-readable failure reasons.

Acceptance Criteria
Rabies Policy Duration and Expiration Source Precedence
Given business configuration sets rabies.duration_years = 3 and business.timezone = "America/Los_Angeles" And pet "Milo" has a Rabies record with admin_date = 2022-08-01 and no clinic_expiry_date And evaluation contexts include today = 2025-08-15 and appointment_at = 2025-07-30T10:00:00-07:00 When the rules engine evaluates vaccine validity and expiry Then policy_applied = "rabies_3yr" and expiry_source = "inferred" and expiry_date = 2025-08-01 (normalized to business local date) And valid_at_today = false and valid_at_appointment = true And next_due_date = 2025-08-01 And failure_reasons contains exactly one item for evaluated_at = "today" with code = "RABIES_EXPIRED" and vaccine = "Rabies" And failure_reasons contains no item for evaluated_at = "appointment" for vaccine = "Rabies"
Bordetella 6/12-Month Policy Evaluation at Appointment Date
Given business configuration sets bordetella.duration_months = 6 And pet has a Bordetella record with admin_date = 2025-02-01 and clinic_expiry_date = 2025-08-01 And evaluation contexts include today = 2025-08-15 and appointment_at = 2025-08-10T09:00:00-07:00 When the rules engine evaluates vaccine validity and expiry Then expiry_source = "stamped" and expiry_date = 2025-08-01 (normalized to business local date) And valid_at_appointment = false and valid_at_today = false And next_due_date = 2025-08-01 And failure_reasons includes exactly one item for evaluated_at = "appointment" with code = "BORDETELLA_EXPIRED" and vaccine = "Bordetella" And failure_reasons includes exactly one item for evaluated_at = "today" with code = "BORDETELLA_EXPIRED" and vaccine = "Bordetella" And when bordetella.duration_months is changed to 12 with the same stamped expiry present, the evaluation results remain unchanged because expiry_source = "stamped" takes precedence over inference
DHPP/DAPP Series Completion and Expiry from Final Dose
Given business configuration sets dhpp.series.min_doses = 3 and dhpp.booster.duration_years = 1 And pet has DHPP doses: dose1.admin_date = 2025-04-01, dose2.admin_date = 2025-05-01, and no third dose And evaluation contexts include today = 2025-08-15 and appointment_at = 2025-09-01T14:00:00-07:00 When the rules engine evaluates vaccine validity and series completion Then valid_at_today = false and valid_at_appointment = false And failure_reasons contains items for both evaluated_at values with code = "DHPP_SERIES_INCOMPLETE", vaccine = "DHPP", and details.missing_doses = 1 and details.next_recommended_dose_due_date = 2025-06-01 When a third DHPP dose is added with admin_date = 2025-06-01 Then expiry_source = "inferred" and expiry_date = 2026-06-01 and next_due_date = 2026-06-01 (normalized to business local date) And valid_at_today = true and valid_at_appointment = true And failure_reasons for vaccine = "DHPP" are empty for both evaluated_at values
Grace Period Application to Near-Expired Vaccines
Given business configuration sets grace_period_days = 7 And pet has a Rabies record with clinic_expiry_date = 2025-08-10 (normalized to business local date) And evaluation contexts include today = 2025-08-15 and appointment_at = 2025-08-15T11:00:00-07:00 When the rules engine evaluates vaccine validity with grace period applied Then valid_at_today = true and valid_at_appointment = true and validity_status = "in_grace_period" for both evaluated_at values And next_due_date = 2025-08-10 And failure_reasons is empty and warnings include exactly one item per evaluated_at with code = "IN_GRACE_PERIOD" and vaccine = "Rabies" When appointment_at is changed to 2025-08-18T09:00:00-07:00 (8 days after expiry) Then valid_at_appointment = false and failure_reasons includes exactly one item with code = "RABIES_EXPIRED_BEYOND_GRACE" and vaccine = "Rabies"
Timezone Normalization for Expiry and Appointment Evaluation
Given business.timezone = "America/New_York" And pet has a Bordetella record with clinic_expiry_date = 2025-08-15 (date-only) And evaluation contexts include today_local = 2025-08-15 and appointment_at = 2025-08-15T23:30:00-04:00 When the rules engine normalizes dates and evaluates validity Then the vaccine is considered valid through 2025-08-15T23:59:59 in the business timezone And valid_at_appointment = true and valid_at_today = true And when appointment_at is 2025-08-16T00:05:00-04:00, valid_at_appointment = false And all emitted dates (expiry_date, next_due_date) are returned in YYYY-MM-DD normalized to the business timezone
Service-Specific Vaccine Requirements Validation
Given service rules require Grooming: [Rabies, Bordetella], Nail Trim: [Rabies], Dog Walk: [] And pet has a valid Rabies vaccination and no Bordetella record And evaluation contexts include today = 2025-08-15 and appointments: Grooming at 2025-08-20T10:00, Nail Trim at 2025-08-20T11:00, Dog Walk at 2025-08-20T12:00 When the rules engine evaluates per service Then for Grooming: overall_service_valid = false and failure_reasons includes exactly one item with code = "BORDETELLA_MISSING", vaccine = "Bordetella", required_for_service = "Grooming", evaluated_at = appointment And for Nail Trim: overall_service_valid = true and no failure_reasons for evaluated_at = appointment And for Dog Walk: overall_service_valid = true and no failure_reasons for evaluated_at = appointment And the response includes per-service summaries listing required_vaccines, satisfied_vaccines, missing_vaccines, and earliest_next_due_date
Machine-Readable Failure Reasons and Next-Due Date Generation
Given multiple rule failures exist for a pet (e.g., Rabies expired beyond grace and DHPP series incomplete) And evaluation contexts include today and one or more appointment_at values When the rules engine produces results Then failure_reasons is an array of objects each with fields: code, vaccine, evaluated_at, service (nullable), severity, details (object), and message_key And code values come from the enumerated set: ["RABIES_EXPIRED", "RABIES_EXPIRED_BEYOND_GRACE", "BORDETELLA_EXPIRED", "BORDETELLA_MISSING", "DHPP_SERIES_INCOMPLETE"] And failure_reasons are deduplicated per vaccine and evaluated_at, and sorted by severity desc then vaccine asc And next_due_date is provided per vaccine when applicable, and overall.earliest_next_due_date equals the minimum of vaccine next_due_date values (normalized to business local date) And the output is deterministic for the same inputs and validates against the published JSON schema for VaxVision policy results
Pass/Fail Decisioning & Fix-It Messaging
"As a client and provider, I want clear pass/fail results with specific next steps so that appointments aren’t derailed by day-of surprises."
Description

Generate an immediate pass/fail decision per pet based on policy evaluation and OCR confidence. Present results in the provider dashboard and send concise SMS feedback to the client with precise, actionable fix-it steps (e.g., which vaccine is missing or expired, which date is unreadable). Offer smart prompts to request only the missing or unclear items. Block or flag appointments that fail requirements with configurable soft/hard gating.

Acceptance Criteria
Real-time Pass/Fail Decision Generation
Given a provider account with an active vaccine policy and a pet profile And the client sends 1–5 vaccination card images via SMS to the thread When OCR extraction completes Then the system evaluates required vaccine types and expiry dates against the policy and produces a Pass or Fail decision per pet And any required field with OCR confidence below the configurable threshold (default 0.85) is treated as failing with reason "Unreadable" And the decision, reason codes, OCR confidences, evaluation timestamp, and policy version are persisted And 95% of decisions complete within 8 seconds of final image receipt; 99% within 15 seconds And multi-pet submissions result in separate per-pet decisions
Provider Dashboard Decision Presentation
Given a decision is produced for a pet When the provider views the pet or appointment in the dashboard Then a status badge displays Pass (green) or Fail (red) for the pet And the vaccines table shows vaccine type, captured date, computed expiry date, and an icon for any low-confidence fields or blur flags And hovering or expanding reveals fail reason codes and which fields are missing/unreadable/expired And a "Fix-It SMS sent" indicator shows delivery status and timestamp And the view updates within 3 seconds of decision via realtime push or polling without manual refresh And all events are recorded in the audit log with actor, time, and values
Client SMS Fix-It Messaging
Given a pet's decision is Fail When the system sends the client SMS Then the SMS is delivered within 10 seconds of decision and contains: pet name, each failing vaccine name, and a concise, actionable instruction per item (e.g., "Upload a clearer photo of the Rabies date" or "Provide DHPP shot date MM/DD/YYYY") And only failing or unclear items are mentioned; verified items are not requested And the message length is kept to <= 2 concatenated segments (<= 306 GSM-7 chars) with dynamic truncation rules while preserving itemized instructions And replying with a new photo or date triggers automatic re-evaluation and a new decision And for Pass decisions, the client receives a confirmation SMS with the soonest vaccine expiry date
Smart Targeted Prompting for Missing/Unclear Items
Given missing vaccines or unreadable fields are detected When composing the request to the client Then the system generates prompts that request only the specific missing vaccine(s) or exact field(s) (e.g., date) needed to pass And each prompt references the pet name and vaccine name to disambiguate multi-pet submissions And if only a date is unreadable, the prompt asks for the date only, not a full re-upload, while still accepting a photo reply And upon receiving a client reply, the system re-evaluates within 10 seconds and updates the decision and dashboard And once all required items are satisfied, the prompting stops automatically
Configurable Appointment Gating (Soft/Hard)
Given provider-level gating is configured as Hard When a pet has a Fail decision and the user attempts to schedule or check-in an appointment Then the action is blocked with a visible reason listing the missing/expired/unreadable items and a link to request fixes; no override is permitted Given provider-level gating is configured as Soft When a pet has a Fail decision and the user attempts to schedule or check-in Then the action is allowed but the appointment is flagged, and the user must enter an override reason; the override is recorded with user, timestamp, and reason And when a previously failing pet passes, the block/flag is removed automatically and any impacted appointments update within 1 minute
OCR Confidence, Blur Detection, and Expiry Calculation
Given vaccination images contain blur or glare above the model threshold When OCR runs Then fields impacted are marked unreadable and contribute to a Fail with reason "Image Unreadable" And required fields with OCR confidence < 0.85 default threshold are treated as missing for policy evaluation And expiry dates are computed from captured dates using policy rules per vaccine type and are displayed in dashboard and included in SMS And if multiple images of the same card are provided, the system fuses field results to raise confidence; if any required field remains below threshold, the decision remains Fail And all thresholds (confidence, blur) are configurable per provider with sensible defaults
Provider Dashboard Review & Override
"As a provider, I want a dashboard to review, correct, or override vaccine records with an audit trail so that I can handle edge cases confidently."
Description

Provide a VaxVision panel in the PawPilot admin to review original images, extracted fields with confidence, matched pet/clinic, and policy evaluation. Enable one-click approve/reject, field corrections (e.g., adjust a date), and overrides with required reason notes. Maintain a full audit trail (actor, change, timestamp) and update client messaging and appointment gating automatically upon changes. Support multi-pet bulk actions and export of vaccine status for compliance reporting.

Acceptance Criteria
Dashboard Displays Original Images & Extracted Data
Given a submission exists with at least one uploaded vaccination image When a provider opens the VaxVision panel for that submission Then the original images render in the viewer with zoom and rotate controls available within 2 seconds for up to 5 images (<=5 MB each) Given extracted fields exist (vaccine type, dose, date administered, expiry, pet name, clinic name) When the panel loads Then each field is shown with its value and a confidence score (0–100%) to one decimal place Given the system has attempted to match a pet and clinic When the panel loads Then the matched pet and clinic are displayed with links to their records, or a "Not matched" flag is shown if no match Given a vaccination policy is configured When the panel loads Then per-vaccine and overall pass/fail results are displayed with explicit failure reasons (e.g., "Rabies expired on 2025-06-01") Given an image quality issue is detected (e.g., blur, glare, crop) When the panel loads Then a quality banner displays the issue type and recommended action
Approve/Reject Triggers Downstream Updates
Given a submission is in Review When the provider clicks Approve Then the status updates to Approved, the evaluation locks, and a success confirmation appears within 1 second Given a submission is in Review When the provider clicks Reject Then a required reason modal is shown and the status updates to Rejected after the reason is saved Given a submission is Approved When approval is saved Then appointment gating for associated pet(s) is lifted and client notification is queued within 60 seconds Given a submission is Rejected When rejection is saved Then appointment gating is enforced and the client receives fix‑it instructions via SMS within 60 seconds Given booking channels are cached When an approval or rejection occurs Then client portal/booking reflects the new status within 60 seconds
Manual Field Corrections & Re-evaluation
Given extracted vaccination fields are displayed When a provider edits a date field Then the input is validated (valid date format, not in the future, logical dose order) and inline errors are shown for invalid entries Given a valid correction is saved When the provider clicks Save Then expiry and policy evaluation are recalculated and the UI updates within 1 second Given an edited field conflicts with other data (e.g., dose sequence) When the provider saves Then a warning explains the conflict and the provider can Confirm or Cancel Given a field is corrected When the save succeeds Then the original and new values are captured for audit
Override With Required Reason & Scope
Given a policy failure exists When the provider selects Override Then a modal requires selection of scope (this appointment only, this pet until date, organization-wide vaccine type) and a free‑text reason of at least 10 characters Given the override form is completed When the provider confirms Then the submission status becomes Approved (Overridden), and scope, reason, and (if applicable) expiry date are stored Given an override is active When viewing the record Then an override badge shows scope and expiry, and gating excludes the overridden failures Given an override has an expiry When the expiry date passes Then the override is automatically removed and policy evaluation returns to normal within 60 minutes
Comprehensive Audit Trail
Given any action occurs (approve, reject, edit field, override, bulk action, export) When the action completes Then an immutable audit entry is written with actor ID, role, action type, entity IDs (submission, pet, clinic), before/after values, timestamp (ISO 8601 UTC), client IP, and reason (if provided) Given audit entries exist When a provider opens the Audit tab Then entries are listed in reverse chronological order and can be filtered by date range, actor, and action type Given an audit entry exists When exported to CSV Then the row contains all captured fields with consistent headers Given role-based access control When a non-admin views audit Then PII fields are redacted per policy and the access is logged
Automatic Client Messaging & Appointment Gating Updates
Given messaging templates are configured When a submission status changes to Approved Then the client receives an SMS confirmation with vaccine details within 60 seconds and delivery status is recorded Given messaging templates are configured When a submission is Rejected or requires action Then the client receives an SMS with missing/expired items and an upload link within 60 seconds and delivery status is recorded Given appointment gating is enabled When a submission is Approved or Approved (Overridden) Then associated pet(s) can book appointments; otherwise booking is blocked with a clear error message Given appointments were held pending vaccination When the related submission becomes Approved Then held slots are released and eligible waitlist clients are notified per rules
Multi-Pet Bulk Actions & Compliance Export
Given a client has multiple pets with vaccine submissions When the provider selects multiple records and clicks Approve or Reject Then all selected records are processed with per-record success/failure feedback, and partial failures do not block others Given a bulk operation is running When progress is displayed Then throughput is at least 50 records per minute up to 1,000 records, with up to 3 automatic retries on transient errors Given filters are set (date range, location, vaccine type, status) When the provider clicks Export Then a CSV containing pet, client, clinic, vaccine type, dose, administered date, expiry date, status, last action timestamp, and audit ID is generated within 30 seconds for up to 10,000 rows Given an export file is downloaded When opened Then column headers match the data dictionary, dates are ISO 8601, and the filename includes the organization slug and YYYYMMDD

AutoGate

Bookings stay locked until required vaccines and waivers are verified. PawPilot texts a “Quick Unlock” link to upload or sign, and if an item is expiring soon it can allow a contingent hold with a clear deadline. Non‑compliant appointments never land on your calendar, saving time and risk.

Requirements

Compliance Rules Engine
"As a grooming business owner, I want to define required vaccines and waivers per service so that bookings are automatically blocked until clients meet my policies."
Description

A configurable policy engine that defines which vaccines and waivers are required per species, pet profile, and service type, including lead times, grace periods, and pre-expiration buffers. The engine evaluates compliance at booking, before check-in, and on schedule changes, returning a decision state (missing, submitted, pending review, verified, expiring, expired) with human-readable reasons. Supports multi-pet appointments, versioned policy sets, and per-location overrides. Integrates with scheduling, Smart Waitlist, messaging, and billing to block non-compliant bookings and trigger next-step actions.

Acceptance Criteria
Booking-time block for missing compliance items
Given a client attempts to book a Dog Grooming service at Location A for pet "Max" with no Rabies vaccine on file and a policy requiring Rabies with a 48-hour lead time is active (policy_version=v3) When the booking is submitted Then the engine evaluates the policy set for species=Dog, service=Grooming, location=Location A and returns a decision payload within 300 ms And the payload includes an item for Rabies with state="missing" and a human_readable_reason containing "Rabies vaccine required for Dog/Grooming at Location A; none on file" And the state for the appointment is "locked" and the scheduling service prevents it from appearing on the calendar And a Quick Unlock SMS with a secure upload link is sent to the account phone number within 5 seconds And the evaluation result stores policy_version=v3, evaluated_at timestamp, and correlation_id on the booking record And each decision state in the payload is one of: missing, submitted, pending_review, verified, expiring, expired
Pre-check-in re-evaluation for newly expired items
Given an appointment that was previously verified is within 2 hours of check-in and the pet’s Bordetella vaccine expired after booking When the check-in flow begins or a staff member opens the appointment in the dashboard Then the engine re-evaluates and returns state="expired" for Bordetella with a reason containing the actual expiration date And check-in is blocked and the appointment remains off the active queue until compliance is restored And a Quick Unlock SMS is sent within 5 seconds prompting renewal/upload And an audit entry is created recording the re-evaluation event and outcome
Contingent hold for expiring soon with buffer
Given a Daycare service for cat "Luna" at Location B on 2025-09-01 09:00, with policy buffer=14 days and lead_time=24 hours, and FVRCP expires on 2025-08-25 When the booking is submitted Then the engine returns state="expiring" for FVRCP with a reason including "expires before service; buffer=14 days" And the system places the appointment in "contingent_hold" (not on the calendar) And a compliance deadline is set to 2025-08-30 09:00 (service_start - lead_time) and stored on the booking And the Quick Unlock SMS includes the deadline timestamp and consequences of missing it
Deadline lapse auto-release and Smart Waitlist backfill
Given an appointment in contingent_hold with a compliance deadline 2025-08-30 09:00 and no verifying document is received by the deadline When the deadline elapses Then the system cancels the hold, prevents the appointment from being added to the calendar, and marks compliance_outcome="deadline_missed" And the slot is released to Smart Waitlist for auto-fill and a notification is sent to the next eligible client within 10 seconds And the client is notified via SMS of the cancellation and the reason
Multi-pet appointment aggregated evaluation
Given a multi-pet appointment for dogs "Max" and "Bella" with a policy requiring Rabies and Bordetella And Max has verified Rabies and missing Bordetella, while Bella has both verified When the booking is submitted Then the engine returns per-pet decision items and an aggregate appointment_status="locked" And the reasons include "Max: Bordetella missing" and none for Bella And a single Quick Unlock SMS includes separate upload links scoped to each pet’s missing items
Policy version re-evaluation on schedule change
Given an appointment originally evaluated on policy_version=v1 for Location A on 2025-08-20 And policy_version=v2 becomes active on 2025-08-22 adding Distemper as required for Grooming When a staff member reschedules the appointment to 2025-08-25 Then the engine re-evaluates using policy_version=v2 for the new date and returns state="missing" for Distemper with an appropriate reason And the appointment remains locked until Distemper is verified And the booking record stores the new evaluation result with policy_version=v2 and previous_version=v1
Per-location override on location change
Given Location A does not require Leptospirosis and Location B requires Leptospirosis with a 72-hour lead_time And a compliant calendar appointment is moved from Location A to Location B When the change is saved Then the engine re-evaluates using Location B overrides and returns state="missing" for Leptospirosis if no record meets the 72-hour lead_time And the system transitions the appointment from calendar to locked state and sends a Quick Unlock SMS including the new requirement and a deadline of service_start - 72 hours
Secure Quick Unlock Link via SMS
"As a client, I want to receive a secure link by text to submit my pet’s documents so that I can quickly unlock my booking without calling."
Description

A time-limited, single-use link delivered via SMS that deep-links clients into a mobile-friendly flow to upload vaccine records and e-sign waivers. The link is scoped to the client, pet(s), and appointment, auto-prefills known data, and supports multi-pet submission in one session. Includes token signing, expiry, rate limiting, re-send, and localization. Tracks delivery, click, and completion events, and updates appointment compliance status in real time.

Acceptance Criteria
SMS Quick Unlock Link Delivery and Tracking
Given an appointment has missing compliance items and the client has a valid mobile number When AutoGate triggers Quick Unlock Then an SMS containing a unique HTTPS link is sent and a sent event is recorded with a timestamp Given the carrier acknowledges delivery Then a delivered event is recorded within 30 seconds; otherwise a delivery_failed event with error code is recorded Given delivery_failed occurs Then the appointment is marked Unlock Pending – Delivery Failed and a re-send action is available to staff
Deep Link Auth and Prefill of Context
Given the client opens the Quick Unlock link before expiry When the link is resolved Then the client is authenticated via token without password and lands on a mobile-friendly flow Then the flow is prefilled with client details, appointment metadata, and all pets on the appointment with required vaccines/waivers per pet Then only missing or expiring items within the tenant-defined threshold are requested; existing valid records are displayed read-only
Multi-Pet Single-Session Submission
Given multiple pets require compliance items When the client uploads vaccine records and signs waivers Then they can complete all pets in one session and submit once Then each file is accepted in jpg, png, pdf, or heic up to 10 MB and mapped to the correct pet and requirement Then a single confirmation summarizes per-pet completion status and any outstanding items
Token Security: Single-Use, Expiry, Tamper Protection
Given a valid unused token When the client completes a successful submission Then the token is immediately invalidated and cannot be reused Given a token is reused after completion Then the API returns 410 Gone and the UI offers a request-new-link option Given current time is past the token expiry Then access is denied with a clear Link expired message and a re-send option Given the token signature or context is invalid Then access is denied (401), no PII is displayed, the event is logged with IP and user agent, and >5 invalid attempts in 10 minutes trigger a 15-minute temporary block
Re-Send and Rate Limiting
Given a staff member or system triggers a re-send Then a new tokenized link is generated, all prior tokens for the appointment are revoked, and an SMS is sent Then re-sends are limited to 3 per 24 hours per appointment per phone number; exceeding the limit blocks the action and displays an admin override note Given more than 5 invalid or expired link opens from the same IP occur within 10 minutes Then further opens receive HTTP 429 and a human verification challenge for 15 minutes
Localization and Timezone-Aware Expiry
Given the client has a preferred language When the SMS and unlock flow render Then all copy (static and dynamic) is localized; if a translation is missing, English is used as fallback Given the tenant's timezone Then the expiry deadline is displayed in local time with timezone abbreviation while enforcement is performed in UTC Then numerals, dates, and consent text are formatted according to the client locale
Event Telemetry and Real-Time Compliance Update
Given SMS is sent, link is clicked, flow is started, and submission is completed or fails Then sent, delivered, clicked, started, completed, and failed events are recorded with timestamps and IDs (client, appointment, pet) Given all required items are satisfied for all pets Then the appointment status updates to Compliant in real time and AutoGate unlocks scheduling Given any required item remains outstanding Then the appointment remains Blocked with per-pet details Given an item is expiring but within contingent threshold Then the appointment updates to Contingent with a deadline timestamp and reminders scheduled
Document Upload and Auto-Verification
"As a receptionist, I want uploaded vaccine cards to be auto-checked for validity so that I don’t have to manually read dates and chase clients."
Description

Support image/PDF uploads and in-app camera capture with guidance on required items (e.g., Rabies, Bordetella). Perform OCR and structured data extraction (pet name, vaccine type, issue/expiration dates, provider), validate against rules, and auto-match to the correct pet. Flag inconsistencies and low-confidence cases for manual review, provide immediate client feedback, and store originals plus extracted metadata with encryption and audit trail.

Acceptance Criteria
SMS Quick Unlock: Upload Rabies Certificate via Mobile Camera
Given a client opens the Quick Unlock link from SMS on a mobile device When they select Camera and capture the document Then the app displays edge guides and glare/blur warnings and prompts for a retake if quality is insufficient And the capture screen clearly labels the requested item (e.g., "Upload Rabies Certificate") And only JPG, PNG, or HEIC up to 15 MB are accepted; larger or other formats are rejected with a clear reason And a 5 MB image uploads successfully over 3G+ within 10 seconds 95% of the time And the client sees an immediate "File received" confirmation state
OCR and Data Extraction from Multi-Page PDF
Given a client uploads a PDF up to 10 pages and 20 MB When OCR and parsing execute Then text is extracted from all pages, including scanned images, and rotation/skew up to 15° is corrected And fields pet_name, vaccine_type, issue_date, expiration_date, and provider_name are extracted with confidence ≥ 0.92 each And dates are parsed using the account locale (MM/DD/YYYY or DD/MM/YYYY); unresolved ambiguity is flagged for review And extraction completes within 8 seconds for a 3-page 10 MB PDF 95th percentile
Rule Validation and Immediate Client Feedback
Given extracted data includes vaccine_type and expiration_date When vaccine_type matches a required item for the appointment type and expiration_date ≥ today + minimum_validity_window (default 0 days) Then the document is marked Valid and the booking is unlocked And the client receives on-screen and SMS confirmation within 5 seconds of upload completion And if vaccine_type is not one of the required items, the client is told exactly which item is required (e.g., "Bordetella") with an upload prompt And if the document is expired, the client is informed the booking remains locked and is prompted to upload a valid document
Auto-Match Document to Correct Pet Profile
Given a client account with multiple pets and an uploaded document containing pet_name When auto-matching runs Then the document is assigned to the pet with the highest fuzzy name match (Levenshtein distance ≤ 2) under the same client_id And if multiple pets meet the threshold, the client is prompted to select the pet before final submission And if no pet meets the threshold, the document is queued for manual review and the client is asked to confirm or enter the pet name And the selected pet_id is stored in the document metadata
Low-Confidence or Inconsistency Routing to Manual Review
Given any required extracted field has confidence < 0.92 or extracted data is inconsistent (e.g., expiration_date < issue_date or provider_name missing) When verification evaluates the document Then the document status becomes Needs Review and the booking remains locked And the client is informed that verification may take up to 4 business hours with a reason (e.g., "Couldn’t read expiration date") And a review task is created containing the original file, extracted fields, confidence scores, and reason codes And upon reviewer Approve the booking unlocks and the client is notified; upon Deny the client is notified with the denial reason
Secure Storage and Audit Trail for Originals and Metadata
Given any document upload or verification decision occurs When the system persists the data Then the original file is stored encrypted at rest (AES-256) and all metadata in an encrypted database And all transport uses TLS 1.2+ with HSTS enabled And an immutable audit log entry is recorded with client_id, pet_id, appointment_id (if available), event_type, UTC timestamp, actor (system/user/reviewer), outcome, and SHA-256 checksum of the original And an admin can retrieve the last 50 audit events by pet_id or appointment_id within 2 seconds 95th percentile
Expiring Soon: Contingent Hold with Deadline
Given the extracted expiration_date falls within the expiring_soon window relative to the appointment date (configurable; default 14 days) When business rules permit contingent holds Then the system places the booking in Pending Unlock with a clearly stated deadline (date/time) for submitting an updated document And the client is informed via on-screen message and SMS of the deadline and what to upload And if a valid replacement is uploaded before the deadline, the booking unlocks automatically and the client is notified And if the deadline passes without a valid document, the booking is blocked and the client is notified; all state changes are logged
Contingent Hold and Deadline Enforcement
"As a scheduler, I want to place contingent holds for near-expiring vaccines so that clients can keep their slot if they update paperwork by a clear deadline."
Description

Enable a temporary hold when an item is expiring soon or missing, with a configurable deadline (e.g., 48 hours before service). Automate reminder sequences via SMS with the Quick Unlock link, and auto-cancel and release the slot to the Smart Waitlist if the deadline is missed. Support deposits for holds, staff exceptions with reason codes, and full event logging for auditability.

Acceptance Criteria
Contingent Hold Creation and Deadline Calculation
Given a client attempts to book and a required vaccine/waiver is missing or expiring within the configured threshold When the booking request is submitted Then the system creates a Contingent Hold on the requested slot instead of a confirmed booking And computes a compliance deadline as service start time minus the configured offset And displays the deadline and outstanding items in both the client SMS and staff views And the hold is not rendered as a confirmed appointment on the calendar
Quick Unlock SMS and Reminder Sequence
Given a Contingent Hold is created When the hold is saved Then an SMS containing the Quick Unlock link is sent to the client within 60 seconds And reminder SMS messages are scheduled at the configured intervals up to the deadline And reminders stop immediately when compliance is completed or the hold is canceled And each SMS delivery status is tracked and visible in the booking timeline
Compliance Submission Unlock Flow
Given a client opens the Quick Unlock link When all required documents are uploaded, required waivers are signed, and validations pass Then the booking converts from Contingent Hold to Confirmed immediately And the compliance deadline and reminder schedule are cleared And a confirmation SMS is sent to the client and a confirmation event appears to staff And if only some items are completed, the hold remains contingent with remaining items clearly listed
Auto-Cancel and Waitlist Release on Deadline Miss
Given a Contingent Hold remains non-compliant when the deadline elapses When the deadline is reached Then the booking is automatically canceled And the slot is released to the Smart Waitlist and offered per waitlist rules And the client receives an SMS explaining the cancellation and how to rebook And staff receive a notification of the auto-cancel And all scheduled reminders for the hold are stopped
Deposit Handling for Holds
Given deposits are required for contingent holds per business settings When a contingent hold is created Then the SMS includes a secure link to pay the deposit And the hold is marked Pending Deposit until payment is received And if the deposit is not paid by the configured timeout, the hold is auto-canceled and the slot is released And upon confirmation, the deposit is applied to the invoice per configured policy And upon cancellation, the deposit is handled (forfeit or refund) per configured policy and a receipt is sent
Staff Exceptions with Reason Codes
Given a staff user with override permission views a contingent hold When they extend the deadline, bypass a requirement, or convert to confirmed Then they must select a reason code from the configured list and may add a note And the change is applied immediately with updated compliance state And the client is notified if their status or deadline changes And the exception is logged with user, timestamp, reason code, and previous/new state And guardrails prevent setting a deadline after the service start time
End-to-End Event Logging and Auditability
Given any AutoGate hold lifecycle event occurs (creation, reminder sent, document upload, validation result, deposit event, deadline change, cancel/confirm, waitlist actions) When the event is processed Then an immutable audit record is written with timestamp, actor (system/staff/client), channel (SMS/Web), event type, before/after state, related booking/deposit IDs, and correlation IDs And logs are queryable in the booking timeline and exportable as CSV for a date range And outbound SMS and Quick Unlock link interactions are recorded with delivery/open status when available
Calendar Gating and Pending Queue
"As a groomer, I want only compliant appointments to appear on my calendar so that I avoid surprise issues and liability."
Description

Prevent non-compliant bookings from appearing on the live calendar by routing them to a Pending Compliance queue that reserves provisional capacity without confirming the appointment. Automatically promote appointments to the calendar upon verification and block check-in if compliance lapses. Display compliance badges on appointment cards, and sync state changes to external calendars and notifications.

Acceptance Criteria
Route Non‑Compliant Booking to Pending Queue
Given a client attempts to book and has missing or expired required items (vaccines/waivers) When the client submits the booking request Then the appointment is created in the Pending Compliance queue and does not appear on the live calendar And a provisional capacity hold is placed for the requested slot that counts toward capacity limits And the client receives an SMS with a Quick Unlock link within 30 seconds And staff can view the pending item with required items listed and any applicable deadline
Auto‑Promote to Calendar on Verification
Given an appointment in the Pending Compliance queue with a provisional hold When all required items are verified before the deadline Then the appointment status changes to Confirmed and appears on the live calendar within 60 seconds And the provisional hold converts to a confirmed reservation without creating duplicates And the client receives a confirmation SMS and staff get a confirmation notification And the queue no longer lists the appointment as pending
Contingent Hold with Compliance Deadline and Auto‑Cancel
Given a booking where at least one required item expires before the service start time or is missing When the booking is created Then a compliance deadline is set (configurable; default 24 hours before appointment start) and displayed to client and staff And reminder SMS messages are sent at configured intervals (default 72h and 24h before deadline) When the deadline passes without verification Then the appointment auto‑cancels, the capacity hold is released immediately, and both client and staff are notified with reason And an audit log records deadline set, reminders sent, auto‑cancel, and notifications
Compliance Badges on Appointment Cards
Given appointment cards in the Pending queue and on the live calendar When an appointment’s compliance state is Pending, Verified, Expiring, or Missing Then a badge is displayed with distinct label and color (Pending/amber, Verified/green, Expiring/yellow, Missing/red) And hovering or tapping shows a tooltip/list of specific items and their statuses And changes to item status update the badge within 5 seconds without full page reload And badges include accessible labels for screen readers
Block Check‑In on Compliance Lapse
Given a confirmed appointment on the live calendar When at check‑in any required item is missing or expired Then the Check‑In action is disabled and a banner shows the blocking reason with a Quick Unlock link And staff with proper permission may apply a documented override that is time‑stamped and audit‑logged When compliance is verified while the client is on‑site Then the Check‑In action becomes enabled within 10 seconds and the appointment proceeds
External Calendar Sync for State Changes
Given a staff member has connected an external calendar When an appointment transitions among Pending, Confirmed, and Cancelled Then the external calendar reflects the change (create/update/delete) with correct title, start/end, location, and a compliance note And updates propagate within 2 minutes at the 95th percentile And no duplicate events are created on repeated updates (idempotent) And on sync failure, the system retries up to 3 times and alerts staff after final failure
Notification Workflows for Compliance Events
Given an appointment enters Pending Compliance Then the client is sent an SMS with a Quick Unlock link immediately and the message delivery status is logged When the appointment is verified Then the client receives a confirmation SMS and staff receive a confirmation notification When a deadline is approaching (24h remaining by default) Then the client receives a reminder SMS and staff receive a due‑soon alert When an appointment auto‑cancels due to non‑compliance Then the client receives a cancellation SMS with reason and rebooking link, and staff receive a cancellation notification
Compliance Dashboard and Admin Overrides
"As an owner, I want a centralized view of compliance and the ability to override when needed so that I can keep the schedule moving while managing risk."
Description

A staff-facing dashboard showing missing and expiring items by client, pet, and appointment, with filters, bulk actions, and quick resend of unlock links. Provide an embedded document viewer for manual verification, role-based overrides with time-bounded exceptions, and complete audit logs. Include expiring-soon alerts and exports for recordkeeping and insurance audits.

Acceptance Criteria
Dashboard Lists and Filters Compliance Items
Given I am a staff user with access to the Compliance Dashboard, When I open the dashboard, Then I see a tabular list of compliance items grouped by client, pet, and appointment with columns: Client, Pet, Item Type, Status (Missing/Pending Review/Verified/Expiring Soon/Expired), Appointment Date, Last Contacted, and Actions. Given the dataset contains more than 50 items, When I navigate the dashboard, Then results are paginated at 50 rows per page and the first page loads within 2 seconds for up to 5,000 total items. Given I apply filters for Status, Item Type, Appointment Date range, Location, Assigned Staff, and Expiration Date range, When I click Apply, Then the list updates to only matching items within 2 seconds and the active filters are visible as chips. Given I enter a search term matching client or pet name, phone, or appointment ID, When I submit the search, Then the list displays matching rows and the total count reflects the filtered results. Given there are no items matching the current filters, When the results load, Then I see an empty state message with a Clear Filters control.
Bulk Resend of Quick Unlock Links
Given I have selected 1–200 eligible rows, When I click Bulk Resend Unlock Links and confirm, Then unique Quick Unlock links are sent via SMS to the corresponding clients within 60 seconds and each row shows a Sent status with timestamp. Given a selected client was sent an unlock link in the past 10 minutes, When I bulk resend, Then the system de-duplicates and skips that client with a Skipped—Recently Sent reason. Given any selected row is ineligible (no mobile, opted out, landline, or already compliant), When I bulk resend, Then those rows are excluded with an Ineligible reason and count in the confirmation dialog. Given the bulk operation completes, When I view activity, Then an entry is created in the audit log with initiator, selection criteria, counts (sent/skipped/failed), and message template ID. Given delivery results are returned from the SMS provider, When a message fails, Then the row displays Failed with the provider error code and a Retry action.
Embedded Document Viewer and Manual Verification
Given a row is Pending Review with an uploaded document, When I click View, Then an embedded viewer opens and renders PDF/JPG/PNG with zoom, rotate, and download controls. Given the document is legible and within validity dates, When I click Verify and select the correct item type and expiration date, Then the item status updates to Verified with verifier name, timestamp, and expiration captured. Given the document is illegible or incorrect, When I click Reject and enter a required reason, Then the status updates to Rejected and a new Quick Unlock link with the rejection reason is sent to the client. Given the entered expiration date is in the past, When I attempt to Verify, Then the system blocks the action with validation “Expiration must be a future date”. Given a verification action occurs, When I return to the list, Then the row is removed from Pending Review and any associated appointment lock is re-evaluated within 5 seconds.
Role-Based Overrides with Time-Bounded Exceptions
Given I am logged in as Admin or Manager, When I open an item that is Missing or Expired, Then I can create an Override by entering reason (required), scope (specific pet and item), and expiry date. Given I set an override expiry date, When I save, Then the expiry must be between now and 30 days from now and the appointment(s) are allowed to book and remain on the calendar until that expiry. Given I am a Staff (non-manager), When I open an item, Then the Override action is not visible. Given an override exists, When I view the row, Then an Override badge with expiry countdown is displayed and a reminder is auto-sent to the client 72 hours before expiry. Given an override is revoked by Admin/Manager, When I confirm revoke, Then the appointment lock is re-applied within 5 seconds and the audit log records who, when, and reason.
Audit Logging for Compliance Actions
Given any of the following actions occur: resend unlock link, manual verify, reject, override create/update/revoke, export, or filter change, When the action is completed, Then an immutable audit log entry is written with user ID, role, timestamp (UTC), IP, target entities (client, pet, appointment, item), action type, and before/after values where applicable. Given I am an Admin, When I open Audit Logs from the dashboard, Then I can filter by date range, user, action type, and entity and view results paginated 100 per page within 2 seconds. Given I export audit logs for a selected range up to 50,000 rows, When I click Export CSV, Then a file with headers and UTC timestamps is generated and downloaded within 30 seconds; larger exports are queued and a secure link is emailed within 15 minutes. Given audit logs exist, When I attempt to edit or delete an entry via UI or API, Then the system prevents modification and records the attempted action.
Expiring-Soon Alerts and Compliance Exports
Given business settings define Expiring Soon threshold (default 14 days), When an item’s expiration date is within the threshold, Then the item is flagged Expiring Soon and appears in the dashboard with the correct badge and days remaining. Given I select Expiring Soon items, When I click Send Reminders, Then clients receive SMS reminders using the selected template and each reminder is logged with delivery status. Given I need records for insurance audit, When I export compliance status for a date range, Then a CSV of clients/pets/items with status, expiration date, verification source (auto/manual), and last action is generated with the practice timezone and downloaded within 20 seconds for up to 20,000 rows. Given a reminder was sent, When the client updates documents via the Quick Unlock link, Then the item status updates automatically and is removed from Expiring Soon within 5 minutes.
Waiver eSign and Template Management
"As a business owner, I want clients to e-sign my waivers digitally so that I can enforce policies and keep records automatically."
Description

Built-in e-signature for waivers with reusable, versioned templates and per-service assignment. Capture legally defensible signatures with timestamp, IP, and SMS verification, generate a signed PDF, and require re-consent when templates change. Support required checkboxes and initial fields, multi-language content, and branded presentation, integrated seamlessly into the Quick Unlock flow.

Acceptance Criteria
SMS-Verified eSignature Capture in Quick Unlock
Given an appointment is locked by AutoGate due to a missing required waiver assigned to its service And the client has a mobile number on file When PawPilot sends a Quick Unlock SMS with a unique, single-use link And the client opens the link and completes SMS one-time code verification within 10 minutes Then the eSign page loads the latest published version of the assigned waiver with business branding And the client can draw or type a signature, enter full name, and complete all required checkboxes and initials And submission is blocked with inline errors until all required fields are completed And on successful submission the appointment’s waiver requirement is marked satisfied and AutoGate releases the booking hold
Signed PDF Generation and Secure Access
Given a waiver is successfully signed When the system generates the signed document Then a PDF is produced embedding the signature, initials, checkbox values, signer full name, date/time (UTC ISO 8601), template title, template version ID, and page numbers And the PDF includes business branding (logo/colors) and an evidence summary page And the PDF file is stored encrypted at rest and linked to the client profile and appointment And staff users with the Waivers:View permission can download the PDF from the booking and client record And shared links for clients use an expiring, signed URL valid for a configurable duration (default 72 hours)
Audit Trail and Evidence Package
Given a waiver is submitted When auditing data is recorded Then the system captures and stores an immutable audit record including signer phone number, SMS verification success, IP address, user agent, UTC timestamp, geo (if permission granted), template version ID, and a SHA-256 hash of the generated PDF And the audit record is viewable in the activity log and exportable as JSON alongside the PDF And any subsequent edits create a new audit entry without modifying prior records
Template Versioning and Re-Consent Rules
Given an admin creates or updates a waiver template When the template is published as a new version Then the version receives an immutable version ID and is recorded as the active version for future signatures And any client who last signed an older major version is flagged as needing re-consent And minor versions are configurable to require or skip re-consent at publish time And the next booking or Quick Unlock flow enforces re-consent before the appointment is released And previously signed PDFs remain associated with their original version and are not altered
Per-Service Assignment and Enforcement
Given templates are assigned to specific services and locations When an appointment contains one or more services Then the system computes the union of required waivers for those services without duplicates And only the latest published version of each assigned template is requested And AutoGate keeps the appointment locked until all computed waivers are signed And if the appointment’s services are edited, the waiver requirements are recomputed and the lock/unlock state updates accordingly
Required Checkboxes and Initials Enforcement
Given a template includes required checkboxes and per-section initials When the client attempts to submit without completing any required field Then the submission is blocked and each missing field is called out with an inline error and focus state And initial fields require at least two characters and are time-stamped upon entry And the final submit button remains disabled until all required fields validate
Multi-Language Content and Locale Selection
Given a template has translations for supported languages And a client has a preferred locale on file When the Quick Unlock link is opened Then the waiver content displays in the client’s preferred language with an option to switch languages And if a translation is unavailable, the content falls back to the business default language And the language used is recorded and reflected in the generated PDF

Nudge Ladder

Timed, friendly SMS reminders escalate from soft check‑ins to deadline‑specific prompts with one‑tap camera upload and e‑sign. Nudges pause the moment docs are complete and resume if they expire, keeping records current without awkward chasing.

Requirements

Configurable Nudge Ladder Engine
"As an independent groomer, I want automated, escalating SMS nudges that pause when clients complete required items so that I keep records up to date without manual follow-ups."
Description

A rules-driven engine to create, schedule, and manage multi-step SMS nudges that escalate from soft check-ins to firm reminders. Each ladder supports configurable steps (timing, copy, tone), triggers (e.g., upcoming appointment, missing deposit, missing document), and stop conditions (e.g., payment received, form completed). The engine pauses automatically when the client completes required items and can resume if those items later expire. It integrates with PawPilot scheduling, client profiles, billing, and document management to personalize messages (pet name, service, date/time) and update state in real time. It supports per-service/per-location ladders, idempotent sends, link tracking, opt-out handling, carrier-compliant sending, and event webhooks for downstream actions.

Acceptance Criteria
Create and Execute Configurable Ladder
Given I create a ladder named "Missing Docs - Grooming" with 3 steps configured: Step 1 at T+0h (soft), Step 2 at T+24h (friendly), Step 3 at T+48h (firm with deadline) And each step has message copy, tone tag, and SMS channel configured And the ladder is set to start on trigger "Missing required document" with priority Normal When I assign this ladder to Service "Full Groom" at Location "Downtown" and activate it And a client meets the trigger criteria at 2025-08-20 10:00 in their timezone Then the system schedules Step 1 for 2025-08-20 10:00, Step 2 for 2025-08-21 10:00, and Step 3 for 2025-08-22 10:00 And the UI shows the next scheduled send time and step labels And editing Step 2 timing to T+30h immediately reschedules it to 2025-08-21 16:00 without duplicating tasks And a test preview renders the full personalized message without sending to the client
Trigger Evaluation and Start Conditions
Given triggers configured: Upcoming appointment (T-72h), Missing deposit, Missing document And the same client has one upcoming appointment and is missing a deposit When the "Missing deposit" event is emitted at 12:00 with dedupeId "evt-123" Then exactly one ladder instance starts for client-service-appointment, scoped to the "Missing deposit" ladder And reprocessing the same event with the same dedupeId within 10 minutes does not create additional ladder instances or scheduled sends And the "Upcoming appointment" ladder does not start for this client if a more specific "Missing deposit" ladder is active for the same appointment And starting conditions are recorded in the audit log with eventId and trigger type
Automatic Pause on Completion and Resume on Expiration
Given an active ladder for a client due to missing deposit and missing rabies certificate When the client pays the deposit via the provided link Then the ladder pauses within 60 seconds and cancels all future deposit-related steps And if other outstanding items remain (e.g., rabies certificate), only the document-related steps remain scheduled When the rabies certificate is uploaded via the link and verified Then the ladder fully completes and emits a "completed" event When the rabies certificate later expires at 00:00 on its expiry date Then the ladder automatically resumes according to configuration (restart at Step 1) and schedules the first step at 08:00 local time respecting quiet hours
Personalization and Link Tracking
Given message templates contain variables: {{client.firstName}}, {{pet.name}}, {{service.name}}, {{appt.startAt}}, {{location.name}} When Step 1 sends to a client with known values Then the delivered SMS contains all variables rendered with correct values and no unresolved placeholders And the SMS includes a unique, shortened tracking link tied to ladderId, stepId, and clientId When the client clicks the link Then the click is recorded within 5 seconds and visible in analytics with step attribution And the e-sign/upload page opens with the client pre-authenticated and the correct document pre-selected
Per-Service and Per-Location Ladder Selection
Given three ladders exist: Global Missing Docs, Location "Downtown" Missing Docs, Service "Full Groom" Missing Docs When a "Missing document" event occurs for a "Full Groom" at "Downtown" Then the Service "Full Groom" ladder is selected and applied When the service-specific ladder is inactive Then the Location "Downtown" ladder is selected When neither service nor location ladders exist Then the Global ladder is selected And the system records which ladder was selected and why in the audit log
Opt-Out and Carrier Compliance
Given a client has replied STOP and is marked opted-out at 09:00 local time When a scheduled step would send at 10:00 Then no SMS is sent, the step is marked "skipped - opted out", and the ladder is closed Given quiet hours are 21:00–08:00 client local time When a step is scheduled for 07:30 Then the send is deferred to 08:00 and recorded as deferred due to quiet hours And all SMS content for the ladder includes required HELP/STOP language on the first message per ladder as configured And throughput limits are enforced per sender ID so that rate never exceeds configured TPS caps And messages over 1600 characters are rejected at configuration time with a validation error
Event Webhooks and Downstream Actions
Given a webhook endpoint is configured with URL, secret, and retry policy (max 5, exponential backoff) When any ladder changes state (started, paused, resumed, completed) or a step sends, skips, or fails Then the system POSTs a JSON payload containing eventType, ladderId, stepId (if applicable), clientId, timestamp, dedupeId, and signature header And the signature verifies using the shared secret And on 5xx or timeout, the system retries up to 5 times with exponential backoff and jitter And on success, the delivery is recorded with response code and latency And failed deliveries raise an alert in the admin UI
One‑Tap Upload & E‑Sign Flow
"As a client, I want to tap a text link to snap a photo of my dog's vaccine card and sign the consent in seconds so that I can confirm my appointment without hassle."
Description

A mobile-first, one-tap experience launched from SMS that lets clients snap photos of required documents (e.g., vaccine records), upload from gallery, and e‑sign consent/policy forms without logging in. The flow pre-fills client and pet details, validates file types and legibility, compresses media on-device, and securely stores artifacts with timestamps. It supports inline signature capture, multi-page uploads, progress autosave, accessibility standards, multilingual copy, and instant callbacks to mark requirements as complete, immediately pausing the active nudge ladder.

Acceptance Criteria
One-Tap SMS Launch with Pre-Filled Details (No Login)
Given a recipient taps a secure SMS deep link When the link opens the mobile web flow Then the One‑Tap Upload & E‑Sign screen loads in 2 seconds or less on a 4G connection And no login or password screen is shown at any point And client and pet details are pre-filled from the tokenized link or profile API And pre-filled fields are editable and edits persist to the draft And the link token is single-use, expires after completion or 7 days (whichever comes first), and invalid tokens show an "Expired or invalid link" message with a safe re-request option
Camera/Gallery Upload with Validation and On-Device Compression
Given the user is on the document upload step When they tap the primary CTA to add a document Then the OS Camera opens within 1 tap (or Gallery within 2 taps) And the user can choose Camera or Gallery And only file types jpg, jpeg, png, heic, pdf are accepted; others show a clear error with guidance And images are compressed on-device to ≤ 2 MB per image while maintaining width ≥ 1500 px And files > 20 MB pre-compression are blocked with retry guidance And upload progress is shown with percentage and success state And uploads use TLS 1.2+ in transit and are stored encrypted at rest with ISO-8601 timestamps and user/pet linkage
Legibility Verification and Multi-Page Support
Given one or more pages are uploaded When legibility checks run Then OCR-based confidence averages ≥ 0.80 or contrast/blur checks meet thresholds And if legibility fails, the user is prompted to retake with tips and cannot proceed until a pass or an override by staff (flagged) exists And the user can add, remove, and reorder pages before submission (up to 10 pages per document) And a combined preview (including pinch-zoom) is available before finalizing And the final stored artifact preserves page order in a single PDF with embedded images
Inline E‑Signature Capture with Audit Trail
Given the user must sign required consent/policy forms When they proceed to sign inline Then the signature pad supports draw and type modes with clear, undo, and re-sign options And the user must check an explicit e‑sign consent checkbox before submission And upon submit, a tamper-evident PDF is generated containing the signed document, full name, signature image, UTC and local timestamps, IP address, and a document hash And the artifact is stored with versioning and is retrievable via secure URL with time-limited access
Autosave Progress and Session Resume
Given the user has entered data or uploaded files When they navigate away, close the browser, or lose connectivity Then progress autosaves within 1 second of each field change or upload completion And resuming via the same valid link restores to the last completed step with previously uploaded files intact And partial uploads resume automatically with up to 3 retries using exponential backoff And drafts are retained for 14 days or until completion, after which draft data is purged per retention policy
Accessibility and Multilingual Compliance
Given the user has accessibility needs or a non-English locale When they use the flow Then all interactive elements have descriptive labels, logical focus order, and hit targets ≥ 44x44 points And color contrast meets WCAG 2.1 AA (≥ 4.5:1) and the flow is screen-reader navigable without traps And motion/animation is reduced when OS reduce-motion is enabled And the flow is fully localized in English and Spanish with auto-detection from browser locale and an in-flow language toggle; all dynamic content and validation messages are localized
Completion Callback to Pause Nudge Ladder and Resume on Expiry
Given all required documents are uploaded, pass validation, and the form is signed When the user submits and receives confirmation Then an idempotent completion webhook is emitted within 1 second containing clientId, petId, requirementId, artifactIds, and timestamps And upon webhook receipt the active Nudge Ladder pauses within 5 seconds and no further nudges are sent for this requirement And if any stored document later reaches its configured expiry date, an expiry webhook is emitted and the Nudge Ladder resumes within 15 minutes with the next appropriate nudge
Deadline‑Aware Messaging & Escalation
"As a sitter, I want reminders that automatically get firmer as deadlines approach so that clients act before I have to cancel or lose income."
Description

Dynamic messaging that injects precise, localized deadlines (e.g., “by Tue 5:00 PM”) and adjusts cadence, tone, and content as deadlines approach. Supports configurable step intervals, final-notice and last-chance templates, and conditional actions at deadline (e.g., auto-release slot to Smart Waitlist, cancel hold, or add late fee). Respects time zones, quiet hours, and throttling rules; includes deposit/payment links with real-time payment confirmation to halt further nudges. Delivery receipts and link engagement are logged to refine subsequent steps.

Acceptance Criteria
Localized Deadline Injection in SMS Content
Given a recipient with time zone America/Los_Angeles and a deadline of 2025-08-20T00:00:00Z, When the SMS is composed, Then the injected deadline renders as "Tue 5:00 PM" in the recipient’s local time format configured for the business. Given a deadline that crosses a DST boundary in the recipient’s time zone, When the SMS is composed, Then the injected deadline reflects the correct post-transition local time. Given the recipient’s time zone is unknown, When composing the SMS, Then the system uses the business default time zone and records tz_fallback=true in the message log.
Escalating Tone and Template Selection by Proximity
Given tone mappings Soft (>48h), Reminder (48–6h), Firm (<=6h), When a nudge is due, Then the selected template matches the current time-to-deadline window. Given "Final-Notice" at T-24h and "Last-Chance" at T-60m are enabled, When those windows occur, Then the respective templates send exactly once and include the localized deadline token. Given a step has already sent, When entering a new window, Then previously sent templates are not resent.
Configurable Step Intervals and Recalculation
Given step intervals configured at T-72h, T-24h, T-2h, When a deadline is set, Then nudges are scheduled at those offsets adjusted to avoid quiet hours. Given an admin updates step intervals, When the configuration is saved, Then all future (unsent) step times recalculate within 5 minutes without creating duplicate jobs. Given a step is due within the next 10 minutes at the time of recalculation, When recalculated time changes, Then the system does not send the old schedule and only the new schedule is honored.
Quiet Hours and Throttling Compliance Across Time Zones
Given quiet hours 20:00–08:00 in the recipient’s time zone, When a nudge is scheduled during quiet hours, Then it is deferred to 08:00 local time on the next allowed day. Given a throttle of max 1 nudge per 6 hours per recipient, When multiple steps become eligible simultaneously after quiet hours, Then only the highest-priority step sends and remaining steps are rescheduled to satisfy the throttle window. Given recipients in different time zones, When bulk nudges are processed, Then each send respects the recipient’s own quiet hours and throttle limits.
Conditional Actions at Deadline Execution
Given action "Auto-release slot to Smart Waitlist" is enabled, When the deadline elapses and required completion is still pending, Then the slot is released, the booking status updates to Released, and an audit log entry is created with action_id and timestamp. Given action "Cancel hold" is enabled, When the deadline elapses unmet, Then the hold is canceled idempotently (no duplicate cancellations on retries) and a final notification SMS is sent. Given action "Apply late fee" is enabled with amount $X, When the deadline elapses unmet, Then the fee is posted to the invoice once and reflected in the billing ledger within 2 minutes.
Payment Link Halt and Resume
Given a nudge includes a deposit/payment link, When a successful payment confirmation is received from the gateway, Then all pending future nudges for that flow are canceled within 60 seconds and the flow status changes to Completed:Payment. Given a payment failure or expiration callback is received before the deadline, When the next step’s time arrives, Then the nudge ladder resumes using the next scheduled template. Given payment is confirmed after the final notice has been sent, When confirmation arrives, Then no additional nudges are sent and the audit log records halt_reason=payment_confirmed.
Engagement Logging and Next-Step Refinement
Given an SMS provider delivery receipt is returned, When it is received, Then the message status (delivered/undelivered) is recorded with timestamp and provider message_id. Given the recipient clicks the embedded link before the next step and completion is still pending, When the next step is due, Then the system suppresses the duplicate prompt and sends the configured "acknowledged" variant instead. Given two consecutive undelivered receipts for a recipient, When scheduling the next step, Then the system pauses the ladder for that recipient and flags the contact for review.
Smart Resume on Document Expiration
"As a walker, I want clients to be nudged when vaccine records expire so that compliance stays current without me tracking dates manually."
Description

Automated monitoring of document validity (e.g., rabies vaccine expiration) using captured dates, metadata, or OCR. When a required doc is near expiry or lapses, the system re-enrolls the client in the correct nudge ladder with appropriate lead time and pre-filled context. It deduplicates overlapping ladders, supports snooze/override by the provider, and updates appointment eligibility rules so bookings aren’t confirmed without current records. All resumes are auditable with timestamps and initiating conditions.

Acceptance Criteria
Auto-Resume for Near-Expiry and Expired Docs
Given a pet has a required document with expiration date T stored, when the nightly monitor runs at 00:05 local time and now is between T - lead_time_days and T and there is no active enrollment for that doc_type/pet_id, then create a new nudge enrollment with variant=renewal and schedule the first send within 15 minutes with cadence per ladder config. Given now >= T and there is no active enrollment for that doc_type/pet_id, when the monitor runs or a booking is attempted, then create a new nudge enrollment with variant=expired and send the first message immediately. Given a new valid document is received and passes validation (doc_type matches, pet_id matches, expiration date in the future), when processed, then pause the current ladder immediately and cancel all future sends. Given a document is renewed and a new expiration date T2 is stored, when now reaches T2 - lead_time_days, then create a new enrollment and do not reuse the old paused enrollment.
Deduplication and Idempotency of Nudge Ladders
Given an active enrollment exists for the same doc_type and pet_id, when another trigger fires for the same condition, then do not create a second enrollment and record a deduped_event with source signals. Given multiple triggers arrive within a 5-minute window, when processing, then use an idempotency key composed of doc_type:pet_id:next_due_date to ensure at most one enrollment is created. Given a renewal enrollment exists and the doc becomes expired, when the state escalates, then update the existing enrollment to variant=expired and adjust cadence instead of creating a new enrollment. Given triggers are for different doc_types for the same pet, when processing, then enroll separately; no cross-doc deduplication occurs.
Pre-Filled Context in Nudge Messages
Given an enrollment is created, when the first message is generated, then the SMS body must include pet_name, doc_type, last_expiry_date (YYYY-MM-DD), and a unique upload_link tied to enrollment_id. Given consent is required for this doc_type, when composing the message, then include a one-tap e-sign link and set require_signature=true on the enrollment. Given any personalization value is missing, when rendering, then apply defined fallbacks (e.g., use client first name; omit clinic_name) and do not send unresolved merge tags. Given a message is sent, when delivered, then track link_clicks and submission events attributed to enrollment_id.
Provider Snooze and Override Controls
Given a provider clicks Snooze on an active enrollment and selects N days, when saved, then no ladder messages are sent during the snooze window and the next send is rescheduled to snooze_end at the start of the next cadence window. Given a provider applies an Override/Waiver with end_date W, when saved, then suppress all ladder sends until W and mark booking eligibility as satisfied until W with reason=override. Given a provider clicks Resume Now on a snoozed or active enrollment, when confirmed, then send the next step immediately and reset the cadence from that send time. Given the provider performs any of the above actions, when saved, then create an audit entry with user_id, action, reason (optional), and timestamp.
Appointment Eligibility Enforcement
Given a booking is attempted for a pet with a required doc that is expired or missing and no active override exists, when the client tries to confirm, then prevent confirmation and present an SMS with the upload link and return a structured error {code: DOCS_REQUIRED} to the booking API. Given a provider has set an override until W, when a booking falls before W, then allow confirmation and annotate the appointment with compliance_status=Overridden. Given an appointment is scheduled and the doc will expire before service time, when detected, then notify provider and client at least lead_time_days before expiry and mark the appointment Requires Docs; if auto_hold_on_noncompliance=true, then set status=Tentative until docs are valid. Given a waitlist offer is generated, when compliance is not met and no override exists, then do not send the offer; queue it until compliance or timeout.
Document Date Extraction and Validation
Given a client uploads an image or PDF of a required document, when OCR and parsing run, then extract an expiration date with confidence >= 0.9 using keywords [expires, exp, valid until]; if confidence < 0.9 or multiple conflicting dates are found, then request date confirmation via SMS. Given a date is extracted or entered, when validating, then accept only future dates (>= today) and reject past dates with an error and prompt to re-upload or correct. Given regional date formats (MM/DD/YYYY vs DD/MM/YYYY), when parsing, then infer format based on locale and keywords and store ISO-8601 (YYYY-MM-DD). Given OCR is unavailable, when the client submits, then require manual date entry and proceed. Given a validated date is stored, when saving, then associate it with doc_type and pet_id metadata and mark validation_source as OCR or User.
Auditability of Resume Events
Given any nudge enrollment is created, updated, paused, resumed, or canceled, when the change occurs, then append an immutable audit log record with timestamp (UTC), actor (system or user), initiating_condition (near_expiry, expired, override, manual), source_signal (OCR, metadata, booking_block), and enrollment_id. Given audit logs exist, when filtering by pet_id or date range, then return results within 2 seconds for up to 10,000 records. Given an admin requests export for a date range, when processed, then provide a CSV with all fields and a SHA-256 checksum to detect tampering. Given a retention policy of 24 months, when older records are reached, then archive or delete per policy and record the event.
Template Personalization & Tone Controls
"As a business owner, I want to choose the tone and wording of each nudge step so that messages feel on-brand and effective."
Description

A template library spanning soft to firm nudge steps with brandable variables (pet name, service, staff, location, date/time) and selectable tone presets. Users can customize copy per step, set emoji/character limits for SMS, preview carrier-length splits, and localize text. Includes conditional snippets (e.g., show deposit link only when unpaid) and reusable blocks for camera upload and e‑sign CTAs. Versioning and approval workflows ensure changes roll out safely across active ladders.

Acceptance Criteria
Apply Tone Preset and Preview per Nudge Step
Given a user edits a nudge step template and selects a tone preset (Friendly, Neutral, Firm) When the user toggles between tone presets Then the selected preset is saved with the template and exposed via API And preset-specific default validations (e.g., emoji cap, punctuation checks) are applied without altering variable tokens And the live preview updates the active tone label, character count, and segment count in real time
Variable Merge with Fallbacks and Validation
Given a template step contains variables {pet_name, service, staff, location, appt_datetime} and configurable fallbacks When previewing or generating a send for a client Then all present variables render with correct values And variables missing data render the configured fallback And variables missing both data and fallback block save/send with an actionable error listing each unresolved variable And appt_datetime renders using the template’s locale setting
SMS Character/Emoji Limits and Carrier Segment Preview
Given a step has a character limit (e.g., 320) and an emoji limit (e.g., 2) configured When the composed text exceeds either limit Then the UI displays current counts and a validation error and prevents save When the content contains only GSM-7 characters Then the preview shows GSM-7 segmenting and segment count When any non-GSM character is present Then the preview switches to UCS-2 segmentation with updated segment count And the UI displays estimated segments and cost per send
Conditional Deposit Snippet Rendering
Given a conditional snippet "Deposit CTA" is inserted into the template When the client’s invoice status is Unpaid Then the snippet renders with CTA copy and a unique, client-scoped shortened URL When the client’s invoice status is not Unpaid Then the snippet is omitted and the final message contains no orphaned punctuation or extra spaces And the preview link resolves to the correct invoice when followed
Reusable Camera Upload and E‑Sign CTA Blocks
Given the user inserts the "Camera Upload" or "E‑Sign" reusable block Then the block renders standardized CTA copy and a unique short link keyed to client, ladder, step, and template version And tracking parameters include source=pawpilot, feature=nudge_ladder, action=camera_upload|esign And attempting to insert the same block twice in one step triggers a validation error and blocks save And removing the block removes its link from the final render
Per-Step Localization and Locale-Aware Formatting
Given a step has localized variants (e.g., en-US, es-ES) and a default When locale selection is set to Auto Then the system selects the variant based on the client’s locale and falls back to default if unavailable When a specific locale is chosen Then preview and sends use that locale’s variant And date/time variables render in that locale’s format And missing translations are listed and block approval until resolved or explicitly waived
Versioning, Approval, and Safe Rollout Across Active Ladders
Given an approved template step is edited Then a new draft version is created and the approved version remains live When the draft is submitted and approved by an authorized approver Then all ladders referencing the template adopt the new version for newly generated messages within 5 minutes And already queued/scheduled messages continue using the prior version And an audit log records editor, approver, timestamps, diff, and affected ladders And a rollback action creates a new version that restores the prior approved content for new messages
Performance Analytics & Compliance Audit Trail
"As an owner, I want to see which nudges work and have a verifiable audit trail so that I can optimize outreach and stay compliant."
Description

Dashboards and exports showing completion rates by ladder and step, time-to-complete, conversion on deposits, no-show reduction, and revenue saved from avoided cancellations. Includes per-client timelines with message content, delivery status, engagement, consent/opt-out events, and form/file signatures with hashes and timestamps. Supports CTIA/GDPR-compliant data retention settings, role-based access, and immutable logs for disputes. Insights feed back into ladder recommendations to improve outcomes.

Acceptance Criteria
Analytics Dashboard KPIs by Ladder and Step
Given a user with Analytics permission selects a date range (up to 12 months) and filters (ladder, step, service, staff, location), When the dashboard loads, Then it displays for each ladder and step: completion rate (%), median and 90th percentile time-to-complete, deposit conversion rate (%), no-show rate and delta vs baseline, and counts (n). Given the same filters are applied to the CSV export, When the export completes, Then all counts and rates in the CSV exactly match the UI and each row includes ladder_id, step_id, date, n, completion_rate, median_ttc_seconds, p90_ttc_seconds, deposit_conversion_rate, no_show_rate, no_show_delta_pct. Given new qualifying events are ingested, When 5 minutes have elapsed, Then the dashboard and exports reflect the new events (data freshness SLA ≤ 5 minutes). Given 10,000+ events in the selected window, When loading the primary dashboard, Then the initial render completes within 3 seconds on a typical broadband connection.
Revenue Saved from Avoided Cancellations
Given a canceled appointment is backfilled via Smart Waitlist within the original service window, When computing revenue saved, Then revenue_saved equals min(original_price, replacement_price) minus discounts applied, floored at 0, recorded with cancellation_id and replacement_booking_id. Given a no-show with a forfeited deposit, When computing revenue saved, Then the forfeited deposit amount is included as revenue_saved with a reference to payment_id and policy_id. Given a baseline period is selected (default previous 8 weeks), When displaying no-show reduction and revenue saved deltas, Then deltas are calculated as (current - baseline)/baseline and labeled with the baseline window. Given the user exports the revenue report, When the file downloads, Then each revenue_saved entry includes client_id, booking_ids, event_timestamps, currency, and totals match the dashboard to the cent.
Per-Client Audit Timeline Completeness and Search
Given a user with Client Audit permission opens a client's Audit Timeline, When the timeline loads, Then every event shows type, message content snapshot, delivery status (queued|sent|delivered|failed), engagement (reply text, click URL), consent/opt-out events, and signature events with SHA-256 hash and UTC timestamp to millisecond precision, plus actor_id and ip if available. Given the user searches the client's timeline by keyword, tag, or date range, When results display, Then filtering completes within 1 second for timelines up to 1,000 events and zero-match queries return an empty state. Given an event has been purged by retention policy, When viewing the timeline, Then a placeholder remains with event type, purge timestamp, and policy_id without revealing redacted content.
Immutable Log Integrity and Evidence Export
Given any audit event is persisted, When an update is attempted, Then the system appends a new version with change_reason and actor_id while preserving the original; no hard deletes are permitted outside retention policies. Given the log chain is verified, When the integrity check runs, Then each event's prev_hash and content_hash validate end-to-end and the API returns integrity_status=valid; any tampering yields integrity_status=invalid with the first failing event_id. Given an evidence export is requested for a client and date range, When the ZIP package is generated, Then it contains JSON lines of events, original message bodies, delivery receipts, signature files, and a manifest.json with SHA-256 checksums that validate on re-hash; all timestamps are ISO 8601 UTC (Z).
CTIA/GDPR-Compliant Data Retention Controls
Given retention policies are configured per data class (messages, delivery receipts, signatures, payments), When the retention window elapses, Then the system purges the data within 24 hours and records a purge event with policy_id and counts purged. Given a legal hold is applied to a client or case_id, When a scheduled purge would occur, Then affected records are skipped and the purge log indicates legal_hold=true. Given a client opts out via STOP, When the event is received, Then marketing and nudge messages are suppressed immediately and the opt-out is recorded in the audit timeline and suppression list; resubscribe only allowed per CTIA keywords (e.g., START). Given a data export request for a client is submitted, When the export completes, Then it includes all retained personal data and audit events within scope and excludes purged items, delivered via a time-limited secure link.
Role-Based Access and Least Privilege
Given predefined roles (Owner, Manager, Staff, Billing, Auditor), When users authenticate, Then each role can only access authorized dashboards and audit views: Auditor (read-only audit logs), Billing (payments and revenue metrics only), Staff (own clients' timelines), Manager (all team clients), Owner (all features). Given an unauthorized API request to an endpoint outside the user's role scope, When the request is made, Then the system returns HTTP 403 and logs an access_denied event with actor_id and endpoint. Given any access to audit content occurs, When a user views or exports data, Then the access is logged with actor_id, timestamp, client_id, resource, and purpose, and is filterable in an Access Log view.
Insights-to-Ladder Recommendations Feedback Loop
Given analytics are available for ladders and steps, When the nightly job runs, Then the system generates ranked recommendations per step (e.g., change send delay, add deposit prompt) with expected lift (%) and confidence score based on the last 8 weeks of data. Given a recommendation is applied to a ladder step, When traffic accrues, Then the system creates an experiment with control and variant labels, enforces minimum sample size (≥200 events per variant or 14 days), and computes outcome metrics with significance (p<0.05) before declaring a winner. Given a user reviews a recommendation, When they open the detail, Then they can see inputs used (metrics and segments), and can accept, schedule, or dismiss; dismissed items do not resurface for 30 days unless metric deltas exceed 10%.

Vet Pingback

When a card is unclear, PawPilot can message the listed clinic a secure verify link. The clinic confirms vaccines and dates in one tap, and the proof attaches to the client’s record. You get trustworthy verification without phone tags or fax roulette.

Requirements

Clinic Lookup & Channel Detection
"As a groomer, I want PawPilot to automatically find the right clinic and pick the best way to reach them so that I don’t have to chase contact information or guess how to send the verify link."
Description

Parse the client’s card to identify the listed veterinary clinic and resolve it to a canonical clinic record with contact methods. Maintain a lightweight clinic directory (name, phone, SMS-capability, email, fax status) and automatically select the optimal outreach channel (SMS first, then email) for delivering a secure verify link. Provide a fallback shareable link for manual delivery when no digital channel is available. Log detection results and chosen channel for traceability and analytics.

Acceptance Criteria
Canonical Clinic Resolution From Client Card
Given a client card contains a clinic name and at least one contact detail (phone or email) When the clinic lookup runs Then the input values are normalized (case-insensitive, punctuation/abbreviation handling, E.164 phone formatting) And the system attempts to match against the clinic directory And if a single match has similarity score ≥ 0.85, the client is linked to that canonical clinic record And if two or more matches are within 0.02 of the top score, the outcome is marked Ambiguous and requires manual selection (no auto-link) And if no match ≥ 0.85 exists, a new lightweight clinic record is created with parsed fields and verified=false And the resolved clinic_id (or Ambiguous/New) is saved on the client record
SMS Capability Detection & Validation
Given a clinic record has a phone number When channel detection runs Then the phone number is normalized to E.164 And carrier/type lookup determines SMS capability And if the number is invalid or identified as non-SMS (e.g., landline without SMS), sms_capable is set to false; otherwise true And detection result (sms_capable true/false and detection timestamp) is stored on the clinic record
Optimal Channel Selection: SMS Over Email
Given a resolved clinic record and a secure verify link to deliver When selecting an outreach channel Then if sms_capable=true and a valid phone exists, select SMS and do not send email And if sms_capable=false and a valid email exists, select Email And if neither a valid SMS number nor valid email exists, do not attempt delivery and surface a fallback shareable link to the user And the chosen_channel is recorded with reason (e.g., sms_preferred, sms_unavailable_email_used, no_digital_channel)
Email Fallback on SMS Dispatch Failure
Given a clinic has sms_capable=true and also has a valid email And an SMS is attempted with the verify link When the SMS provider returns Failed or Undeliverable within 5 minutes of dispatch Then an email containing the same verify link is automatically sent to the clinic email And the communication log records both attempts with statuses (sms_failed, email_sent) And chosen_channel is updated to email with fallback_reason=sms_failed
Fallback Shareable Link Generation & Controls
Given no valid SMS number or email is available for the resolved clinic When a user requests a fallback shareable link Then the system generates a unique secure token with at least 128 bits of entropy And the link expires 7 days from creation or immediately after first successful verification, whichever comes first And the UI presents Copy Link and Share options And access to an expired or already-used link returns an error and does not expose client data
Traceability Logging & Analytics Readiness
Given any clinic lookup and channel selection operation When the operation completes Then an immutable log entry is written containing request_id, client_id, resolved_clinic_id (or Ambiguous/New), lookup_outcome, match_score (if applicable), chosen_channel, delivery_attempts with provider statuses, and timestamps And logs are retained for at least 365 days and are queryable for analytics by outcome and channel And personally identifiable contact values in analytics exports are masked (e.g., phone last 4, email username redacted)
One-Tap Verify Link Generation
"As a clinic staffer, I want a secure, single-tap link to confirm vaccine status so that I can respond quickly without creating an account or making a phone call."
Description

Generate a signed, single-use verify URL per clinic request that is bound to the client, pet, and requested vaccine types. Include token-based authentication, short expiration (configurable), and automatic invalidation after completion. The link opens a mobile-friendly page requiring no login, prefilled with pet identifiers and requested vaccines, enabling clinics to confirm in one tap. Track link lifecycle events (created, delivered, opened, completed) for reliability and reporting.

Acceptance Criteria
Signed, Single-Use Verify Link Creation and Binding
Given a verification request containing requestId, clientId, petId, clinicId, and requestedVaccines When the system generates a verify link Then the link contains a signed token whose payload includes requestId, clientId, petId, clinicId, requestedVaccines, expiresAt, and a unique nonce And the server rejects any request where the token signature is invalid with HTTP 401 and displays "Invalid link" And modifying any token payload field without a valid signature results in HTTP 401 and no state change And the link is created in "unused" state and associated to the requestId
Configurable Short Expiration Enforcement
Given the default link expiration is configured to 24 hours When a link is generated at T0 Then its expiresAt equals T0 + 24h And opening the link at T0 + 23h 59m succeeds And opening the link at or after T0 + 24h returns HTTP 410 and displays "Link expired" And when expiration is reconfigured to 2 hours, links generated after the change expire at T0 + 2h, while previously issued links retain their original expiry
Token Validation and No-Login Access
Given a valid, unexpired, unused verify link When the link is opened on a mobile device over HTTPS Then the page loads without login or account creation prompts And the server authenticates solely by validating the token and its expiry And the initial page load completes in ≤2 seconds at the 95th percentile on a 3G profile And all network requests are HTTPS
Mobile Prefill and One-Tap Confirmation
Given the verify page is opened from a valid link Then it displays pet name, petId, client last name, and requestedVaccines as read-only prefilled fields And a single primary "Confirm" action is visible without scrolling on a 375px wide device When the clinic taps "Confirm" Then all requestedVaccines are marked verified with completedAt timestamp and verifier = clinicId And the server response returns HTTP 200 within 1 second and transitions the link to "completed"
Automatic Invalidation After Completion
Given a verify link is valid and unused When a completion request is successfully processed Then the link transitions to "used" with usedAt timestamp And any subsequent access to the same link returns HTTP 410 and displays "Link already used" And concurrent completion attempts are idempotent; only the first succeeds, and no duplicate verification records are created
Lifecycle Event Tracking (Created, Delivered, Opened, Completed)
Given a verify link's lifecycle When events occur (created, delivery_attempted, delivered, opened, completed, expired) Then each event is recorded with timestamp, requestId, linkId, clinicId, and channel where applicable And delivery_failed is recorded with provider status code when delivery does not succeed And events are viewable via the admin UI and queryable by requestId and date range And the "opened" event captures the first open only and increments an open_count metric for subsequent opens
Attach Verification Proof to Client Record
Given a verify link is completed When the verification is saved Then a verification record is attached to the client and pet records containing requestId, clinicId, petId, verifiedVaccines, verifiedOn, and linkId And the record is visible in the client record UI within 5 seconds of completion And the record is immutable; any correction requires a new verification entry linked to the prior via supersedesId
Clinic Confirmation Page
"As a clinic receptionist, I want a simple confirmation page with clear options so that I can provide accurate vaccine details in under a minute."
Description

Provide a minimal, mobile-first confirmation interface for clinic staff to: (a) confirm vaccines are current and supply dates, (b) mark specific vaccines with dates/expiry, (c) indicate unable to verify, and (d) optionally upload proof (PDF/photo). Capture clinic responder name and role, with an optional attestation checkbox. Validate inputs (date formats, required fields) and support partial confirmations. Optimize for sub‑60 second completion time.

Acceptance Criteria
Confirm Current Vaccines with Dates
Given a valid secure link, when the page loads on a mobile device, then a checklist of common vaccines (Rabies, Bordetella, DHPP, Leptospirosis, Influenza, Other) is displayed with administered and expiry date inputs for each. Given at least one vaccine is marked as current with valid dates entered, when the user reviews the summary, then the Submit button is enabled. Given the user submits with at least one vaccine confirmed, when the API call completes, then a 201 response is returned and each confirmed vaccine (name, administered date, expiry date) is attached to the client record.
Mark Specific Vaccines with Admin and Expiry
Given a vaccine is selected, when the administered date is entered, then the date must accept MM/DD/YYYY and ISO (YYYY-MM-DD) formats and normalize to ISO on save. Given both administered and expiry dates are provided, when validation runs, then expiry must be the same or after administered; otherwise, inline error is shown and submission is blocked. Given a vaccine is not applicable, when the user marks it as N/A or leaves it unselected, then no date inputs are required for that vaccine.
Indicate Unable to Verify
Given the clinic cannot verify records, when the user selects "Unable to verify", then all vaccine inputs are disabled and an optional reason field appears. Given "Unable to verify" is selected, when the user submits, then the system records status = UNABLE_TO_VERIFY with timestamp, responder details, and reason (if provided) and returns 201. Given an invalid token or expired link, when the page is accessed, then the interface shows an "Link expired or invalid" message and no data can be submitted.
Upload Proof Document
Given the user chooses to attach proof, when a file is selected, then only PDF, JPG, or PNG up to 10 MB per file (max 3 files) are accepted; otherwise, an error is shown and file is rejected. Given a valid file is selected, when upload starts, then a progress indicator is shown; when upload completes, then a thumbnail/filename is displayed with an option to remove before submission. Given the user submits with one or more uploaded files, when the API responds 201, then each file is stored and linked to the client record and can be retrieved via secure URL.
Capture Responder Identity and Attestation
Given the page loads, when the user begins filling the form, then Name (required) and Role (required) inputs are present and must be completed before Submit is enabled. Given the optional attestation checkbox is present, when the user checks it, then the attestation flag and timestamp are recorded; when unchecked, submission remains allowed without error. Given the user submits, when validation passes, then responder name and role are persisted with the submission and appear on the verification record.
Input Validation and Partial Confirmation
Given at least one vaccine is confirmed with valid dates, when other vaccines are left unselected or marked unknown, then submission is allowed (partial confirmation). Given any date field is incomplete or invalid, when the user attempts to submit, then the form scrolls to the first error and displays inline error messages without losing entered data. Given all required fields are valid, when the user submits, then the submission is idempotent: repeat submissions with the same token payload update the existing record rather than creating duplicates.
Mobile-First Performance, Accessibility, and Integrity
Given a 4G mobile connection (400ms RTT, 1.6 Mbps), when the page is first loaded, then First Contentful Paint occurs within 2.5s and the interactive form is usable within 3.5s (p95). Given typical clinic usage, when measuring from page load to successful submission, then median completion time is under 60 seconds across at least 100 real sessions. Given the form is rendered, when tested with a screen reader, then all inputs have labels, focus order is logical, contrast meets WCAG AA, and the form is fully usable via keyboard/touch. Given a successful submission, when the token is reused, then the page displays a "Already submitted" state with read-only data to prevent duplicate entries.
Automatic Record Attachment & Audit Trail
"As a groomer, I want verified vaccine proof attached to the client record with a clear audit trail so that I can trust compliance and streamline scheduling decisions."
Description

Upon clinic submission, update the pet’s record with verified vaccine types, dates, and expiry, and attach any provided documents. Stamp verification metadata (clinic ID, responder name/role, timestamp, request ID, IP) and render a “Verified by Clinic” badge on the client’s profile and upcoming appointments. Maintain an immutable audit log for edits, retractions, and subsequent verifications. Trigger follow-up tasks for expiring vaccines based on recorded dates.

Acceptance Criteria
Attach Verified Vaccine Data to Pet Record
Given a valid clinic verification submission for pet P including vaccine types, administered dates, expiry dates, and optional document attachments When the submission is received on the verify endpoint and passes schema validation Then the system atomically updates pet P's record with the submitted vaccine types, administered dates, and expiry dates And any attached documents are stored and linked to pet P's record and the verification event And existing unverified vaccine entries are preserved; matching vaccine types are marked as verified and updated with the new dates And if any step of storage fails, no changes are committed and an error is logged for review
Persist Verification Metadata
Given the clinic completes the verification When the verification payload is saved Then the verification record includes clinicId, responderName, responderRole, verifiedAt (UTC timestamp), requestId, and sourceIp And these metadata fields are required (non-null) and immutable after creation And the metadata is visible in the verification detail view for staff users
Render Verified by Clinic Badge
Given pet P has at least one current (non-expired as of today) clinic verification on file When a staff user views the client's profile Then a 'Verified by Clinic' badge is displayed with a tooltip showing last verified date and clinic name Given pet P has an upcoming appointment A When viewing appointment A in the schedule or details view Then the 'Verified by Clinic' badge is displayed if all required vaccines for P are valid through the appointment date And if all verified vaccines are expired as of the appointment date, the badge is not shown and an 'Expires/Expired' indicator is displayed
Immutable Audit Log for Edits and Retractions
Given an existing verification record for pet P When any edit, retraction, or subsequent verification occurs Then an audit entry is appended capturing action type (created|edited|retracted|superseded), actor (clinic/staff/system), timestamp, requestId (if applicable), and sourceIp (if applicable), with before/after value diffs where relevant And audit entries are append-only and cannot be updated or deleted via UI or API And the audit trail is retrievable in chronological order for pet P and for the specific verification record
Idempotent and Deduplicated Processing by Request ID
Given a clinic submission is received with requestId R for pet P When an identical submission with the same requestId R is received again within 48 hours Then the system returns a successful idempotent response and makes no additional changes or duplicate audit entries Given a subsequent verification with a new requestId R2 is received When it validates successfully Then a new verification version is created and previous verification is marked as superseded in the audit trail
Follow-Up Tasks for Expiring Vaccines
Given pet P has a recorded vaccine with expiry date E When the current date is within 30 days of E Then a follow-up task is automatically created with due date E and linked to pet P and the client, without creating duplicates And if the vaccine is updated to a later expiry date, any open follow-up tasks are updated accordingly; if extended beyond the 30-day window they are closed And if a verification is retracted and no other valid verification exists, an immediate follow-up task is created to obtain updated proof
Consent & Privacy Controls
"As a business owner, I want clear consent and privacy safeguards so that Vet Pingback operates respectfully and compliantly for my clients and clinic partners."
Description

Capture and store owner consent to contact the clinic via intake flows and appointment prompts. Provide configurable templates that state the purpose of verification and data usage. Enforce least-data sharing on the verify link (only necessary pet and owner identifiers). Offer configurable data retention windows for proof documents and verifications, plus a deletion workflow. Apply access controls, rate limiting, and token scope restrictions to protect clinic pages and stored artifacts.

Acceptance Criteria
Intake Flow: Owner Consent Capture & Audit Trail
- Given an owner starts the intake flow for a pet with Vet Pingback enabled When the consent step is displayed Then the consent checkbox is unchecked by default and required to proceed - Given the owner checks consent and submits Then the system stores consent with: owner_id, pet_id, template_version_id, timestamp (UTC), actor="owner", channel, IP/device fingerprint, and locale - Given consent is stored Then an immutable audit record is created and visible to staff with read permission - Given an owner revokes consent later When staff marks consent revoked or owner submits a revoke form Then the system records a revocation event with timestamp and prevents new clinic contacts starting immediately - Given consent is absent or revoked Then any attempt to send a clinic verify link is blocked with a reason logged
Appointment Prompt: Consent Gate Before Clinic Contact
- Given a staff member schedules an appointment that requires vaccine verification When no valid consent exists for the pet/owner Then the system prompts for consent within the appointment flow with the active template - Given the owner declines consent Then the appointment can be saved but clinic verification actions remain disabled and flagged "Consent required" - Given the owner grants consent in the prompt Then consent is persisted and the verification task is enabled without additional steps - Given a consent validity window is configured (e.g., 24 months) Then the prompt is skipped if existing consent is within window and not revoked
Consent Template: Configurable Content, Placeholders, and Localization
- Given an admin opens Templates > Vet Pingback Consent When creating or editing a template Then fields for Purpose and Data Usage are required and cannot be saved empty - Given placeholders {clinic_name},{pet_name},{owner_name},{appointment_date},{retention_window} are used Then preview renders with sample data and saved template validates only allowed placeholders - Given multiple locales are configured Then the admin can create locale-specific variants and set a default locale - Given a template is updated Then a new version is created and historical versions remain read-only and selectable for audit - Given a consent is captured Then the exact template version and locale used are recorded with the consent
Verify Link: Least-Data Payload and Redaction
- Given a clinic opens a verify link Then only whitelisted fields are displayed: pet_name, species, owner_last_name, owner_phone_last4, appointment_date, clinic_name - Given the verify link or page URL is inspected Then no PII beyond a signed token appears in the URL, and no email, full phone, address, or medical details are present in query or body - Given application logs and analytics Then any displayed identifiers are masked (e.g., phone last4 only) and non-whitelisted attributes are not logged - Given API responses for the verify page are intercepted Then the JSON payload contains no keys outside the whitelist
Clinic Verify Link: Token Scope, Expiration, and Rate Limiting
- Given a verify link is generated Then it contains a signed, 128-bit entropy token scoped to {clinic_id, pet_id, verification_request_id} and action=confirm_only - Given the token is used Then it is single-use; subsequent attempts return 410 Gone and are logged - Given time passes beyond the configured expiry (default 72 hours) Then the link returns 401/expired message and cannot be used - Given excessive requests to a clinic verify endpoint occur Then rate limits apply: 20 requests/min per IP and 200/day per clinic; excess requests return HTTP 429 with Retry-After - Given a request is made over HTTP Then it is redirected to HTTPS and HSTS is enabled
Data Retention Windows and Automated Purge
- Given an admin opens Privacy Settings When configuring retention for proof documents and verification records Then selectable windows include 30, 90, 180, and 365 days, with a default of 180 - Given records reach end of retention Then a purge job deletes documents and verification data within 24 hours and removes derived caches/thumbnails - Given a purge completes Then accessing prior links returns 404 and audit logs record purge id, scope, and timestamp - Given retention settings change Then changes apply prospectively and do not resurrect previously purged data
Manual Deletion Workflow and Access Controls for Stored Artifacts
- Given a staff user with Admin role opens a client record When selecting Delete Proof on a verification Then the system requires a typed confirmation and reason before proceeding - Given deletion is confirmed Then the artifact is purged within 15 minutes and is no longer visible to roles below Manager - Given a staff user without permission attempts access Then access is denied (HTTP 403) and the attempt is logged - Given a download or view of a proof document Then the event is logged with user_id, timestamp, IP, and purpose-of-use note if provided
Outreach Automation & Reminders
"As a groomer, I want automated reminders to clinics when they don’t respond so that verifications complete without me having to follow up manually."
Description

Automate initial clinic outreach with the secure verify link via the selected channel. If no completion occurs, send timed reminders with configurable cadence and attempt limits. Detect delivery failures (SMS undeliverable, email bounce) and switch channels when possible. Allow manual resend and cancellation from the PawPilot dashboard. Record outreach attempts, outcomes, and timing for each request to drive reliability metrics.

Acceptance Criteria
Initial Outreach Send via Preferred Channel
Given a Vet Pingback request with clinic contact information and a preferred outreach channel is set When the request is created or outreach is triggered Then the system sends a message containing the secure verify link via the preferred channel within 30 seconds And the attempt is logged with request ID, channel, timestamp, and provider message identifier
Reminder Cadence and Attempt Limit Enforcement
Given an outreach request remains incomplete and a reminder cadence and maximum attempt limit are configured When the scheduled reminder time is reached Then the system sends a reminder containing the secure verify link And the system repeats sends according to the configured cadence until the clinic completes verification or the maximum attempt limit is reached And when the maximum attempt limit is reached without completion Then the system stops further automated reminders and records the terminal outcome as "Max attempts reached"
Delivery Failure Detection and Channel Switching
Given an outreach attempt is sent via SMS and the provider returns an undeliverable status or an outreach attempt is sent via email and the message bounces And an alternate channel with valid contact information is available When the failure status is received by the system Then the system records the failure with error code and description And switches future outreach for this request to the alternate channel starting with the next scheduled attempt And logs the channel switch event with reason, timestamp, and new channel
Manual Resend from Dashboard
Given a user views the outreach request in the PawPilot dashboard When the user clicks "Resend" Then the system immediately sends the verify message via the current active channel for the request And increments the attempt count And logs the manual resend action with user ID, timestamp, channel, and message identifier
Manual Cancellation from Dashboard
Given a user views the outreach request in the PawPilot dashboard When the user clicks "Cancel Outreach" Then the system cancels all scheduled reminders for the request And prevents any further automated sends And records the terminal outcome as "Canceled by user" with user ID and timestamp
Completion Handling Stops Automation
Given the clinic completes verification via the secure link When the completion event is received by the system Then the request is marked as complete And all pending or scheduled reminders for that request are immediately canceled And the completion time and the channel of the last successful delivery are recorded for metrics
Comprehensive Outreach Audit Logging
Given an outreach request has one or more send attempts When reviewing the stored audit data for the request Then each attempt record includes attempt sequence number, channel, timestamp, provider message identifier (if applicable), delivery status, and error code/reason on failure And the request record includes aggregate fields such as total attempts, time-to-first-send, time-to-completion (if completed), and terminal outcome And all audit records are persisted and queryable to support reliability metrics
Verification Notifications & Queue
"As a groomer, I want a clear queue and notifications for vet verifications so that I can act quickly and schedule confidently."
Description

Provide an in-app queue that lists all active verification requests with statuses (Pending, Delivered, Opened, Completed, Failed/Expired), filters, and search. Surface request context from appointments and client profiles. Send real-time notifications to the business when a verification completes or fails, and display next best actions (resend, change channel, request from owner). Include basic analytics on completion times and success rates.

Acceptance Criteria
Queue Listing with Statuses and Sorting
Given verification requests exist in statuses Pending, Delivered, Opened, Completed, and Failed/Expired When the user opens the Vet Pingback queue Then the list displays one row per request with columns: Client, Pet, Clinic, Status, Created At, Last Update, Appointment Date, and Age And the default sort is Last Update descending And each row’s status chip reflects the backend status exactly And time and dates are shown in the business’s timezone And a maximum of 50 rows render per page with pagination controls present when more than 50 requests exist And only requests belonging to the current business account are visible
Filters and Search Across Requests
Given the queue is loaded with multiple requests When the user applies a multi-select Status filter (Pending, Delivered, Opened, Completed, Failed/Expired) Then only requests matching the selected statuses are shown and the result count updates accordingly When the user sets a Created Date range filter Then only requests created within that range are displayed When the user filters by Clinic name Then only requests for that clinic are displayed When the user searches by client name, pet name, clinic name, phone number, or request ID Then matching requests are returned case-insensitively within 2 seconds for datasets up to 10,000 requests And an empty state is shown when no results match And active filters and the search query persist during in-app navigation within the Vet Pingback area
Request Detail Panel with Appointment and Client Context
Given the queue is visible When the user clicks a request row Then a detail panel opens showing: Appointment date/time and service, Client name and phone, Pet name and key attributes (if available), Required vaccine list, Clinic name and contact, Channel used, and a status timeline with timestamps And if the request is Completed, a link to the attached proof is present and downloadable/viewable in-app And data in the panel matches the underlying appointment and client records exactly And links from the panel open the client profile and appointment in a new tab/route without losing current queue filters
Real-time Notifications on Verification Completion or Failure
Given a verification request is being tracked When its status transitions to Completed Then an in-app notification appears within 5 seconds including Client, Pet, Clinic, Appointment date, and a View Verification deep link When its status transitions to Failed or Expired Then an in-app notification appears within 5 seconds including the failure type and a Retry deep link And each status transition triggers at most one notification per request And clicking the deep link opens the corresponding request detail and marks the notification as read
Next Best Actions: Resend, Change Channel, Request from Owner
Given a request is in Pending, Delivered, or Opened status When the user clicks Resend Then a new verify link is sent over the current channel, the action is logged with timestamp and user, and a 15-minute cooldown prevents immediate repeat resends Given a request is not Completed When the user selects Change Channel (e.g., SMS to Email) and provides a valid destination Then the verify link is sent via the chosen channel, the channel field updates, and the attempt is logged Given a request is Failed or Expired, or has 2 unsuccessful attempts When the user clicks Request from Owner and confirms Then the owner receives a link to provide proof, the request is tracked, and on successful owner submission the proof attaches and the status moves to Completed (Owner Provided) And for Completed requests, all action buttons are disabled And every action (resend, channel change, owner request) is recorded in the activity log with actor, timestamp, and outcome
Analytics: Completion Times and Success Rates
Given verification activity exists in the selected date range (default last 7 days) When the user opens the Analytics section of Vet Pingback Then the dashboard displays: total requests, success rate, median completion time, and 75th percentile completion time And success rate is calculated as Completed ÷ (Completed + Failed/Expired) for the period; Pending/Delivered/Opened are excluded from the denominator And completion time is measured from first Delivered to Completed timestamp And analytics can be filtered by channel and clinic, updating metrics within 5 seconds of filter changes And metrics update within 60 seconds of new events And when fewer than 10 requests exist for the period, a “Not enough data” message replaces percentile metrics

Waiver Wallet

All signed waivers live in one place—per service and version—with time‑stamped signatures tied to each booking. When policies change, clients get an automatic re‑consent request by SMS, keeping your coverage current and reducing disputes.

Requirements

Waiver Versioning & Policy Management
"As a business owner, I want to create and publish service-specific waiver versions with effective dates so that clients always agree to the latest policies without losing prior records."
Description

Provide a version-controlled waiver system scoped by service type (grooming, walking, sitting) with effective dates, change logs, and draft/publish states. Each service can define a required waiver template, track iterations, and set which version is active. The system stores the full text of each version, diffs between versions for internal review, and maps required fields (e.g., pet info, medical disclosures). Publishing a new version automatically updates the active policy reference for upcoming bookings and triggers re-consent workflows without altering historical signatures tied to prior versions.

Acceptance Criteria
Publish Active Waiver Version by Service
Given a service type with no active waiver, when an admin publishes draft v1 with effective date set to now, then v1 becomes Active for that service and is assigned a version ID and published timestamp. Given a service type with active waiver v1, when an admin publishes v2 with effective date set to now, then v2 becomes Active for new bookings created after the publish timestamp, and v1 remains referenced by existing bookings and signatures. Given a waiver v2 is published for Grooming, when a Walking booking is created, then the Walking booking references Walking’s active version and is not affected by Grooming v2. Given publish completes, then the system stores the full waiver text, required field schema, version metadata (service, version ID, author, effective date, published timestamp), and an immutable content hash.
Draft State, Publish Gate, and Change Log
Given an admin edits a draft waiver version, when they click Save, then a change log entry is stored with author ID, timestamp, and a non-empty summary (minimum 10 characters). Given a draft waiver lacks a change summary, when Publish is attempted, then publish is blocked and a validation error is shown requiring a change summary. Given a draft is created from vN, when View Diff is clicked, then a diff against vN shows added, removed, and modified text sections and field schema changes with counts.
Effective Date Controls and Booking Mapping
Given a new waiver version vN+1 is published with a future effective date, when a booking is created for a service date on or after that effective date, then the booking references vN+1; bookings with service dates before the effective date reference the current active version. Given vN+1 is published with an effective date in the past, when published, then it becomes active immediately for bookings created after the publish time; existing bookings retain their original waiver version reference. Given a booking is rescheduled across the effective date threshold, when the new service date qualifies for vN+1, then the booking updates to require vN+1 and any prior incomplete signature for vN is invalidated for that booking.
Automatic Re-Consent SMS Triggering
Given a new waiver version is published for a service, when a client has an upcoming booking for that service without a signed waiver for the new version, then an SMS re-consent request is sent within 5 minutes and tracked with status (Queued, Sent, Delivered, Completed, Failed). Given a client completes the waiver via the SMS link, when submission succeeds, then the booking displays Signed for that version with signer name, timestamp, and device/IP metadata. Given a booking already has a signed waiver for the current active version, when publish occurs, then no re-consent SMS is sent for that booking.
Required Field Schema and Validation
Given an admin defines required fields for a waiver version (e.g., pet name, medical disclosures, vet contact), when the version is published, then the schema is stored and enforced for all signatures of that version. Given a client attempts to submit a waiver missing any required field or with invalid format, when Submit is clicked, then submission is blocked and field-level error messages are shown; no signature record is saved. Given all required fields are valid and the signature is captured, when Submit is clicked, then the system stores field values, signature artifact, consent acknowledgement, timestamp, and links them to the booking and waiver version.
Version Diff for Internal Review
Given a staff user with Manage Waivers permission, when they open any waiver version, then they can select any prior version in the same service to view a diff showing added/removed/modified text and required field schema changes with change counts. Given the diff view is opened, then it displays version metadata for both versions (service, version ID, author, effective date, published timestamp) and is read-only.
Historical Signatures and Audit Integrity
Given a booking has a signed waiver for version vN, when a new version vN+1 is published, then the booking’s signature remains linked to vN and is not altered. Given staff view a signed booking, then the waiver record shows version ID, signature timestamp, signer identity, and an immutable content hash matching the stored waiver text for vN. Given staff open the waiver’s audit log, then entries show publish events and change logs with author and timestamps, and no retroactive edits to signed content are permitted.
Mobile eSignature & Secure Timestamp
"As a client, I want to sign waivers on my phone via a text link so that I can quickly confirm policies without logging into a portal."
Description

Enable legally compliant electronic signatures via SMS-delivered smart links to a mobile-friendly waiver form. Capture signer identity fields, consent checkboxes, and signature input (typed or draw) with server-side timestamp, timezone, IP address, and user agent for evidentiary value. Bind the signature cryptographically to the waiver content hash and store an immutable event record. Support accessibility, localization, and retry-safe flows. On completion, attach the signed record to the client profile, pet, and targeted booking, and update compliance status in real time.

Acceptance Criteria
SMS Smart Link, Session Resume, and Idempotent Submit
Given a booking requiring a waiver and a verified client mobile number, When the groomer triggers the waiver request, Then the client receives an SMS containing a unique, single-use, signed URL that expires in 24 hours and the system rate-limits sends to 3 per hour. Given the signed URL is expired or already used, When the client opens it, Then the page displays an "Expired link" message and offers a one-tap resend without creating a new waiver session. Given the client partially completes the waiver, When they reopen the same link before expiry, Then the form restores prior inputs and returns to the last completed step. Given a network interruption during submission, When the client retries within 5 minutes, Then the submission is processed exactly once and no duplicate signed records are created. Given the client number is opted-out or unverified, When an SMS send is attempted, Then the send is blocked, an error is logged, and a non-SMS resend option is offered.
Mobile Waiver Form Inputs and Signature Methods
Given the waiver form loads, When the client proceeds, Then required identity fields (full legal name and mobile number at minimum) and mandatory consent checkboxes are present and must be completed before signing. Given the client selects Type to Sign, When they enter their legal name and initials and check the acknowledgment, Then a signature artifact is generated and stored with the typed values and acknowledgment flag. Given the client selects Draw to Sign, When they draw their signature, Then the system captures the vector data (points and timing) and stores it along with the device user agent. Given validation rules, When any required identity field or consent checkbox is missing, Then the Submit action is disabled and inline error messages are shown. Given the client taps Submit with all required inputs, When the request reaches the server, Then the server persists identity fields, consent booleans, and signature data and returns a 201 Created response.
Server-Side Audit Trail and Secure Timestamp
Given a signature submission is accepted, When the server records the event, Then it stores a server-generated UTC timestamp with millisecond precision synchronized within ±2 seconds of NTP, the server timezone offset, client IP address, and user agent string. Given a signed record exists, When an authorized user views the audit trail, Then the UTC timestamp, business-local timestamp, IP, and user agent are displayed and exportable. Given any user attempts to modify audit fields, When the change is submitted, Then the system rejects the change with a 403/blocked action and logs the attempt immutably.
Cryptographic Binding to Waiver Content
Given the waiver content is rendered for signing, When the server prepares the session, Then it computes a SHA-256 hash of the exact content bytes and includes the hash and waiver version ID in the signing payload. Given signature submission is saved, When the record is finalized, Then the signature is stored alongside the content hash and an event record is signed with the platform key and chained via previous-hash to provide tamper evidence. Given the signed record is viewed or exported, When validation runs, Then the system recomputes the content hash and verifies it matches the stored hash; on mismatch, the record is flagged Invalid and a warning blocks unannotated export. Given the waiver content changes after signing, When the original record is validated, Then validation still passes because it is bound to the original version and hash.
Accessibility Compliance (WCAG 2.2 AA)
Given keyboard-only navigation, When completing all steps, Then every interactive element is reachable in logical order with a visible focus indicator and the flow is completable without a pointing device. Given a screen reader (NVDA, JAWS, or VoiceOver), When reading the form, Then all fields have programmatic labels, roles, and states announced correctly, and error messages are associated to inputs. Given color contrast testing, When automated (axe-core) and manual checks run, Then text and interactive elements meet a contrast ratio of at least 4.5:1. Given motor accessibility needs, When using Type to Sign, Then the client can complete the signature without drawing gestures and without time limits. Given automated audits, When Lighthouse and axe-core are run on the form, Then the accessibility score is at least 90 with no critical violations.
Localization and Timezone Consistency
Given the client language preference is Spanish (es), When the SMS and waiver form are delivered, Then all user-facing copy and validation messages appear in Spanish; otherwise default to English (en). Given locale-sensitive formatting, When dates and times are displayed to the client, Then they follow the client's locale conventions while the stored audit timestamp remains in UTC. Given the business timezone is configured, When staff view the signed record, Then the timestamp is shown in the business timezone with explicit offset labeling. Given a missing translation key, When the form renders, Then English fallback text is shown and a missing-key log entry is created for remediation.
Record Attachment and Real-Time Compliance Update
Given a signature is finalized, When the server completes the workflow, Then the signed record is attached to the client profile, associated pet(s), and the specific booking and is visible on each entity's Waiver tab within 5 seconds. Given the booking requires the current waiver version, When the signed record matches the current version, Then the booking compliance status updates to Waiver On File within 5 seconds and appears in the schedule and booking detail. Given the waiver version changes during a signing session, When the client submits an older version, Then the system prompts for re-consent to the new version and does not mark the booking compliant until completed. Given export is requested for a dispute, When an admin downloads the signed packet, Then it contains the waiver content, content hash, signature artifact, and the full audit trail (UTC/local timestamps, IP, user agent) along with a validation result.
Waiver Enforcement at Booking
"As a groomer, I want bookings to be automatically blocked until the correct waiver is signed so that I don’t perform services without coverage."
Description

Gate bookings behind current waiver compliance. At scheduling time, rescheduling, or Smart Waitlist auto-fill, the system checks whether the client has an in-force signature for the required service/version. If not, it inserts a mandatory re-consent step via SMS before confirming the slot. Provide configurable enforcement (block, soft warn, or allow with override), attach compliance status to the booking, and surface clear prompts in SMS and the admin calendar. Handle edge cases such as multi-pet bookings, service changes, and last-minute fills to ensure coverage remains current.

Acceptance Criteria
Block Non-Compliant Client at New Booking
Given enforcement mode for the selected service is "Block" And the client lacks an in‑force signature for the required service and current waiver version When the client attempts to confirm a new booking via SMS or an admin attempts to create it Then the system prevents confirmation and sets booking status to "Pending Waiver" And the client receives an SMS containing a secure waiver link for the required version And the admin calendar surfaces a "Pending Waiver" badge on the tentative booking card And the booking auto-confirms only after a successful, time‑stamped signature is recorded and tied to the booking And if the waiver is not signed, the booking remains unconfirmed and no confirmation SMS is sent
Soft Warn at Reschedule with Auto Update on Signature
Given enforcement mode for the selected service is "Soft Warn" And the client lacks an in‑force signature for the required service/version When a booking is rescheduled by the client via SMS or by an admin Then the booking remains confirmed and a re‑consent SMS is sent to the client And the booking is tagged with compliance status "Out‑of‑date" and a "Waiver Needed" badge appears on the admin calendar And upon receiving a valid, time‑stamped signature, the booking’s compliance status updates to "Compliant" within 60 seconds And an audit log records the reschedule event, SMS dispatch, and signature completion
Allow with Admin Override and Audit Trail
Given enforcement mode for the selected service is "Allow with Override" And the client is not compliant for the required service/version When an admin attempts to confirm a booking Then the system presents an override prompt requiring a reason and records the admin identity and timestamp And if the admin confirms override, the booking is confirmed, compliance status is set to "Overridden", and a re‑consent SMS is sent And if the admin cancels override, the booking remains pending and no confirmation SMS is sent And users without override permission cannot complete the override And override is not available when enforcement is set to "Block"
Smart Waitlist Auto‑Fill Requires Re‑Consent Before Confirmation
Given Smart Waitlist selects a candidate for a vacant slot And enforcement for the target service is "Block" And the candidate lacks an in‑force signature for the required service/version When auto‑fill attempts to place the candidate Then the system sends a re‑consent SMS with a waiver link and marks the placement as "Pending Waiver" And the slot is held for the configured reserve window And if a valid, time‑stamped signature is recorded within the reserve window, the booking confirms automatically and the client receives a confirmation SMS And if not signed within the window, the placement is released and auto‑fill advances to the next candidate, logging the reason "Failed Compliance"
Multi‑Pet Booking with Mixed Service Requirements
Given a booking includes multiple pets with potentially different services/waiver versions When evaluating waiver compliance Then the system determines required waiver versions per pet/service And the booking is considered compliant only if all required waivers are in‑force for the attending client And re‑consent SMS includes links only for missing waivers per pet/service And in "Block" mode, confirmation is withheld until all required signatures are completed and tied to the booking And in "Soft Warn" mode, the booking remains confirmed but is flagged with a per‑pet compliance breakdown in the admin calendar
Service or Policy Version Change Triggers Re‑Check
Given an existing booking that is currently compliant for Service A at waiver version vX When the service on the booking is changed to Service B or the required waiver version increments to vY Then the system immediately re‑evaluates compliance for the new service/version And if not compliant, it updates the booking to "Pending Waiver" (Block) or flags it "Waiver Needed" (Soft Warn) and sends a re‑consent SMS And upon signature capture for the new version, the booking updates to "Compliant" and the admin calendar badge clears And all changes (service change, version change, SMS, signature) are written to the audit log tied to the booking
SMS Re-Consent Automation
"As a dog walker, I want the system to text clients for updated consent when policies change so that coverage stays current without manual follow-up."
Description

Automatically request re-consent by SMS when a waiver version is published or when a client’s consent is expiring. Generate personalized messages with a smart link to the correct waiver version, track delivery/opens/signature status, and send scheduled reminders with escalating urgency. Respect quiet hours, opt-out rules, and rate limits. Update the booking and client records upon completion and notify the provider in the inbox. Provide analytics on completion rates, time-to-consent, and message performance.

Acceptance Criteria
Auto Re-Consent on New Waiver Version Published
Given a provider publishes a new waiver version for a service When the version status is set to Active Then queue SMS re-consent messages within 5 minutes to all clients with upcoming bookings for that service who have not signed the new version And exclude clients who have already signed the new version And exclude canceled bookings and past bookings And do not message opted-out numbers; log a suppression reason on the contact And each SMS includes a smart link that resolves to the correct waiver version tied to the client and their next booking
Re-Consent Scheduling on Imminent Consent Expiry
Given a client’s existing consent will expire within the configured threshold (default 14 days) When the threshold is reached Then schedule an SMS re-consent within 60 minutes, respecting quiet hours And cancel the scheduled send if the client signs before the send time And ensure only one active re-consent campaign per client per waiver at a time
Personalized SMS Content and Smart Link Routing
Given a re-consent SMS is generated When rendering the message Then include the provider display name, client first name, service name, and next booking date/time in the SMS And include a short smart link that deep-links to the exact waiver version required And associate any signature from that link to the targeted client and booking And after signature, subsequent opens of the link show a confirmation state rather than requesting re-consent
Tracking and Analytics for the Re-Consent Funnel
Given re-consent messages are sent When events occur Then record SMS delivery states (queued, sent, delivered, failed) with timestamps And track link opens, waiver viewed, and signature completed with timestamps And surface per-client and per-booking status in their timelines And provide an analytics view showing completion rate, average time-to-consent, and breakdown by reminder step and template, filterable by date range, service, and waiver version And update analytics within 15 minutes of events And allow CSV export of analytics data for the selected filters
Reminder Cadence with Escalating Urgency
Given a re-consent request is unsatisfied When 24 hours elapse after the initial message Then send Reminder 1 using the R1 template And when 72 hours elapse after Reminder 1, send Reminder 2 using the R2 template with increased urgency And when 7 days elapse after Reminder 2, send a Final Reminder And stop all reminders immediately upon signature or opt-out And cap re-consent reminders at 3 per campaign
Compliance with Quiet Hours, Opt-Out, and Rate Limits
Given the system is preparing to send a re-consent SMS When the client’s local time falls within quiet hours (default 8pm–8am) Then defer sending to the next allowed window And do not send to numbers that have opted out (e.g., replied STOP) or are globally suppressed And enforce rate limits: maximum 1 re-consent SMS per client per 24 hours and maximum 240 re-consent SMS per account per hour; queue excess sends And log deferrals, suppressions, and rate-limit queuing in an auditable trail
Record Updates and Provider Inbox Notification
Given a client completes and submits the waiver via the smart link When the signature is validated Then update the booking with consentStatus=Signed, waiverVersionId, and signedAt timestamp And update the client record with latestSignedWaiverVersionId and signedAt timestamp And remove the client from any active re-consent queues or reminder schedules And create a provider inbox notification within 1 minute that includes client name, service, booking date/time, and a direct link to the signed waiver
Waiver Wallet Admin Repository & Search
"As an admin, I want a single place to find any signed waiver by client, service, or booking so that I can resolve questions quickly."
Description

Offer a centralized, searchable repository of all signed waivers, organized by client, pet, service, version, and booking. Include quick filters, full-text search within waiver text, and inline preview of signed PDFs. Allow manual upload of external waivers, merge/resolve duplicates, and annotate records. Provide role-based access controls and an activity log for viewing/exporting actions. Surface compliance status at a glance and enable one-click navigation from a booking to its linked waiver and vice versa.

Acceptance Criteria
Central Repository Organization & Cross-Linking
1. Repository list displays columns: Client, Pet, Service, Policy Version, Booking ID/Link, Signature Timestamp, Compliance Status, and Source. 2. Clicking a Booking ID opens the target booking detail with the Waiver section focused within 1 click. 3. From a waiver detail, a single click navigates to the linked booking (if any) or prompts to link a booking. 4. Each waiver is uniquely identifiable and linked to exactly one policy version and zero or one booking. 5. Compliance status badge rules: 5.1 Compliant: waiver signed on/after current policy effective date and policy version matches current for the service; not expired. 5.2 Outdated: waiver version older than current policy version for the service. 5.3 Missing: no signed waiver for the service/pet/client within the configured lookback or no link to booking requiring it. 5.4 Pending Re-consent: newer policy version exists and re-consent request sent but not yet signed. 5.5 Expired: waiver has an explicit expiration date prior to today. 6. Compliance status badges are visible in list and detail views and are filterable. 7. Sorting by any visible column is available and applied within 1 second on datasets up to 10k records. 8. Empty-state messaging appears when no waivers match the current filter.
Quick Filters and Full-Text Search
1. Filters available: Client, Pet, Service, Policy Version, Compliance Status, Signature Date Range, Source (System/Manual), and Has Booking (Yes/No). 2. Full-text search matches terms within waiver text, policy title, and annotations (when "Include annotations" is toggled on). 3. Search supports phrase queries using quotes and basic boolean AND by space; special characters are safely ignored. 4. Combined filters and full-text search use AND semantics and return the correct subset. 5. Results return within 2 seconds for up to 100k waivers and paginate 25/50/100 per page. 6. Matching terms are highlighted in result snippets without altering stored documents. 7. New or updated waivers become searchable within 60 seconds of ingestion. 8. Clearing filters and search resets the list to the default view.
Inline PDF Preview
1. Clicking Preview opens an inline viewer (modal or side panel) rendering the signed PDF without a download. 2. Preview loads within 1.5 seconds for files up to 10 MB over a 20 Mbps connection. 3. Viewer supports zoom in/out, page navigation, and download (if role permits). 4. Metadata panel in preview shows Client, Pet, Service, Policy Version, Booking (if linked), Signature Timestamp. 5. Users without view permission do not see the preview control and direct access attempts return HTTP 403. 6. If the file is missing or corrupted, the UI shows a non-blocking error state with a retry option and a link to download (if permitted). 7. Mobile view stacks viewer and metadata and remains usable at 320 px width.
Manual Upload of External Waivers
1. Accepted file type: PDF; max size 25 MB; files are virus-scanned and quarantined on failure. 2. Required fields to save: Client, Service, Signature Date; optional: Pet, Booking, Policy Version (may be set to External/Unknown). 3. On save, the record is created with Source = Manual and appears in the repository within 60 seconds. 4. Duplicate detection warns when file hash matches an existing waiver for the same Client within ±5 minutes of Signature Date; user can Link Existing or Create New. 5. Validation errors are field-specific and prevent save until resolved. 6. Manually uploaded waivers can be linked to a booking at upload time or later without re-uploading.
Duplicate Merge and Resolution
1. User can select 2–5 waiver records and initiate Merge. 2. System blocks merge if selected waivers belong to different Clients and explains the constraint. 3. User selects a canonical record and resolves conflicting fields (Pet, Service, Booking, Signature Timestamp, Policy Version) via a field-by-field chooser with a default to canonical. 4. On confirm, non-canonical records are marked Merged and hidden from default views; all references (e.g., booking links) are reassigned to the canonical record. 5. A detailed merge entry is written to the activity log including before/after snapshots and actor identity. 6. Admin can undo a merge within 24 hours, restoring original records and references. 7. Search index reflects the merge (or undo) within 60 seconds.
Record Annotations
1. Roles allowed to add annotations: Staff, Admin; Viewer cannot add or edit. 2. Annotation fields: text (max 1000 chars), visibility = Internal; system stores author and timestamp. 3. Authors (or Admin) can edit their annotation within 15 minutes of creation; after 15 minutes only Admin can edit. 4. Deleting an annotation creates an audit tombstone; deleted text is not shown but the deletion event is logged. 5. Annotations are displayed chronologically on the waiver detail and are not embedded into the PDF. 6. Full-text search includes annotations only when the "Include annotations" filter is enabled.
Role-Based Access Control and Activity Logging/Export
1. Roles and permissions: 1.1 Admin: full read/write, merge, export logs. 1.2 Staff: read, upload, annotate, link/unlink bookings; no merge; no log export. 1.3 Viewer: read-only; no preview if restricted; no downloads if disabled by policy. 2. All sensitive actions (view detail/preview, download, upload, annotate, merge, link/unlink, export) are permission-checked; unauthorized attempts return HTTP 403 and are hidden in UI. 3. Activity log captures: timestamp (UTC), actor, role, action, target type and ID, outcome (success/fail), and metadata (e.g., count of records exported). 4. Log list is filterable by date range, actor, action, target type and is paginated. 5. Log export to CSV for a given filter set completes within 10 seconds for up to 50k rows and includes a header row. 6. Activity log entries are immutable; corrections are appended as new entries referencing the original.
Audit Trail & Dispute-Ready Export
"As a sitter, I want a dispute-ready export of a client’s signed waiver so that I can prove consent if a chargeback or complaint occurs."
Description

Generate an immutable export package for any signed waiver that includes the waiver text version, content hash, signature artifact, timestamp, signer metadata, message logs (request, reminders), and the linked booking details. Produce a court-ready PDF plus a ZIP of raw JSON events, with optional PII redaction and shareable expiring links. Maintain a tamper-evident audit trail with checksums to verify integrity over time and record who generated each export.

Acceptance Criteria
One-click court-ready export for a signed waiver
Given an authorized user is viewing a signed waiver tied to a booking When the user clicks “Export” for that waiver Then the system generates a court-ready PDF and a ZIP of raw JSON events within 60 seconds for waivers ≤ 2 MB And the PDF includes: waiver text, version ID, content hash, signature artifact, signature timestamp, signer metadata, booking details, and an SMS log summary And the ZIP includes: raw JSON events (waiver, signature, consent request, reminders, booking), all with timestamps and IDs, plus a manifest.json listing each file’s SHA-256 and the package schema version And the export is assigned a unique immutable Export ID and stored with a package-level checksum And the user receives success confirmation with secure download links for the PDF and ZIP
PII redaction options during export
Given the export dialog presents redaction options [None, Mask PII, Remove PII] When a user selects Mask PII and generates an export Then the PDF/ZIP mask sensitive fields (phone: last 4 only; email: first letter + domain; address: city/state only; IP: /24 masked; GPS: 2 decimal places) And manifest.json records Redaction-Policy and fields affected And underlying stored artifacts remain unchanged; only the exported representation is redacted When a user selects Remove PII and generates an export Then the specified PII fields are omitted from the PDF/ZIP and manifest.json lists the removed fields And the default option is None unless an organization policy overrides it
Expiring share links with access logging
Given a user generates a shareable link for an export with a chosen TTL and optional password When the link is created Then the link has at least 128-bit entropy, includes no PII, and is valid until TTL expiry (default 7 days; configurable 1–30 days) And access requires the password if set and is rate-limited to prevent brute force When the link is accessed before expiry Then the system serves the files and logs access (timestamp, IP, user-agent, export ID, actor if authenticated) When the link expires or is revoked Then subsequent requests return 410 Gone and are logged And the creator can revoke at any time and view the access log
Tamper-evident audit trail and checksum verification
Given an export package is generated Then each file (PDF, JSON events, manifest) is hashed with SHA-256 and recorded in manifest.json And the manifest is signed by the system’s private key and includes generator user ID, role, timestamp, and client IP And a Verify Integrity action/endpoint can be run at any later time to validate hashes and signature, returning PASS/FAIL with details When any file in the package is altered Then Verify Integrity returns FAIL and identifies the mismatched checksums And the original export remains read-only and time-stamped in the audit log
Complete waiver, signature, message logs, and booking inclusion
Given a signed waiver exists with related SMS communications and a booking When an export is generated Then the package includes: waiver text body, version ID, waiver content hash, signature artifact (image or e-sign blob ref), signature timestamp And signer metadata: full name, phone, email, client profile ID (subject to redaction settings) And SMS logs: request and reminder messages with timestamps, message IDs, delivery status, and template IDs or content snippet And booking details: booking ID, service type, scheduled start time, provider, location (if available), and payment/deposit status And all records cross-reference via IDs and timestamps consistently, with no missing required fields
Export reflects original waiver version at time of consent
Given a waiver has multiple versions and a client signed for a specific booking date When exporting for that booking Then the PDF/ZIP include the exact waiver text and version ID effective at the signature timestamp, along with that version’s content hash And a version lineage summary is included for context without replacing the historic text And integrity verification confirms the content hash matches the stored version from the signature event
Permissions, performance, and failure handling for exports
Given role-based access controls are configured When a Staff member without Export permission attempts to export Then the system denies with 403 and creates no export artifact When an Owner/Admin or permitted Staff exports Then the job starts immediately; for waivers ≤ 2 MB it completes within 60 seconds at the 95th percentile; larger jobs are queued with visible status And all attempts (success/failure) are logged with actor, timestamp, export ID (if created), and correlation ID And transient failures are retried up to 3 times with exponential backoff; final failures return a descriptive error and no partial package is exposed

PetPack Sync

Manage compliance per pet in multi‑pet households from a single SMS thread. PawPilot shows which pets are cleared, who needs updates, and offers to split or hold bookings until everyone’s compliant—preventing last‑minute cancellations and protecting your schedule.

Requirements

Household Compliance Snapshot in SMS
"As an independent groomer, I want to see each pet’s compliance status directly in the SMS thread so that I can book or adjust appointments without switching screens or risking last-minute cancellations."
Description

Display a real-time, per-pet compliance status overlay within each client’s SMS thread, consolidating all pets in the household. The overlay shows which pets are cleared, expiring soon, or missing requirements using color-coded badges and concise labels, with tap/click actions that insert prebuilt SMS prompts to request missing items. It reads compliance data from pet profiles and the policy engine, updates instantly when documents are uploaded, and supports multi-location rules. Includes pagination for households with many pets, accessibility labels, offline/latency safe states, and error handling to ensure groomers/walkers can make quick, informed booking decisions without leaving the conversation.

Acceptance Criteria
Overlay Shows Per‑Pet Compliance Status in SMS Thread
Given a client SMS thread is opened for a household with one or more pets When the overlay loads household compliance data from pet profiles and the policy engine Then each pet in the household is listed once with name and avatar/initial in a single overlay component And each pet displays a color-coded status badge: Cleared (green #28A745), Expiring Soon (amber #FFC107 when any required item expires within 14 days), Missing Requirements (red #DC3545 when any required item is missing or expired) And each badge includes concise labels for up to two items (≤30 chars each) with overflow shown as “+X more” And the overlay renders above the SMS composer within 1.5 seconds on a broadband connection and shows a skeleton placeholder until data is resolved And the overlay reflects the current policy engine computation time-stamped “Updated just now” upon initial load
Quick Actions Insert Prebuilt SMS Prompts
Given the overlay shows a pet with missing or expiring items When the user taps/clicks the Request/Remind action for that pet or specific item Then a prebuilt SMS template is inserted into the message composer (not auto-sent) within 200 ms And the template auto-fills pet name, item names, due dates (if applicable), and the secure upload link; includes deposit link if required by policy And variables resolve according to the groomer’s selected template set; falls back to default if none configured And the cursor is placed at the end of the inserted text and the user can undo in one action (Cmd/Ctrl+Z) And if the composed message exceeds 480 characters, a non-blocking warning appears with a count of remaining characters
Real‑Time Refresh on Compliance Changes
Given the SMS thread with the compliance overlay is visible And a client or staff uploads a required document or marks a requirement complete When the policy engine updates the pet’s compliance status Then the overlay reflects the change without page refresh within 5 seconds And badges and labels update to the new state atomically (no intermediate contradictory states) And a subtle “Updated just now” indicator is shown for 3 seconds without stealing focus And no duplicate update toasts appear for multiple changes within a 2‑second window (coalesced)
Location‑Aware Compliance Rules
Given a business operates multiple locations with differing compliance policies And the client household can be scheduled at more than one location When the thread’s active location context is set (via current booking or location switcher in the overlay) Then each pet’s compliance is evaluated against the active location’s policy set And the overlay displays the active location name and allows switching locations And switching locations recomputes and updates all badges and labels within 2 seconds And if no location is set, the overlay prompts “Select location to evaluate compliance” and shows neutral (grey) badges until selection
Pagination for Large Households
Given a household has more than 6 pets When the SMS thread opens Then the overlay shows pets 1–6 by default and displays a pager “1–6 of N” with Next/Prev controls And changing pages updates the visible pet list within 300 ms without scrolling the thread or clearing composer contents And page size is configurable between 4 and 8 in settings; default is 6 And the overlay preserves the last viewed page during the session and resets to page 1 on thread reopen
Accessibility and Keyboard Support
Given a user relies on assistive technologies or keyboard navigation When interacting with the compliance overlay Then all interactive elements have accessible names/roles and are reachable in a logical Tab order And color badges meet WCAG 2.1 AA contrast; status is also conveyed via text (e.g., “Cleared”, “Expiring in 7 days”, “Missing: Rabies”) And screen readers announce pet name and status in a single phrase (e.g., “Milo — Missing: Rabies certificate”) And Enter/Space activates actions; Esc closes any overlay popover or menu; focus returns to the invoking control And there are no keyboard traps and focus indicators are visible at all times
Resilience: Offline, Errors, and Fallback States
Given the device is offline or the policy engine/data API is unavailable or slow When the SMS thread opens or the overlay requests updates Then the overlay shows an inline banner indicating Offline or Error with a Retry action And if offline, last-known compliance data (if ≤30 days old) is shown with a timestamp “As of HH:MM, TZ”; items are not marked Cleared if the cache is older than 30 days And if a request times out after 5 seconds, a timeout message is shown and quick actions are disabled with tooltip “Unavailable until data loads” And upon reconnection or service recovery, the overlay auto-retries and refreshes within 5 seconds, removing banners on success And all failures are logged with an error code for diagnostics without blocking SMS composition
Configurable Compliance Policy Engine
"As a business owner, I want to configure which documents and vaccines are required for each service so that compliance is accurate and consistent across my team and locations."
Description

Provide a rules engine and admin UI to define and version compliance requirements per service type (groom, walk, sit), provider, and location. Support requirement types (vaccinations with expiry, flea/tick treatments, signed waivers, temperament notes) with validity durations, grace periods, and conditional logic (e.g., breed/age exceptions). Expose an API/service that evaluates a pet against active policies and returns pass/fail with reasons and expiry dates. Store historical evaluations for auditability, handle policy migrations, and ensure low-latency evaluation to power SMS overlays and booking decisions.

Acceptance Criteria
Policy Definition, Scoping, and Versioning
Given an admin is in the Policy Builder UI When they create a policy scoped by serviceType, providerId, and locationId and add requirement types (vaccination_with_expiry, flea_tick_treatment, signed_waiver, temperament_notes) each with validity_duration and optional grace_period Then the policy is saved as a new immutable version with effective_start (and optional effective_end) and appears in the policy list And conditional rules using breed and age predicates can be added and are persisted And validation prevents conflicting or duplicate requirements within the same scope and surfaces clear errors
Policy Evaluation API Selects Correct Version
Given active policy versions exist for the requested scope and a pet profile includes attributes (breed, ageMonths) and compliance records When POST /compliance/evaluate is called with petId, serviceType, providerId, locationId, and timestamp T Then the engine selects the policy version effective at T and returns HTTP 200 with overall_pass boolean and an array of requirement_results And conditional rules are applied based on pet attributes And the response conforms to the published OpenAPI schema
Per-Requirement Outcomes, Reasons, and Expiries
Given a requirement may pass, be in grace, fail, or be exempt by conditional logic When an evaluation is executed Then each requirement_result includes requirement_id, status in {pass, grace, fail, exempt}, reason_code, reason, computed_expiry_at (or null), observed_at, and last_recorded_value And all timestamps are ISO 8601 UTC And overall_pass is true only when no requirement has status fail and the policy allows grace where applicable
Grace Period Logic and Bookability Flag
Given a requirement is expired but within its configured grace_period and the policy sets bookable_in_grace = true When an evaluation is executed Then the requirement status is grace, overall_pass is true, and bookable is true And if bookable_in_grace = false then overall_pass is false and bookable is false And the response includes grace_ends_at for each requirement in grace
Historical Evaluation Storage and Audit Retrieval
Given evaluations are generated for a pet under a specific policy version When the system persists the result Then an immutable record is stored with petId, policy_version, inputs_hash, outputs_hash, evaluated_at, evaluator_build, and actor And when GET /compliance/audit?petId=&from=&to= is requested Then it returns paginated matching records and each record is retrievable by its id
Policy Migration and Re-Evaluation Strategy
Given a new policy version is published with migration_strategy in {on_next_interaction, immediate_recalc} When the migration is executed Then for immediate_recalc the system evaluates all in-scope active pets within 24 hours and records evaluations tagged with the new policy_version And for on_next_interaction pets are re-evaluated on their next evaluation request and tagged with the new policy_version And prior evaluation records remain unchanged and queryable
Low-Latency and Reliability Targets for Evaluation
Given normal operating conditions at 10k evaluations/min and up to 200 concurrent requests When a 15-minute load test is executed Then p95 latency is <= 150 ms, p99 latency is <= 300 ms, and error rate is < 0.1% And during a dependency outage the service returns last_known_evaluation if <= 5 minutes old with cache_hit = true, otherwise returns HTTP 503 with retry_after And evaluation outputs are identical for cached and live paths given the same inputs
Split-or-Hold Booking Assistant
"As a scheduler, I want guidance to either split a multi-pet booking or place a timed hold when some pets aren’t compliant so that I protect my schedule while accommodating the client."
Description

When a household includes both compliant and non-compliant pets, automatically surface choices during booking: split the appointment to proceed with cleared pets now, or hold the original slot until all pets are compliant. Holds include configurable expirations, deposit requirements, and automatic release rules. The assistant generates one-tap console actions and SMS-ready messages, updates the calendar, prevents double booking, and logs decisions. It integrates with capacity management to maintain accurate availability and respects business policies on deposits and hold durations.

Acceptance Criteria
Mixed-Compliance Options Presented in Console and SMS Preview
Given a booking is initiated for a household with at least one compliant and one non-compliant pet When the assistant evaluates pet compliance Then it presents two primary actions labeled "Split booking" and "Hold slot" in the booking console within 1 second And these actions are not displayed when all pets are compliant or all pets are non-compliant And each action shows a summary including selected pets, estimated duration, price, deposit requirement, and (for Hold) expiration derived from the current business policy And an SMS preview is generated for each action including household name, compliant pet names, non-compliant pet names, deposit link (if required), and hold expiration timestamp in the business’s timezone And the console provides one-tap buttons to send the selected SMS
Split Booking Creates Appointments and Updates Capacity
Given the user chooses "Split booking" for a mixed-compliance household When they confirm the split Then the system creates an appointment only for compliant pets in the requested slot, respecting capacity and required buffers And the calendar shows the new appointment immediately and available capacity for that time is reduced accordingly And non-compliant pets remain unbooked with their required updates listed in the household view And an SMS is sent listing booked pets, date/time, duration, price, and a link to complete requirements for remaining pets And if capacity is insufficient at confirmation, no appointment is created and the user receives a conflict message with the nearest alternative times And the decision is logged with timestamp, user, pets booked, and the applied policy snapshot
Hold Slot Applies Expiration and Deposit Policy
Given the user chooses "Hold slot" for a mixed-compliance household When they confirm the hold Then the system creates a hold on the original time slot and service duration, reserving capacity And the hold’s expiration datetime and required deposit amount are set from the active business policy and are visible in the console and calendar And the client receives an SMS containing hold details, the expiration datetime (with timezone), and a payment link for the deposit if required And while the hold is active, new bookings that would exceed capacity during the held period are prevented And if the deposit is paid before expiration, the hold status updates to "Funded" and remains reserved; if the deposit is not paid by the deadline, the hold is released automatically And creation and status changes of the hold are logged with policy references
Automatic Release Restores Availability and Triggers Notifications
Given a hold reaches its expiration or its deposit deadline passes without payment When the release job runs Then the hold is removed, capacity is restored, and the time slot appears available on the calendar within 30 seconds And the household receives an SMS notifying them of the release with a link to rebook And a log entry records the release reason, timestamp, and impacted slot And if Smart Waitlist backfill is enabled, the slot is offered to the waitlist according to configured rules and offer messages are queued And any pending payment links associated with the hold are voided or marked expired
Concurrency Control Prevents Double Booking
Given two users attempt to split or hold the same slot concurrently When both actions are submitted Then at most one action succeeds based on the most recent availability and the other receives a "Slot no longer available" message within 2 seconds And the system performs atomic capacity checks so the sum of bookings and holds never exceeds capacity And if a hold exists for a slot, new booking attempts that would exceed capacity during the hold are blocked with an explanatory message referencing the existing hold And any partial failures roll back with no orphaned holds or partial appointments
Decision, Message, and Policy Audit Logging
Given any Split or Hold action is taken When the action completes Then an audit record is persisted capturing actor identity, household and pet IDs, action type, selected option, policy version applied, computed expiration, deposit terms, calendar slot, and outcome And the SMS template ID, rendered SMS text, and any payment link IDs are stored with the record And audit records are retrievable by administrators via UI and API with filters by date range, household, and action type And all audit records include immutable timestamps and are protected from edits; corrections are recorded as new entries with references
Business Policy Enforcement and Overrides
Given active business policies define hold duration limits, deposit requirements, service durations, and minimum appointment lengths When a user initiates Split or Hold Then the assistant applies those policies to compute durations, expirations, and required deposits And attempts to create a hold exceeding the maximum allowed duration or below the minimum deposit are prevented with a clear error And authorized roles can override a policy value only after providing an override reason; the override is logged with user, reason, old/new values, and the policy version And the resulting booking or hold remains compliant with capacity, buffers, and service rules
SMS Document Capture & Auto-Verification
"As a client, I want to upload my pet’s documents from my phone via a text link so that I can quickly get my pets cleared without logging into an app."
Description

Send secure, branded short links via SMS that open a mobile-friendly flow for clients to upload vaccination proof, complete waivers, and confirm flea/tick treatments. Support camera/photo library, PDF upload, and e-signature. Use OCR to extract pet name, vaccine type, and expiration dates; validate against policy rules; and auto-update the pet profile on success. Low-confidence or mismatched submissions are queued for manual review. Provide confirmation texts, encrypted storage, retention controls, and a full audit trail for who submitted what and when.

Acceptance Criteria
Secure Branded SMS Link Delivery
Given a staff member requests compliance documents for one or more pets from the client in an existing SMS thread When the request is sent Then the client receives an SMS in the same thread containing a unique, single-use HTTPS short link under the business’s configured branded domain And the link is tied to the client and target pet(s) and expires per admin-configured TTL And reopening an already-used or expired link displays an error page with instructions to request a new link And all link creations and opens are logged with timestamp and client identifier
Mobile Capture: Camera, Library, PDF, and E‑Signature
Given the client opens the link on a mobile device (iOS/Android) or desktop When the client uploads documents Then the client can capture via camera, select from photo library, or upload a PDF And only supported MIME types (JPEG, PNG, PDF) are accepted; unsupported files are rejected with an inline error And the client can submit multiple pages/images in one session And the flow supports e‑signature for waivers (typed or drawn) and a checkbox to confirm flea/tick treatment And the UI is responsive and completes submission within three steps: select docs, review/annotate/sign, submit And successful submission returns a confirmation screen
OCR Extraction and Policy Validation
Given a document is submitted When OCR processing runs Then the system extracts pet name, vaccine type, and expiration date with confidence scores per field And vaccine names are normalized using the policy’s synonym list And the submission is matched to the intended pet using extracted name and the request context And validation rules verify required vaccines are present and unexpired per the business’s policy And on pass, the pet’s profile is auto‑updated with vaccine records and expiration dates and compliance status is set to Compliant And the system records the extracted fields, confidence scores, and validation outcome
Low‑Confidence or Mismatch Routed to Manual Review
Given OCR completes When any required field confidence falls below the admin‑configured threshold or the extracted pet does not match the intended pet Then the submission is not auto‑applied and is queued for manual review with reason codes And staff are alerted in the review queue with the original files, extracted values, and confidence scores And the client receives an SMS stating the documents were received and are pending review And the pet’s compliance status remains Unverified until staff approval
Multi‑Pet Household Mapping and Booking Options
Given a client has multiple pets tied to the same SMS thread and an upcoming booking When the client submits documents via a single link Then the flow allows selecting which pet each document applies to or auto‑assigns based on OCR name match And after processing, the system marks compliant pets as Cleared and highlights pets still needing updates And if not all pets are compliant, the client is offered SMS reply options to Split the booking (for cleared pets only) or Hold the entire booking And the booking state updates according to the client’s choice and is reflected on the scheduler
Client Notifications and Receipts
Given a submission is processed When auto‑verification passes Then the client receives a confirmation SMS summarizing accepted vaccine(s) and expiration date(s) per pet and a reference ID And when the submission is queued for review, the client receives a pending review SMS with next steps And when staff reject a submission, the client receives an SMS with the rejection reason and a new upload link And all outbound SMS notifications are logged to the client thread
Security, Retention, and Audit Trail
Given any document is uploaded or processed When stored or transmitted Then files and data are encrypted in transit (TLS) and at rest using industry‑standard encryption And access is restricted to authorized users and logged And retention controls allow admins to set a retention period and purge schedule for stored documents And purged documents are permanently deleted per schedule while preserving a non‑sensitive audit record And the audit trail records submitter (phone), pet, document type, timestamps (sent, opened, submitted, processed), processing outcome, approver (if any), and any changes to pet compliance And the audit trail is immutable and exportable as CSV/JSON
Multi-Pet Waitlist Safeguards
"As an operator, I want the waitlist to respect multi-pet holds and splits so that I don’t accidentally double-book or lose revenue from protected slots."
Description

Integrate PetPack Sync with the Smart Waitlist to avoid availability conflicts. When a hold is placed for a multi-pet booking due to non-compliance, protect the slot from being auto-filled. If the booking is split, automatically release only the surplus capacity to the waitlist. Enforce guardrails that prevent partial-household overbooking, ensure deposit logic stays consistent, and emit analytics on reclaimed time and conversion outcomes.

Acceptance Criteria
Hold Protects Slot From Auto-Fill
Given a multi-pet booking with one or more non-compliant pets and a hold is initiated via PetPack Sync When the Smart Waitlist engine evaluates candidates for the overlapping resource and time window Then the booking’s time blocks are excluded from auto-fill selection while hold_status = active And attempts to auto-fill for that window are logged with reason_code = 'held_non_compliance' And zero new appointments are created for that resource/time during the active hold And the hold TTL and expires_at timestamp are persisted and visible via API GET /holds/{id}
Split Booking Releases Surplus Capacity Only
Given an original booking for N pets occupying total_capacity_minutes C and a split retains k compliant pets (k < N) When the split is confirmed Then only (N - k) pets’ capacity (C_released) is marked releasable to the waitlist And the retained k pets keep their original service blocks unchanged And the waitlist engine may auto-fill up to C_released minutes only, without overlapping the retained blocks And audit log entry records C_total, C_released, pets_retained, pets_released
Partial-Household Overbooking Guardrail
Given a household with an active booking for k pets and unresolved non-compliance for m additional pets When a user (auto-fill or staff) attempts to add any additional same-household pet into a time window overlapping the retained blocks Then the system rejects the action with error_code = 'household_partial_overbook' and HTTP 409 And no additional capacity is consumed beyond the previously released amount And the conflict check is enforced identically via UI, SMS flow, and API
Deposit Consistency on Hold and Split
Given a deposit policy of P per pet and an original booking for N pets with deposit_paid_amount D When a hold is placed due to non-compliance Then no additional deposit is captured automatically and the deposit requirement remains attached to the booking When the booking is split to retain k pets Then required_deposit = k * P And if D > required_deposit, a refund/credit for (D - required_deposit) is queued with reason 'split_adjustment' And if D < required_deposit, an SMS deposit link for (required_deposit - D) is sent and booking remains pending until paid And deposits for any waitlist auto-fill client are collected independently and are not drawn from the original household’s deposit
Analytics for Reclaimed Time and Conversion Outcomes
Given analytics is enabled When a hold is created, split, released, auto-filled, or expires Then events are emitted: 'mp_hold_created', 'mp_split_released', 'mp_reclaim_converted', 'mp_reclaim_expired' And each event includes booking_id, household_id, pets_total, pets_retained, pets_released, capacity_minutes_released, timestamp, resource_id And conversion metrics include time_to_fill_minutes and deposit_collected for the replacement booking And events are queryable in the analytics store within 5 minutes of emission
Hold Expiry Safe Release
Given a hold with expires_at set and no compliance resolution by expiry When the hold expires Then the held capacity not retained by any split is released to the waitlist within 60 seconds And the waitlist engine includes the released blocks in the next auto-fill cycle And the client is notified via SMS with a templated 'hold_expired' message And any unused deposit authorizations are voided and logged with reason 'hold_expired'
Proactive Compliance Reminders
"As a dog walker, I want automatic reminders to nudge clients about expiring vaccines so that appointments aren’t cancelled at the last minute."
Description

Automatically detect upcoming or missing compliance items per pet and schedule a templated SMS cadence from the existing thread. Messages include dynamic tokens, links to document capture, and options to snooze or opt out. Escalate reminder tone as the appointment approaches, respect quiet hours and regional messaging rules, and surface an at-risk bookings view in the dashboard so staff can intervene early.

Acceptance Criteria
Auto-Detect Compliance Gaps and Schedule Cadence
Given an upcoming appointment for a household with one or more pets and a detection window configured to 30 days before the appointment And compliance rules are defined per species/service and pet When the appointment is created or updated, or a pet’s compliance record changes Then within 10 minutes the system evaluates each pet and identifies any missing or expiring items due before the appointment And creates a reminder cadence per pet–item–appointment with the first send scheduled in the next allowed window And associates each cadence with the existing client SMS thread for the household And prevents duplicate cadences for the same pet–item–appointment And marks the booking as At Risk if any required item is outstanding and logs the detection action
Template Personalization and Token Resolution
Given an active reminder template supporting tokens {client_first_name}, {pet_name}, {item_name}, {appointment_date}, {due_date}, {doc_link}, {deposit_link} When a reminder is generated for a pet–item–appointment Then all tokens resolve with accurate values for that pet and appointment And {doc_link} deep-links to document capture for the specific pet and item, is unique, and is trackable And {deposit_link} is included when a deposit is required by policy And links are shortened and click-tracked without altering destination parameters And if any required token lacks a value, the system applies a configured fallback or blocks send with a clear error and audit log And the final rendered SMS remains ≤320 GSM-7 characters or is segmented with proper concatenation metadata; content is unchanged by segmentation And the preview in the dashboard matches the exact outbound content
Snooze and Opt-Out Handling
Given a client receives a compliance reminder in the existing thread When they reply SNOOZE or tap a snooze link Then the cadence for that pet–item is paused for the configured duration (default 3 days) and rescheduled in the next allowed window And the dashboard shows a Snoozed badge with resume timestamp When they reply STOP/UNSUBSCRIBE or tap an opt-out link Then all compliance reminder cadences for that client thread are terminated while transactional booking messages remain enabled And an opt-out confirmation is sent per regional policy and the consent change is recorded with timestamp and source And staff can manually re-enable reminders with explicit confirmation and reason logging
Escalation Tone and Timing
Given an active reminder cadence for an appointment on date D When the timeline reaches D-14, D-7, D-3, and D-1 thresholds Then the next reminder uses the escalated template tier (informational, reminder, urgent, final) respectively And if the compliance item becomes completed, all remaining messages in that cadence are canceled within 5 minutes And escalation advances at most one tier per send and never repeats a tier already sent for that cadence And the final reminder is scheduled no later than 12 hours before the appointment, adjusted to the nearest allowed send window
Quiet Hours and Regional Messaging Compliance
Given account quiet hours are configured and each recipient’s timezone is determined (saved address preferred, area code fallback) When a scheduled send falls inside quiet hours or a local restricted window Then the send is deferred to the earliest allowed time and recorded as Deferred with the applied rule And messages include required compliance language and opt-out instructions for the recipient’s region and are sent via a compliant sender ID where applicable And no more than the configured max compliance reminders (default 2) are sent per recipient per day And audit logs capture timezone used, restriction applied, and final send timestamp
At-Risk Bookings Dashboard
Given staff open the At-Risk view for a selected date range Then bookings with any outstanding compliance items are listed with client, affected pets, item(s), due date, appointment date, risk level, last message, next scheduled send, and snooze/opt-out flags And results are filterable by date range, service, risk level, and staff, and sortable by appointment date and risk level And selecting a booking shows a timeline of detection events, messages, clicks, uploads, and staff actions, with buttons to Split, Hold, Send Now, or Dismiss risk And counts and totals match underlying data and the view reflects changes within 2 minutes of state updates And exporting to CSV includes the displayed fields and applied filters
Single Thread Multi-Pet Messaging and Split/Hold Offers
Given a household has multiple pets with mixed compliance status for an upcoming appointment When reminders are sent Then messages are sent in the existing client thread and clearly identify the pet name and item for each outstanding requirement And the message includes actionable options to SPLIT or HOLD the booking When the client selects SPLIT Then compliant pets remain on the original appointment, non-compliant pets are moved to a draft hold or waitlist per settings, no double-booking occurs, and confirmations are sent When the client selects HOLD Then the entire booking is set to a Hold status with next steps and deposit rules applied as configured And all changes are synchronized to the calendar and are reversible by staff with audit logging

Thread Handoff

Seamlessly reassign a job without breaking the client conversation. When you move a booking to another contractor, the masked SMS thread, pet notes, and schedule context transfer instantly, while the previous contractor’s access is revoked. Clients keep seeing one consistent number and you avoid lost context, awkward reintroductions, and privacy leaks.

Requirements

Atomic Thread Handoff Engine
"As a scheduler, I want to reassign a booking in one action so that the client’s conversation and all job context move seamlessly to the new contractor."
Description

Provide a transactional service to reassign a booking from one contractor to another without breaking the client’s masked SMS conversation. The engine transfers conversation context pointers, pet notes, job checklist, arrival window, and reminder schedules in an all-or-nothing operation, preserving the existing masked number (DID) for continuity. It updates ownership, message routing, and dependencies atomically with idempotency and concurrency controls, integrating with SMS gateway, calendar, CRM, and billing subsystems. Expected outcome: the handoff completes in seconds with no lost messages, no duplicate notifications, and consistent state across services.

Acceptance Criteria
Preserve Masked Number and Seamless Routing
Given a booking assigned to Contractor A with an active masked DID X and an existing SMS thread with the client And at least one client message has been received in the last 24 hours When the booking is reassigned to Contractor B via the handoff API Then the masked DID for the client remains X And inbound client SMS after the handoff route to Contractor B within 2 seconds And outbound messages sent by Contractor B appear from DID X And Contractor A cannot send or receive messages on DID X for this booking And Contractor B can view at least the last 100 prior messages in the thread (or all if fewer)
Atomic Cross-Service Commit and Rollback
Given a booking with linked CRM contact, calendar event, reminder schedule, job checklist, and pending billing item And a simulated failure is injected in the billing subsystem after the calendar update succeeds When the handoff from Contractor A to Contractor B is initiated Then the operation aborts and all intermediate updates are rolled back And the booking remains owned by Contractor A And DID routing remains to Contractor A And no reminders are rescheduled, no checklists are transferred, and no client notifications are sent And an audit log entry records the failure with a correlation ID and compensating actions taken
Idempotent Handoff API
Given a valid handoff request with idempotency key K to reassign booking BKG-123 from Contractor A to Contractor B When the same request is submitted three times within 60 seconds Then exactly one handoff is executed And each request returns HTTP 200 with the same handoffId and final state And no duplicate reminders, calendar events, or billing updates are created And message routing reflects the single successful handoff to Contractor B
Concurrent Handoff Conflict Resolution
Given booking BKG-456 is owned by Contractor A And Admin 1 attempts handoff to Contractor B while Admin 2 attempts handoff to Contractor C within 200 ms of each other When both handoff requests are processed Then exactly one request succeeds and the other fails with HTTP 409 Conflict including the winning owner id And the final owner is consistent across SMS routing, calendar, CRM, and billing And only one audit trail entry marks a successful handoff And the client receives no duplicate or conflicting notifications
Transfer of Operational Context
Given a booking with job checklist items, pet notes, an arrival window, and scheduled reminders assigned to Contractor A When the booking is reassigned to Contractor B Then the job checklist, pet notes, arrival window, and reminder schedule are associated with Contractor B And scheduled reminders retain their original send times and templates And Contractor A's access to the checklist, pet notes, and thread is revoked immediately And Contractor B receives a single internal notification of the handoff
No Lost or Duplicate Messages During Handoff
Given an active SMS conversation on DID X for booking BKG-789 And two client messages are sent during the 5-second window spanning the handoff When the handoff completes Then both client messages are delivered exactly once to Contractor B And message order is preserved relative to send timestamps And Contractor A receives no copies of these messages post-handoff And no system duplicate notifications are generated for the client
Performance and SLA Compliance
Given a representative production dataset and normal load of 60 handoffs per hour When measuring end-to-end handoff duration over 100 consecutive handoffs Then 95% complete in 5 seconds or less and 99% in 10 seconds or less And the routing switch (inbound to new owner) occurs within 2 seconds at p99 after success is committed And the handoff operation maintains 99.9% success rate without operator intervention over a rolling 30 days
Immediate Access Revocation
"As an admin, I want the previous contractor’s access removed immediately after a handoff so that client data remains private and secure."
Description

Upon successful handoff, instantly revoke the previous contractor’s access to the booking, chat transcript, pet profiles, addresses, and schedules. Remove them from notifications and reminder sequences, invalidate scoped tokens, and clear cached data in apps to prevent read-after-revoke. Provide confirmation of revocation and surface errors if any permission persists. This enforces privacy, prevents data leakage, and supports compliance obligations.

Acceptance Criteria
Instant Access Revocation on Handoff Completion
Given a booking is assigned to Contractor A and an admin reassigns it to Contractor B When the handoff operation returns success Then Contractor A loses authorization to view or modify the booking, chat transcript, pet profiles, client addresses, and schedules within 2 seconds And any subsequent API calls by Contractor A to these resources return 401/403 with code "access_revoked" And the revocation timestamp and affected scopes are recorded for auditing
Token and Session Invalidation Across Channels
Given Contractor A has active web/mobile sessions, API tokens, and websocket subscriptions for the booking When the booking is handed off to Contractor B Then all access and refresh tokens scoped to the booking for Contractor A are invalidated within 2 seconds And any active websockets/subscriptions for the booking are closed within 2 seconds And Contractor A cannot mint new access tokens for the revoked scopes
Notification and Reminder Suppression Post-Revocation
Given Contractor A is subscribed to booking-related notifications and reminder sequences When the handoff completes Then Contractor A is removed from all future notifications and reminder sequences for the booking immediately And any scheduled reminders or notifications targeting Contractor A for that booking are canceled within 5 seconds And no further events for the booking generate notifications to Contractor A
Masked SMS Routing Switch and Old Contractor Block
Given the client replies to the existing masked SMS thread after handoff When a client message arrives post-handoff Then the message is delivered only to Contractor B and is not visible to Contractor A And any attempt by Contractor A to send a message on the masked thread after handoff is rejected with 401/403 and is not delivered to the client And routing logs show the new contractor as the exclusive recipient from the revocation timestamp onward
Attachment and File URL Access Revocation
Given Contractor A previously accessed chat attachments and file URLs for the booking When the handoff completes Then all signed URLs or file links scoped to the booking are rotated/invalidated immediately And Contractor A receives 401/403 on any attempt to access prior attachments after revocation And new attachment URLs require authorization that maps to Contractor B
Device Cache Purge and Read-After-Revoke Prevention
Given Contractor A's device has cached chat snippets, pet notes, and addresses for the booking When the handoff completes Then a silent push instructs the device to purge cached data for the booking within 10 seconds And opening the booking or chat on Contractor A's app after revocation displays no data and an "Access revoked" state And the booking is removed from local search indices and recent lists on Contractor A's app within 30 seconds
Admin Confirmation, Error Surfacing, and Audit Log
Given an admin initiates a handoff When revocation processing completes Then the admin sees a confirmation summarizing scopes revoked, tokens invalidated, and subscriptions removed And if any revocation step fails, the admin sees a surfaced error listing failing resources, with automatic retries scheduled and a "Retry now" control And an immutable audit log entry is created with actor, previous contractor, new contractor, booking ID, scopes, timestamps, and outcomes
Client Number Continuity Routing
"As a client, I want to keep texting the same number so that my conversation continues smoothly without learning a new contact."
Description

Maintain the same outbound and inbound masked phone number for the client before and after handoff. Update routing so that all new and queued messages from the client are delivered to the newly assigned contractor while keeping the contractor’s personal number hidden. Ensure compatibility with SMS/MMS, deliverability checks, sender ID policies, and retry logic. Handle messages in-flight during the transition with ordered delivery and no duplicates.

Acceptance Criteria
Inbound Routing to New Contractor Post-Handoff
Given a client thread uses masked number X with Contractor A And the booking is reassigned to Contractor B at timestamp T When the client sends an SMS or MMS to X after T Then the message appears in Contractor B’s inbox within 5 seconds And the message does not appear in Contractor A’s inbox or notifications And the client sees the same sender number X And the message activity log records "Routed to Contractor B" with correlation ID
Outbound Continuity and Masked Number Preservation
Given a thread with masked number X was reassigned from Contractor A to Contractor B at T And there exist scheduled outbound messages created before T When those scheduled messages send after T Then they are sent from number X And they are visible only in Contractor B’s outbox and thread And Contractor A cannot send new messages to this thread (attempts return an error) And no personal phone numbers of Contractor A or B are exposed in any message metadata
Handoff Race Conditions: Ordered Delivery and De-duplication
Given messages M1 and M2 are received within ±10 seconds of the handoff time T When the system processes M1 and M2 Then they are appended to the thread in carrier timestamp order And each inbound message is delivered exactly once to Contractor B And no duplicate entries exist in the thread or notifications And the event log shows a single delivery record per carrier message ID
Queue Rebinding at Handoff Cutover
Given there are queued or retrying messages associated with the thread at T When the handoff to Contractor B completes Then all queued inbound messages are routed to Contractor B And all queued/scheduled outbound messages are owned by Contractor B And their statuses continue from prior state without reset And no queued messages are lost or sent to Contractor A
MMS Compatibility and Attachment Transfer
Given the client sends an MMS with one or more attachments to number X after T And the thread was reassigned from Contractor A to Contractor B at or before T When the MMS is processed Then all attachments and captions are accessible to Contractor B And Contractor A cannot download or preview the attachments And media links remain valid to Contractor B for at least 24 hours And the masked number X remains unchanged in the client’s view
Access Revocation and Privacy Safeguards
Given the thread was reassigned from Contractor A to Contractor B at T When Contractor A attempts to view, search, reply to, or export the thread after T Then the system blocks access with 403/Unauthorized and no data leakage And Contractor A receives no further notifications about the thread And neither party can see the other’s personal phone number at any time And an audit entry records the revocation with actor, timestamp, and thread ID
Deliverability, Sender ID Compliance, and Retries
Given outbound messages to the client are sent from masked number X after T When a carrier rejects a message due to throttling, spam filtering, or sender ID policy Then the system retries up to 3 times over 10 minutes with exponential backoff And the sender number X is not changed during retries And the final status (Delivered or Failed) is recorded and visible to Contractor B And a pre-send deliverability check validates sender ID and media support; if unsupported, the message is downgraded per policy or blocked, and the decision is logged
Calendar, Reminders, and Smart Waitlist Reassignment
"As a dispatcher, I want calendars, reminders, and waitlist suggestions to update automatically on handoff so that the schedule stays accurate without manual cleanup."
Description

Automatically transfer booking ownership to the new contractor’s calendar, adjust travel buffers, and recalculate ETAs while preserving the original appointment time when possible. Rebind reminder sequences (confirmations, prep, and follow-ups) to the new contractor and suppress duplicates. If the target contractor is unavailable, surface Smart Waitlist candidates ranked by proximity and service fit, and place a temporary hold on the selected slot to prevent double-booking.

Acceptance Criteria
Ownership Transfer While Preserving Original Time
Given an existing booking with start time T assigned to Contractor A And Contractor B has no blocking events at T When the booking is reassigned to Contractor B Then the booking appears on Contractor B’s calendar within 5 seconds with start time T And the booking is removed from Contractor A’s calendar within 5 seconds And the appointment ID and client thread ID remain unchanged And travel buffer and ETA are recalculated using Contractor B’s most recent job location or home base per routing settings
Time Preservation Fallback and Admin Confirmation
Given Contractor B has a conflict at start time T When reassignment is attempted Then the system suggests the nearest available slot within ±60 minutes of T ranked by route efficiency And it displays the impact on ETA and travel buffer for each suggested slot And it requires explicit admin confirmation before changing the appointment time And if no suitable slot exists within ±60 minutes, the reassignment is blocked with a clear error message and no changes are made
Reminder Sequences Rebinding and Deduplication
Given confirmation, prep, and follow-up reminders are scheduled for the booking under Contractor A When the booking is reassigned to Contractor B Then all future reminders are rebound to Contractor B’s sender profile and schedule rules And no duplicate messages are sent for any reminder trigger And reminders maintain their relative offsets to the appointment time And previously sent messages remain in the client thread history unchanged And reminder send windows respect Contractor B’s quiet hours and working hours
Smart Waitlist Ranking and Display
Given the target contractor is unavailable at time T When Suggest Waitlist is invoked Then the system lists eligible candidates (minimum 3 if available) ranked by proximity (50%), service fit (30%), and schedule capacity (20%) And each candidate entry shows estimated arrival time, incremental travel time, and service eligibility notes And ineligible candidates (skill/service mismatch or outside service area) are excluded from the list
Temporary Hold and Double-Booking Prevention
Given an admin selects a waitlist candidate for the booking at slot T When a temporary hold is placed Then the selected contractor’s calendar shows a tentative hold within 5 seconds And other users are prevented from creating bookings that overlap the held slot for that contractor And the hold auto-expires after 15 minutes if not confirmed, releasing the slot And upon confirmation, the hold converts to a firm booking and any other tentative holds for the booking are released
Calendar Consistency and Data Integrity Post-Reassignment
Given a booking is reassigned between contractors When the operation completes Then no duplicate or orphaned calendar entries exist for either contractor And the client-facing local time remains unchanged unless a new time was explicitly confirmed And the booking appears in all relevant calendar views and filters for Contractor B And cached availability for both contractors is refreshed within 30 seconds
Payment and Deposit Rebinding
"As a business owner, I want deposits and payouts to follow the job after a handoff so that money flows correctly without recharging the client."
Description

Move the booking’s payment intent, deposit link, and payout destination to the new contractor while preserving invoice numbers, taxes, and ledger continuity. Validate the new contractor’s payout account, reauthorize or transfer holds when required, and prevent double charges or orphaned deposits. Update receipts and client-facing payment links without changing the client’s experience. Provide safeguards for partial refunds and adjustments if pricing differs by contractor.

Acceptance Criteria
Successful Rebinding With Existing Authorized Deposit Hold
Given a booking with an active, uncaptured authorization and a previously sent deposit link, and the new contractor has a verified payout account When an admin reassigns the booking and confirms payment rebinding Then the booking’s invoice number remains unchanged And the tax rate and jurisdiction remain unchanged And the payout destination updates to the new contractor’s account And the authorization is transferred or voided-and-reauthorized without creating duplicate holds (exactly one active authorization remains) And the previously sent client deposit link continues to work and pays into the new contractor’s account And the previous contractor loses access to the booking’s payment objects and refund/capture actions (API/UI return 403) And an audit log entry is recorded with timestamp, actor, old contractor, new contractor, and authorization handling method And no duplicate charges or orphaned deposit records are created
Rebinding Blocked for Unverified Payout Account
Given the new contractor has no verified payout destination on file When an admin attempts to reassign and rebind payment Then the operation is blocked with a clear error explaining verification is required And the booking’s payment intent, invoice number, taxes, links, and payout destination remain unchanged And the client is not notified and no new links are sent And a failure audit log entry is recorded with reason "Unverified payout account" And the system status reflects that rebinding was not applied
Price Increase After Rebind: Additional Charge
Given the booking price increases from X to Y (>X) after reassignment, an original deposit or charge exists, and the client has a valid payment method on file When the payment is rebound to the new contractor Then the system calculates the delta (Y−X) and associated tax using the original tax rules And it attempts to collect the delta via the existing authorization or by charging the saved card within 2 minutes And if automatic collection fails, a new client SMS link is sent from the same number requesting payment of the delta And the invoice number remains unchanged and the receipt shows the new contractor’s name And ledger entries include a linked "Adjustment" line for the delta, preserving continuity And no duplicate base charges are created
Price Decrease After Rebind: Partial Refund
Given the booking price decreases from X to Y (<X) after reassignment and a charge for X (or deposit D) has been captured When the payment is rebound to the new contractor Then the system initiates a partial refund for the delta (X−Y) and associated tax to the client’s original payment method within 60 seconds And the refund is issued from the appropriate account while preserving client transparency (no unexpected card descriptors) And the invoice number remains unchanged and the receipt reflects the new contractor’s name and adjusted totals And ledger entries show a linked "Partial Refund" with reference to the original charge And the new contractor’s payout reflects the adjusted net amount And no cross-account refund failures or duplicate refunds occur
Concurrency Control During Rebind and Capture
Given the previous contractor triggers capture within 2 seconds of an admin initiating rebind When the system processes both operations Then exactly one capture is completed for the booking And the capture is attributed to the correct contractor per the final assignment And any competing capture is voided or rejected without charging the client twice And the booking and ledger end in a consistent state with a single captured charge And an audit log records the race resolution with idempotency keys
Client Link and Receipt Continuity After Rebind
Given the client has previously received payment links and has an active SMS conversation thread When payment is rebound to the new contractor Then all previously sent payment links remain valid and resolve to the updated payment endpoint without changing the visible URL domain or SMS number And new links generated post-rebind inherit the same short-link format and booking ID And client receipts show the new contractor’s legal name and payout descriptor while retaining the same invoice number And no client-facing 404s, broken links, or duplicate receipts occur
Ledger Continuity and Reporting Accuracy
Given a booking has pre-rebind financial entries and reporting exports enabled When the payment is rebound to the new contractor Then the general ledger remains continuous with a single invoice number and linked adjustment/refund entries as applicable And finance exports for the affected date range include exactly one invoice row for the booking plus any linked adjustment/refund rows And tax reports preserve the original tax jurisdiction and recompute amounts consistently with any price changes And reconciliation reports attribute future payouts to the new contractor and remove them from the previous contractor for this booking And no orphaned deposit entries remain in the previous contractor’s ledger
Client-Friendly Handoff Messaging
"As a client, I want a simple confirmation that my appointment is still set so that I feel confident nothing changed on my end."
Description

Send an optional, neutral SMS to the client confirming their appointment remains intact after reassignment, without exposing contractor identities or internal details. Offer branded templates, merge fields (date, window, pet name), scheduling (immediate or next quiet window), and opt-out compliance. Ensure exactly one confirmation is sent and that it threads within the existing conversation for a seamless experience.

Acceptance Criteria
Single Confirmation on Reassignment (Idempotent)
Given a booking is reassigned from Contractor A to Contractor B with handoff ID H123 When the handoff message job is processed Then exactly one confirmation SMS is enqueued and sent for H123 And any duplicate processing of H123 or message delivery retries do not create additional sends And the booking thread is marked with confirmation_sent=true for H123 within 5 seconds of enqueue And the send record includes the booking ID, handoff ID, and thread ID for audit
Thread Continuity via Masked Number
Given the client has an existing masked SMS thread When the confirmation is sent Then the From number equals the thread’s masked number And the message appears in the same client conversation thread And no contractor or personal numbers are used or revealed And the message delivery receipt is associated to the same thread ID
Neutral Content and Privacy Guardrails
Given the confirmation template is rendered When inspecting the outbound payload Then it does not include contractor names, phone numbers, email addresses, or internal notes And allowed merge fields are limited to {business_name, appointment_date, time_window, pet_name} And phrasing avoids identifying staff (e.g., no 'John', 'walker', 'groomer' names) And the message passes automated PII/redaction checks with 0 violations
Template and Merge Field Accuracy
Given a branded template with merge fields is selected And the booking has appointment_date=2025-08-18 15:00, time_window=3–5 PM, pet_name=Rex, client_tz=America/Los_Angeles When the message is rendered Then merge fields are substituted correctly using client_tz and locale formatting (e.g., Aug 18, 3–5 PM) And missing optional fields are omitted without leftover tokens or broken grammar And the final character count does not exceed 320 GSM-7 characters (or is segmented correctly) And the rendered preview exactly matches the sent content snapshot
Send Timing: Immediate vs Next Quiet Window
Given the sender selects Immediate timing When the reassignment is saved Then the confirmation is enqueued within 10 seconds Given the sender selects Next Quiet Window And the client thread has no inbound messages for ≥15 minutes and local time is within 8am–8pm When conditions are met Then the confirmation is sent within 2 minutes And if conditions are not met within 2 hours, the message is sent at the next 8am local time after achieving 15 minutes inactivity
Opt-out Compliance and Suppression
Given the client number is in an opted-out state (STOP received or on a suppression list) When a reassignment occurs Then no confirmation is sent or queued And the suppression reason is logged on the booking timeline Given the client has opted in again (START) When a subsequent reassignment occurs Then normal sending rules apply And STOP/HELP/START keywords are respected on replies to the confirmation
Optional Messaging Toggle Respected
Given the account or booking has 'Send handoff confirmation' set to Off When a reassignment is completed Then no confirmation is sent or scheduled And no template rendering occurs Given the toggle is On When a reassignment is completed Then a confirmation is sent per idempotency, threading, content, timing, and compliance rules
Audit Trail and Rollback Safety
"As an operations manager, I want a full audit trail and the ability to roll back a handoff so that I can recover safely from mistakes without disrupting the client."
Description

Record every handoff with immutable logs capturing initiator, timestamps, old/new contractor IDs, assets transferred, notifications sent, and payment updates. Provide an admin view and export for compliance and support. Support a time-bound rollback that restores routing, access, calendar, reminders, and payouts if the new contractor declines or errors occur, with conflict resolution for any messages received during the interim.

Acceptance Criteria
Immutable Handoff Audit Log Creation
Given a booking handoff is initiated by an authorized user When the handoff completes successfully Then an immutable audit log entry is appended containing: event_id, booking_id, initiator_user_id, org_id, old_contractor_id, new_contractor_id, timestamps {initiated_at, completed_at} in UTC ISO8601, assets_transferred [thread_id, pet_note_ids, schedule_entry_ids], notifications_sent [{type, channel, recipient_role, recipient_id, sent_at}], payment_updates {deposit_state, payout_route_before, payout_route_after} And Then the log entry is write-once; update/delete operations are rejected with 405 and recorded as separate audit events And Then the entry includes an integrity checksum that validates on read; checksum mismatch raises a tamper_suspected alert and blocks export Given a handoff attempt fails When the failure is detected Then a failed audit event is appended with error_code, error_message, and zero assets_transferred
Admin Audit View and Export
Given an Admin opens the Audit Trail and applies filters by date range, booking_id, or contractor_id (any combination) When results are requested for up to 10,000 matching records Then the list renders within 2 seconds and displays all required fields per audit schema And When the Admin selects Export CSV or JSON for the current filter Then the file is generated within 10 seconds and contains exactly the filtered records with all fields, using UTC ISO8601 timestamps And Then only users with Admin role can access the Audit Trail and export; non-admin requests return 403 and are logged as access_denied audit events And Then exported files include a header with schema version and a file checksum field
Time-Bound Rollback on Decline or Manual Request
Given a booking has been handed off and the new contractor declines within the configured rollback window When the decline is received Then rollback executes automatically and restores message routing, access rights, calendar ownership, scheduled reminders, and payout routing to the previous contractor within 30 seconds And Then an audit event handoff_rolled_back is appended with reason=declined_by_new_contractor and references the original handoff event_id Given an Admin submits a manual rollback within the configured rollback window When the Admin confirms the rollback Then the same restoration occurs within 30 seconds and is logged with reason=manual_request Given a rollback request occurs after the configured window When the request is submitted Then the system rejects it with error code ROLLBACK_WINDOW_EXPIRED and makes no state changes
Rollback Conflict Resolution for Interim Messages
Given client or contractor messages are exchanged between handoff completion and rollback When rollback completes Then all interim messages persist in the single masked thread in chronological order; no messages are lost or duplicated And Then the previous contractor regains visibility to the full thread including interim messages; the new contractor’s access is revoked And Then messages sent by the new contractor remain attributed to them and visible to the previous contractor And Then unread counts and notifications are recalculated for the previous contractor, who is notified of the number of unread client messages And Then an audit event records interim_message_count and message_ids affected
Access Revocation and Privacy Preservation
Given a handoff completes When access changes are applied Then the previous contractor’s access to the booking, masked SMS thread, pet notes, and calendar entries is revoked within 10 seconds, and the new contractor receives equivalent access within 10 seconds And Then the client-facing phone number remains unchanged throughout handoff and rollback; no reintroduction SMS is auto-sent to the client Given a rollback completes When access changes are applied Then the new contractor’s access is revoked within 10 seconds and the previous contractor’s access is reinstated within 10 seconds And Given a revoked user attempts to access any related resource via UI or API When the request is made Then the system returns 403, logs the attempt as access_denied with resource identifiers, and does not expose PII (real phone numbers or emails) in the response
Payment Update and Reversal on Rollback
Given a booking with a deposit and future payout is handed off When the handoff completes Then payout routing is updated to the new contractor for that booking, deposit ownership is updated per policy, and both changes are recorded in the audit log with before/after values Given a rollback occurs within the configured window When rollback executes Then payout routing and any deposit ownership are reverted to the previous contractor; if a client payment was captured during the interim, payout is routed according to business rules to the correct contractor, and ledger entries reflect transfer and reversal with no double charge And Then payment-related changes are idempotent; replaying the same rollback request produces no additional ledger entries And Then payment provider webhooks received during the interim are reconciled to the correct contractor after rollback and logged

Window Guard

Lock crew access to only the time blocks they’re assigned. Outside the shift window, messages auto‑reply with your branded policy, and sensitive fields (address, contact, payments) stay hidden. You set rules for after‑hours, late arrivals, and grace periods—protecting boundaries, minimizing off‑schedule pings, and reducing errors.

Requirements

Shift Window Access Control
"As an owner, I want crew access automatically limited to their assigned time windows so that boundaries are enforced and off-schedule work and errors are reduced."
Description

Enforce time-window–based access so crew members can only view and act on jobs during the blocks they are assigned. Outside their shift window, job details, action buttons (check-in/out, charge, reschedule), and navigation links are disabled. The control applies consistently across mobile/desktop apps and APIs, with real-time checks that respect user and client time zones, daylight saving changes, and offline caching rules for the next upcoming shift. Integrates with PawPilot Scheduling to derive assignments and with Messaging to block out-of-window initiations. Expected outcome: fewer off-schedule interactions, reduced errors, and tighter operational boundaries.

Acceptance Criteria
In-Window Access Enablement and Outside-Window Blocking
Given a crew member is assigned a shift from 09:00 to 13:00 in the shift’s timezone, and current time is within that window, When they open an assigned job on mobile or desktop, Then address, contact, service notes, and route/map links are visible, and action buttons (Check-In, Check-Out, Charge, Reschedule) are enabled. Given the same assignment and current time is outside that window (excluding configured grace periods), When the job is opened, Then address and contact are masked, route/map links are disabled, and action buttons are disabled and labeled with reason “Outside shift window”. Then every allow/deny decision is logged with userId, jobId, windowState, evaluatedAt (ISO-8601), platform (mobile/desktop), and decisionSource (real-time/cache).
Time Zone and DST Compliance
Given the crew’s device timezone differs from the shift’s timezone in Scheduling, When access is evaluated, Then the decision uses the shift’s IANA timezone and not the device timezone. Given a DST start (spring-forward) occurs during a shift window in America/New_York on 2025-03-09, When evaluating a 01:30–03:30 shift, Then access is allowed from 01:30 to 03:30 civil time with the skipped hour handled correctly, and no duplicate overlapping access is granted. Given a DST end (fall-back) occurs during a shift window in America/New_York on 2025-11-02, When evaluating a 01:30–02:30 shift, Then access remains a continuous one-hour window and is not counted twice. Then the UI displays both the crew’s local time and the shift time labels wherever the window state is shown.
Offline Caching for Next Upcoming Shift
Given the device has synced within the last 24 hours and goes offline, When the crew has exactly one upcoming assigned shift, Then only that shift’s jobs are cached with full details; other shifts’ jobs have sensitive fields removed. Given the device is offline and current time is outside the assigned window (and pre-start grace), When the crew opens a cached job, Then sensitive fields remain masked and all actions are disabled. Given the device is offline and current time is within the assigned window or within post-end grace, When the crew attempts Check-In/Check-Out, Then the actions are queued locally with timestamps; on reconnect, If the window/grace is still valid, Then the actions are committed; Else they are rejected with error OUT_OF_WINDOW and the user is notified. Then cached shift data is purged automatically at shiftEnd + postEndGrace.
Messaging Auto-Reply and Initiation Block
Given it is outside a crew member’s shift window (respecting configured pre-start/post-end grace), When a client sends an SMS that would route to that crew, Then an auto-reply is sent within 60 seconds using the configured branded policy template and is logged with messageId and policyId. Given it is outside the window, When the crew attempts to start or reply to a client message, Then the send action is blocked, a policy banner is shown, and the attempt is logged with reason OUT_OF_WINDOW. Given it is inside the window, When client or crew messages are exchanged, Then no auto-reply is sent and no blocks occur.
API Enforcement for Crew Context
Given a crew-scoped API token calls GET /jobs/{id} outside the assigned window (respecting grace), When the response is returned, Then the HTTP status is 200, sensitive fields (address, contact, payment methods) are masked/omitted, and header X-Window-State: out-of-window is present. Given the same context and a crew-scoped token calls POST /jobs/{id}/check-in, /check-out, /charge, or /reschedule outside the window, Then the HTTP status is 403 with error.code = SHIFTOUT_WINDOW and header X-Window-Ends-At set to the next start timestamp (ISO-8601). Given current time is within the window, When the same endpoints are called, Then they succeed (2xx), include full fields, and header X-Window-State: in-window is present.
Grace Periods and Late Arrivals
Given admin configures PreStartGrace=10m and PostEndGrace=15m, When access is evaluated, Then within PreStartGrace: job details and navigation links are visible, but Check-In/Charge/Reschedule are disabled; within PostEndGrace: Check-Out and Charge are enabled only if a prior Check-In exists; Reschedule remains disabled. Given admin sets LateArrivalThreshold=30m with Policy=RequireNote, When a crew member checks in after scheduledStart + 30m but before scheduledEnd, Then the app requires a reason note and captures GPS and timestamp; without a note, check-in is blocked. Given admin sets LateArrivalPolicy=HardBlock beyond 60m, When a crew member attempts to check in after scheduledStart + 60m, Then check-in is rejected with message “Late arrival exceeds policy” and the event is logged.
Auto-Reply with Branded Policy
"As a business owner, I want automatic branded policy replies to off-window messages so that expectations are clear and manual follow-ups are minimized."
Description

Automatically respond to out-of-window crew messages with a configurable, branded policy message. Templates support variables (crew name, client name, shift start/end, grace period), localization, and links to approved workflows (e.g., notify dispatcher or request override). The system prevents loops, respects quiet hours, and logs each auto-reply. Integrates with the SMS relay, policy rules engine, and notification system to escalate urgent keywords to admins. Expected outcome: clear expectations, fewer manual follow-ups, and consistent policy enforcement.

Acceptance Criteria
Out-of-Window Message Auto-Reply
Given a crew member has an assigned shift window (start/end) and an active after-hours policy And an inbound SMS from the crew arrives outside the shift window and outside any configured grace period When the message is processed by the SMS relay Then the system sends the configured branded policy auto-reply within 5 seconds And exactly one auto-reply is sent per inbound message And the auto-reply uses the currently active template version for the crew’s team And no auto-reply is sent for messages received inside the shift window
Grace Period and Override Rules Enforcement
Given an organization has grace period rules (pre-shift and post-shift) defined in the policy rules engine And a dispatcher override can be set on a conversation When a crew message arrives within the defined grace period Then the system applies the configured rule (suppress, soft notice, or full auto-reply) and records the rule path And if a dispatcher override is active for the conversation, the policy auto-reply is suppressed And if the message arrives outside the grace period, the standard out-of-window policy applies
Template Variables, Localization, and Branding
Given a branded policy template containing variables {crew_name}, {client_name}, {shift_start}, {shift_end}, {grace_period} And an organization locale and timezone configuration exist When an auto-reply is generated Then all variables resolve with current assignment data and timezone-adjusted values And dates/times are formatted per the organization locale; if locale is missing, default to en-US And no unresolved placeholders (e.g., {shift_start}) appear in the sent SMS; safe defaults are used instead And the auto-reply includes the configured brand signature/prefix per org settings
Approved Workflow Links in Auto-Reply
Given the template includes links to approved workflows (e.g., notify dispatcher, request override) And link domain allowlist is configured When an auto-reply is sent Then all workflow links are generated using an approved domain and contain no PII in the URL And each link opens the intended workflow and returns HTTP 200 on first step And each link click is logged with conversation ID, user/crew ID (if available), and timestamp And if link generation fails, the auto-reply is still sent without the link and the omission is logged as a warning
Quiet Hours Respect
Given organization quiet hours are configured And a crew message that would normally trigger an out-of-window auto-reply is received during quiet hours When the message is processed Then no auto-reply is sent during quiet hours And the suppression reason "quiet_hours" is logged with message ID and rule path And once quiet hours end, no catch-up auto-reply is sent automatically
Auto-Reply Loop Prevention and Rate Limiting
Given the system has auto-reply loop prevention and rate-limiting enabled When multiple inbound messages that qualify for auto-reply are received within a short interval Then the system sends at most one policy auto-reply per conversation per 12-hour window And the system does not auto-reply to messages containing STOP, UNSUBSCRIBE, HELP, START per carrier rules And the system suppresses replies to messages matching known auto-responder patterns (e.g., "autoreply", "out of office") And each suppression decision is logged with reason and cooldown expiration time
Urgent Keyword Escalation and Logging
Given a list of urgent keywords is configured in the policy rules engine (e.g., emergency, injured, lost) When an inbound crew message contains any urgent keyword regardless of window status Then the system escalates to designated admins via the notification system within 10 seconds And the escalation includes conversation link, sender, message body snippet, and policy context And duplicate escalations for the same conversation are de-duplicated within a 10-minute window And every auto-reply decision (sent or suppressed) is logged with timestamp, template ID, locale, rule path, actor=system, and delivery status
Sensitive Data Redaction & Field-Level Permissions
"As an owner, I want sensitive client and payment details hidden outside shift windows so that privacy is protected and unauthorized use is prevented."
Description

Hide sensitive client fields (address, access codes, contact info, payment methods, deposit links) whenever a user is outside the allowed window or not assigned to the job. Redaction applies to UI elements, notifications, exports, and API responses, with masked placeholders and guidance on how to gain proper access. All access attempts are audited. Integrates with the RBAC module and the Window Guard rules engine. Expected outcome: stronger privacy and fewer data mishandling incidents.

Acceptance Criteria
UI Redaction Outside Window or Unassigned
Given a crew user without assignment to Job J or currently outside the allowed window (including configured grace periods) When the user opens the job details screen Then the fields address, access codes, client contact (phone/email), payment methods, and deposit links are displayed as masked placeholders labeled "Hidden — Window Guard" And a "Request access" CTA with the branded policy text is visible And the raw values are not present in the DOM, local storage, app memory, or network responses (verified via dev tools/proxy) And upon entering the allowed window and being assigned, the fields unmask on refresh within 5 seconds And P95 page render latency attributable to redaction checks is ≤ 50 ms
Notification Content Redaction and Auto‑Reply
Given a crew user outside the allowed window or not assigned to Job J When the user sends, replies to, or triggers a message about Job J Then outgoing notifications (SMS, email, push, in‑app) to that user omit or mask sensitive fields and links And an automated branded policy auto‑reply is sent to the user explaining access restrictions and how to gain access And roles with explicit override permission (e.g., Admin) receive the full content And deposit links in notifications are replaced with a non-actionable placeholder and a "Request deposit link" CTA for non‑authorized users And no sensitive content is logged in message templates, delivery logs, or analytics for restricted recipients
Exports and API Field‑Level Redaction
Given a user token belonging to a crew user outside the allowed window or not assigned to Job J When the user requests job/client data via CSV export or API endpoints (list/details) Then sensitive fields (address, access codes, contact, payment methods, deposit links) are returned as masked values or null with reason code WG‑RED‑001 And direct endpoints for sensitive objects (e.g., /clients/{id}/payment-methods, /jobs/{id}/deposit-link) return 403 with a branded policy message and correlation ID And webhooks and partner integrations receive redacted payloads under the same rules And P95 additional latency due to redaction at the API layer is ≤ 100 ms And responses include an "access_evaluation" object indicating rule source (RBAC+WindowGuard) and outcome without exposing sensitive details
Comprehensive Audit of Sensitive Access Attempts
Given any attempt to view, export, notify, or retrieve sensitive fields When the attempt is processed (granted, redacted, or blocked) Then an immutable audit record is written within 2 seconds including timestamp (UTC), user ID, role, job/client IDs, action type, channel (UI/API/Export/Notification), window status (in/out), rule IDs matched, outcome, and requester IP/user agent And audit records are tamper‑evident via hash chaining and are searchable/filterable by Admin with RBAC rights And audit exports exclude sensitive field values but include redaction/deny reasons and correlation IDs
RBAC and Window Guard Rules Interoperability
Given RBAC permissions and Window Guard rules are both enabled When evaluating access to a sensitive field for User U on Job J Then access is granted only if RBAC permits the field AND Window Guard permits based on assignment and time window And users with the "OverrideWindowGuard" permission can view after providing a reason which is recorded in audit logs And changes to assignment or window rules take effect for new evaluations within 60 seconds And concurrent reassignment events resolve deterministically so only the current assignee within window is unredacted
Payment Methods and Deposit Link Protection
Given a crew user outside the allowed window or not assigned to Job J When the user attempts to view or use a payment method or deposit link Then the UI shows masked placeholders and a "Request deposit link" flow; no tokenized URLs are rendered client‑side And direct navigation to a deposit link URL without authorization returns 403 and triggers an audit entry And valid deposit links are single‑use, role‑scoped, and expire at window end plus configured grace period (default 5 minutes) And within the allowed window for the assigned user, the link functions and all actions are logged with correlation IDs
Configurable After-Hours, Late Arrival, and Grace Rules
"As an owner, I want to configure after-hours, late-arrival, and grace-period rules so that edge cases are handled consistently without manual intervention."
Description

Provide an admin-configurable ruleset to define when access opens before a shift (e.g., 15 minutes early), how long it remains after end, and special handling for late arrivals and after-hours contact. Rules support per-team defaults and per-client overrides, holiday/blackout dates, and time-zone awareness. Enforcement covers UI, API, and messaging, with previews showing how policies apply for a given crew member and date. Expected outcome: consistent handling of edge cases with less manual intervention.

Acceptance Criteria
Window Open/Close and Grace Enforcement
Given a crew assignment 09:00–11:00 with early_open_minutes=15 and post_grace_minutes=20 in the appointment time zone When the crew requests access at 08:44, 08:45, 11:20, and 11:21 Then 08:44 and 11:21 requests are denied (API 403; UI banner "Outside access window"), sensitive fields [address, contact, payments] are masked, and inbound SMS receives the branded policy auto‑reply And 08:45 and 11:20 requests grant full access without masking And all four events are logged with timestamp, channel, and applied_rule_id
Late Arrival Adaptive Extension
Given a late_arrival rule extend_post_grace_by_lateness=true and max_extension_minutes=30 for a shift 09:00–11:00 And the crew checks in at 09:17 via the app When the crew accesses at 11:25 and 11:48 Then 11:25 is allowed (post window extended to 11:37), 11:48 is denied And client inbound between 09:00–09:17 receives the "running late" auto‑reply template And audit log records late check‑in, computed extension minutes, and template used
Override Precedence: Client vs Team Defaults
Given team defaults early_open_minutes=15 and post_grace_minutes=20 and Client A override early_open_minutes=30 and post_grace_minutes=10 When Crew accesses Client A at 08:35 (for 09:00 start) and 11:05 (end 11:00) Then 08:35 is allowed and 11:05 is denied per Client A override And for Client B without override, 08:44 is denied and 08:45 allowed per team default And removing Client A override takes effect within 1 minute and reverts behavior to team defaults
Time‑Zone Aware Window Calculation
Given an appointment time zone America/Denver and crew device time zone America/New_York with shift 09:00–11:00 (Denver) and early_open_minutes=15 When the crew accesses at 08:45 Denver time (10:45 NY time) Then access is allowed and UI/API display the effective window with TZ label (America/Denver) and local conversion helper And API responses include window_open and window_close in ISO 8601 with explicit time zone
Holiday/Blackout Date Handling
Given a blackout date 2025‑12‑25 with policy no_access and auto_reply_template=holiday_closed When the crew attempts access for Client C any time on 2025‑12‑25 Then access is denied, sensitive fields are masked, and inbound SMS receives holiday_closed auto‑reply And if Client C has override allow_on_blackout=true, access follows normal window rules for that date
Admin Policy Preview and Messaging Sample
Given an admin opens Policy Preview for Crew X and Client C on 2025‑10‑03 America/Chicago And inputs shift 09:00–11:00 with team defaults early=20 and post=15 and a client late_arrival max_extension=30 When generating the preview Then the preview shows window_open=08:40, window_close=11:15, effective rule sources (team default, client override, holiday=none), and sample SMS for inside/outside window And the Preview API returns the same computed values and templates with correlation_id
Cross‑Channel Enforcement and Audit Logging
Given an access attempt outside the window at 07:00 for a 09:00 start When the actor uses UI, REST API, and replies via SMS Then UI masks sensitive fields and shows reason_code=WG‑OUTSIDE‑WINDOW, API returns 403 with error_code=WG‑OUTSIDE‑WINDOW, and SMS receives after_hours auto‑reply And a unified audit log entry captures actor, channel, reason_code, effective_policy_id, and outcome for each attempt
Admin Overrides & Exceptions UI
"As a dispatcher or owner, I want a simple override to grant temporary access outside the window so that urgent exceptions can be handled without weakening overall controls."
Description

Enable authorized admins/dispatchers to grant temporary, scoped access outside the normal window for a specific job, client, or time range. Overrides are time-bound, require a reason, and notify relevant parties. The UI is accessible from the schedule, job card, and message thread, with one-tap apply/revoke and clear audit trails. Expected outcome: fast handling of legitimate exceptions without weakening default protections.

Acceptance Criteria
Create Time-Bound Job-Level Override from Schedule View
Given I am an authorized admin on the Schedule view When I select a job outside the crew’s allowed window and tap "Override" Then I can choose scope = Job, set start and end times within configured limits, and must enter a non-empty reason And the Save action remains disabled until all required fields are valid And on Save, an active override is created and a confirmation message displays the window and scope
Scoped Access Applies Only to Targeted Job and Assignee
Given an active job-scoped override exists for Job J, Assignee A, window [t1, t2] When Assignee A accesses Job J between t1 and t2 from any entry point (Schedule, Job Card, Message Thread) Then address, contact, payment fields, and messaging are visible and usable for Job J And when Assignee A accesses any other job/client or outside [t1, t2], access remains blocked per Window Guard And when any other crew member attempts to access Job J, access remains blocked
Mandatory Reason and Time Validation on Override Creation
Given the Override modal is open When the reason field is blank or whitespace-only Then Save is disabled and a validation message indicates a reason is required When start/end are missing, end ≤ start, or the duration exceeds the configured maximum Then Save is disabled and validation messages indicate the specific fields to correct
Override Notifications to Relevant Parties
Given an override is created, revoked, or expires When the action occurs Then the assigned crew and the creating admin receive a notification within 60 seconds containing the reason and the time window And a system message is appended to the job’s message thread indicating the action, actor, and window And notifications respect the workspace’s channel settings (e.g., SMS, push, email)
Auto-Expiry Re-Locks Access and Logs Event
Given an active override with end time t2 When the current time passes t2 Then the override automatically transitions to expired status And access to sensitive fields and messaging is blocked on next request and existing sessions are invalidated within 60 seconds And an audit log entry is recorded with action = Expired, including timestamp, actor = System, and the original reason
One-Tap Revoke from Schedule, Job Card, and Message Thread
Given an active override exists When an authorized admin views the job on Schedule, Job Card, or Message Thread Then a Revoke control is visible When the admin taps Revoke and confirms Then the override ends immediately, access is re-locked within 30 seconds, relevant parties are notified, and an audit entry is recorded
Audit Trail Completeness and Immutability
Given overrides are created, revoked, or expired When I open the Audit Trail for the job or client Then each event shows actor, timestamp, scope (job/client/time-range), affected assignees, reason, start/end, and action type And entries are read-only and cannot be edited or deleted by any user And I can filter audit entries by date range, actor, action type, and job/client
Audit Logging & Policy Impact Reporting
"As an owner, I want logs and reports of blocked access and auto-replies so that I can verify policy adherence and measure impact on off-schedule pings."
Description

Capture and surface metrics on blocked access attempts, auto-replies sent, redactions triggered, overrides created, and policy exceptions. Provide filters by team member, client, date range, and location, with export (CSV) and webhook events for BI tools. Include trend charts and KPIs that quantify reductions in off-schedule pings and policy violations. Expected outcome: transparency, compliance evidence, and continuous policy optimization.

Acceptance Criteria
Blocked Access Attempts Logging
Given a crew member attempts to access client details or send a message outside their assigned window When Window Guard evaluates the access request Then the request is blocked and an audit event of type "blocked_access_attempt" is recorded with event_id (UUID), occurred_at (UTC ISO 8601), team_member_id, client_id, location_id, policy_id, channel (sms|app|api), reason_code, outcome=blocked, shift_window_start, shift_window_end, grace_period_applied (boolean) And the event becomes queryable in reporting within 60 seconds of occurrence And the event is included in daily counts and unique client counts with an aggregation lag of no more than 5 minutes
Auto‑Reply Events Logged
Given an inbound or outbound message is subject to Window Guard after‑hours rules When an auto‑reply is sent by the system instead of delivering the message Then an audit event of type "policy_auto_reply_sent" is recorded with event_id, occurred_at, triggered_by (crew|client), team_member_id (if applicable), client_id, location_id, policy_id, message_id, template_id, channel, outcome=auto_replied And the auto‑reply count KPI increments by 1 within 5 minutes And the original message body is stored hashed (SHA‑256) and not in plaintext in the audit payload
Redaction Triggers Logged
Given a user outside the allowed window views a job or client record When Window Guard redacts sensitive fields (address, contact, payments) Then an audit event of type "redaction_triggered" is recorded with event_id, occurred_at, viewer_role, viewer_id, client_id, location_id, policy_id, fields_redacted (array), context (ui|api) And the UI/API response returns redacted placeholders for each field redacted And the "redactions_triggered" KPI increments by 1 and is visible in reporting within 5 minutes
Overrides and Policy Exceptions Tracked
Given an admin grants a temporary override or defines a policy exception When the override/exception is created, modified, or expires Then audit events ("override_created", "override_modified", "override_expired") are recorded with event_id, occurred_at, approver_id, requester_id (if any), scope (team_member|location|client), reason, start_at, end_at, affected_policies (array) And events allowed during an active override are linked via override_id for attribution in reports And the Overrides KPI shows count and total override_hours for the selected date range
Filterable Reporting by Member, Client, Date, Location
Given audit events exist across multiple users, clients, dates, and locations When the user applies filters for team member(s), client(s), date range, and location(s) Then all tables, KPIs, and charts reflect only events matching all active filters And date filtering is inclusive of start and end dates in the account timezone And multi‑select filters support AND within a dimension and AND across dimensions, with results returned in under 2 seconds for up to 100k events
CSV Export and Webhook Delivery for BI
Given a filtered report view When the user requests a CSV export Then the CSV is generated within 30 seconds with headers [event_id, occurred_at, event_type, team_member_id, client_id, location_id, policy_id, channel, reason_code, outcome, extras_json] and row count equals the number of filtered events And when a webhook subscription is configured for event_types [blocked_access_attempt, policy_auto_reply_sent, redaction_triggered, override_created, override_modified, override_expired, kpi_daily] Then new events are delivered within 10 seconds on average with payload including id, type, occurred_at, attributes, and HMAC‑SHA256 signature header And deliveries retry with exponential backoff at least 3 times and include an idempotency key to prevent duplicates
Trend Charts and KPI Reduction Calculations
Given a dashboard date range and a configurable baseline period (default previous 28 days) When the user views Trend Charts and KPIs Then charts display daily counts for off‑schedule pings and policy violations with selectable granularity (day/week) And KPIs show current rate, baseline rate, and reduction_pct computed as (baseline_rate - current_rate) / baseline_rate rounded to 0.1% And data points reflect applied filters and exclude days with no events by treating them as zero, not missing And all chart and KPI values reconcile to the underlying event counts and are reproducible via CSV export within ±0.5%

SafeReveal Address

Keep exact addresses and entry codes masked until the contractor taps “On My Way” and is within the start window and proximity threshold. Details reveal just‑in‑time and auto‑remask after completion. Clients get tighter privacy and fewer early knock‑ons; you get on‑time arrivals without oversharing location data.

Requirements

Proximity-Gated JIT Reveal
"As a contractor, I want job details to unlock only when I’m en route and near the location so that I can arrive on time without exposing the client’s exact address and codes prematurely."
Description

Reveal exact street address, unit number, entry codes, and access notes only when the contractor has tapped “On My Way,” is within the appointment’s start window, and is inside a configurable proximity radius of the job location. Use GPS with network-location fallback and perform periodic checks until all conditions are met. Prior to unlock, show non-sensitive guidance (e.g., neighborhood, nearest intersection). If conditions stop being met (e.g., contractor exits the radius), re-hide details until requalified. Support per-service and per-client radius presets, background location polling with battery safeguards, and masking of sensitive fields across SMS, app, and dashboard until reveal.

Acceptance Criteria
JIT Reveal Gated by Status, Start Window, and Proximity
Given a scheduled appointment with a defined start window and an effective proximity radius R (meters) And the contractor has tapped "On My Way" And the current time is within the appointment start window And the latest location fix has accuracy <= min(100m, R/2) And the computed distance to the job location <= R When conditions are evaluated Then the app reveals exact street address, unit number, entry codes, and access notes within 2 seconds And an audit log entry records timestamp, user, location source, accuracy, distance, and decision outcome
Location Source Fallback and Periodic Checks
Given location permissions are granted And "On My Way" is active When reveal conditions are not yet met Then the app polls location every 60s when estimated distance > 2km, every 15s when 500m–2km, and every 5s when <= 500m And if a GPS fix with accuracy <= 100m is not available within 8s, the system evaluates conditions using network-based location if accuracy <= 500m And if neither source provides accuracy <= 500m within 120s, the UI displays "Awaiting location" and details remain masked And polling suspends within 3s when "On My Way" is turned off
Re-hide on Exit and Re-qualification
Given sensitive details have been revealed When any gating condition becomes false (distance > R sustained for > 60s, time leaves start window, or "On My Way" is off) Then the app re-masks all sensitive fields in app and dashboard within 5s And all subsequent SMS/API responses redact sensitive values And a re-hide event is audit-logged with reason and current metrics When the contractor re-enters R and all gating conditions are true again Then sensitive details re-reveal without requiring app restart
Pre-Reveal Coarse Guidance Only
Given reveal conditions are not satisfied When the contractor views the job Then the UI shows only non-sensitive guidance (e.g., neighborhood, nearest intersection within ~300m, parking instructions marked non-sensitive) And exact street number, unit, entry codes, and full access notes are not displayed And SMS/push notifications contain only coarse guidance tokens and never exact address numbers or codes
Configurable Proximity Radius with Hierarchy
Given a global default radius of 200m, optional per-service radius, and optional per-client radius When evaluating proximity Then the effective R equals per-client if set, else per-service if set, else global default And acceptable radius values are 50m to 5000m inclusive; values outside this range are rejected with a validation error And changes to any radius take effect on the next location evaluation and are reflected in the job detail UI
Background Polling and Battery Safeguards
Given the app is backgrounded with "On My Way" active and OS location permission granted When estimated distance > 2km Then the app targets <= 1 location check per 5 minutes using low-power/significant-change updates When 500m < distance <= 2km Then the app targets <= 4 checks per minute When distance <= 500m Then the app targets <= 12 checks per minute And in a 60-minute simulated session, incremental battery consumption is <= 3% on reference devices And if OS permission is revoked or device location is disabled, polling stops within 5s, a prompt is shown, and no reveal occurs
Cross-Channel Masking and Auto-Remask After Completion
Given a job exists When reveal conditions are not met Then sensitive fields are masked in the mobile app, web dashboard, and all outgoing SMS/push content (e.g., numbers replaced with bullets; codes redacted) When the job is marked Completed or Canceled Then any previously revealed sensitive details are re-masked across all channels within 5s and remain inaccessible thereafter And audit logs store only masked representations; plaintext sensitive values are never persisted in logs
Start Window Enforcement
"As a client, I want my exact location and entry info hidden until the agreed start window so that I avoid early knock-ons and maintain privacy."
Description

Gate reveals by the scheduled start window with configurable early and late tolerances. Block early reveals to prevent premature knock-ons; if early, show countdown and allow optional client-approved early access. When late or outside the end of the window, require client/admin extension or override before reveal. Handle reschedules and time-shifts automatically, recalculating eligibility to keep the reveal aligned with the active appointment window.

Acceptance Criteria
Early Reveal Block With Countdown
Given an appointment with start window [S, End] in its local timezone and early_tolerance_minutes = e And current server time t < S - e minutes When the contractor attempts to reveal address/entry code Then the system blocks the reveal and keeps details masked And a countdown to (S - e) is displayed in mm:ss, updating every 1 second And a "Request Early Access" action is available And an audit log event is recorded with type="reveal_blocked", reason="early_outside_tolerance", remaining_seconds >= 0
Client-Approved Early Access
Given an appointment where current server time t < S - e minutes And the client has approved early access for this appointment occurrence When the contractor attempts to reveal address/entry code Then the system reveals the details immediately And an audit log event is recorded with type="reveal_granted", reason="client_approved_early", approver="client" And the reveal remains subject to any other gating rules outside the time window (if applicable)
Reveal Within Window Allowed
Given an appointment with early_tolerance_minutes = e and late_tolerance_minutes = l And current server time t satisfies S - e <= t <= End + l When the contractor attempts to reveal address/entry code Then the system reveals the details without requiring additional approvals And an audit log event is recorded with type="reveal_granted", reason="within_window"
Late Reveal Requires Extension or Override
Given current server time t > End + l for the appointment When the contractor attempts to reveal address/entry code Then the system blocks the reveal and keeps details masked And the UI presents options to "Request Client Extension" and "Admin Override" And until an approved extension updates End so that t <= End + l, or an admin override is recorded, reveal remains blocked And an audit log event is recorded with type="reveal_blocked", reason="late_outside_tolerance"
Automatic Recalculation On Reschedule Or Time-Shift
Given the appointment's start and/or end time is changed (rescheduled/time-shifted) When the change is saved Then eligibility to reveal is recalculated against the updated window and tolerances within 2 seconds And the UI state updates accordingly: enable reveal if now eligible; disable/show countdown if now ineligible And an audit log event is recorded with type="eligibility_recomputed" including old/new window values
Boundary, Timezone, And Clock Handling
Given calculations use the appointment's defined timezone When current server time t equals exactly S - e or exactly End + l Then the reveal is permitted (inclusive bounds) And all gating decisions and countdowns use server time; client/device clock skew up to 60 seconds does not change eligibility And daylight saving transitions are handled using timezone-aware arithmetic (no off-by-one-hour errors) And an audit log event is recorded on boundary decisions with type="boundary_decision" and evaluated_timestamps
Secure Detail Delivery Channel
"As a contractor, I want a secure way to access addresses and codes from SMS or the app without exposing them in plaintext so that I can work efficiently and protect client data."
Description

Prevent plaintext leakage of addresses and entry codes in SMS by delivering sensitive details via a time-bound, device-bound magic link or in-app view that evaluates eligibility in real time. Links expire on single use or after a short TTL, require lightweight authentication (e.g., OTP/magic link), and are non-previewable in notifications. In-app reveals use a secure screen with optional copy restrictions for codes, and close-to-rehide behavior. For SMS-only users, provide a lightweight web reveal with the same checks and auto-expiration. Never store revealed secrets in message transcripts; mask in notifications and logs.

Acceptance Criteria
Magic Link Masked Delivery and Non-Previewable Messaging
Given a scheduled job with address and entry code When sending SMS/push to the contractor Then the message body must contain no plaintext address or entry code and must display masked placeholders And the message includes a unique magic link with a signed token and TTL ≤ 15 minutes And the reveal endpoint exposes no OpenGraph/Twitter meta or previewable content And opening the link without authentication reveals no secret data
Single-Use, Time-Bound, Device-Bound Access
Given the contractor taps the magic link on device D within the TTL When they complete lightweight authentication (OTP or existing app session) Then the details reveal once and the token is immediately invalidated (single-use) And subsequent opens of the same link on any device return Expired/Used and reveal nothing When the same link is opened on device E ≠ D, Then access is denied and no secrets are revealed When opened after TTL expiry, Then access is denied with an explicit Expired state and no secrets are revealed
Real-Time Eligibility Gates: OMW, Window, Proximity
Given the contractor is assigned to the job When they attempt to reveal details Then the system validates in real time that: contractor status is On My Way; current time is within the job's start window; current location is within the configured proximity threshold; and assignment matches And if any check fails, no details are revealed and a specific block reason is shown (not eligible yet, outside window, too far, not assigned) And if all checks pass, the details are revealed
In-App Secure Reveal Screen and Rehide
Given the contractor uses the PawPilot app and is eligible When they reveal details in-app Then the address and entry code display only on a Secure Reveal screen And copy of the entry code is blocked or replaced with a masked value when copy restrictions are enabled And the screen prevents OS screenshots where supported and excludes secrets from app switcher previews And upon app backgrounding, navigation away, or 60s of inactivity, the details are rehidden and require re-auth to view again
SMS-Only Web Reveal Parity and Security
Given the contractor does not have the app When they open the magic link in a mobile browser Then a secure web reveal page enforces the same eligibility checks, single-use, TTL, device binding, and OTP authentication And the page never includes secrets in the URL, page metadata, or HTTP caches and sets noindex/nocache headers And details auto-rehide on tab close, background, or 60s inactivity and require re-auth on return And copy restrictions for codes are enforced when enabled
No Secrets Persisted in Transcripts, Notifications, or Logs
Given any outbound/inbound SMS, push notification, conversation transcript, analytics event, or server log When messages and events are stored or displayed Then addresses and entry codes are never persisted in plaintext and are masked/redacted (e.g., “123 M••• St”, “••••”) And audit logs store only metadata (who, when, job ID, outcome) without secret values And support/admin views and exports also show only masked values And attempts to reveal are logged without secret content
Auto-Remask and Post-Completion Invalidations
Given a job transitions to Completed or Canceled When a reveal link or view is opened or refreshed after that transition Then access is invalidated immediately and no details are shown regardless of remaining TTL And any previously open reveal views auto-rehide within 10 seconds of the status change And future notifications and transcripts continue to display only masked placeholders
Auto-Remask on Completion
"As a client, I want my address and entry code to be re-hidden after the appointment so that my information isn’t accessible later on."
Description

Automatically re-hide addresses, entry codes, and access notes as soon as the visit is marked complete or after a configurable post-visit buffer. Remove sensitive values from active views, notifications, and conversation transcripts while retaining encrypted storage for future appointments. Prevent re-access without a new eligibility event or authorized override. Ensure exports, receipts, and reports show sanitized placeholders instead of secrets.

Acceptance Criteria
Immediate Remask on Completion (Buffer = 0)
Given a visit with SafeReveal details visible due to an active eligibility event and the post-visit buffer set to 0 minutes When the contractor marks the visit as Complete Then the address, entry code, and access notes are replaced in all active views with the placeholder "[Hidden]" within 3 seconds And subsequent API/UI fetches for the visit return masked values for these fields And any copy/share actions for these fields are disabled or return masked content And a remask event is recorded with timestamp, visit ID, and actor
Timed Remask After Configurable Buffer
Given the organization sets Post-Visit Remask Buffer = 15 minutes and secrets are currently visible for an in-progress visit When the visit is marked Complete Then the secrets remain visible for up to 15 minutes after completion and are automatically remasked at buffer expiry And if the device is offline at expiry, the server remasks immediately and the client masks within 5 seconds of reconnect And no notifications sent after buffer expiry contain secrets
Blocked Re-Access Without Eligibility or Authorized Override
Given a visit whose secrets are remasked When the contractor attempts to view or fetch address, entry code, or access notes via UI or API Then access is denied and masked values are shown, with server responding 403/REQUIRES_ELIGIBILITY for API requests And secrets are only re-exposed after a new eligibility event (On My Way + within start window + within proximity threshold) or an authorized override by a user with appropriate role and MFA And all successful overrides are time-bounded to the same rules and are auditable with reason code
Sanitized Data in Exports, Receipts, and Reports
Given completed visits included in CSV/PDF exports, client receipts, and admin reports When the export/report is generated Then address, entry code, and access notes appear as "[Hidden]" and never include raw values And non-sensitive fields (city, neighborhood, appointment time, fees) remain unchanged And API reporting endpoints return the same sanitized placeholders
Transcript and Notification Sanitization Post-Visit
Given a visit is completed and secrets are remasked When viewing the in-app conversation transcript for that visit Then any system-inserted messages that previously displayed secrets now display "[Hidden]" placeholders And new outbound notifications (SMS, push, email) triggered after completion exclude secrets even if templates contain secret tokens And attempting to resend pre-completion messages will send sanitized content
Encrypted Retention and Future Re-Eligibility
Given secrets are remasked for a completed visit and stored encrypted When a new appointment is scheduled for the same client/location and a new eligibility event occurs Then the previously stored secrets can be re-exposed for the new appointment under eligibility rules And secrets remain masked again upon that appointment’s completion according to the configured buffer And at no time are secrets stored or transmitted unencrypted at rest or in transit
SafeReveal Policy Controls
"As a business owner, I want to configure how and when details reveal so that the feature matches my workflows and risk tolerance."
Description

Provide business owners with a settings panel to configure proximity radius, early/late tolerances, reveal scope (address vs. entry codes vs. notes), and per-service or per-client overrides. Include role-based permissions for overrides, an emergency reveal policy, default templates for notifications, and a test mode to preview behavior. Offer sensible defaults and clear explanations to align security with operational needs.

Acceptance Criteria
Configure Global Proximity and Timing Defaults
- Given an Owner opens SafeReveal Policy Controls for the first time, When the page loads, Then defaults are pre-populated: proximity radius = 150m, early tolerance = 10m, late tolerance = 15m, reveal scope = Address only, emergency reveal = Disabled, test mode = Off. - Given default values are shown, When the Owner clicks Save without changes, Then the settings persist and apply to appointments created after the save time. - Given the Owner edits proximity radius, When a value < 50m or > 1000m is entered, Then an inline error prevents save and specifies the allowed range. - Given the Owner edits early/late tolerances, When a value < 0m or > 60m is entered, Then an inline error prevents save and specifies the allowed range. - Given info icons are present, When the user hovers/taps an info icon, Then a tooltip appears with a concise explanation and an example relevant to the field.
Reveal Scope Configuration (Address, Entry Codes, Notes)
- Given reveal scope toggles exist for Address, Entry Codes, and Notes, When the Owner enables Address and disables Entry Codes and Notes, Then only the address reveals at trigger and other fields remain masked in app, SMS, and APIs. - Given reveal scope is set to All, When trigger conditions are met, Then all three fields reveal simultaneously within 3 seconds and are fully redacted again after completion. - Given an Owner changes reveal scope, When the change is saved, Then it applies to appointments created after the save and existing appointments retain prior scope with a banner "Uses prior policy". - Given external integrations consume reveal events, When reveal occurs, Then payloads include only fields permitted by the current scope.
Proximity and Time Window Triggering
- Given a contractor taps "On My Way", And current time is within [start time − early tolerance, start time + late tolerance], And device location is within the configured radius, When both signals are received, Then reveal occurs within 3 seconds and a toast confirms "Details revealed". - Given only time condition is met, When proximity condition is not met, Then masking persists and a banner states "Move within X m to reveal" with X = remaining distance. - Given only proximity condition is met, When time condition is not met, Then masking persists and a banner states "Wait until start window to reveal" with the countdown in minutes. - Given the job is marked Complete or the contractor exits the radius post-completion, When completion is recorded, Then all revealed fields auto-remask within 10 seconds and SMS deep links expire. - Given location cannot be obtained, When the contractor attempts reveal, Then the app prompts for a one-time location share; if declined, reveal does not occur and a reason is logged.
Per-Service and Per-Client Overrides with Precedence
- Given global settings exist, When a Service-level override is created for "Dog Walking", Then new Dog Walking appointments use the override values instead of global defaults. - Given both Service-level and Client-level overrides exist, When an appointment matches both, Then the Client-level override takes precedence and the UI shows a banner "Client override active". - Given an override is deleted, When an affected appointment is recalculated, Then it reverts to the next applicable policy within 5 seconds. - Given bulk CSV import of overrides, When a valid file is uploaded, Then valid rows are created, invalid rows are rejected with row numbers and error reasons, and a summary report is displayed without creating partial entries for invalid rows.
Role-Based Permissions for Overrides
- Given roles Owner, Manager, and Contractor, When permissions are enforced, Then only Owner and Manager can create/edit global and service-level overrides; only Owner can configure emergency reveal; Contractors have read-only access to policy summaries. - Given an unauthorized Manager attempts to change a Client-level override without assigned permission, When they click Save, Then access is denied with message "Insufficient permissions" and an optional Request Change workflow can be initiated. - Given any policy change occurs, When it is saved, Then an audit log captures actor, timestamp, scope, old values, new values, and optional reason, and is viewable by Owner and Manager.
Emergency Reveal Policy
- Given emergency reveal is Disabled, When a Contractor requests emergency reveal, Then the request is queued for Owner approval and no data is revealed until approved. - Given emergency reveal is Enabled with 2FA, When an authorized role initiates emergency reveal, Then the user completes 2FA and provides a reason, after which all fields reveal immediately regardless of time/proximity, the client is notified via the "Emergency Reveal Used" template, and an audit entry is created. - Given an emergency reveal session is active, When 60 minutes elapse or the job is marked Complete (whichever comes first), Then all fields auto-remask and any reveal access tokens expire.
Notification Templates and Test Mode Preview
- Given default notification templates exist, When the Owner opens Templates, Then default content is pre-filled with placeholders (e.g., {contractor_name}, {start_time}, {address}), and edits persist after Save. - Given Test Mode is turned On, When the Owner simulates a reveal for a selected appointment, Then no real data is unmasked for contractors; instead, a preview card shows mock data, and a [TEST] SMS/email is sent only to the Owner. - Given Test Mode is Off, When the Owner clicks Simulate Reveal, Then the system blocks the action and instructs the user to enable Test Mode. - Given an invalid placeholder is used in a template, When the Owner clicks Save, Then validation fails with the invalid tokens highlighted and Save is prevented.
Access Audit & Alerts
"As an admin, I want a detailed audit of address/code access so that I can monitor security and investigate issues."
Description

Record every reveal attempt and success with timestamp, user identity, device fingerprint, approximate location snapshot, eligibility state, and whether an override was used. Provide an audit dashboard with filters and export, plus configurable alerts for unusual activity (e.g., repeated early attempts, reveals outside expected areas). Offer optional client notifications summarizing when and by whom access occurred to build trust.

Acceptance Criteria
Log Reveal Attempt — Ineligible (Early/Out-of-Range)
Given a SafeReveal-enabled job and a contractor who is outside the start window or outside the proximity threshold When the contractor attempts to reveal access details Then the request is denied and no secrets are revealed And an audit record is created with fields: eventType="reveal_attempt", outcome="denied", timestamp (ISO 8601 UTC), userId, jobId, deviceFingerprint, approximateLocation (lat, lon rounded to 3 decimals), eligibilityState with reason ("early_window" or "outside_proximity"), overrideUsed=false And the audit record is available via the dashboard within 3 seconds of the attempt
Log Reveal Success — Eligible and Just-in-Time
Given a SafeReveal-enabled job where the contractor has tapped "On My Way", is within the start window, and within the proximity threshold When the contractor reveals access details Then the reveal succeeds And an audit record is created with fields: eventType="reveal_success", outcome="allowed", timestamp (ISO 8601 UTC), userId, jobId, deviceFingerprint, approximateLocation (lat, lon rounded to 3 decimals), eligibilityState="eligible", overrideUsed=false And the audit log does not store plaintext address lines or entry codes
Audit Dashboard — Filtering, Sorting, Pagination
Given an admin user is on the Access Audit dashboard When they filter by date range, userId, outcome (allowed/denied), eligibilityState, overrideUsed, jobId, and a map bounding box Then only matching audit records are shown and the total count reflects the filtered set And results are paginated (50 per page by default) with server-side pagination And sorting by timestamp ascending/descending changes the order accordingly And queries returning up to 10,000 records complete in ≤2 seconds
Audit Export — CSV with Required Fields
Given an admin has applied filters on the Access Audit dashboard When they request an Export CSV Then a CSV is generated containing a header and one row per record with: timestamp (ISO 8601 UTC), eventType, outcome, userId, jobId, deviceFingerprint, latitudeRounded, longitudeRounded, eligibilityState, overrideUsed And the export respects the active filters and date range And for up to 50,000 records the file is available to download within 60 seconds
Configurable Alerts — Repeated Early Attempts
Given an alert rule "Repeated early attempts" is configured as N=3 in M=15 minutes per user When a user triggers 3 or more denied reveal_attempt events with reason "early_window" within 15 minutes Then an alert is sent to the configured channels with userId, count, time window, and latest jobId And the alert is delivered within 2 minutes of the threshold being met And subsequent alerts for the same user are suppressed for 30 minutes or until acknowledged And the alert issuance and acknowledgement are recorded in the audit log
Client Notifications — Opt-in Access Summary
Given a client has opted in to access summaries and quiet hours are configured in their timezone When a reveal_success occurs for the client's job Then the client receives an SMS within 5 minutes outside quiet hours containing provider first name, business name, and the access timestamp in the client's timezone And if the event occurs during quiet hours, the SMS is queued and sent at the end of quiet hours And no exact address or entry codes are included in the SMS And notification delivery status (sent, failed) is recorded and viewable in the audit dashboard
Offline & Emergency Override
"As support staff, I want a secure override process for edge cases so that work can continue without compromising client privacy."
Description

Enable a controlled fallback when location services are unavailable or eligibility cannot be met but the job must proceed. Support identity verification and one-time reveals via OTP, admin-mediated temporary unlock with reason capture, and optional client consent prompts. All overrides are time-limited, fully logged, and notify stakeholders as configured. Include basic spoofing detection and warnings when signal quality is insufficient to reliably validate proximity.

Acceptance Criteria
OTP One-Time Reveal (No GPS)
Given the contractor is assigned to a job within the start window and the app cannot obtain a reliable location When the contractor requests an override and verifies identity with a 6-digit OTP sent to the registered phone number Then the system reveals the exact address and entry code once for that job And the OTP expires in 5 minutes with a maximum of 3 attempts And the reveal session auto-expires after 30 minutes or upon job completion, whichever occurs first And an audit log captures contractor ID, job ID, timestamp, reason "No GPS", OTP result, and reveal duration And configured stakeholders receive an override notification within 10 seconds without exposing the entry code
Admin Temporary Unlock With Reason Capture
Given an admin with override permission selects a job requiring access while eligibility cannot be met When the admin enters a reason (minimum 10 characters), sets a duration between 5 and 60 minutes (default 30), and confirms Then the system reveals the exact address and entry code to the assigned contractor only for the set duration And proximity checks are bypassed during the duration but identity checks remain enforced And the admin may revoke the override at any time; upon revoke, details remask within 5 seconds And an immutable audit record stores admin ID, reason, duration, grant/revoke timestamps, and job ID And contractor and client notifications are sent per configuration; client content excludes the entry code and includes reason summary (max 100 characters)
Client Consent Prompt for Override
Given a contractor requests an override while outside the proximity threshold and client consent is required by configuration When the system sends the client an SMS with an approve/deny consent link that expires in 10 minutes Then an approval grants a reveal window of 30 minutes for the assigned contractor; a denial or expiry prevents the reveal And both contractor and client receive confirmation of the outcome And the consent response is logged with timestamp, job ID, client contact hash, and IP hash And if no response within 10 minutes, the system notifies the admin channel for escalation
Spoofing Detection and Signal Quality Warnings
Given the app detects location accuracy worse than 150 meters, mock-location enabled, or movement exceeding 150 km/h within the past 2 minutes When a user requests a proximity-based reveal or an override Then the system displays a warning indicating unreliable signal and potential spoofing And automatic OTP override is blocked until admin approval unless policy enables emergency mode And the detection signals, decision path, and actor are logged with a risk flag "High" And the admin channel is notified within 10 seconds and must supply a reason code "Spoofing suspected" to approve
Auto-Remask After Override or Completion
Given a reveal was granted via any override method When the job is marked complete, cancelled, or the override window expires Then the exact address and entry code remask within 5 seconds across all clients And any cached reveal fields are cleared from device secure storage immediately upon remask And subsequent access attempts require a new eligible reveal And the remask event is logged with source event, timestamps, and device/client identifiers
OTP Brute-Force and Rate Limiting
Given OTP verification is required for an override When a user enters 3 incorrect OTP attempts within 10 minutes Then OTP verification for that job locks for 15 minutes and optionally notifies the admin channel And error messaging remains generic and does not disclose which digits were incorrect And after lockout, only one new OTP can be requested; each OTP is unique and single-use And all attempts are logged with anonymized device fingerprint and IP hash
Stakeholder Notifications and Privacy
Given an override is granted by any method When notifications are dispatched to client, contractor, and admin Then the client message excludes entry codes and includes job time, contractor name, and reason summary And the contractor message includes override type, remaining duration, and a do-not-share reminder And the admin message includes the grantor, method, and a link to the full audit record And notifications are delivered within 10 seconds via configured channels with retries for up to 2 minutes And delivery outcomes (success/failure) are logged to the job timeline

Payment Shield

Let contractors request tips or pre‑approved add‑ons via secure, masked links—no cards shown, no cash needed. Funds route to you with same‑day payout and role‑based caps/approvals. This captures more revenue at the door, avoids awkward asks, and maintains strict separation between crew and client payment details.

Requirements

Secure Masked Payment Link Generation
"As a contractor, I want to send a secure, masked payment link by SMS so that clients can pay tips or add-ons without sharing card details with me."
Description

Generate single-use, tokenized payment URLs that never expose cardholder data to contractors. Each link encapsulates merchant context, appointment ID, payer identity, amount and purpose (tip vs. add-on), optional itemization, and an expiration timestamp. The checkout page is mobile-first and brandable, with PSP-hosted fields or redirect to keep PawPilot out of PCI scope; only non-sensitive tokens are stored. Links are embeddable in PawPilot SMS threads, include UTM/metadata for attribution, and support wallets (Apple Pay/Google Pay), cards, and ACH where available. Track open, attempt, success, and failure events for real-time status in the appointment timeline and waitlist automations.

Acceptance Criteria
Generate Single‑Use Masked Payment Link for Tip via SMS
Given a confirmed appointment and payer identity and a specified tip amount When a contractor requests a payment link in PawPilot Then a unique tokenized URL is generated that contains merchant context, appointment ID, payer identifier, purpose=tip, currency, amount, and expiration timestamp And the URL does not include any cardholder data or PII in its path, query, or preview And the link can be inserted into the active PawPilot SMS thread with a single action And a creation record is stored containing only non-sensitive tokens and metadata (no PAN, CVV, full ACH routing/account)
Enforce Expiration and Single‑Use Redemption
Given a payment link with a future expiration timestamp When the payer completes a successful payment using the link Then subsequent visits to the link return an "already paid" state and no additional charges can be initiated Given a payment link after its expiration timestamp When the link is opened Then the payer sees an "expired link" message and no payment methods are rendered And all post-expiration or post-consumption opens emit a failure event with reason codes (expired, consumed) And the link token has at least 128 bits of entropy (e.g., >=22 URL-safe characters) and is not guessable via sequential IDs
PSP‑Hosted Checkout Keeps PCI Out‑of‑Scope
Given the payer opens the checkout from the payment link When the checkout form renders Then all card and ACH entry fields are hosted by the PSP (iFrame or PSP domain) and never by PawPilot And network requests containing PAN/CVV/ACH data are sent only to PSP domains And PawPilot stores only PSP tokens and non-sensitive metadata for the transaction And a PCI scope verification confirms SAQ A eligibility (no storage, processing, or transmission of cardholder data by PawPilot)
Contextual Payment Method and Wallet Availability
Given an iOS device with Apple Pay available and the PSP configured for Apple Pay When the checkout loads Then Apple Pay is displayed and usable to authorize the specified amount Given an Android device with Google Pay available and the PSP configured for Google Pay When the checkout loads Then Google Pay is displayed and usable Given a US payer with ACH enabled in the PSP When the checkout loads Then ACH is offered and usable And cards are always available And if a method is not supported by device, region, or PSP configuration, it is not shown
UTM/Metadata Attribution and SMS Embedding
Given a generated payment link When inserted into a PawPilot SMS thread Then the link uses PawPilot's short domain and includes UTM parameters (utm_source=pawpilot, utm_medium=sms, utm_campaign=payment_shield) and metadata (appointment_id, contractor_id) And on open, the UTM/metadata are preserved through redirects and recorded on the event And the SMS message renders a clickable link without exposing sensitive metadata
Event Tracking and Real‑Time Timeline Updates
Given a payment link is opened, a payment is attempted, a payment succeeds, or a payment fails When each event occurs Then an event record is created with type (open, attempt, success, failure), timestamp, link_id, appointment_id, payer_id, device info, and failure reason codes where applicable And the appointment timeline displays the event within 5 seconds of occurrence And waitlist and automation rules can reference the latest payment status for the appointment
Add‑On Itemization and Brandable Mobile Checkout
Given a contractor generates an add-on payment link with two or more line items and applicable taxes When the payer opens the checkout on a mobile device Then the checkout shows merchant branding (logo/colors), the itemized list, subtotal, taxes, and total that match the metadata And the PSP-hosted checkout adapts to a small-screen viewport with no horizontal scroll and passes WCAG AA color contrast for brand elements And on success, the transaction record stores purpose=add-on and the itemization payload
SMS Tip/Add-on Request Flow
"As a contractor, I want to request a tip or add-on directly within the client SMS thread so that I can capture incremental revenue without switching apps or handling cash."
Description

Provide an in-thread SMS workflow that lets contractors compose a tip request or select from a catalog of pre-approved add-ons tied to the active appointment. Pre-fill client and appointment data, show preset tip percentages and fixed add-on prices with tax rules, allow quick-edit of quantities within policy, and generate a preview message with the secure link. Handle client replies (e.g., Paid, Declined, Questions) and update appointment balance, notes, and notifications automatically. Support resend, edit-and-reissue (with new token), and localization of copy. Ensure graceful fallbacks if MMS/long SMS limits are reached by using short links and concise templates.

Acceptance Criteria
Compose Tip Request in SMS Thread
Given a contractor opens the active client SMS thread and selects Payment Shield → Tip And the appointment has a valid client, date/time, and service total When the contractor opens the tip composer Then the system pre-fills client name, appointment date/time, and appointment ID without exposing any payment details And shows organization-configured preset tip options (default 10%, 15%, 20%) and a Custom option And enforces tip policy caps (e.g., max 50% of service total or org-configured limit) When the contractor selects a preset or enters a custom tip within policy and taps Preview Then a preview message is generated showing the tip amount, currency, and a secure masked payment link And nothing is sent until the contractor taps Send
Select Pre‑Approved Add‑On from Catalog
Given a catalog of pre‑approved add‑ons with fixed prices and tax rules is configured And the contractor is in an active appointment SMS thread When the contractor selects Payment Shield → Add‑Ons and chooses one or more items Then the system displays each item’s unit price, applicable taxes, and allows quantity edits within policy bounds (min/max) And calculates subtotal, tax, and total in the appointment currency in real time When the contractor taps Preview Then the preview shows item names, quantities, subtotal, tax, and total with a secure masked payment link ready to send
Policy-Constrained Quick Edit and Role-Based Approval
Given role-based caps exist for per-item quantity, total add-on value, and tip percentage When a contractor edits quantities or enters a custom tip/add-on total Then the system validates against policy immediately and blocks values that exceed limits with an inline error And users without price-edit permission cannot modify unit prices of pre‑approved add‑ons When a requested amount exceeds a role cap but is within an approvable threshold Then an approval request is sent to an approver, and the send action is disabled until approval arrives And the approval/denial notification reaches the requester within 10 seconds and is audit-logged with approver, timestamp, and deltas
Preview Message and Secure Masked Payment Link
Given a tip or add‑on request is in Preview state When the contractor taps Send Then the system generates a short URL containing only a single-use opaque token with at least 120 bits of entropy And the URL contains no client PII or pricing details in query parameters And the token expires after 24 hours or immediately after successful payment, whichever comes first And contractors never see or receive client card details at any point in the flow And the client-facing payment page displays the correct breakdown (tip/add-ons, tax, total) and supports secure payment without exposing PAN in SMS or to contractors
Client Reply Handling and Automatic Updates
Given a payment request SMS has been sent to the client When the client completes payment via the link Then the appointment balance is updated within 10 seconds, an internal note is appended with amount and timestamp, and both contractor and client receive confirmation SMS And a same-day payout is queued to the business account upon payment capture and marked as Queued for same‑day payout in the appointment timeline When the client replies Paid but no payment is recorded Then the system replies with the payment link and instructions and marks the thread Awaiting Payment When the client replies Declined Then the request is marked Declined, the contractor is notified, and the appointment note is updated When the client replies with anything else (Questions) Then the system responds with a helpful message and routes the thread to human follow-up, tagging it as Client Question
Resend and Edit‑and‑Reissue with New Token
Given an outstanding payment request exists When the contractor taps Resend Then the same request message and tokenized link are re-sent, the original expiry is unchanged, and the resend is audit-logged When the contractor edits line items or tip amount and taps Reissue Then a new token and short link are generated, the prior token is immediately invalidated, and the client sees a Superseded notice if opening the old link And the new SMS is sent and all changes (diff) are audit-logged with actor and timestamp
Localization and SMS Length Fallbacks
Given the client has a preferred locale or one is inferred When generating the SMS copy and payment page Then text, currency symbol, date/time, and number formats are localized accordingly And if the composed SMS exceeds 160 GSM-7 characters or 70 UCS-2 characters Then a compact template is used with a short link so the final SMS fits within a single segment And if MMS is unavailable or would be used, the system sends SMS-only content with concise text and the short link
Role-Based Caps and Approval Workflow
"As an owner, I want caps and approvals for tips and add-ons so that field staff stay within policy and we avoid overcharging clients."
Description

Enable admins to configure policies by role, team, and location: per-transaction caps, daily/weekly aggregate limits, allowed add-on SKUs, and thresholds that require managerial approval. When a request exceeds a threshold, trigger an approval flow (SMS/in-app) to designated approvers with context and one-tap approve/deny. Enforce policy at link creation (block or route to approval), log decisions with reasons, and present clear messaging to contractors and clients. Support temporary overrides with expiry and audit trails, plus holiday/event-based policy schedules.

Acceptance Criteria
Policy configuration by role, team, and location
Given an admin with policy permissions is in Payment Shield settings When they create or edit a policy with scoped targets (roles, teams, locations), per-transaction caps, daily and weekly aggregate limits, allowed add-on SKUs, approval thresholds, approver list (users/groups), and optional schedules Then the policy saves successfully with field validation (required fields present; numeric limits ≥ 0; SKU IDs valid) And the policy is versioned and auditable (who, what changed, when, why) And scope precedence applies (most specific scope overrides broader) and is displayed in the UI And the policy becomes effective immediately on save unless future-dated by schedule
Per-transaction hard cap enforcement at link creation
Given a scoped policy defines a per-transaction hard cap for the contractor’s role/team/location And a contractor attempts to create a Payment Shield link exceeding that cap When the contractor submits the link creation Then the system blocks link creation and no client link is generated or sent And the contractor sees a clear error message including the cap value and next steps (contact manager or request approval if available) And an audit record is created with policy ID, attempted amount, contractor ID, and reason “exceeds per-transaction cap”
Approval flow for threshold exceedance with one-tap approve/deny
Given a scoped policy defines an approval threshold below the hard cap And a contractor creates a link that exceeds the threshold but not the hard cap When the request is submitted Then an approval request is created referencing the policy and request context (contractor, client alias, items/SKUs, amount, location, reason) And designated approvers receive SMS and in-app notifications with one-tap Approve/Deny actions And on Approve within the configured SLA window, the contractor and client are notified; the link becomes payable And on Deny or SLA timeout, the contractor and client are notified; the link is canceled or marked unpayable And all actions (request, approve/deny, timestamps, approver identity, deny reason mandatory) are logged in the audit trail
Daily and weekly aggregate limit enforcement
Given a policy defines daily and weekly aggregate limits for the contractor’s scope And the system tracks the contractor’s approved and paid Payment Shield requests within the current period(s) When a new link would cause the aggregate total to exceed a configured limit Then the system enforces the policy by blocking or routing to approval per the configured threshold rules And the contractor sees messaging showing current total, limit, and overage amount And an audit entry records the enforcement action, period, totals, and policy ID
Allowed add-on SKUs enforcement
Given a policy specifies an allowed list of add-on SKU IDs for a role/team/location When a contractor searches for or selects add-ons while composing a link Then only allowed SKUs are displayed/selectable by default And any attempt to include a disallowed SKU is blocked with a message naming the disallowed item and listing allowed categories/SKUs And the block is recorded with contractor ID, SKU ID, and policy ID in the audit log
Temporary override with expiry and full auditability
Given a manager creates a temporary override with explicit scope (user/role/team/location), fields changed (e.g., cap, thresholds, allowed SKUs), a required reason, and an expiry date/time When the override is active Then link creation and approval evaluation use the override values instead of the base policy And upon expiry, evaluation automatically reverts to the base policy with no gap And if pending approvals or links are impacted at expiry, the contractor and manager are notified And the override lifecycle (create, modify, expire, revoke) is fully logged with who, when, scope, fields changed, and reason
Holiday/event-based policy schedules with precedence and timezone
Given an admin configures a holiday/event schedule with start/end, timezone, and adjusted caps/thresholds/SKU rules When the current time at the contractor’s location falls within the scheduled window Then the scheduled policy takes precedence over the base policy for evaluation and enforcement And outside the window, the base policy applies And all evaluations and audit entries include the active schedule ID and timezone used
Same-day Payout and Split Routing
"As a finance manager, I want same-day payouts with policy-based routing of tips and add-ons so that cash flow is predictable and accounting remains accurate."
Description

Route captured funds to the primary business account on a same-day payout schedule with configurable cut-off times. Support policy-based splits so tip portions credit contractor earnings while add-ons flow to the business, all from a single client checkout. Deduct processing fees per policy, handle payout failures with retries and alerts, and reconcile PSP events to internal ledgers. Provide clear payout visibility per appointment, contractor, and day, and ensure compliance with tax reporting rules for tips and contractor disbursements.

Acceptance Criteria
Same-day Payout Scheduling with Configurable Cut-off Times
Given a payment is captured before the configured cut-off time in the business location’s timezone, When the payout scheduler runs, Then a payout is created for the same banking day to the primary business account. Given a payment is captured at or after the cut-off time or on a non-banking day, When the payout scheduler runs, Then the payout is scheduled for the next available banking day. Given the cut-off time configuration is changed, When new payments are captured, Then only captures after the change use the new cut-off; previously scheduled payouts are not rescheduled. Given regional bank holidays and weekends are configured, When computing payout dates, Then payout dates skip those days. Given multiple captures occur on the same day, When payouts are created, Then each capture is linked to its payout and displays the scheduled payout date and status.
Single Checkout with Policy-Based Split Routing for Tips and Add-ons
Given a single client checkout includes a tip and one or more add-ons, When the payment is captured, Then the tip amount is credited to the contractor’s earnings balance and the add-on amounts are credited to the business revenue balance. Given a single client checkout includes no tip, When the payment is captured, Then 100% of the captured amount (less fees per policy) is credited to the business revenue balance. Given a checkout is associated to one appointment and one contractor, When the payment is captured, Then the system enforces a single-contractor split and rejects multi-contractor line items with a clear error. Given ledger postings are created for the capture, When the posting batch is reviewed, Then credited amounts plus recorded fees equal the gross captured amount with documented rounding rules applied. Given line items are tagged by type, When routing is executed, Then only items tagged as tip route to the contractor earnings and only items tagged as add-on route to the business revenue.
Processing Fee Allocation per Policy
Given policy = "Business pays all fees", When a payment with tip and add-ons is captured, Then the contractor receives the full gross tip and the business absorbs 100% of processing fees. Given policy = "Fees deducted from tip first", When a payment is captured, Then fees are applied to the tip up to the tip amount and any remaining fees are applied to business add-ons; contractor receives tip minus the allocated fees. Given policy = "Pro rata by line item", When a payment is captured, Then total fees are allocated across tip and add-ons proportional to their gross amounts, and allocations are recorded per line item. Given the fee policy is changed, When new payments are captured, Then the new policy applies only to those captures; prior captures are unchanged. Given rounding is required during fee allocation, When allocations are posted, Then rounding discrepancies do not exceed $0.01 per capture and are posted to a rounding adjustment account.
Payout Failure Retries and Alerts
Given a scheduled payout attempt fails due to a PSP or bank error, When the failure event is received, Then the payout status is set to Failed, a retry is scheduled using the configured backoff strategy, and the failure reason is stored. Given a payout fails, When the failure is recorded, Then alerts are sent to business admins via email and in-app with the error code and next retry time, and to the contractor if their disbursement is affected. Given retries are configured with a maximum attempt count, When the max is reached without success, Then the payout moves to Action Required, funds remain in the platform balance, and a Resolve action is available to update payout details and trigger a manual retry. Given idempotency keys are used per intended payout, When retries are executed, Then duplicate payouts are not created and the payout record preserves a single external payout ID upon success. Given a retry succeeds, When the success event is received, Then the payout status updates to Paid, alerts are sent, and any prior failure alerts are reconciled with a success notice.
PSP Event Reconciliation to Internal Ledgers
Given PSP webhook events for charge, capture, fee, transfer/transfer-reversal, payout/payout-failure, refund, and dispute are received, When reconciliation runs, Then each event is matched to an internal transaction using stable IDs and amounts. Given all events for a capture have been received, When the reconciliation report is generated, Then internal ledger balances for that capture net to zero (debits equal credits) and the capture is marked Reconciled. Given an event cannot be matched, When 30 minutes have elapsed since receipt, Then the event is flagged as Unmatched, an alert is created, and it appears in the reconciliation exception queue. Given delayed or out-of-order events are received, When they arrive, Then the system re-attempts matching and clears exceptions automatically if a match is found. Given a user views a transaction, When details are expanded, Then all linked PSP IDs, timestamps, and amounts are displayed with a full audit trail of ledger postings.
Payout Visibility by Appointment, Contractor, and Day
Given a user with admin permissions opens the payouts view, When filtering by day or date range, Then totals for gross, fees, net to business, net to contractors, and payout statuses are displayed and exportable to CSV. Given a contractor logs in, When viewing their earnings, Then they see per-appointment and per-day breakdowns of tips, fees applied per policy, net received, scheduled payout date, and payout status for only their appointments. Given an appointment is selected, When viewing its payout tab, Then the UI shows the split routing details (tip vs add-on), fee allocations, linked payout ID(s), scheduled/actual payout date, and reconciliation status. Given a timezone is selected at the org level, When dates and cut-off dependent values are shown, Then all times are presented in that timezone. Given data is exported, When the CSV is downloaded, Then column totals in the export match on-screen totals and the ledger within $0.01 tolerance for rounding.
Tax Reporting Segregation for Tips and Contractor Disbursements
Given line items are categorized as Tip or Add-on, When transactions are posted, Then tips are flagged as contractor earnings for tax reporting and add-ons are flagged as business revenue. Given a contractor has not provided required taxpayer information, When a payout including their tip earnings is about to be created, Then the payout is blocked, the status is set to Action Required, and the contractor and admin are prompted to provide the missing information. Given jurisdictional thresholds are configurable, When cumulative contractor disbursements and tips are tracked year-to-date, Then the system shows progress toward thresholds and includes them in year-end reporting exports. Given an admin generates tax reports, When monthly or annual CSV exports are produced, Then each contractor’s file includes gross tips, fees allocated to tips, and net disbursed, along with contractor identifiers, and sums tie back to the ledger. Given tax category mappings are changed, When new transactions occur, Then the new mappings apply only to future transactions; historical transactions retain their original categories for auditability.
Fraud Controls and Link Expiry
"As a product owner, I want safeguards on payment links so that we minimize misuse, chargebacks, and revenue leakage."
Description

Implement layered risk controls: single-use tokens, short-lived expirations with configurable TTL, strict amount binding, HMAC-signed parameters, and domain/referrer checks. Enforce rate limiting and device/IP velocity checks, auto-cancel forwarded or tampered links, and require reissue for edits. Support 3DS/SCA where mandated, and verify all success webhooks with signature validation to prevent spoofed confirmations. Provide anomaly alerts to admins and temporarily lock risky actors pending review.

Acceptance Criteria
Enforce Single-Use, Short-Lived Payment Links
Given an organization default TTL of 30 minutes and a per-link TTL configured between 5 minutes and 24 hours When a link is opened after its expiresAt timestamp Then the client sees an "Link expired" message, the API returns HTTP 410, and no authorization is attempted Given a payment link token that has completed one successful authorization When the link is opened again by any client Then the API returns HTTP 410 and no additional charges are created (idempotent safeguard) Given an admin updates the default TTL to 15 minutes When a new link is generated thereafter Then the new link inherits the 15-minute TTL while previously issued links retain their original TTL
Amount Binding with HMAC and Tamper Auto-Cancel
Given a payment link signed with HMAC over {amount_total,currency,line_items,max_tip,merchant_id,recipient_id,expiresAt,nonce} When any bound parameter in the request differs from the signed values or the signature verification fails Then the API returns HTTP 403, flags the token as tampered, invalidates the link, and no charge is attempted Given a client attempts to alter the amount via URL query or POST payload When the submitted amount exceeds amount_total or max_tip as signed Then the request is rejected with HTTP 422, and the event is logged for review
Domain and Referrer Allowlisting with Forwarding Protection
Given payment links are hosted on an allowlisted domain set in organization settings When a link is requested on a non-allowlisted host Then the request is blocked with HTTP 403 and the token is invalidated Given the HTTP Referer header is present When the Referer is not in the allowlist Then the request is blocked with HTTP 403; if this occurs twice for the same token within 10 minutes, the token is canceled Given a token is first accessed from Device/IP A When the same token is accessed from two or more additional distinct device fingerprints within 10 minutes without a successful authorization Then the token is marked as forwarded and canceled, requiring a new link to be issued
Rate Limiting and Velocity Lockouts
Given a single token and client device/IP When more than 5 payment attempts occur within 10 minutes Then subsequent attempts receive HTTP 429 and the token is temporarily locked for 15 minutes Given an IP address across any tokens When more than 20 payment attempts occur within 10 minutes Then subsequent attempts receive HTTP 429 and the IP is temporarily locked for 30 minutes Given a token, device fingerprint, or IP is locked due to velocity rules When an admin views the security dashboard Then the lock reason, timestamp, and auto-unlock time are visible and the admin can manually clear the lock
Edits Require Reissue and Revocation of Old Link
Given staff edits any bound field (amount_total, currency, line_items, max_tip, recipient_id) after a link is issued When the update is saved Then the existing token is revoked immediately and a new link with a new token/signature is generated; the old link returns HTTP 410 with a "Link replaced" message Given a client clicks a revoked link When the page loads Then the UI displays that the link has been replaced and offers a button to request the updated link from the provider
3DS/SCA Enforcement by Region and Risk
Given a card issued in the EEA and a card-not-present transaction When payment is initiated Then SCA is enforced via 3DS; if the challenge fails, the authorization is declined and the link remains usable within TTL for retry Given a non-EEA card When the gateway indicates no step-up is required Then payment proceeds without 3DS while recording liability shift and authentication outcome fields Given the gateway requests step-up for risk When the client completes the challenge successfully Then the charge is created with authentication data attached and the token is invalidated
Webhook Signature Verification and Idempotency
Given a payment success webhook is received When the signature header fails verification against the configured secret or the timestamp is outside a 5-minute tolerance Then the event is rejected with HTTP 400, ignored for state changes, and an audit log entry is created Given a duplicate webhook with the same event_id is received When it is processed Then no duplicate side effects occur (idempotent handling) and a 200 OK is returned Given a verified success webhook is processed When the payment record is updated to succeeded Then the system emits internal events, sends receipts, and marks the token as consumed
Receipts, Ledgering, and Reconciliation
"As a bookkeeper, I want clear receipts and reconciliation reports so that I can close the books quickly and resolve discrepancies."
Description

Automatically send branded receipts via SMS/email upon successful payment, itemizing base service, add-ons, tip, taxes, fees, and payout routing. Post double-entry movements to PawPilot’s internal ledger by appointment, client, and contractor, supporting partial refunds and adjustments with immutable audit trails. Offer daily reconciliation reports (payments, fees, payouts, splits), downloadable exports, and accounting integrations (e.g., QuickBooks/Xero). Expose a dispute workflow with evidence collection tied to the appointment record.

Acceptance Criteria
Immediate Branded Receipt Delivery (SMS/Email)
Given a payment is successfully captured for an appointment When the processor returns a success callback Then send an SMS receipt to the client phone on file within 60 seconds that itemizes base service, add-ons, tip, taxes, fees, and total charged And include business branding and a secure short link to the full receipt And include masked payment method (brand + last4 only), transaction_id, and appointment_id And if an email is on file, send a matching email receipt within 60 seconds And if SMS delivery fails, send the email within an additional 120 seconds and log the SMS error And do not display full card number, CVV, or unmasked links in any channel
Balanced Double‑Entry Ledger Posting on Payment
Given a payment is captured When posting to the internal ledger Then create immutable journal entries where total debits equal total credits (variance = $0.00) And tag entries with appointment_id, client_id, contractor_id, payment_id, journal_id, and UTC timestamp And credit revenue by component (base service, add-ons), credit tax liability for taxes, credit tips payable for tips And debit processor clearing/bank for the gross received and debit processing expense for fees And post within 30 seconds of capture and expose journal_id for audit retrieval
Partial Refunds and Adjustments with Audit Trail
Given an authorized user initiates a partial refund on a posted payment When the refund amount is less than or equal to the original captured amount and not previously refunded Then post reversing ledger entries that balance to zero and link to the original journal_id And update payout liabilities and contractor splits to reflect the refund And send an updated receipt/credit note via SMS/email within 2 minutes showing refunded items and new totals And store an immutable audit record including actor, timestamp, reason, and attachments (if any) And prevent hard deletes; prior states remain accessible via audit history
Daily Reconciliation Report Generation and Delivery
Given a business timezone is configured When the daily reconciliation job runs at 02:00 local time for the prior day Then generate a report with totals for payments, refunds, fees, taxes, tips, payouts, and contractor splits And include opening and closing balances for processor clearing and tips payable that tie to ledger (variance = $0.00) And provide downloadable CSV and PDF in the dashboard and email the report link to admin users And include appointment_id and payment_id per row; row counts equal the number of ledger postings in the period And complete report generation within 10 minutes
Accounting Integrations: QuickBooks/Xero Sync
Given an accounting integration is connected and account/tax mappings are valid When a payment, refund, or adjustment posts in the ledger Then create corresponding objects in QuickBooks/Xero within 5 minutes (e.g., sales receipt/journal entry/credit note) And apply mapped revenue accounts, tax codes, classes/tracking (e.g., contractor as class/vendor) and include appointment_id/payment_id in memo/reference And ensure idempotent sync using a stable external_id to avoid duplicates; retry up to 3 times on transient errors And surface sync status and errors in the dashboard with the ability to re-try failed items
Dispute Workflow and Evidence Collection
Given a dispute/chargeback webhook is received from the processor When the dispute record is created Then open a case linked to the appointment_id and payment_id and place the disputed amount on hold in the ledger And automatically collect evidence (appointment details, SMS transcript, approval logs, receipt, timestamps, photos) and allow staff to upload additional files and notes And enable submission to the processor from within PawPilot before the deadline, recording submission timestamp and evidence hash in the audit log And update dispute status via webhooks and release holds or post loss write-offs accordingly; notify admins via email
Payout Routing, Splits, and Role‑Based Caps
Given role-based caps and approval rules are configured When add-ons and tips are approved and payment is captured Then compute contractor and business shares per policy and post payable/expense entries accordingly And schedule same-day payout for eligible contractor amounts and generate contractor payout statements itemizing appointments, add-ons, tips, fees, and net payout And block postings that exceed caps and route to approval with notifications; log approvals with actor and timestamp immutably And ensure payout batch totals tie to payable reductions in reconciliation (variance = $0.00)

Role Cards

Apply click‑to‑use permission presets (Trainee, Contractor, Dispatcher) that define exactly what each role can view or edit—pet notes, waivers (read‑only), schedule shifts, photo uploads, and more. Fast onboarding, fewer mistakes, and consistent data hygiene as you scale beyond a party of one.

Requirements

Role Preset Catalog
"As an owner, I want ready-made role presets I can apply to team members so that I can onboard quickly and keep access consistent across my business."
Description

Provide click-to-apply role templates (e.g., Trainee, Contractor, Dispatcher, Admin/Owner) with predefined, granular permissions across core PawPilot objects and actions: clients, pets, appointments, shift scheduling, Smart Waitlist operations, waivers (read-only by default), photo/media uploads, messaging, invoices, deposits, refunds, and workspace settings. Include a catalog UI with preset summaries, detailed permission matrices, and a preview mode showing what each role can see/edit. Integrate with user management to set a default role during invitation, per-location defaults, and safeguards against assigning over-privileged roles. Ensure tenant isolation, internationalized labels/help text, and analytics to track preset adoption. Outcome: faster onboarding, fewer permission errors, and consistent data hygiene as teams scale.

Acceptance Criteria
Preset Catalog UI and Permission Matrix
Given I am an Admin/Owner with permission to manage roles When I open Role Cards > Preset Catalog Then I see preset cards for Trainee, Contractor, Dispatcher, and Admin/Owner And each card shows a one-line summary of capabilities And selecting a preset opens a permission matrix listing: Clients, Pets, Appointments, Shift Scheduling, Smart Waitlist Operations, Waivers, Photo/Media Uploads, Messaging, Invoices, Deposits, Refunds, Workspace Settings And each matrix row displays the allowed actions per object (e.g., View, Create, Edit, Delete, Approve/Refund as applicable) And Waivers are marked Read-only by default across presets unless explicitly elevated in the template And switching between presets updates the matrix values without page reload
Role Preview Mode (See/Edit Simulation)
Given I open any preset in the catalog When I click Preview as <Preset Name> Then the app navigation displays only areas visible to that role And forbidden routes accessed via direct URL respond with a 403/Insufficient permissions screen And controls for disallowed actions are hidden or disabled with an “Insufficient permissions” tooltip And Waivers content is viewable but not editable in preview unless the role has edit permission And exiting preview returns me to the same catalog state
Invitation Flow Applies Preset and Per-Location Default
Given Location L has its default role set to Contractor And I open Invite User scoped to Location L When the invitation form loads Then the Role field is prefilled with Contractor And I can override it by selecting a different preset When I send the invite with preset P Then the pending member record stores location=L and preset=P And upon acceptance the user’s effective permissions match preset P And an audit log entry records actor, invitee, preset, location, and timestamp
Over-Privilege Assignment Safeguards
Given I am not an Admin/Owner When I attempt to assign an Admin/Owner role to another user Then the assignment is blocked with the message “You cannot assign roles above your own” And the Save/Invite action remains disabled Given there is exactly one Admin/Owner in the workspace When any user attempts to remove or downgrade that Admin/Owner Then the action is blocked with the message “Cannot remove last Admin/Owner”
Tenant Isolation for Presets and Assignments
Given two separate workspaces A and B with users UA (tenant A) and UB (tenant B) When UA views the preset catalog or fetches a preset by ID via UI/API Then only presets scoped to tenant A (including system defaults) are returned And referencing a preset ID from tenant B returns 403/404 without leaking existence details And applying a preset in tenant A has no effect on users in tenant B
Internationalized Labels and Help Text
Given the workspace locale is set to Spanish (es-ES) When I open the Preset Catalog and Permission Matrix Then preset names, permission labels, and help tooltips render in Spanish And any missing translation keys fall back to English without placeholder artifacts And localized text does not overflow or clip in cards or matrix cells And accessible labels (e.g., aria-labels) reflect the selected locale
Analytics: Preset Adoption and Application Events
Given analytics is enabled When a preset is previewed, applied to a user, a per-location default is set/changed, or an invitation is sent with a preset Then an analytics event is emitted with fields: eventName, tenantId, actorUserId, targetUserId (if applicable), presetKey, locationId (if applicable), timestamp And events conform to the defined JSON schema and are visible in the analytics console within 5 minutes And no PII beyond user and tenant identifiers is included in the payload
Permission Enforcement Engine
"As a business owner, I want permissions enforced consistently across the system so that staff can’t view or change data they shouldn’t."
Description

Implement a centralized role-based access control (RBAC) service that enforces resource- and action-level permissions, including field-level rules (view vs. edit) and explicit read-only enforcement for waivers. Apply checks across all entry points: backend APIs, background jobs, web dashboard, mobile surfaces, and any SMS-triggered staff actions. Use a deny-by-default policy, with permission caching for performance and graceful degradation offline. Provide structured error responses and events for denied actions, plus admin override with justification. Integrate with scheduling, Smart Waitlist, billing/deposits, media storage, and messaging modules to ensure consistent, auditable enforcement. Outcome: uniform permission behavior, reduced risk of data leaks/edits, and simpler maintenance.

Acceptance Criteria
Deny-by-Default Enforcement Across All Entry Points
Given a user lacks an explicit permission for an action on a resource When they attempt the action via web dashboard, API, mobile, SMS, or background job Then the system denies the request with no side effects and returns 403 (or 401 if unauthenticated) Given a newly created role has no grants When a user with that role signs in Then only non-privileged surfaces render and no protected data is returned from any endpoint Given an unauthenticated request is made to a protected endpoint When the request is processed Then the response is 401 without disclosing whether the resource exists
Field-Level Rules and Waivers Read-Only
Given a user has view-only permission on pet profiles When they open pet notes Then edit controls are disabled client-side and any API update attempts return 403 Given any non-admin role accesses a signed waiver When they attempt to modify, delete, or upload a new version via any surface Then the operation is blocked, UI indicates read-only, and the API returns 403 Given a request attempts to update both allowed and disallowed fields on a resource When the update is submitted Then the operation is rejected atomically with 400 and a list of forbidden fields in the response
Cross-Module Enforcement: Scheduling, Waitlist, Billing, Media, Messaging
Given a Contractor lacks billing:send_deposit permission When they attempt to send a deposit link Then the system returns 403 and no link is generated or sent Given a Trainee has schedule:view but not schedule:modify When they attempt to reschedule a booking via Smart Waitlist Then the action is denied and the waitlist remains unchanged Given a user has media:upload permission When they upload visit photos Then the upload succeeds and stored objects have private ACL; without permission, the API returns 403 and no object is created Given an SMS keyword triggers a staff action (e.g., "RESCHEDULE 1234") When the actor’s role lacks the required permission Then the system responds with a standard denial template and performs no state changes
Permission Caching and Offline Behavior
Given user permissions are cached with a defined TTL When the same permission is checked repeatedly within the TTL Then the 95th percentile check latency is ≤ 5 ms and results match authoritative policy Given an admin changes a user’s role When the change is saved Then caches for that user are invalidated and reflect the new role within 10 seconds across services Given the mobile app is offline with last-synced permissions When a user attempts a read they previously had view access to Then the read is allowed from local cache with a "stale" indicator Given the mobile app is offline When a user attempts a write action Then the action is saved as a local draft, not committed server-side, and upon reconnect is permission-checked; if unauthorized, the draft is discarded and the user is notified
Structured Denial Responses and Audit Events
Given an API action is denied by the RBAC engine When responding to the client Then return HTTP 403 with JSON containing fields: code, action, resource, role, reason, correlationId Given a UI action is denied When rendering feedback to the user Then show a standard error banner with a non-sensitive reason and the correlationId Given any denied action occurs on any entry point When the denial is finalized Then emit a PermissionDenied event to the audit stream within 2 seconds including actorId, role, action, resource, entryPoint, timestamp, correlationId
Admin Override With Justification and Audit
Given an admin with override scope encounters a denied action When they provide a justification of at least 15 characters and confirm Then the specific action proceeds once and is fully audited with actor, target, action, justification, timestamp, entryPoint, correlationId Given an admin attempts an override without a justification meeting the minimum length When they submit the override Then the override is rejected and the original denial stands Given an overridden action is executed When audit logs are written Then an OverrideUsed event is emitted and subsequent identical actions without override remain denied
Background Jobs and Service Accounts Fail Closed
Given a background job performs mutations When it runs Then it authenticates using a service account role and only executes actions allowed to that role Given a queued job requires a permission that has since been revoked When the job executes Then it fails closed with no side effects and emits a PermissionDenied event with jobId and resource details Given a data backfill or migration script is invoked When no explicit admin token with audit context is provided Then the script is blocked from executing any write operations and logs an attempted unauthorized operation
Role Assignment & Invite Flow
"As an admin, I want to assign and change team roles from one place so that I can grant the right access quickly and safely."
Description

Create an assignment flow to set a user’s role during invite and manage changes thereafter. Include bulk assignment, effective-dated changes, and side-by-side comparison of current vs. new permissions. Display an at-a-glance permissions summary on each user profile, with links to the underlying matrix. Send notifications to impacted users on role changes and require elevated auth (e.g., MFA recheck) for role escalations. Propagate updates in real time to active sessions and revoke capabilities immediately. Integrate with onboarding checklists and team directories. Include guardrails (cannot remove last Admin) and safe rollback. Outcome: faster, safer administration of team access with clear visibility for owners.

Acceptance Criteria
Assign Role During Invite
Given an owner or admin opens Invite User, When they select a Role Card preset and enter recipient details, Then the Send Invite action is enabled only after a role is selected. Given the invite is sent, When the invitee accepts, Then the user account is created with the selected role applied and visible in Team Directory. Given the invite review step, When the sender clicks View Permissions, Then an at-a-glance summary and a link to the full permissions matrix are displayed. Given a role is selected, When the invite is sent, Then the user's onboarding checklist is created from the selected role template.
Bulk Role Assignment from Team Directory
Given an admin selects 2–200 users in Team Directory, When Apply Role Preset is clicked and a Role Card is chosen, Then all selected users are updated in one operation and a success count is shown. Given any selected user has a pending effective-dated change that conflicts, When the bulk change is submitted, Then the user is excluded with a clear reason and the remaining users proceed. Given the bulk change completes, When viewing Activity, Then a single batch entry with per-user results is present.
Effective-Dated Role Change with Side-by-Side Comparison
Given a user with an existing role, When an admin initiates a role change, Then a side-by-side comparison of current vs. new permissions is shown with differences highlighted. Given the date picker, When a past date is selected, Then validation prevents submission and explains the constraint. Given a future effective date is selected, When the change is scheduled, Then a pending change banner appears on the user profile with the date and new role. Given a scheduled change exists, When canceled before the effective date, Then the schedule is removed and no changes occur. Given the effective date/time arrives, When the system processes the schedule, Then the role is updated and the user and admin are notified.
Real-Time Permission Propagation and Immediate Revocation
Given a role change is applied, When the affected user has active sessions, Then permission updates propagate to all sessions within 5 seconds. Given a role change reduces permissions, When the user attempts a newly forbidden action, Then the action is blocked immediately with a standardized error message. Given cached authorization exists, When the change is applied, Then caches are invalidated and capability checks reflect the new role on the next request.
Role Escalation Requires MFA Recheck
Given an admin attempts to escalate a user to a higher-privilege role, When they submit the change, Then an MFA step-up prompt is required before the change is committed. Given the MFA prompt is presented, When the admin fails or cancels within 5 minutes, Then the change is aborted and no updates occur. Given the MFA step succeeds, When the change is committed, Then an audit entry records who approved, what changed, and when.
Guardrails: Cannot Remove Last Admin and Safe Rollback
Given a workspace with exactly one active Admin, When an action would remove or downgrade that Admin, Then the action is blocked with an explanatory message and no changes are saved. Given any role change has been applied, When an owner or admin clicks Rollback within 30 days, Then the prior role and permissions are restored within 5 seconds and a rollback entry is logged. Given a rollback occurs, When the affected user has active sessions, Then permissions are updated in real time and any regained access is available immediately.
User Profile Permissions Summary with Link to Matrix
Given an admin views a user profile, When the Permissions Summary card is visible, Then it lists key capabilities (e.g., view/edit pet notes, waivers read-only, schedule shifts, upload photos) and the current Role Card. Given the Permissions Summary card, When View Full Matrix is clicked, Then the detailed permissions matrix opens to that user's role. Given a future-dated role change exists, When viewing the user profile, Then a notice shows the upcoming role and effective date.
Custom Role Builder
"As an owner, I want to create and save custom permission sets so that PawPilot fits my specific team structure and policies."
Description

Enable creation of custom Role Cards by toggling fine-grained permissions (resources, actions, field-level rules) to suit unique workflows. Support naming, description, cloning, versioning, and deprecating custom presets. Validate for conflicts (e.g., edit requires view) and enforce minimum safety rules (e.g., waivers remain read-only unless explicitly elevated). Provide import/export of role definitions (JSON) for multi-location consistency. Surface usage analytics and an impact report showing which users and capabilities change when a custom role is edited. Outcome: flexibility to adapt PawPilot permissions to diverse operations without ad hoc workarounds.

Acceptance Criteria
Create Custom Role with Fine-Grained Permissions
Given an admin with Manage Roles permission When they open the Custom Role Builder, configure resource/action/field-level rules, enter a unique name (3–50 characters; letters, numbers, spaces, hyphens only) and an optional description (0–280 characters), and click Save Then the role is created within 2 seconds, appears in the Role Cards list, persists on reload, and can be assigned to users; assigned users immediately have the configured permissions enforced across Pets, Schedule, Waivers, Billing, and Photos features
Dependency Validation: Edit Requires View
Given the Custom Role Builder is open When Edit is enabled for any resource without View Then a blocking validation appears naming the resource and rule, Save is disabled, and enabling View clears the error And when Delete is enabled without Edit (or Approve without View) Then the same dependency validations trigger and must be resolved before Save is enabled
Safety Rule: Waivers Read-Only and Elevation Control
Given creating or editing a role When no explicit waiver elevation is granted Then Waivers permissions default to View-only and cannot include Edit/Delete When an account Owner elevates Waivers to Edit Then a 2-step confirmation with required justification (minimum 10 characters) is shown, an audit log entry (who, when, what changed) is recorded, and Save is allowed; non-Owners cannot elevate
Versioning and Deprecation Lifecycle
Given a role version v1 is assigned to users When changes to permissions are saved Then a new version v2 is created, v1 remains immutable, and current assignees stay on v1 until explicitly migrated And a human-readable diff of permissions/field rules and a timestamped changelog entry are stored When v1 is deprecated Then it cannot be assigned to new users, it is labeled Deprecated in UI/API, a deprecation note (10–200 characters) is required, and a migration assistant is available to move assignees to a newer version with confirmation; all actions are audit-logged
Clone Existing Role Preset
Given any role (preset or custom) exists When the admin clicks Clone Then a new draft role v1 is created with identical permissions and field rules, the name is prefilled as "[Original Name] Copy" (editable), the description is copied, and a derivedFrom reference is stored And Save enforces name uniqueness and all dependency/safety validations
Import/Export Role Definition (JSON)
Given an existing custom role When Export is clicked Then a JSON file downloads conforming to schema role-definition v1 with checksum and version metadata When Import is used with a valid file Then a dry-run presents diffs and runs all dependency/safety validations; on Confirm, the role is created/updated; name collisions resolve with "-imported" suffix unless Overwrite is selected; resource identifiers are remapped for the target account; invalid schemas are rejected with an error code and line number
Impact Report and Usage Analytics
Given an admin edits a custom role that has assignees When they click Review Then an impact report shows the list of affected users, counts by location, and a before/after capability diff; Save requires explicit confirmation acknowledging impact And upon Confirm, changes are applied, affected users receive a notification via configured channel, and analytics update within 15 minutes to reflect assignment counts, most-used permissions, and recent changes
Contextual UI Controls
"As a trainee, I want the interface to only show what I’m allowed to do so that I don’t make mistakes or need constant supervision."
Description

Make the UI permission-aware by hiding or disabling restricted actions and rendering read-only states for protected fields (e.g., waivers). Provide inline explanations/tooltips for disabled controls and a non-blocking "request access" path where applicable. Ensure server-side enforcement mirrors client behavior to prevent bypass. Implement a centralized permission directive/component library used across pages (schedule, client/pet profiles, billing, media). Handle empty states for no-access views and ensure accessibility (ARIA) and localization. Outcome: fewer user errors, clearer guidance for trainees/contractors, and a consistent experience aligned with Role Cards.

Acceptance Criteria
Schedule Page: Trainee Restricted Actions
Given a user with role Trainee lacking Schedule.Edit and Schedule.Manage When the user opens the Schedule page Then the primary actions "New Shift" and "Bulk Publish" are hidden from the toolbar Given a user with role Trainee lacking Schedule.Edit When per-shift action controls render Then "Edit", "Reassign", and "Delete" are disabled with aria-disabled="true" and data-perm="Schedule.Edit" Given a disabled per-shift control When it receives hover or keyboard focus Then a tooltip appears within 200 ms stating the required role (e.g., "Requires Dispatcher or Owner") and dismisses on blur/escape Given a user with role Trainee When they navigate directly to a schedule edit deep-link URL Then the page renders in read-only mode with no save/apply buttons and shows an inline non-blocking notice explaining the restriction
Client/Pet Profile: Read-only Pet Notes and Waivers
Given a user with role Contractor having PetNotes.View and not PetNotes.Edit When opening a Pet Profile Then the Pet Notes section renders as read-only (no editable inputs, no Save button, text selectable) and displays a lock icon with explanatory tooltip Given any role When viewing the Waivers section Then waiver content is read-only (no edit affordances), with available actions limited to Preview/Download, and an inline message clarifies that waivers are protected Given a user without PetNotes.Edit When attempting to type, paste, or programmatically modify Pet Notes Then input is blocked and no draft/save state is created
Protected Views: Standard No-Access Empty States (Billing, Media)
Given a user lacking Billing.View When navigating to /billing via direct URL Then the page renders the standardized No-Access template (lock illustration, localized headline, reason, and secondary help link), and the Billing nav item is not visible in primary navigation Given a user lacking Media.View When opening a Client's Media tab Then thumbnails/list are not requested from the API and the No-Access template is shown with an optional "Request Access" link if the action is requestable Given a No-Access template is rendered When a screen reader user navigates the page Then the template includes role="alert" or aria-live="polite" for the headline and provides a keyboard-focusable primary action (if present)
Request Access: Non-Blocking Flow for Denied Edit
Given an action is requestable by policy and the user lacks permission When the user focuses a disabled control or read-only section Then a "Request Access" link is visible and reachable via keyboard Given the user clicks "Request Access" When the request is submitted Then a POST /access-requests is sent with userId, role, resourceId, action, and context; the API returns 202; a success toast appears within 500 ms; and the current UI remains read-only (no modal blocks task flow) Given a successful access request was submitted in the last 24 hours for the same resource/action by the same user When the user re-opens the area Then the "Request Access" link is suppressed and an inline note indicates a pending request
Centralized Permission Component Adoption Across Pages
Given the permission map defines actions for schedule, client/pet profiles, billing, and media When restricted controls render on these pages Then each control is wrapped with the shared <PermGate> (or directive) providing consistent hide/disable/read-only behavior and tooltips Given the codebase linter rules and unit tests run When scanning for restricted controls Then no instances of ad-hoc permission checks (e.g., role === 'Trainee') are found outside the shared component/directive, and tests cover at least one control per page type Given a change to a role preset (e.g., Contractor gains Media.Upload) When the permission map is updated Then the change propagates across all pages without code changes to individual controls
Server Enforcement Parity With Client Rules
Given a user lacking Schedule.Edit When attempting to call PUT /shifts/{id} via API or devtools Then the server responds 403 with error.code="PERMISSION_DENIED" and no mutation occurs Given a user lacks Billing.View for an organization When requesting GET /billing/summary Then the server responds 404 to avoid information disclosure for non-visible resources Given a request includes protected fields (e.g., waivers, PetNotes) without corresponding edit rights When PATCH /clients/{id} is submitted Then the server rejects the request with 403 and a field-level error array specifying blocked fields Given end-to-end negative tests run When simulating direct API calls for denied actions across schedule, profiles, billing, and media Then all actions are blocked consistently with the same error schema and audited with event type "permission_denied"
Accessible Disabled States and Localized Explanations
Given a control is disabled due to permissions When rendered in the DOM Then it has aria-disabled="true", remains keyboard focusable if discoverability is required, and exposes a tooltip via aria-describedby that is announced by screen readers Given tooltip, inline notice, and empty-state texts When the app locale is switched (e.g., en-US to es-ES) Then all strings are translated using the i18n keys and no hardcoded English strings are present Given a disabled control and its tooltip When evaluated for contrast Then text and iconography meet WCAG AA (>= 4.5:1) and focus outlines meet AA contrast requirements Given keyboard-only navigation When tabbing through a page with disabled controls Then focus order is logical, tooltips can be invoked via focus, and Escape closes them
Access Audit Logs
"As an owner, I want an audit trail of permission changes and denied actions so that I can ensure accountability and resolve issues quickly."
Description

Record a comprehensive audit trail for role-related activities and access outcomes: role assignments/changes, custom role publishes, permission grants, and denied action attempts. Capture actor, target user, resource/action, before/after state, timestamp, IP/device, and optional justification. Provide searchable filters, exports (CSV/JSON), retention controls, and webhooks to forward events to external SIEM/BI tools. Surface alerts for unusual patterns (e.g., repeated denials by a user). Integrate logs with user profiles and resource timelines. Outcome: accountability, faster troubleshooting, and compliance-ready visibility into access governance.

Acceptance Criteria
Comprehensive Role Event Logging
Given an Admin assigns, changes, or revokes a role or publishes a custom role When the action is confirmed Then an audit event is appended within 2 seconds containing: event_id (UUID), actor_user_id, actor_role, target_user_id, action_type ∈ {role_assigned, role_changed, role_revoked, custom_role_published, permission_granted, permission_revoked}, resource_type, resource_id (nullable), before_state (JSON), after_state (JSON), outcome = "allowed", timestamp (UTC ISO 8601), ip_address, device_fingerprint, justification (nullable), request_id, version And the event store is append-only; updates/deletes are disallowed; corrections are new events referencing prior event_id And before_state/after_state accurately reflect permission diffs with sensitive values redacted And duplicate events for the same request_id are prevented (idempotency)
Denied Action Attempt Logging and Justifications
Given a user lacks permission for a protected action (e.g., edit waiver, view pet notes) When they attempt the action Then an audit event is recorded with outcome = "denied" including: actor_user_id, attempted_action, resource_type, resource_id, evaluated_permissions, reason_code, timestamp (UTC), ip_address, device_fingerprint, session_id, justification (nullable), request_id And if justification_required = true for the action, Then a non-empty justification (<= 500 chars) must be supplied or the request is rejected with 400 and no state change And consecutive denials in the same session are correlated via session_id and a denial_count field increments
Search, Filter, and Export Audit Events
Given an Admin opens the Audit Logs page When they filter by actor_user_id, target_user_id, action_type, outcome, resource_type, date range (UTC), ip_address, justification presence Then the result set matches the filters exactly and sorts by timestamp asc/desc; pagination supports 25/50/100 per page And for 10,000 matching events, p95 query response time is ≤ 2 seconds When Export CSV is triggered with current filters Then a UTF-8 CSV with headers and all filtered rows is generated; values are properly quoted; timestamps are UTC ISO 8601 When Export JSON is triggered Then a newline-delimited JSON (NDJSON) file with one event per line is generated with the same fields And exports up to 100,000 rows complete within 60 seconds via an expiring (24h) download link
Webhook Forwarding to External SIEM/BI
Given a webhook destination with shared secret is configured and enabled When a new audit event is created Then the system POSTs the event JSON within 5 seconds including headers: X-PawPilot-Signature (HMAC-SHA256), X-Event-Id, X-Event-Type, X-Event-Version And deliveries are at-least-once with exponential backoff retries for non-2xx/timeouts up to 24 hours and are idempotent by X-Event-Id And admins can send a test event, view last delivery status/code/latency, and pause/resume the destination And sensitive fields are redacted per policy before delivery
Retention and Deletion Controls
Given an Admin sets audit retention to 90 days When the nightly purge job runs Then events older than 90 days are permanently deleted and excluded from UI, exports, API, and webhooks And available retention options include 30/90/365 days and custom up to 730 days; changes apply prospectively and are themselves audited And events on legal hold (by tag) are exempt from purge until the hold is removed; hold changes are audited And GDPR erasure for a specific user removes or pseudonymizes PII fields within 7 days while preserving event structure and integrity
Anomaly Detection Alerts for Access Patterns
Given anomaly rules are enabled When any user records ≥ 5 denied attempts for the same action within a 10-minute window Then an alert is created within 1 minute, displayed in the Admin Alerts panel, and an email notification is sent to Owners And rule thresholds and lookback windows are configurable; alerts are deduplicated to at most 1 per user/action per 15 minutes and auto-resolve after 24 hours without recurrence And the alert links to a pre-filtered audit log view including actor, action, count, window, and last_seen timestamp
User Profile and Resource Timeline Integration
Given an Admin views a user profile When they open the Access & Roles tab Then a chronological timeline of that user's role changes, permission grants/revokes, and access denials is displayed with pagination, local time display, and deep links to full audit entries Given an Admin views a resource (e.g., pet or waiver) When they open its timeline Then permission/access-related events for that resource are shown with actor, action, and timestamp and link to the full audit view And access to audit timelines is restricted to roles with audit.read; unauthorized users see no audit data and a standard error
Scheduling & Waitlist Permissions
"As a dispatcher, I want permissions to manage shifts and the Smart Waitlist so that I can quickly fill cancellations without exposing sensitive billing or waiver data."
Description

Define and enforce granular capabilities for schedule and Smart Waitlist operations: create/edit shifts, reassign appointments, fill cancellations, send deposit links, confirm/cancel bookings, and trigger client SMS messages. Restrict trainees to view-only and allow contractors to act only on their own jobs; grant dispatchers full scheduling and waitlist controls without exposing billing or waiver edits. Wire these permissions into the scheduling UI, drag-and-drop interactions, and automated SMS workflows to prevent unauthorized changes. Provide clear UI affordances and logs for actions taken. Outcome: accelerated fill rates with guardrails that prevent costly scheduling mistakes.

Acceptance Criteria
Trainee: View-Only Scheduling & Waitlist
Given a user with role Trainee is authenticated When they open the Scheduling calendar or Smart Waitlist views Then all action controls (Create/Edit Shift, Reassign, Fill from Waitlist, Send Deposit, Confirm, Cancel, Drag Handle) are disabled or hidden Given a Trainee attempts any scheduling or waitlist action via the UI When they click or drag Then the action is prevented, an "Insufficient permissions" notice is shown, no data is changed, and no SMS is sent Given a Trainee calls any scheduling or waitlist API endpoint directly When the request is made Then the API returns 403 SCHED_PERMISSION_DENIED and no side effects occur
Contractor: Own-Job Scheduling Actions Only
Given a user with role Contractor is authenticated When viewing appointments assigned to them Then controls to Confirm, Cancel, Edit Notes, Send Deposit Link, and Fill cancellations for their own jobs are enabled Given a Contractor acts on an appointment not assigned to them When they attempt to Create/Edit/Reassign/Fill/Confirm/Cancel or Send Deposit Then the UI prevents the action and the API returns 403 without side effects Given a Contractor attempts to reassign any appointment to another staff member When the action is initiated (UI or API) Then the action is blocked and logged as denied Given a Contractor drags one of their own appointments to a new time within allowed business rules When the change is dropped Then the appointment updates successfully, applicable client SMS is sent using the correct template, and the action is logged
Dispatcher: Full Scheduling & Waitlist; No Billing/Waiver Edits
Given a user with role Dispatcher is authenticated When using Scheduling and Smart Waitlist Then controls for Create/Edit Shift, Reassign, Fill from Waitlist, Send Deposit Link, Confirm, Cancel, and drag-and-drop are enabled and function Given a Dispatcher attempts to access Billing or edit Waivers When navigating to those views or performing edit actions Then UI hides or disables edit controls and API returns 403 for edit attempts; waiver view remains read-only Given a Dispatcher fills a cancellation from the Waitlist When the deposit is paid by the client Then the booking auto-confirms and the slot is updated
Role-Based UI Affordances for Scheduling Controls
Given any authenticated user When the scheduling UI renders Then only controls permitted by their role are visible or enabled; forbidden actions are hidden or disabled with a tooltip "Insufficient permissions" Given a disabled control is hovered or invoked When interaction occurs Then a consistent permission tooltip or toast appears and no network request is sent Given the user role changes mid-session When the page is refreshed or a role-change event is received Then the UI updates control states within 2 seconds to reflect the new permissions
Drag-and-Drop Scheduling Changes Respect Permissions
Given role Dispatcher When dragging an appointment to a new time or provider Then the drop succeeds if business rules pass, the appointment updates, conflicts are recalculated, applicable client SMS is sent, and the action is logged Given role Contractor When dragging their own appointment Then the drop is allowed for time changes within allowed bounds but not allowed to reassign to another provider; blocked drops show a permission message and do not change data Given role Trainee When attempting any drag Then the drag handle is disabled and no changes occur Given any unauthorized drag attempt When a drop is blocked by permissions Then the server returns 403 if a request is made and no SMS is sent
Smart Waitlist Fill and Deposit Link Respect Permissions
Given role Dispatcher When selecting "Fill from Waitlist" for an open slot Then the system suggests eligible clients by priority, allows sending a deposit link, sends the SMS upon confirmation, tracks deposit status, and auto-confirms on successful payment Given role Contractor When filling a cancellation for a job assigned to them Then they can invite waitlisted clients and send a deposit link for that specific job; attempts to fill cancellations not assigned to them are blocked (UI + 403) Given role Trainee When attempting "Fill from Waitlist" Then the control is disabled or hidden and no requests are made Given any blocked waitlist action When attempted Then no SMS or deposit link is sent
Action and SMS Audit Logging
Given any scheduling or waitlist action is attempted or completed When the event occurs Then an audit record is written with action type, acting user id and role, target entity ids, before/after state, timestamp, origin (UI/API), outcome (success/denied), and related SMS/deposit identifiers Given audit records exist When viewed by Dispatcher or Admin in the Activity Log Then entries are visible within 5 seconds of the action, immutable, and exportable to CSV Given a non-privileged role (Trainee/Contractor) attempts to edit or delete audit records When the attempt occurs Then the action is blocked and returns 403

Trail Ledger

An immutable audit trail of access and actions: who viewed a client, when addresses were revealed, messages sent, schedule edits made, and policy acknowledgments logged. Exportable for disputes, insurance, and payroll reconciliation—delivering accountability and peace of mind without extra admin.

Requirements

Append-only Ledger Core
"As an owner-operator, I want a tamper-evident record of all critical actions so that I can prove what happened during audits and disputes."
Description

Implement a tenant-scoped, append-only audit ledger that records all critical events (views, address reveals, outbound/inbound messages, schedule changes, policy acknowledgments, payments/deposits) with immutable, tamper-evident storage. Each event includes a globally unique ID, UTC timestamp with source and server clocks, actor identity and role, resource references (client, job, message), channel (SMS, web, API), and a cryptographic hash chain to detect alteration or deletion. Data is encrypted at rest, partitioned by tenant for performance and data isolation, and written via idempotent APIs to prevent duplicates under retries. The ledger supports high-write throughput, backpressure, and durability guarantees, and persists minimal redacted summaries where full content is sensitive while keeping content hashes for verification.

Acceptance Criteria
Immutability and Hash Chain Integrity
Given a ledger with existing events, When a new event is appended, Then its previous_hash equals the last event's hash and the computed ledger hash chain remains consistent end-to-end. Given any attempt to update or delete a past event by event_id, When the request is made, Then the system rejects it with 405 and no stored data changes. Given an offline verification job, When it recomputes hashes across all events, Then any alteration or deletion is detected and the index of first divergence is reported.
Idempotent Write API Under Retries
Given a client provides an idempotency key per event, When the same request is retried N times, Then exactly one event is stored and subsequent responses return 200 with the original event_id. Given two concurrent writes with the same idempotency key, When processed, Then only one event is appended and the other returns 200 with the same event_id or 409 IdempotentConflict without creating a duplicate. Given a second submission with the same client_event_id but a different idempotency key, When submitted, Then the API returns 409 DuplicateEvent and no new record is written.
Event Schema Completeness and Validation
Given an append request, When required fields are missing or malformed (non-unique event_id, non-UTC timestamps, missing actor_role, invalid channel), Then the API returns 422 with field-level errors and nothing is stored. Given a valid append request, When stored, Then the event contains: globally unique event_id, source_timestamp_utc, server_timestamp_utc, actor_id, actor_role, tenant_id, resource references (client_id/job_id/message_id as applicable), channel, event_type, previous_hash, and event_hash. Given events of type view, address_reveal, message_inbound, message_outbound, schedule_change, policy_ack, and payment/deposit, When appended, Then each validates its required resource references and is accepted with 201.
Sensitive Content Redaction and Hash Verification
Given an event includes sensitive content, When written to the ledger, Then only a minimal redacted summary is persisted and a content_hash (e.g., SHA-256) of the original content is stored. Given original content and the stored content_hash, When verification is executed, Then the computed hash must match exactly; otherwise verification fails. Given a read or export of ledger data, When performed, Then no raw sensitive content is returned—only redacted summaries and hashes.
Tenant Partitioning and Data Isolation
Given tenant-scoped credentials, When writing or reading events, Then operations are restricted to the provided tenant_id and never return or mutate another tenant’s events. Given a cross-tenant access attempt (mismatched tenant_id), When executed, Then the system returns 403 and no data is revealed or written. Given two tenants A and B, When tenant A spikes to 2,000 events/sec for 10 minutes, Then tenant B’s p99 write latency remains ≤ 200 ms and error rate ≤ 0.1%.
High Throughput, Backpressure, and Durability
Given sustained load of 5,000 events/sec per region for 10 minutes, When writes are performed, Then p95 write latency ≤ 100 ms, p99 ≤ 250 ms, and successful write rate ≥ 99.9% excluding deliberate backpressure responses. Given downstream storage slowdown, When backlog exceeds threshold, Then the API responds with 429 and Retry-After, and no acknowledged writes are lost; upon recovery, backlog drains without reordering within a tenant. Given a process or node failure, When the system restarts, Then all previously acknowledged events are present with contiguous hashes and no duplicates.
Encryption at Rest and Key Management
Given any ledger data persisted to storage, When inspected, Then encryption at rest is enabled using cloud KMS–managed keys (AES-256 or stronger). Given a scheduled key rotation, When new events are written and existing events are read, Then operations succeed without downtime and both old and new data remain accessible per KMS policy. Given direct storage access without KMS permissions, When attempted, Then ciphertext is unreadable and no plaintext is recoverable.
Access & Address View Tracking
"As an account owner, I want a precise log of who viewed client details and when so that I can enforce privacy policies and ensure accountability."
Description

Capture and record every access to client records with fine-grained detail, including when a profile is viewed, when a masked address is revealed, by whom, from which device/IP, and from what context (e.g., conversation thread, booking screen). Require a selectable reason code when revealing addresses, differentiate masked preview vs full reveal, and annotate whether access was manual or system-driven. Store role and permission snapshot at access time to support after-the-fact reviews. Provide safeguards to ensure tracking on mobile and web, even in offline/queued scenarios.

Acceptance Criteria
Log Client Profile View (Web & Mobile)
Given an authenticated user with access to a client record When the user opens the client's profile from any entry point (conversation, booking, client list, waitlist, search, deeplink) Then an audit event of type "client.profile.view" is created within 500 ms of the profile view rendering And the event includes userId, orgId, roleSnapshot, permissionSnapshot, occurredAt (UTC), appPlatform (web|iOS|Android), appVersion, deviceModel, ipAddress (nullable), context (from allowed set), manualOrSystem="manual", and requestId And the event persists if the user navigates away within 1 second And no duplicate "client.profile.view" event is created for re-renders within the same screen instance
Address Reveal With Required Reason & Preview Differentiation
Given a client profile shows a masked address When the screen loads Then no reveal event is logged, and at most one "client.address.preview" event is logged per screen instance Given the user taps Reveal Address Then a reason modal appears with selectable reason codes: Navigate to appointment, Verify identity, Emergency, Pickup/Drop-off, Other And the confirm action is disabled until a reason code is selected And if "Other" is selected, a free-text reason between 1 and 140 characters is required When the user confirms Then the full address is displayed And a "client.address.reveal" event is logged with reasonCode and reasonOther (nullable), including userId, roleSnapshot, permissionSnapshot, device/IP, occurredAt (UTC), context, and manualOrSystem="manual" And closing the modal or cancelling does not reveal the address and logs no reveal event
Unauthorized Address Reveal Attempt Handling
Given a user without the can_reveal_address permission When they attempt to reveal a client's address Then the address remains masked and a blocking message is shown: "You don't have permission to reveal addresses." And a "client.address.reveal.denied" event is logged with userId, orgId, roleSnapshot, permissionSnapshot, occurredAt (UTC), context, appPlatform, appVersion, device/IP, and manualOrSystem="manual" Given a user with the can_reveal_address permission When they attempt to reveal a client's address Then the reason capture flow is enforced as defined in the Address Reveal criteria before the address is shown
Role & Permission Snapshot Is Immutable
Given a user with role "Staff" and effective permissions P at time T1 When the user views a client profile and an audit event is logged Then the event stores roleSnapshot="Staff" and permissionSnapshot=P And after the user's role changes to "Admin" at time T2 Then the previously stored event from T1 remains unchanged when retrieved (roleSnapshot and permissionSnapshot are identical to values at T1) And subsequent events reflect the new role/permissions at T2+
Manual vs System-Driven Access Annotation
Given a background automation accesses a client profile for system processing (e.g., prefetch or export) When the access occurs Then a "client.profile.view" event is logged with manualOrSystem="system" and actorId="system" And no "client.address.reveal" events can be generated by system-driven processes Given a human user navigates to a client profile When the access occurs Then the event is logged with manualOrSystem="manual" and actorId=userId
Offline/Queued Audit Event Capture & Sync
Given the device is offline when a profile view or address reveal occurs When the event should be logged Then it is persisted locally with clientEventId, clientOccurredAt (monotonic), payload, and retry metadata And the action proceeds only after required inputs are captured (e.g., reason code for reveal) When connectivity is restored Then queued events are transmitted within 60 seconds And the server assigns eventId and serverReceivedAt (UTC) while preserving event order using clientOccurredAt And events survive app restarts and are deduplicated via clientEventId
Device/IP and Field Validation on Event Log
Given an audit event is created When the event is validated Then ipAddress, if present, matches IPv4 or IPv6 format; otherwise ipAddress=null and captureError is set And appVersion matches SemVer (MAJOR.MINOR.PATCH) And context is one of the allowed enumerations (conversation, booking, client_list, waitlist, search, deeplink, unknown) And events failing validation are not persisted and are retried up to 3 times with backoff; upon final failure, a non-blocking error is recorded to telemetry
Messaging Audit with Redaction
"As a groomer, I want verifiable logs of messages and deposit links being sent and delivered so that I can resolve client disputes and reduce no-shows."
Description

Log all outbound and inbound SMS/MMS interactions with delivery lifecycle states, including provider message IDs, send/receive timestamps, delivery receipts, failures, and retries. Store content in a privacy-safe manner by redacting PII and retaining a content hash and length, and hashing media attachments while preserving file type and size metadata. Link each message event to the related client, staff member, job, and deposit link when applicable. Normalize statuses across providers and capture URL shortener expansions and click events when available. Maintain causality by correlating messages within threads and associating automation rules that initiated the send.

Acceptance Criteria
Outbound SMS Lifecycle Logged with Provider Normalization
Given an outbound SMS is submitted via any configured provider When delivery lifecycle webhooks/events are received (queued, sending, sent, delivered, failed, undeliverable, expired) Then the audit record stores a single logical message_id, provider_message_id per attempt, normalized_status in {queued, sending, sent, delivered, failed, undeliverable, expired}, attempt_number, provider_error_code/message (if any), and precise UTC timestamps for initial send, last update, and terminal state And retries are recorded as attempts linked to the same logical message with causality preserved And message direction = outbound and channel = SMS are stored
Inbound Message Logged with Thread Causality
Given an inbound SMS or MMS is received from a known client number When the system matches it to an existing conversation thread and related records Then the audit event stores direction = inbound, channel = SMS|MMS, provider_message_id, receive_timestamp_utc, and thread_id And links client_id, job_id (if resolvable), and staff_id (if assigned) And if it is a reply to a specific outbound message within the thread, parent_message_id is set to maintain causality And if job cannot be determined, job_id is null and linking_status = unresolved
Content Redaction and Text Hashing
Given any SMS body content When persisting the audit record Then raw body text is not stored; only redacted_body with tokens [REDACTED:PHONE], [REDACTED:EMAIL], [REDACTED:ADDRESS], [REDACTED:URL] replacing detected PII patterns And body_hash = SHA-256 of the original body, body_length_bytes of the original body, and charset are stored And read APIs return only redacted_body, body_hash, body_length_bytes, and charset
MMS Media Hashing and Metadata Preservation
Given an MMS contains one or more media attachments When persisting the audit record Then for each attachment the system stores media_hash = SHA-256 of the binary, media_size_bytes, media_mime_type, and attachment_index; the binary or external URL is not stored And total_attachments is recorded for the message And read APIs expose only the metadata and hash for attachments
Entity Linking: Client, Staff, Job, and Deposit Link
Given any inbound or outbound message event When persisting the audit record Then the event links to client_id, staff_id (if applicable), job_id (if applicable), and deposit_link_id (if a PawPilot deposit link is present) And for messages without a deposit link, deposit_link_id is null And referential integrity is enforced: each linked id must exist or the field is null with linking_status = unresolved
URL Expansion and Click Tracking
Given a message body contains one or more URLs (including supported shorteners) When the system processes the message and receives provider or internal expansion data Then each short_url is expanded to final_url and stored in audit metadata with url_expanded = true And when click webhooks are received, click events are recorded with message_id, short_url, final_url, contact/client reference (if resolvable), and click_timestamp_utc And if expansion or click data is unavailable, url_expanded = false or click_tracking = false is stored
Automation Rule Attribution
Given an outbound message is initiated by an automation rule When the audit record is created Then automation_rule_id and automation_run_id are stored and linked to the message event And for a manually sent message, origin = manual is stored and staff_id is required And reports/queries can filter messages by origin = automation or manual
Schedule & Smart Waitlist Change Log
"As an operations manager, I want a complete audit of schedule edits and waitlist fills so that payroll and client dispute reviews are accurate and fast."
Description

Record all scheduling mutations with before/after diffs, including job creation, time changes, staff reassignments, cancellations, no-shows, deposits requested/paid/refunded, and Smart Waitlist auto-fills. Annotate whether a change was manual or automated, capture the decision snapshot for auto-fills (candidate list, ranking signals, chosen contact), and store reason codes for edits and cancellations. Provide the ability to reconstruct the schedule for any historical day from the ledger, supporting payroll reconciliation and dispute timelines.

Acceptance Criteria
Log Before/After Diffs for Schedule Mutations
Given a job exists or is being created When any of the following mutations occur: job_created, time_changed, staff_reassigned, canceled, marked_no_show, deposit_requested, deposit_paid, deposit_refunded, smart_waitlist_autofilled Then the system creates a ledger entry with: event_type, job_id, actor_type (user/system/client), actor_id (nullable for system), occurred_at (UTC), before_snapshot (null for creation), after_snapshot, diff_fields[], correlation_id And the diff reflects old and new values for any changed fields including start_at, end_at, staff_id, client_id, status, price, deposit_status, deposit_amount And ledger entries are append-only; attempts to update or delete a ledger entry are rejected and logged as a security event
Annotate Manual vs Automated Changes
Given any schedule mutation is initiated When the initiator is a human user via UI or API Then the ledger entry records source=manual and includes user_id and surface (UI/API) When the initiator is an automation (e.g., Smart Waitlist) Then the ledger entry records source=automated and includes automation_name, automation_version, automation_run_id And a correlation_id ties related actions (e.g., decision snapshot, message send, job update) within the same run
Capture Smart Waitlist Decision Snapshot
Given an open slot triggers Smart Waitlist evaluation When an auto-fill decision is made Then the ledger entry stores decision_snapshot containing: candidate_list [{contact_id, rank, score, key_signals[], eligibility_flags}], chosen_contact_id, reason_for_selection, evaluated_at (UTC) And any message dispatch is recorded with template_id, channel, message_id, sent_at (UTC) And the outcome is captured as accepted/declined/timeout with resolved_at (UTC) And if no candidate is eligible, the entry records reason=no_eligible_candidates and evaluated_count
Enforce Reason Codes for Edits and Cancellations
Given a user attempts a time edit, staff reassignment, cancellation, or marks a no-show When the change is submitted Then a reason_code from the configured set for that mutation type is required and an optional free_text_note (<=500 chars) may be provided And the reason_code and free_text_note are recorded on the ledger entry And for client-initiated cancellations via SMS, the system maps the intent to a standardized reason_code and records the originating message_id And if a required reason_code is missing, the change is blocked with a validation error and no ledger entry is written
Reconstruct Historical Schedule by Day
Given a request to reconstruct the schedule as_of a specified timestamp and timezone When the reconstruction function/endpoint is invoked Then the system replays ledger entries and returns the schedule state (jobs with start/end times, staff, status, price, deposit_status) exactly as of that instant And repeated reconstructions with the same as_of yield identical results And the reconstructed state for a validation dataset matches a golden snapshot And each job includes a pointer to the last ledger event_id that produced its current state
Export Ledger for Disputes and Payroll
Given a user requests a ledger export filtered by date range and optional staff_id, client_id, and event_types When the export is generated in CSV and JSON formats Then each record includes: event_id (ULID), occurred_at (UTC), event_type, job_id, actor_type, actor_id, source, correlation_id, reason_code, before_snapshot, after_snapshot, diff_fields And the export includes a file-level hash and per-record content_hash to verify immutability And a totals section reports counts by event_type that match in-app/query results for the same filters And the export is available via download and via API using a signed URL with a 24-hour expiry
Policy Acknowledgment Capture
"As a business owner, I want verifiable records of policy acceptance per client and booking so that I meet insurance and compliance requirements."
Description

Version and hash every policy document (terms, cancellation, liability waivers) and record client acknowledgments with timestamp, channel (SMS link, web), device/IP, locale, and policy version at acceptance time. Support SMS "YES" consent capture with canonicalized text snapshots and hashes, and link acknowledgments to bookings where relevant. Preserve evidence sufficient for insurers and chargeback responses while minimizing stored PII through selective redaction and hashing.

Acceptance Criteria
Policy Document Versioning and Hashing
Given a policy document is created, When it is saved, Then the system assigns a new immutable policy_version_id, computes and stores a SHA-256 hash of the exact document bytes, records created_at (UTC) and created_by, and marks the version as active. Given an existing policy_version_id, When an admin attempts to edit its contents, Then the system prevents modification and requires creating a new version; the original version remains retrievable and unchanged. Given multiple versions of the same policy exist, When the current version is requested, Then the system returns the most recently created active version and its hash.
Web Policy Acknowledgment Recording
Given a client opens a secure web policy link and clicks "I Agree", When the acceptance is submitted, Then the system records an immutable acknowledgment with fields: client_id, policy_version_id, accepted_at (UTC), channel="web", ip_truncated, user_agent_hash, locale, evidence_record_hash. Given a recorded web acknowledgment, When it is retrieved by client_id and policy_version_id, Then all fields are present and match the original capture, and the record is read-only.
SMS YES Consent Capture and Canonicalization
Given a policy consent request SMS containing a unique request_id tied to policy_version_id is sent, When the client replies with text that canonicalizes to "YES" (trim whitespace and uppercase), Then the system records an acknowledgment with fields: client_id, policy_version_id, accepted_at (UTC), channel="sms", request_id, message_id, msisdn_hash, locale, canonicalized_text="YES", canonicalized_text_hash, evidence_record_hash. Given a reply that does not canonicalize to "YES", When it is received, Then no acknowledgment is recorded and the client is sent a clarification SMS containing a secure web confirmation link. Given multiple outstanding consent requests, When a "YES" reply is received, Then the acknowledgment is linked to the most recent open request_id for that client.
Acknowledgment Linked to Bookings
Given a client accepts a policy during a booking flow that includes booking_id context, When the acknowledgment is recorded, Then the acknowledgment stores booking_id and is retrievable from the booking details. Given a booking is canceled or modified after acceptance, When the acknowledgment is viewed, Then the acknowledgment remains intact and continues to reference the original booking_id and policy_version_id.
PII Minimization via Redaction and Hashing
Given an acknowledgment is recorded, When storing IP address, Then the system stores IPv4 addresses truncated to /24 and IPv6 addresses truncated to /64. Given an acknowledgment is recorded, When storing phone number (MSISDN), Then the system stores the last 4 digits in clear and a salted SHA-256 hash of the full E.164 number; the full number is not stored in the acknowledgment record. Given an acknowledgment is recorded, When storing device information, Then the system stores a salted SHA-256 hash of the user agent string and does not retain the raw user agent in the acknowledgment record. Given an acknowledgment is recorded, When storing SMS reply content, Then the system stores canonicalized_text and a SHA-256 hash of the raw reply; the raw SMS body is not stored in the acknowledgment record. Given an acknowledgment is recorded, When associating client identity, Then the system stores client_id only and does not duplicate name or email in the acknowledgment record.
Evidence Export for Disputes and Insurance
Given a user with role "Owner" or "Compliance" requests an Evidence Export for a date range, When the export is generated, Then the file includes for each record: client_id, policy_version_id, policy_hash, accepted_at, channel, ip (truncated), device_hash, locale, booking_id (if any), canonicalized_text (if SMS), canonicalized_text_hash, evidence_record_hash. Given the export request is created, When it completes, Then the export is made available as a signed CSV and JSON, with a manifest containing a SHA-256 checksum for each file, and an audit log entry is recorded with who, when, and filters used. Given the export is requested in "evidence" mode with justification text, When it is generated, Then IP addresses are untruncated and MSISDN hashes are accompanied by the last 7 digits; access to the file expires after 72 hours.
Immutability and Tamper Evidence
Given an acknowledgment record exists, When any user attempts to update or delete it, Then the system disallows the operation; only an additional record with a supersedes_of reference and reason can be added without altering the original. Given an acknowledgment record is retrieved, When its evidence_record_hash is recomputed from stored fields, Then it matches the stored value, demonstrating tamper-evidence. Given any acknowledgment record is viewed or exported, When the action occurs, Then a Trail Ledger entry records actor_id, action, timestamp (UTC), and record_id.
Ledger Query & Export Console
"As a business owner, I want to query and export a filtered audit package so that I can share precise evidence with insurers and payroll without extra admin work."
Description

Provide an admin console to search and filter the audit ledger by date range, user, client, job, event type, and channel, with instant results and pagination. Offer timeline views per client and per job, event detail drawers, and bulk selection. Enable export to CSV, JSON, and a PDF evidence package that includes an integrity manifest (root hash, signatures), optional watermarks, and ready-made templates for disputes, insurance claims, and payroll reconciliation. Respect role-based permissions and automatically redact fields based on viewer rights.

Acceptance Criteria
Multi-filter Search with Instant Results and Pagination
Given a ledger containing at least 1,000,000 events across multiple users, clients, jobs, event types, and channels When the admin sets a date range, selects a user, client, job, event type(s), and channel(s), and applies the filter Then the results show only events matching all selected filters (logical AND), sorted by timestamp descending, with a visible total count Given a result set of 10,000 events or fewer When filters are applied or changed Then the first page renders within 500 ms and shows up to the configured page size (default 50), and an empty-state is shown if count = 0 Given a result set greater than 10,000 and up to 1,000,000 events When filters are applied or changed Then the first page renders within 2,000 ms and subsequent page navigations render within 1,000 ms Given the user navigates pages When moving Next/Previous or jumping to a page Then no event appears twice or is skipped across pages, the final page contains <= page size events, and Next is disabled on the last page Given filters are applied When the page is reloaded or the URL is shared Then the same filters and page are restored from the URL query parameters Given a date range filter with explicit start and end When events occur exactly at the start or end timestamp Then those boundary events are included in results
Timeline Views per Client and per Job
Given a client with events across multiple jobs and channels When the user opens the client's timeline view Then events are displayed in reverse chronological order and include views, address reveals, messages sent, schedule edits, and policy acknowledgments Given a specific job with associated events When the user opens the job's timeline view Then only events related to that job are shown with the same event coverage and ordering Given a timeline with more than one page of events When the user scrolls or paginates Then the next batch loads within 800 ms and maintains position without jitter Given event type and channel filters in timeline view When the user toggles them Then the visible events update instantly (<= 400 ms for <= 5,000 events) and the filter state is indicated Given an event in the timeline When the user selects "Copy link" Then a shareable deep link opens the timeline scrolled to and highlighting that event
Event Detail Drawer with Redaction
Given an event is visible in any list or timeline When the user clicks the event row Then an event detail drawer opens within 300 ms Given a "message sent" event and the viewer has permission to view message content When the drawer opens Then it shows the message body, channel, recipient, and delivery status; otherwise the body is replaced with [REDACTED] Given a "schedule edit" event When the drawer opens Then it shows before and after values for the changed fields Given an "address revealed" event and the viewer lacks permission to view addresses When the drawer opens Then the address fields are replaced with [REDACTED], and no raw data is present in the DOM or network responses Given any event When the drawer opens Then it shows event ID, type, actor identity, timestamp with timezone, related client and job, and a "Copy Event ID" action Given the drawer is open When the user presses ESC or clicks outside Then the drawer closes without altering selection or filters
Bulk Selection Across Pagination
Given a filtered result set spanning multiple pages When the user selects "Select all on this page" Then only the current page's events are selected and a selection count is shown Given a filtered result set When the user selects "Select all N results" Then all events matching the current filters are selected across pages and the count equals the total results Given a selection is active When the user changes any filter Then the selection is cleared after an explicit confirmation prompt and the selection count resets to 0 Given multiple events are selected When the user invokes a bulk action Then only the selected events are affected and the action button is disabled when selection count = 0
CSV and JSON Exports Fidelity
Given a set of selected events or an applied filter with no explicit selection When the user exports to CSV Then the file is UTF-8 encoded with a header row and one row per event reflecting redactions, and the row count equals the selected/result count up to the export limit Given a set of selected events or an applied filter with no explicit selection When the user exports to JSON Then the file is a valid UTF-8 JSON array of event objects reflecting redactions, preserving ISO 8601 UTC timestamps Given an export of up to 50,000 events When the export is requested Then the file is generated and downloaded within 30 seconds and the exported events are identical to those visible in the UI for the same filters Given an export is generated When it is opened Then event ordering matches the UI (timestamp descending) and field names match the API/column labels shown in the console
PDF Evidence Package with Integrity Manifest and Templates
Given selected events or the current filtered result set When the user exports to PDF Then a single PDF is generated containing a cover page (exporting user, generation timestamp, applied filters), a chronologically ordered event timeline, and an integrity manifest section Given the PDF integrity manifest When verified Then it includes a cryptographic root hash of included events and a platform signature that validates against PawPilot's published public key Given watermarking is toggled on with a supplied label When the PDF is generated Then every page contains a visible diagonal watermark with that label; when toggled off, no watermark is present Given the user selects a template (Dispute, Insurance Claim, Payroll Reconciliation) When the PDF is generated Then the content layout matches the template, including the required summary sections and appendices for that template Given a set of up to 5,000 events When exporting to PDF Then the file is generated within 60 seconds and opens without errors in standard PDF viewers
Role-Based Access Control and Automatic Redaction
Given a user without ledger viewing rights When they attempt to access the Ledger Query & Export Console Then access is denied with a 403 error and no ledger data is returned Given a user with view rights but without export rights When they attempt to export Then export controls are hidden or disabled and direct export endpoint calls return 403 Given a user lacking permission to view sensitive fields (e.g., client address, message content) When they view lists, timelines, details, or any export Then those fields are redacted consistently as [REDACTED] and cannot be recovered from the UI, files, or network responses Given an authorized user performs a search, view, or export When the action is completed Then the action itself is logged in the audit trail with actor, timestamp, scope (filters/selection), and outcome (success/failure)
Integrity Sealing & Tamper Alerts
"As an owner, I want automatic integrity checks and alerts on the audit trail so that any tampering or data loss is detected immediately."
Description

Periodically seal the ledger by computing and storing signed Merkle roots over recent events, with optional external timestamping to provide independent proof of existence. Continuously verify hash chains, detect gaps or out-of-order sequences, and raise alerts when anomalies or write failures occur. Surface an integrity health report in admin settings and expose audit verification endpoints to validate exported evidence packages against stored manifests.

Acceptance Criteria
Scheduled Merkle Sealing Runs
Given the sealing interval is configured to 10 minutes and there are unsealed events with contiguous sequence numbers since the last seal When the sealing job executes at the interval boundary Then a new seal record is created within 30 seconds containing: the exact inclusive sequence range covered, the computed Merkle root for those events, a creation timestamp, and the signer key identifier And the seal record includes a digital signature verifiable against the platform’s public key endpoint And no events outside the stated range are included And no overlapping or duplicate seal ranges exist Given there are no new events since the last successful seal When the sealing job executes Then no new seal record is created and an idempotent "no-op" status is logged Given a transient datastore outage occurs during sealing When the job retries according to backoff policy (max 3 retries over 5 minutes) Then either the seal completes successfully and is persisted exactly once, or a failure is recorded with no partial/duplicate seals
External Timestamping of Seals (Optional)
Given external timestamping is enabled and the provider is reachable When a new seal is created Then the Merkle root is submitted to the provider within 60 seconds and a receipt/proof identifier is stored with status "pending" And the status is updated to "confirmed" once the provider returns a verifiable proof Given external timestamping is enabled and the provider is unreachable When a new seal is created Then submission is retried with exponential backoff for up to 24 hours, recording each attempt And the seal remains valid locally even if timestamping is pending or fails Given external timestamping is disabled When a new seal is created Then no outbound call is made and the seal record shows timestamping "disabled"
Continuous Hash-Chain Verification and Tamper Alerts
Given the verifier is running continuously When it scans from the last verified sequence to the head Then it validates that each event hash links to its predecessor and that each sealed range’s Merkle root matches recomputation And verification progress and last-checked sequence are recorded Given a gap (missing sequence), out-of-order insertion, or content hash mismatch is detected When the verifier encounters the anomaly Then an integrity alert is created within 2 minutes containing anomaly type, affected sequence range, detected-at timestamp, and severity And an admin email notification is sent and an in-app alert banner is shown And a webhook event "integrity.alert.raised" is emitted if webhooks are configured Given an anomaly is resolved (e.g., by replay or recovery) and the verifier rechecks When the condition no longer reproduces Then the alert is marked "resolved" with resolver, resolution timestamp, and verification evidence
Gap and Out-of-Order Detection Rules
Rule: Sequence numbers must be strictly increasing by 1 with no duplicates within a ledger partition; any deviation is flagged as a Gap (missing) or Duplicate/OutOfOrder Rule: Event timestamps must not be earlier than their immediate predecessor by more than the allowed clock skew (<= 2 seconds); violations are flagged as OutOfOrderTime Rule: Seal ranges must be contiguous and non-overlapping; any overlap or hole is flagged as SealRangeAnomaly Rule: Recomputed Merkle root for a sealed range must equal the stored root; mismatches are flagged as RootMismatch Rule: All anomalies are recorded with unique IDs, precise sequence ranges, and are queryable via the admin UI and API
Integrity Health Report in Admin Settings
Given an organization admin opens Admin Settings > Integrity Health When the page loads Then it displays: time of last successful seal, last external timestamp status (disabled/pending/confirmed/failed), percentage of events verified in the last 24 hours, oldest unverified sequence (if any), count of open anomalies by type, and verifier job status (healthy/degraded) And data refreshes at least every 60 seconds while the page is open And no PII (client names/addresses/messages) is displayed, only metadata and counts Given there are open anomalies When the admin views the report Then each anomaly type shows a count and "View details" link that navigates to filtered anomaly logs Given the admin clicks "Export Health Snapshot" When the export completes Then a JSON file is downloaded containing the same fields plus the current public key KID and seal summary for the last 5 seals
Audit Verification Endpoint Validates Evidence Packages
Given an authenticated admin POSTs an evidence package containing event records, seal manifests, and (if available) external timestamp proofs to /api/audit/verify When the server processes the package Then it returns 200 with a JSON result containing: verified (true/false), checkedEventCount, checkedSealCount, mismatches (array with type and sequence ranges), timestampProofs (per seal with status) And verification recomputes Merkle roots for provided ranges and cross-checks against stored manifests Given the package has been tampered (e.g., an event body altered) When verification runs Then verified is false and mismatches includes at least one RootMismatch referencing the affected sequence range Given the caller is unauthenticated or lacks permission When POST /api/audit/verify is called Then the request is rejected with 401/403 and no verification is performed Given excessive requests are made to the endpoint When rate limits are exceeded Then responses return 429 with Retry-After and no work is performed beyond the limit

Product Ideas

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

Two-Tap Deposit Defender

Text clients a one-tap Apple Pay/Google Pay deposit link. Unpaid holds auto-expire in 10 minutes, instantly offering the slot to the Smart Waitlist.

Idea

RouteSnap Rebalancer

Auto-suggest tight reschedules by text to erase dead time between walks. Fills micro-gaps with nearby waitlist clients using live geo-clustering.

Idea

Raincheck Roulette

Detect severe weather and pretext clients with reschedule choices and deposit carry-forward options. Instantly broadcasts freed slots to waitlists in unaffected nearby zones.

Idea

Pop-Up Day Builder

Schedule pop-up events by text with timed booking waves. On-site QR/text check-in auto-stamps arrival and releases no-shows to a live standby list.

Idea

Bilingual Auto-Reply Brain

Detect client language and send matched templates instantly. Keeps per-client language defaults and translates care notes with pet-specific terms, preserving tone.

Idea

Compliance Lockstep

Gate bookings behind SMS-uploaded vaccination cards and signed waivers. Auto-verify expiry dates and text renewal nudges before appointments.

Idea

Masked Crew Access

Grant contractors masked-SMS access to assigned clients and time blocks only. Role-based permissions hide addresses and payments while enabling replies and schedule updates.

Idea

Press Coverage

Imagined press coverage for this groundbreaking product concept.

P

PawPilot Launches Smart Waitlist and Deposit Rules to End No‑Shows for Independent Pet Pros

Imagined Press Article

PawPilot today announced the general availability of its SMS‑first scheduling, reminders, and billing platform built specifically for independent groomers, dog walkers, and sitters. Designed to text clients where they already reply, PawPilot automates confirmations, fills last‑minute cancellations in minutes via a Smart Waitlist, and moves payments from days‑late to same‑day—all without requiring clients to download an app. Early users report 83% of canceled slots refilled automatically, a 58% reduction in no‑shows, and more than six hours of admin time saved each week. The reality for solo and small pet‑care operators is a constant battle with churn and chaos: late texts, last‑minute drops, and awkward payment chases that eat nights and weekends. PawPilot addresses these pain points end‑to‑end in the SMS thread, with deposit links, live countdown holds, and a cascading waitlist that converts open time back into revenue while the pro keeps working. At the heart of today’s release are two pillars: Smart Waitlist and Deposit Rules. Smart Waitlist pairs with Waitlist Cascade to automatically invite the next best‑fit client the moment a 10‑minute hold expires, continuing until the slot is filled. Deposit Rules let pros set smart policies by service, price tier, client status, or time window—such as 50% for new clients or a flat morning hotspot deposit—so every SMS payment link enforces the right policy, every time. PawPilot rounds out the flow with Smart Expiry to auto‑tune hold windows by urgency and client reliability, Tap Cascade to fall back gracefully from Apple or Google Pay to card‑on‑file, Gentle Nudges that recover borderline payments with a one‑tap 5‑minute extension, and Policy Tapback to log time‑stamped acknowledgement of cancellation and refund policies on every booking. “Independent pet pros are the backbone of neighborhood pet care, but too often they run on copy‑paste, handwritten notes, and hope,” said Ari Kim, founder and CEO of PawPilot. “We built PawPilot to turn every text into a confirmed booking and every open minute back into revenue. When your calendar breathes and your policies enforce themselves, you get your evenings back.” For Solo‑Stack Groomers who run mobile or home‑based operations solo, the system’s speed and reliability is a difference‑maker. “Before PawPilot I’d lose half a day each week to back‑and‑forth and chasing deposits,” said Casey L., a mobile groomer in Austin. “Now cancellations refill within minutes from my Smart Waitlist, deposits land same‑day, and clients get clear receipts and policies. My no‑shows are down to almost zero.” Cash‑Flow Catch‑Up users—pros transitioning away from manual invoicing—benefit from deposit links and automated nudges that eliminate late payments and awkward follow‑ups. Every receipt includes a one‑tap policy confirmation, and any exceptions are logged for later reference, cutting disputes and chargebacks without extra effort. Here’s how it works in practice. When a client cancels, PawPilot immediately scans the waitlist and sends a text invitation with a live countdown hold and deposit link. If the client pays via Apple Pay or Google Pay, the spot locks and an instant receipt and policy acknowledgment are texted back. If a payment method fails, Tap Cascade routes to a saved card or secure entry. If the client hesitates, Gentle Nudges provide a subtle reminder and, when appropriate, a one‑tap 5‑minute extension. If the hold expires, Waitlist Cascade moves to the next best‑fit client in the same thread automatically until the slot is filled—often in minutes. Because every step lives in the SMS thread the client already uses, there’s no login friction and fewer dropped bookings. Pros can set and forget Deposit Rules by service or client status and rely on Smart Expiry to be stricter during peak demand and more flexible for VIPs, maximizing fair access while keeping fill rates high. Early results across beta cohorts are consistent: 83% of cancellations refilled automatically within the hold window, a 58% reduction in no‑shows after deposits and policy acknowledgments were enabled, and more than six hours per week reclaimed from admin tasks—time users reinvested into extra services or actual rest. PawPilot is available today nationwide. Setup takes minutes: import contacts, define services and Deposit Rules, and share your booking number. Clients never need to download an app; they simply reply to texts as usual. Same‑day payouts are available for eligible users, with transparent fees, and policy templates help new users start with best practices from day one. “Clients love the clarity. They know exactly how long they have to confirm, what’s expected if they cancel late, and they get a clean receipt and policy link every time,” said Nia R., a sitter rebuilding her roster after a move. “I went from chasing to choosing—my calendar is full and I have backups for everything.” About PawPilot: PawPilot is the SMS‑first operating system for independent groomers, walkers, and sitters. By meeting clients where they already respond—text—PawPilot automates scheduling, reminders, deposits, and payments, protecting time and cash flow for the people who care for our pets. Media contact: Jordan Reyes, Head of Communications press@pawpilot.co +1 415 555 0184 www.pawpilot.co

P

PawPilot Unveils Route Intelligence Suite to Maximize Walk Density and Same‑Day Payouts

Imagined Press Article

PawPilot today introduced the Route Intelligence Suite, a new set of SMS‑driven tools that compress dead time between walks, keep routes at capacity, and protect on‑time arrivals for independent dog walkers. Built into PawPilot’s SMS‑first scheduling and billing platform, the suite combines real‑time gap detection with one‑tap micro‑shifts and proximity‑based fill‑ins so walkers can earn more in the same hours and collect payment the same day. For Route‑Block Walkers running fixed time blocks and neighborhood loops, minutes lost to key swaps, elevator holds, and short‑notice changes can quickly add up to missed appointments and unbillable gaps. PawPilot’s Route Intelligence Suite eliminates the guesswork and endless back‑and‑forth by scanning the day in real time and sending clients precise, courteous texts that keep everything moving without requiring the pro to open a calendar app. The suite includes Gap Radar to continuously scan for 5–20 minute voids and slippage, One‑Tap Shift to request tiny schedule slides that respect each client’s preferences, Proximity Fill to ping nearby waitlisters or flexible regulars within the walker’s radius, Buffer Guard to enforce travel and building constraints, Live Reflow to automatically recalc the route and text updated ETAs as confirmations roll in, and Block Optimizer to simulate micro‑swaps that increase walk density in a single tap. “Walkers aren’t dispatchers—they’re athletes in motion taking care of pets and managing the street in real time,” said Priya Desai, head of product at PawPilot. “We designed Route Intelligence to do the invisible math for them, proposing realistic adjustments that preserve trust and punctuality while turning lost minutes into paid minutes.” Here’s how a typical block unfolds with PawPilot. Gap Radar notices a creeping 12‑minute void between two mid‑day walks. With a tap, One‑Tap Shift sends a polite ask to an adjacent client—“Can we slide 10 minutes earlier?”—respecting their quiet hours and preferences. If a quick slide isn’t possible, Proximity Fill invites a nearby waitlisted client or a flexible regular with a precise ETA and an instant confirm link. Buffer Guard filters out any suggestion that would require an unrealistic dash, while Live Reflow recomputes the sequence as confirmations come in and pushes updated ETAs to affected clients automatically. The walker stays hands‑free; the route stays full and on time. “Before PawPilot, I was constantly choosing between going late or leaving money on the table,” said Marco G., a Route‑Block Walker in Brooklyn. “Now the system spots gaps and offers a fix by text while I’m still on the move. My days feel smoother, my regulars love the updates, and I’m taking home more without adding hours.” Growth‑Mode Pack Leaders who are adding services or contractors can centralize all client SMS in one branded thread while keeping a unified schedule behind the scenes. Role‑based rules and masked numbers maintain a single, consistent client experience even as coverage expands, while same‑day payouts and SMS pay links keep cash flow tight across the team. PawPilot’s SMS‑first design is critical. Clients don’t have to download an app, remember a password, or respond to unfamiliar numbers. Each request—whether it’s a micro‑shift, a waitlist invitation, or an updated ETA—arrives as a clear, friendly text with one‑tap choices. Confirmations log instantly; policies and receipts attach to the booking record; and the audit trail records who approved what and when. Route Intelligence integrates with deposit logic so high‑demand blocks can require small holds that reduce late cancels. For especially tight days, Smart Expiry can shorten holds, while Gentle Nudges recover borderline confirms without pressuring clients. The outcome is a calmer day with higher completion rates and fewer unpaid holes. “Walk density is revenue,” added Desai. “If we can give walkers back 15–30 minutes per block across a week, the compounding impact is real—more completed walks, fewer rushes, and a healthier business.” PawPilot is available today for independent walkers and small teams. Getting started takes minutes: import your clients, define blocks and preferences, and let Route Intelligence begin scanning your day. No apps for clients, no complex dashboards to learn—just clear, timely texts that keep routes full and on‑time. Same‑day payouts are available for eligible users; transparent pricing and a free onboarding session help new walkers realize value immediately. About PawPilot: PawPilot is the SMS‑first operating system for independent groomers, walkers, and sitters. By converting texts into confirmed bookings, on‑time arrivals, and same‑day payments, PawPilot helps pet pros do more of the work they love with fewer admin hours and better cash flow. Media contact: Jordan Reyes, Head of Communications press@pawpilot.co +1 415 555 0184 www.pawpilot.co

P

PawPilot Introduces Weather‑Smart Scheduling to Turn Storms into Same‑Week Revenue

Imagined Press Article

PawPilot today announced Weather‑Smart Scheduling, a suite of street‑level tools that detect hyperlocal weather risks, pretext clients with safer options, and keep days productive when storms threaten to wash out revenue. Built into PawPilot’s SMS‑first scheduling and billing platform, the release includes Microburst Radar, Safe‑Window Picker, CarryForward Credit, DryZone Broadcast, Storm Reflow, and Weather Grace—features that work together to prevent day‑of surprises and convert weather chaos into quick, confirmed rebooks. Weather hits pet pros unevenly. A pop‑up shower can stall one side of town while the other side stays dry; a heat spike can make a mid‑afternoon walk unsafe while a morning window remains perfect. Seasonal Gap‑Fillers and mobile groomers, in particular, face volatile calendars and income swings as the forecast shifts. Weather‑Smart Scheduling arms them with proactive, client‑friendly moves—by text—so they can stay in control and avoid losing the day. Microburst Radar scans the next few hours at street level, tagging at‑risk appointments on the map and splitting the day into affected vs. safe zones. For any flagged booking, Safe‑Window Picker calculates the three best alternative windows that respect travel buffers, building access, and client preferences. Clients receive a one‑tap SMS choice with live timers; confirmations log instantly; and updated ETAs go out automatically. CarryForward Credit applies deposit rules you control to any weather move, with clear receipts that show full or partial credit, expiry dates, and cross‑service applicability. When weather clears an area, DryZone Broadcast uses dynamic radius and geo‑clustering to invite waitlisters and flexible regulars in nearby unaffected zones, filling the sudden openings with precise ETAs and safe‑window tags. Storm Reflow resequences the day around incoming weather, compressing safe bookings and sliding at‑risk appointments into newly opened windows, while automated ETA texts keep everyone aligned. Weather Grace adds human kindness at scale: late‑cancel forgiveness, brief hold extensions, or fee waivers based on severity and client reliability, recorded with a one‑tap acknowledgement to preserve goodwill and documentation. “Pet pros don’t need another weather app—they need an action plan,” said Ari Kim, founder and CEO of PawPilot. “Weather‑Smart Scheduling turns forecasts into concrete, client‑approved moves that protect both safety and revenue. Instead of waking up to panic texts, our users wake up to a prioritized list of smart reschedules and confident backups.” For mobile groomers like Pop‑Up Pro Piper who run packed days and rely on tight timing, the difference is immediate. “Microburst Radar called a storm line two hours before I saw clouds,” said Piper, a mobile groomer who hosts pop‑ups in local parks. “Safe‑Window Picker texted three options, and most clients responded in minutes. I kept the day full, moved a handful to tomorrow with CarryForward Credit, and nobody felt inconvenienced.” Neighborhood walkers see similar gains. “I used to spend my lunch break apologizing and guessing ETAs,” said After‑Hours Adapter Alex, who covers late shifts for hospital staff. “Now the system shifts my safe windows, texts updated ETAs, and even offers small grace where it makes sense. Clients feel informed instead of frustrated.” Because all of these moves occur in the SMS thread clients already use, adoption is immediate. There are no apps to download, no logins to forget, and no unfamiliar numbers to mistrust. Every rebook comes with an updated receipt and policy line via Policy Tapback; every exception is logged in an audit‑ready trail. Deposit logic stays consistent with your rules, eliminating the awkwardness of manual waivers or one‑off promises that are hard to enforce later. The result is a resilient, professional response to unpredictability. Even during stormy weeks, early users report steady completion rates, fewer last‑minute cancellations, and more predictable cash flow. For Seasonal Gap‑Fillers in rural or climate‑impacted regions, DryZone Broadcast ensures that recovered windows don’t go to waste, pushing invitations to the nearest ready‑to‑book clients with the highest acceptance likelihood. PawPilot’s Weather‑Smart Scheduling is available today for all users at no extra cost during the introductory period. Setup takes minutes: enable weather scanning, review default grace policies, and confirm deposit carry‑forward settings. From there, PawPilot proactively monitors the day and offers the next‑best action via simple, friendly texts you approve in a tap. About PawPilot: PawPilot is the SMS‑first operating system for independent groomers, walkers, and sitters. By converting texts into confirmed schedules, safe reschedules, and same‑day payments, PawPilot gives pet pros control over their calendar and cash flow—rain or shine. Media contact: Jordan Reyes, Head of Communications press@pawpilot.co +1 415 555 0184 www.pawpilot.co

P

PawPilot Rolls Out Bilingual Messaging and Translation Engine for Pet Care SMS at Scale

Imagined Press Article

PawPilot today released a comprehensive bilingual messaging and translation engine that helps pet pros serve diverse neighborhoods without breaking the flow of SMS. The new capabilities—LingoSense, ToneMirror, PetLexicon, Template Twins, CodeSwitch Guard, and FormFlow Translate—detect and honor client language preferences in real time, preserve the pro’s voice across languages, and ensure care notes and policies are accurately understood by every household. For walkers and sitters like Bilingual Bridge Ben who move seamlessly between languages in a single afternoon, clarity and tone matter just as much as speed. Pet care is intimate work; terms of care, timing, and policy nuance must be correct and culturally appropriate. PawPilot’s bilingual engine lives where the conversation already happens—the SMS thread—so professionals can communicate clearly and consistently without toggling apps, copying into translators, or retyping the same messages twice. LingoSense detects the client’s language, locks a per‑client default with a confidence score, and provides a one‑tap override. ToneMirror preserves the pro’s intent—casual, professional, or warm—by making the right choices for pronouns, honorifics, and local idioms, so confirmations and nudges feel authentically you in both languages. PetLexicon adds a pet‑specific glossary that remembers breed names, coat notes, commands, meds, and behavior flags in both languages, preventing misunderstandings that could impact safety or service quality. Template Twins lets pros create paired SMS templates once; PawPilot generates and syncs the second language automatically, adapting grammar and number so dates, times, and prices read naturally. CodeSwitch Guard understands bilingual households and mid‑thread switches, replying in the client’s current language and adding concise bilingual summaries of essential details—like time, address, deposit—when a mixed‑language group is involved. FormFlow Translate extends the experience to waivers, vaccination requests, and intake forms, localizing field labels, errors, and policy acknowledgments end‑to‑end. Signed consents attach in the client’s language plus an English mirror for the pro’s records. “Great service is great communication,” said Priya Desai, head of product at PawPilot. “We’ve seen pros try to choose between speed and clarity. Our goal was to let them have both—fast, natural texting in the right language with no extra steps and no loss of personality.” For compliance‑minded urban groomers like Compliance‑Conscious Casey, bilingual forms and policy acknowledgments remove guesswork and liability risk. “I used to worry that my cancellation policy wasn’t understood,” said Casey M., a groomer in Chicago. “Now bookings come with clear bilingual terms, one‑tap consent, and a timestamp. Clients feel respected and I sleep easier.” After‑Hours Adapter Alex, who serves shift workers and nightlife staff, sees a similar impact on late‑hour clarity. “People are tired when they text me at midnight,” Alex said. “With Template Twins and ToneMirror, I can send a friendly confirm in the right language in seconds and everyone wakes up to the same clear plan.” The bilingual engine integrates seamlessly with PawPilot’s Smart Waitlist, Deposit Rules, and Policy Tapback. Waitlist invitations arrive in the right language with the correct deposit logic; receipts and policy lines mirror across languages; and the audit trail records consent and changes in both. There are no apps for clients to download or new inboxes to monitor—just one consistent, branded SMS thread that adapts intelligently to each household. PawPilot’s approach respects boundaries and avoids complexity. Pros set their tone once, review language defaults per client as needed, and rely on CodeSwitch Guard to keep mixed‑language groups on the same page. PetLexicon ensures that sensitive pet details—medication schedules, behavioral flags, door codes—translate precisely, preventing costly misunderstandings on busy days. The result is more bookings, fewer corrections, and stronger relationships with multilingual communities. Early testers report faster response times, higher acceptance of deposits and policy acknowledgments, and fewer mid‑service clarifications. For pros expanding into new neighborhoods, bilingual messaging reduces friction and builds trust from the first interaction. The bilingual suite is available today to all PawPilot users. Setup takes minutes: enable LingoSense, review tone settings with ToneMirror, and convert your most‑used templates with Template Twins. From there, PawPilot keeps both languages in sync automatically while you focus on care. About PawPilot: PawPilot is the SMS‑first operating system for independent groomers, walkers, and sitters. By meeting clients where they already reply—text—PawPilot turns conversations into confirmed bookings, clear policies, and same‑day payments across languages. Media contact: Jordan Reyes, Head of Communications press@pawpilot.co +1 415 555 0184 www.pawpilot.co

P

PawPilot Debuts Compliance Automation to Lock Bookings Behind Verified Vaccines and Waivers

Imagined Press Article

PawPilot today launched a comprehensive compliance automation stack that ensures every booking is backed by current vaccination records and signed waivers—without manual chasing. The new capabilities—VaxVision, AutoGate, Nudge Ladder, Vet Pingback, Waiver Wallet, and PetPack Sync—live entirely in the SMS thread, cutting back‑and‑forth, preventing day‑of surprises, and reducing liability for independent groomers, walkers, and sitters. Compliance is where good intentions meet real‑world friction. Pros want to protect pets and themselves, but requesting documents, verifying dates, and recording consent often falls to late‑night copy‑paste marathons. PawPilot replaces this scramble with a clear, friendly flow that collects, verifies, and logs everything automatically while clients reply to a simple text. VaxVision lets clients snap a photo of vaccination cards right in the SMS thread. PawPilot auto‑reads vaccine types and dates, matches the pet and clinic, flags blur or missing shots, and calculates expiry. AutoGate locks bookings until requirements are met; when something is missing or expiring soon, PawPilot texts a Quick Unlock link that guides clients through the minimal steps to move forward. Nudge Ladder escalates reminders from soft check‑ins to deadline‑specific prompts with one‑tap camera upload and e‑sign; nudges pause the moment docs are complete and resume if they expire later. When a card is unclear, Vet Pingback messages the listed clinic a secure verification link. The clinic confirms vaccines and dates in one tap, and the proof attaches to the client’s record. Waiver Wallet stores all signed waivers by service and version with time‑stamped signatures tied to each booking; when policies change, clients receive an automatic re‑consent request by SMS. PetPack Sync manages compliance per pet in multi‑pet households from a single thread, making it easy to split or hold bookings until everyone is cleared. “Pros shouldn’t have to choose between being thorough and being booked,” said Ari Kim, founder and CEO of PawPilot. “By making compliance part of the same SMS flow as scheduling and deposits, we keep standards high and calendars safe without adding friction or awkward follow‑ups.” Compliance‑Conscious Casey, an urban groomer who prides herself on meticulous records, called the change transformative. “I used to screenshot everything, set a dozen reminders, and hope for the best,” Casey said. “Now VaxVision catches expiries before I do, clients upload right in the thread, and AutoGate prevents non‑compliant bookings from ever landing on my calendar.” Multi‑Pet Maestro Maya, who manages complex households with meds and staggered walks, emphasized the clarity for clients. “PetPack Sync shows exactly which pet is cleared and who needs updates,” Maya said. “If one pet isn’t ready, I can split the booking in a tap and keep the rest on schedule. No surprises at the door.” Because all actions occur via SMS, clients engage quickly. There are no portals to create, no apps to install, and no scanning gymnastics. Receipts and policy acknowledgments attach automatically to each booking with Policy Tapback; the immutable Trail Ledger records who viewed what, when addresses were revealed, messages sent, and policy acknowledgments logged—exportable for disputes, insurance, and payroll reconciliation. Compliance Automation integrates with PawPilot’s core scheduling and payment flows. Deposit Rules can require holds for new or lapsed clients; Smart Expiry tightens windows during peak demand; and Waitlist Cascade ensures that if a booking fails compliance by a deadline, the slot immediately invites the next best‑fit waitlister. The result is a safer, steadier business with fewer last‑minute cancellations and a dramatic reduction in day‑of turnaways. Getting started is simple. Import clients, enable VaxVision and AutoGate, and choose default waiver templates. PawPilot handles the rest: it detects expiries, sends the right nudges at the right time, collects signatures, and attaches everything automatically to the booking record. Same‑day payouts and clear receipts keep the financial side smooth while compliance stays rock‑solid. About PawPilot: PawPilot is the SMS‑first operating system for independent groomers, walkers, and sitters. By transforming texts into confirmed, compliant bookings with same‑day payments, PawPilot gives pet pros the confidence and capacity to grow. Media contact: Jordan Reyes, Head of Communications press@pawpilot.co +1 415 555 0184 www.pawpilot.co

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.